fastaar-python 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastaar/__init__.py +5 -0
- fastaar/client.py +239 -0
- fastaar/exceptions.py +16 -0
- fastaar/signature.py +53 -0
- fastaar_python-1.0.3.dist-info/METADATA +144 -0
- fastaar_python-1.0.3.dist-info/RECORD +8 -0
- fastaar_python-1.0.3.dist-info/WHEEL +5 -0
- fastaar_python-1.0.3.dist-info/top_level.txt +1 -0
fastaar/__init__.py
ADDED
fastaar/client.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import urllib.parse
|
|
3
|
+
import urllib.request
|
|
4
|
+
from urllib.error import HTTPError, URLError
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from fastaar.exceptions import FastaarException
|
|
8
|
+
|
|
9
|
+
BASE_URL = "https://fastaar.com"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FastaarClient:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_key: str,
|
|
16
|
+
timeout_seconds: int = 15,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Initialize the Fastaar client.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
api_key: Your Fastaar API key (e.g. fk_live_... or fk_test_...)
|
|
23
|
+
timeout_seconds: Connection and read timeout in seconds
|
|
24
|
+
"""
|
|
25
|
+
self.api_key = api_key
|
|
26
|
+
self.timeout_seconds = timeout_seconds
|
|
27
|
+
|
|
28
|
+
# -------------------------------------------------------------------------
|
|
29
|
+
# Payments
|
|
30
|
+
# -------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
def create_payment(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
Create a payment intent.
|
|
35
|
+
|
|
36
|
+
Reusing the same `invoice_number` while a previous payment for it is still
|
|
37
|
+
active (not `failed`/`expired`) raises a FastaarException with error type
|
|
38
|
+
`duplicate_invoice_number` (HTTP 409) instead of creating a duplicate — look
|
|
39
|
+
the existing payment up with `find_by_invoice_number()` rather than retrying
|
|
40
|
+
blindly. Supply `success_url`/`cancel_url` to return the customer to your site
|
|
41
|
+
after checkout; Fastaar appends `payment_id` (and `invoice_number`) to them.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
params: Dictionary containing:
|
|
45
|
+
- amount: int|float|str (required)
|
|
46
|
+
- invoice_number: str (required)
|
|
47
|
+
- customer_id: int (optional) - an existing customer belonging to your merchant account
|
|
48
|
+
- success_url: str (optional)
|
|
49
|
+
- cancel_url: str (optional)
|
|
50
|
+
- metadata: dict (optional)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The payment object dictionary, including `id`, `status`, and `checkout_url`.
|
|
54
|
+
"""
|
|
55
|
+
result = self._request("POST", "/api/v1/payments", body=params)
|
|
56
|
+
if not isinstance(result, dict):
|
|
57
|
+
raise FastaarException("Fastaar API returned an unexpected response format.", "api_error")
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
def get_payment(self, payment_id: str) -> Dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Retrieve a payment by its reference (the `id` returned at creation).
|
|
63
|
+
"""
|
|
64
|
+
encoded_id = urllib.parse.quote(payment_id, safe="")
|
|
65
|
+
result = self._request("GET", f"/api/v1/payments/{encoded_id}")
|
|
66
|
+
if not isinstance(result, dict):
|
|
67
|
+
raise FastaarException("Fastaar API returned an unexpected response format.", "api_error")
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
def list_payments(self, params: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
|
71
|
+
"""
|
|
72
|
+
List payments, newest first.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
params: Dictionary containing optional filters:
|
|
76
|
+
- status: str (optional)
|
|
77
|
+
- invoice_number: str (optional)
|
|
78
|
+
- per_page: int (optional)
|
|
79
|
+
- page: int (optional)
|
|
80
|
+
"""
|
|
81
|
+
if params:
|
|
82
|
+
query = "?" + urllib.parse.urlencode(params)
|
|
83
|
+
else:
|
|
84
|
+
query = ""
|
|
85
|
+
|
|
86
|
+
result = self._request("GET", f"/api/v1/payments{query}")
|
|
87
|
+
if not isinstance(result, list):
|
|
88
|
+
raise FastaarException("Fastaar API returned an unexpected response format.", "api_error")
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
def find_by_invoice_number(self, invoice_number: str) -> Optional[Dict[str, Any]]:
|
|
92
|
+
"""
|
|
93
|
+
Find the most recent payment for one of your invoice numbers, or None if none.
|
|
94
|
+
"""
|
|
95
|
+
payments = self.list_payments({"invoice_number": invoice_number})
|
|
96
|
+
return payments[0] if payments else None
|
|
97
|
+
|
|
98
|
+
def refund_payment(self, payment_id: str, amount: Optional[float] = None) -> Dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Refund a payment, in full or in part. Only payments with status `completed` or
|
|
101
|
+
`partially_refunded` can be refunded. Pass an amount to refund only part of the
|
|
102
|
+
remaining balance; omit it to refund whatever is still refundable.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The updated payment object. `status` is `refunded` once fully refunded, or
|
|
106
|
+
`partially_refunded` if some balance remains.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
FastaarException: if the payment is not in a refundable state, or the amount
|
|
110
|
+
exceeds the remaining refundable balance.
|
|
111
|
+
"""
|
|
112
|
+
encoded_id = urllib.parse.quote(payment_id, safe="")
|
|
113
|
+
body = {"amount": amount} if amount is not None else None
|
|
114
|
+
result = self._request("POST", f"/api/v1/payments/{encoded_id}/refund", body=body)
|
|
115
|
+
if not isinstance(result, dict):
|
|
116
|
+
raise FastaarException("Fastaar API returned an unexpected response format.", "api_error")
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
def list_refunds(self, payment_id: str) -> List[Dict[str, Any]]:
|
|
120
|
+
"""
|
|
121
|
+
List a payment's refund history, newest first — one entry per refund call, even
|
|
122
|
+
across several partial refunds.
|
|
123
|
+
"""
|
|
124
|
+
encoded_id = urllib.parse.quote(payment_id, safe="")
|
|
125
|
+
result = self._request("GET", f"/api/v1/payments/{encoded_id}/refunds")
|
|
126
|
+
if not isinstance(result, list):
|
|
127
|
+
raise FastaarException("Fastaar API returned an unexpected response format.", "api_error")
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
# -------------------------------------------------------------------------
|
|
131
|
+
# Customers
|
|
132
|
+
# -------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def list_customers(self, params: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
|
135
|
+
"""
|
|
136
|
+
List customers, newest first.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
params: Optional filters — email, phone, per_page, page.
|
|
140
|
+
"""
|
|
141
|
+
query = "?" + urllib.parse.urlencode(params) if params else ""
|
|
142
|
+
result = self._request("GET", f"/api/v1/customers{query}")
|
|
143
|
+
if not isinstance(result, list):
|
|
144
|
+
raise FastaarException("Fastaar API returned an unexpected response format.", "api_error")
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
def create_customer(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
148
|
+
"""
|
|
149
|
+
Create a customer.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
params: name (required), phone (required), email, address, notes.
|
|
153
|
+
"""
|
|
154
|
+
result = self._request("POST", "/api/v1/customers", body=params)
|
|
155
|
+
if not isinstance(result, dict):
|
|
156
|
+
raise FastaarException("Fastaar API returned an unexpected response format.", "api_error")
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
def get_customer(self, customer_id: int) -> Dict[str, Any]:
|
|
160
|
+
"""Retrieve a customer by ID."""
|
|
161
|
+
result = self._request("GET", f"/api/v1/customers/{customer_id}")
|
|
162
|
+
if not isinstance(result, dict):
|
|
163
|
+
raise FastaarException("Fastaar API returned an unexpected response format.", "api_error")
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def update_customer(self, customer_id: int, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
167
|
+
"""Update a customer (partial — only sent fields are changed)."""
|
|
168
|
+
result = self._request("PATCH", f"/api/v1/customers/{customer_id}", body=params)
|
|
169
|
+
if not isinstance(result, dict):
|
|
170
|
+
raise FastaarException("Fastaar API returned an unexpected response format.", "api_error")
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
def _request(
|
|
174
|
+
self,
|
|
175
|
+
method: str,
|
|
176
|
+
path: str,
|
|
177
|
+
body: Optional[Dict[str, Any]] = None,
|
|
178
|
+
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
|
|
179
|
+
url = BASE_URL + path
|
|
180
|
+
|
|
181
|
+
headers = {
|
|
182
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
183
|
+
"Accept": "application/json",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
data = None
|
|
187
|
+
if body is not None:
|
|
188
|
+
headers["Content-Type"] = "application/json"
|
|
189
|
+
data = json.dumps(body).encode("utf-8")
|
|
190
|
+
|
|
191
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
with urllib.request.urlopen(req, timeout=self.timeout_seconds) as response:
|
|
195
|
+
status_code = response.getcode()
|
|
196
|
+
response_body = response.read().decode("utf-8")
|
|
197
|
+
except HTTPError as e:
|
|
198
|
+
status_code = e.code
|
|
199
|
+
try:
|
|
200
|
+
response_body = e.read().decode("utf-8")
|
|
201
|
+
except Exception:
|
|
202
|
+
response_body = ""
|
|
203
|
+
except (URLError, Exception) as e:
|
|
204
|
+
# Check if reason is present
|
|
205
|
+
reason = getattr(e, "reason", str(e))
|
|
206
|
+
raise FastaarException(
|
|
207
|
+
message=f"Could not reach the Fastaar API: {reason}",
|
|
208
|
+
error_type="connection_error",
|
|
209
|
+
status_code=0,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
decoded = json.loads(response_body)
|
|
214
|
+
except json.JSONDecodeError:
|
|
215
|
+
decoded = None
|
|
216
|
+
|
|
217
|
+
if status_code >= 400 or not isinstance(decoded, (dict, list)):
|
|
218
|
+
error_message = None
|
|
219
|
+
error_type = "api_error"
|
|
220
|
+
|
|
221
|
+
if isinstance(decoded, dict):
|
|
222
|
+
error_details = decoded.get("error", decoded)
|
|
223
|
+
if isinstance(error_details, dict):
|
|
224
|
+
error_message = error_details.get("message")
|
|
225
|
+
error_type = error_details.get("type", "api_error")
|
|
226
|
+
|
|
227
|
+
if not error_message:
|
|
228
|
+
error_message = f"Fastaar API returned HTTP {status_code}."
|
|
229
|
+
|
|
230
|
+
raise FastaarException(
|
|
231
|
+
message=error_message,
|
|
232
|
+
error_type=error_type,
|
|
233
|
+
status_code=status_code,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# In case Fastaar envelopes data, return decoded['data'] if it exists, otherwise full decoded
|
|
237
|
+
if isinstance(decoded, dict) and "data" in decoded:
|
|
238
|
+
return decoded["data"]
|
|
239
|
+
return decoded
|
fastaar/exceptions.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class FastaarException(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Exception raised for errors in the Fastaar SDK.
|
|
4
|
+
|
|
5
|
+
Attributes:
|
|
6
|
+
message -- explanation of the error
|
|
7
|
+
error_type -- stable API error code, e.g. "authentication_error",
|
|
8
|
+
"subscription_required", "transaction_limit_reached",
|
|
9
|
+
"connection_error", or "api_error"
|
|
10
|
+
status_code -- HTTP status code returned by the API (0 if connection error)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, error_type: str = "api_error", status_code: int = 0):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.error_type = error_type
|
|
16
|
+
self.status_code = status_code
|
fastaar/signature.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WebhookSignature:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def verify(
|
|
11
|
+
secret: Union[str, bytes],
|
|
12
|
+
raw_body: Union[str, bytes],
|
|
13
|
+
signature_header: str,
|
|
14
|
+
tolerance_seconds: int = 300,
|
|
15
|
+
) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Verify the X-Fastaar-Signature header (`t=<ts>,v1=<hmac>`) against
|
|
18
|
+
the raw request body using your merchant webhook secret.
|
|
19
|
+
"""
|
|
20
|
+
if not signature_header or not secret:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
# Match signature header pattern: t=<timestamp>,v1=<hmac-sha256>
|
|
24
|
+
match = re.match(r"^t=(?P<t>\d+),v1=(?P<v1>[a-f0-9]{64})$", signature_header)
|
|
25
|
+
if not match:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
timestamp = int(match.group("t"))
|
|
30
|
+
except ValueError:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
# Check timestamp tolerance
|
|
34
|
+
current_time = int(time.time())
|
|
35
|
+
if abs(current_time - timestamp) > tolerance_seconds:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
# Ensure raw_body is a string
|
|
39
|
+
if isinstance(raw_body, bytes):
|
|
40
|
+
raw_body = raw_body.decode("utf-8")
|
|
41
|
+
|
|
42
|
+
# Prepare secret and message payload
|
|
43
|
+
secret_bytes = secret.encode("utf-8") if isinstance(secret, str) else secret
|
|
44
|
+
payload_bytes = f"{timestamp}.{raw_body}".encode("utf-8")
|
|
45
|
+
|
|
46
|
+
# Generate expected signature
|
|
47
|
+
expected = hmac.new(
|
|
48
|
+
secret_bytes,
|
|
49
|
+
payload_bytes,
|
|
50
|
+
hashlib.sha256
|
|
51
|
+
).hexdigest()
|
|
52
|
+
|
|
53
|
+
return hmac.compare_digest(expected, match.group("v1"))
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastaar-python
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Python SDK for the Fastaar payment gateway — accept bKash & Nagad payments in Bangladesh.
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# Fastaar Python SDK
|
|
15
|
+
|
|
16
|
+
Accept bKash & Nagad payments on any Python website or application via [Fastaar](https://fastaar.com).
|
|
17
|
+
|
|
18
|
+
This is a zero-dependency SDK utilizing Python's standard library.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Install the package via `pip` (or configure it in your `requirements.txt` / `pyproject.toml`):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install fastaar-python
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Create a payment & redirect to checkout
|
|
29
|
+
|
|
30
|
+
Here is an example using a generic Python web application:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import os
|
|
34
|
+
from fastaar import FastaarClient
|
|
35
|
+
|
|
36
|
+
fastaar = FastaarClient(api_key=os.getenv('FASTAAR_API_KEY')) # fk_live_... or fk_test_...
|
|
37
|
+
|
|
38
|
+
# The key must have the `payments:write` ability (and not be expired) or this
|
|
39
|
+
# call returns a 403 `ability_denied` / 401 `authentication_error` error.
|
|
40
|
+
payment = fastaar.create_payment({
|
|
41
|
+
'amount': 1250,
|
|
42
|
+
'invoice_number': 'ORDER-42', # required — your order reference
|
|
43
|
+
'customer_id': customer['id'] if customer else None, # optional — attach an existing customer
|
|
44
|
+
'success_url': 'https://shop.example.com/thanks', # optional, customer returns here
|
|
45
|
+
'cancel_url': 'https://shop.example.com/cart', # optional
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
# Redirect the customer to checkout
|
|
49
|
+
checkout_url = payment['checkout_url']
|
|
50
|
+
print(f"Redirecting customer to: {checkout_url}")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`invoice_number` is idempotent: if a payment already exists for it and hasn't reached `failed`
|
|
54
|
+
or `expired`, creating another one raises a `FastaarException` with error type
|
|
55
|
+
`duplicate_invoice_number` (HTTP 409) instead of creating a duplicate — so a dropped connection
|
|
56
|
+
never double-charges. Use `find_by_invoice_number()` to look the existing payment up rather than
|
|
57
|
+
retrying blindly.
|
|
58
|
+
|
|
59
|
+
## Confirm the order from a webhook
|
|
60
|
+
|
|
61
|
+
Verify the signature header to ensure webhook security:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import os
|
|
65
|
+
from fastaar import WebhookSignature, FastaarException
|
|
66
|
+
|
|
67
|
+
# Get raw request body as bytes/string and signature header from request
|
|
68
|
+
raw_body = request.body # must be the raw string or bytes of the request body
|
|
69
|
+
signature = request.headers.get('X-Fastaar-Signature', '')
|
|
70
|
+
|
|
71
|
+
secret = os.getenv('FASTAAR_WEBHOOK_SECRET')
|
|
72
|
+
|
|
73
|
+
if not WebhookSignature.verify(secret, raw_body, signature):
|
|
74
|
+
# Signature verification failed
|
|
75
|
+
return "Invalid signature", 400
|
|
76
|
+
|
|
77
|
+
# Parse the event
|
|
78
|
+
event = request.json()
|
|
79
|
+
|
|
80
|
+
if event['event'] == 'payment.completed':
|
|
81
|
+
order_id = event['data']['invoice_number']
|
|
82
|
+
payment_id = event['data']['id']
|
|
83
|
+
# Mark the order as paid idempotently using the payment_id as key
|
|
84
|
+
print(f"Payment completed: {payment_id} for invoice {order_id}")
|
|
85
|
+
|
|
86
|
+
return "OK", 200
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Other payment calls
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Retrieve one payment by its ID (e.g. "01jxyz...")
|
|
93
|
+
payment = fastaar.get_payment('01jxyz...')
|
|
94
|
+
|
|
95
|
+
# Look up most recent payment by your reference
|
|
96
|
+
payment = fastaar.find_by_invoice_number('ORDER-42')
|
|
97
|
+
|
|
98
|
+
# List payments
|
|
99
|
+
payments = fastaar.list_payments(params={'status': 'completed'})
|
|
100
|
+
|
|
101
|
+
# Refund a completed (or partially refunded) payment
|
|
102
|
+
payment = fastaar.refund_payment('01jxyz...') # refund the full remaining balance
|
|
103
|
+
payment = fastaar.refund_payment('01jxyz...', 200) # or refund only part of it
|
|
104
|
+
refunds = fastaar.list_refunds('01jxyz...') # full refund history, newest first
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Customers
|
|
108
|
+
|
|
109
|
+
Store customer records to attach them to payments collected via payment links.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# Create a customer — name and phone are required
|
|
113
|
+
customer = fastaar.create_customer({
|
|
114
|
+
'name': 'Rahim Uddin',
|
|
115
|
+
'phone': '01712345678',
|
|
116
|
+
'email': 'rahim@example.com', # optional
|
|
117
|
+
'address': 'Dhaka, Bangladesh', # optional
|
|
118
|
+
'notes': 'VIP customer', # optional
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
# Retrieve, update, list
|
|
122
|
+
customer = fastaar.get_customer(customer['id'])
|
|
123
|
+
customer = fastaar.update_customer(customer['id'], {'name': 'Rahim Ahmed'})
|
|
124
|
+
customers = fastaar.list_customers({'email': 'rahim@example.com'})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Error Handling
|
|
128
|
+
|
|
129
|
+
Errors raise `fastaar.FastaarException` with `error_type` (e.g. `authentication_error`, `subscription_required`, `transaction_limit_reached`, `connection_error`) and `status_code`.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from fastaar import FastaarException
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
payment = fastaar.create_payment({'amount': 100, 'invoice_number': 'ORDER-42'})
|
|
136
|
+
except FastaarException as e:
|
|
137
|
+
print(f"API Error: {e}")
|
|
138
|
+
print(f"Type: {e.error_type}")
|
|
139
|
+
print(f"Status Code: {e.status_code}")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Test mode
|
|
143
|
+
|
|
144
|
+
Use an `fk_test_` key: payments auto-complete on the checkout page without real money, and webhooks fire exactly like production with `"livemode": false`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
fastaar/__init__.py,sha256=BBPMU-Yv3JQ-9e_wvP21MuvFaLMwcxLJI7Fom0CLXX4,205
|
|
2
|
+
fastaar/client.py,sha256=ETxwyibVj3ighJVbLgf6MXlYMqeu9WJnJF2uNeqxqwU,9710
|
|
3
|
+
fastaar/exceptions.py,sha256=uVOXg7oc5ictqRuTJOuvfVY1vQ5DEI6JFWwLBv6mr4g,657
|
|
4
|
+
fastaar/signature.py,sha256=NAnXcjlmaG_jzGUIQnL4TMNUd4L5HwLWYSTw4v6SrFE,1568
|
|
5
|
+
fastaar_python-1.0.3.dist-info/METADATA,sha256=sAMCJpcqhDbfwmCa8m76hCLy3GDHV7C7RdbcbBlnGio,4920
|
|
6
|
+
fastaar_python-1.0.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
fastaar_python-1.0.3.dist-info/top_level.txt,sha256=DsGjqdIYVu85l7X2ZWZ-KbWlmlZ5-fRBWd5ar-yF2rM,8
|
|
8
|
+
fastaar_python-1.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastaar
|