pybkash 0.1.0__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.
- pybkash/__init__.py +6 -0
- pybkash/exception_handlers.py +10 -0
- pybkash/exceptions.py +8 -0
- pybkash/main.py +570 -0
- pybkash/models.py +324 -0
- pybkash/token_manager.py +174 -0
- pybkash-0.1.0.dist-info/METADATA +97 -0
- pybkash-0.1.0.dist-info/RECORD +10 -0
- pybkash-0.1.0.dist-info/WHEEL +4 -0
- pybkash-0.1.0.dist-info/licenses/LICENSE +21 -0
pybkash/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .exceptions import APIError
|
|
2
|
+
|
|
3
|
+
def raise_api_exception(response: dict):
|
|
4
|
+
status_code = response.get("statusCode")
|
|
5
|
+
if not status_code: # no status_code sent by api, the api does it when nothing goes wrong
|
|
6
|
+
return
|
|
7
|
+
if status_code == "0000":
|
|
8
|
+
return
|
|
9
|
+
raise APIError(status_code=response["statusCode"], message=response["statusMessage"])
|
|
10
|
+
|
pybkash/exceptions.py
ADDED
pybkash/main.py
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
from httpx import Client as SyncClient, AsyncClient as HttpxAsyncClient
|
|
2
|
+
from .token_manager import Token, AsyncToken
|
|
3
|
+
from .exception_handlers import raise_api_exception
|
|
4
|
+
from .models import (
|
|
5
|
+
PaymentCreation,
|
|
6
|
+
AgreementCreation,
|
|
7
|
+
PaymentExecution,
|
|
8
|
+
AgreementExecution,
|
|
9
|
+
RefundExecution,
|
|
10
|
+
AgreementQuery,
|
|
11
|
+
PaymentQuery,
|
|
12
|
+
Transaction,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseClient:
|
|
17
|
+
def _create_agreement_object(self, response: dict) -> AgreementCreation:
|
|
18
|
+
return AgreementCreation(
|
|
19
|
+
status_code=response["statusCode"],
|
|
20
|
+
status_message=response["statusMessage"],
|
|
21
|
+
payment_id=response["paymentID"],
|
|
22
|
+
bkash_url=response["bkashURL"],
|
|
23
|
+
callback_url=response["callbackURL"],
|
|
24
|
+
success_callback=response["successCallbackURL"],
|
|
25
|
+
failure_callback=response["failureCallbackURL"],
|
|
26
|
+
cancel_callback=response["cancelledCallbackURL"],
|
|
27
|
+
payer_reference=response["payerReference"],
|
|
28
|
+
agreement_status=response["agreementStatus"],
|
|
29
|
+
agreement_create_time=response["agreementCreateTime"],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _create_payment_object(self, payment_response: dict) -> PaymentCreation:
|
|
33
|
+
payment = PaymentCreation(
|
|
34
|
+
status_code=payment_response["statusCode"],
|
|
35
|
+
status_message=payment_response["statusMessage"],
|
|
36
|
+
payment_id=payment_response["paymentID"],
|
|
37
|
+
bkash_url=payment_response["bkashURL"],
|
|
38
|
+
callback_url=payment_response["callbackURL"],
|
|
39
|
+
success_callback=payment_response["successCallbackURL"],
|
|
40
|
+
failure_callback=payment_response["failureCallbackURL"],
|
|
41
|
+
cancel_callback=payment_response["cancelledCallbackURL"],
|
|
42
|
+
)
|
|
43
|
+
return payment
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _create_payment_execution_object(self, response: dict) -> PaymentExecution:
|
|
48
|
+
return PaymentExecution(
|
|
49
|
+
status_code=response["statusCode"],
|
|
50
|
+
status_message=response["statusMessage"],
|
|
51
|
+
payment_id=response["paymentID"],
|
|
52
|
+
payer_reference=response["payerReference"],
|
|
53
|
+
customer_msisdn=response["customerMsisdn"],
|
|
54
|
+
trx_id=response["trxID"],
|
|
55
|
+
amount=response["amount"],
|
|
56
|
+
transaction_status=response["transactionStatus"],
|
|
57
|
+
payment_execute_time=response["paymentExecuteTime"],
|
|
58
|
+
currency=response["currency"],
|
|
59
|
+
intent=response["intent"],
|
|
60
|
+
merchant_invoice_number=response["merchantInvoiceNumber"],
|
|
61
|
+
agreement_id=response.get("agreementID"),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _create_agreement_execution_object(self, response: dict) -> AgreementExecution:
|
|
66
|
+
return AgreementExecution(
|
|
67
|
+
status_code=response["statusCode"],
|
|
68
|
+
status_message=response["statusMessage"],
|
|
69
|
+
payment_id=response["paymentID"],
|
|
70
|
+
agreement_id=response["agreementID"],
|
|
71
|
+
payer_reference=response["payerReference"],
|
|
72
|
+
customer_msisdn=response["customerMsisdn"],
|
|
73
|
+
agreement_execute_time=response["agreementExecuteTime"],
|
|
74
|
+
agreement_status=response["agreementStatus"],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _create_agreement_query_object(self, response: dict) -> AgreementQuery:
|
|
78
|
+
return AgreementQuery(
|
|
79
|
+
status_code=response["statusCode"],
|
|
80
|
+
status_message=response["statusMessage"],
|
|
81
|
+
payment_id=response["paymentID"],
|
|
82
|
+
agreement_id=response["agreementID"],
|
|
83
|
+
payer_reference=response["payerReference"],
|
|
84
|
+
payer_account=response["payerAccount"],
|
|
85
|
+
payer_type=response["payerType"],
|
|
86
|
+
agreement_status=response["agreementStatus"],
|
|
87
|
+
agreement_create_time=response["agreementCreateTime"],
|
|
88
|
+
agreement_execute_time=response["agreementExecuteTime"],
|
|
89
|
+
mode=response["mode"],
|
|
90
|
+
verification_status=response["verificationStatus"],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _create_payment_query_object(self, response: dict) -> PaymentQuery:
|
|
94
|
+
return PaymentQuery(
|
|
95
|
+
status_code=response["statusCode"],
|
|
96
|
+
status_message=response["statusMessage"],
|
|
97
|
+
payment_id=response["paymentID"],
|
|
98
|
+
payer_reference=response["payerReference"],
|
|
99
|
+
mode=response["mode"],
|
|
100
|
+
payment_create_time=response["paymentCreateTime"],
|
|
101
|
+
amount=response["amount"],
|
|
102
|
+
currency=response["currency"],
|
|
103
|
+
intent=response["intent"],
|
|
104
|
+
merchant_invoice=response["merchantInvoice"],
|
|
105
|
+
transaction_status=response["transactionStatus"],
|
|
106
|
+
verification_status=response["verificationStatus"],
|
|
107
|
+
agreement_status=response.get("agreementStatus"),
|
|
108
|
+
agreement_create_time=response.get("agreementCreateTime"),
|
|
109
|
+
agreement_execute_time=response.get("agreementExecuteTime"),
|
|
110
|
+
agreement_id=response.get("agreementID"),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _create_refund_execution_object(self, response: dict) -> RefundExecution:
|
|
114
|
+
return RefundExecution(
|
|
115
|
+
original_trx_id=response["originalTrxID"],
|
|
116
|
+
refund_trx_id=response["refundTrxID"],
|
|
117
|
+
transaction_status=response["transactionStatus"],
|
|
118
|
+
amount=response["amount"],
|
|
119
|
+
currency=response["currency"],
|
|
120
|
+
completed_time=response["completedTime"],
|
|
121
|
+
status_code=response["statusCode"],
|
|
122
|
+
status_message=response["statusMessage"],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _create_trx_object(self, response: dict) -> Transaction:
|
|
127
|
+
return Transaction(
|
|
128
|
+
# always-present fields → strict access
|
|
129
|
+
trx_id=response["trxID"],
|
|
130
|
+
initiation_time=response["initiationTime"],
|
|
131
|
+
completed_time=response["completedTime"],
|
|
132
|
+
transaction_type=response["transactionType"],
|
|
133
|
+
customer_msisdn=response["customerMsisdn"],
|
|
134
|
+
payer_account=response["payerAccount"],
|
|
135
|
+
transaction_status=response["transactionStatus"],
|
|
136
|
+
amount=response["amount"],
|
|
137
|
+
currency=response["currency"],
|
|
138
|
+
organization_short_code=response["organizationShortCode"],
|
|
139
|
+
status_code=response["statusCode"],
|
|
140
|
+
status_message=response["statusMessage"],
|
|
141
|
+
|
|
142
|
+
# optional fields → safe access
|
|
143
|
+
service_fee=response.get("serviceFee"),
|
|
144
|
+
payer_type=response.get("payerType"),
|
|
145
|
+
credited_amount=response.get("creditedAmount"),
|
|
146
|
+
max_refundable_amount=response.get("maxRefundableAmount"),
|
|
147
|
+
original_trx_amount=response.get("originalTrxAmount"),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Client(BaseClient):
|
|
152
|
+
def __init__(self, token: Token) -> None:
|
|
153
|
+
if not isinstance(token, Token):
|
|
154
|
+
raise TypeError(
|
|
155
|
+
f"Client requires a Token instance, got {type(token).__name__} instead. "
|
|
156
|
+
f"Use AsyncToken with the asynchronous AsyncClient class."
|
|
157
|
+
)
|
|
158
|
+
self.token = token
|
|
159
|
+
self._client = SyncClient(base_url=token.base_url)
|
|
160
|
+
|
|
161
|
+
def close(self) -> None:
|
|
162
|
+
"""Closes the HTTP client connection.
|
|
163
|
+
|
|
164
|
+
Should be called when done using the client to clean up resources.
|
|
165
|
+
"""
|
|
166
|
+
self._client.close()
|
|
167
|
+
|
|
168
|
+
def create_agreement(self, callback_url: str, payer_reference: str) -> AgreementCreation:
|
|
169
|
+
"""Creates a new bKash agreement for tokenized payments.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
callback_url: URL where bKash redirects after user authentication
|
|
173
|
+
payer_reference: Unique reference for the payer
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
AgreementCreation: Agreement creation response with payment_id and bkash_url
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
APIError: If agreement creation fails
|
|
180
|
+
"""
|
|
181
|
+
data = {
|
|
182
|
+
"mode": "0000",
|
|
183
|
+
"callbackURL": callback_url,
|
|
184
|
+
"payerReference": payer_reference,
|
|
185
|
+
}
|
|
186
|
+
response = self._client.post(url="/tokenized/checkout/create",headers=self.token.get_headers(), json=data)
|
|
187
|
+
response.raise_for_status()
|
|
188
|
+
response_json = response.json()
|
|
189
|
+
raise_api_exception(response_json)
|
|
190
|
+
return self._create_agreement_object(response_json)
|
|
191
|
+
|
|
192
|
+
def create_payment(self, callback_url: str, payer_reference: str, amount: int, agreement_id: str | None = None, invoice_number: str | None = None, merchant_association_info: str | None = None) -> PaymentCreation:
|
|
193
|
+
"""Creates a new bKash payment.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
callback_url: URL where bKash redirects after user authentication
|
|
197
|
+
payer_reference: Unique reference for the payer
|
|
198
|
+
amount: Payment amount in BDT
|
|
199
|
+
agreement_id: Optional agreement ID for tokenized payment (enables PIN-only flow)
|
|
200
|
+
invoice_number: Optional merchant invoice number
|
|
201
|
+
merchant_association_info: Optional merchant association information
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
PaymentCreation: Payment creation response with payment_id and bkash_url
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
APIError: If payment creation fails
|
|
208
|
+
"""
|
|
209
|
+
data = {
|
|
210
|
+
"mode": "0011", # mode for url based payment without agreement
|
|
211
|
+
"payerReference": str(payer_reference),
|
|
212
|
+
"callbackURL": callback_url,
|
|
213
|
+
"amount": amount,
|
|
214
|
+
"currency": "BDT",
|
|
215
|
+
"intent": "sale",
|
|
216
|
+
"merchantInvoiceNumber": "0000"
|
|
217
|
+
}
|
|
218
|
+
if agreement_id:
|
|
219
|
+
data["mode"] = "0001" # change mode for agreement payment
|
|
220
|
+
data["agreementID"] = agreement_id
|
|
221
|
+
if invoice_number:
|
|
222
|
+
data["merchantInvoiceNumber"] = invoice_number
|
|
223
|
+
if merchant_association_info:
|
|
224
|
+
data["merchantAssociationInfo"] = merchant_association_info
|
|
225
|
+
response = self._client.post(url="/tokenized/checkout/create",headers=self.token.get_headers(), json=data)
|
|
226
|
+
response.raise_for_status()
|
|
227
|
+
response_json = response.json()
|
|
228
|
+
raise_api_exception(response_json)
|
|
229
|
+
return self._create_payment_object(response_json)
|
|
230
|
+
def _execute(self, payment_id: str) -> dict:
|
|
231
|
+
"""Executes Agreements and Payments via payment_id"""
|
|
232
|
+
data = {
|
|
233
|
+
"paymentID" : payment_id
|
|
234
|
+
}
|
|
235
|
+
response = self._client.post(url="/tokenized/checkout/execute",headers=self.token.get_headers(), json=data)
|
|
236
|
+
response.raise_for_status()
|
|
237
|
+
response_json = response.json()
|
|
238
|
+
raise_api_exception(response_json)
|
|
239
|
+
return response_json
|
|
240
|
+
def execute_payment(self, payment_id: str) -> PaymentExecution:
|
|
241
|
+
"""Executes a payment after user authentication.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
payment_id: The payment ID from create_payment()
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
PaymentExecution: Execution response with transaction details and status
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
APIError: If payment execution fails
|
|
251
|
+
"""
|
|
252
|
+
response_json: dict = self._execute(payment_id)
|
|
253
|
+
return self._create_payment_execution_object(response_json)
|
|
254
|
+
|
|
255
|
+
def execute_agreement(self, payment_id: str) -> AgreementExecution:
|
|
256
|
+
"""Executes an agreement after user authentication.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
payment_id: The payment ID from create_agreement()
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
AgreementExecution: Execution response with agreement_id and status
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
APIError: If agreement execution fails
|
|
266
|
+
"""
|
|
267
|
+
response: dict = self._execute(payment_id)
|
|
268
|
+
return self._create_agreement_execution_object(response)
|
|
269
|
+
|
|
270
|
+
def _query(self, payment_id: str) -> dict:
|
|
271
|
+
"""queries payments and agreements"""
|
|
272
|
+
data = {
|
|
273
|
+
"paymentID" : payment_id
|
|
274
|
+
}
|
|
275
|
+
response = self._client.post(url="/tokenized/checkout/payment/status",headers=self.token.get_headers(), json=data)
|
|
276
|
+
response.raise_for_status()
|
|
277
|
+
response_json = response.json()
|
|
278
|
+
raise_api_exception(response_json)
|
|
279
|
+
return response_json
|
|
280
|
+
|
|
281
|
+
def query_agreement(self, payment_id: str) -> AgreementQuery:
|
|
282
|
+
"""Queries the status and details of an agreement.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
payment_id: The payment ID from create_agreement()
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
AgreementQuery: Agreement details including status and agreement_id
|
|
289
|
+
|
|
290
|
+
Raises:
|
|
291
|
+
APIError: If query fails
|
|
292
|
+
"""
|
|
293
|
+
response_json: dict = self._query(payment_id)
|
|
294
|
+
return self._create_agreement_query_object(response_json)
|
|
295
|
+
|
|
296
|
+
def query_payment(self, payment_id: str) -> PaymentQuery:
|
|
297
|
+
"""Queries the status and details of a payment.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
payment_id: The payment ID from create_payment()
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
PaymentQuery: Payment details including status and transaction information
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
APIError: If query fails
|
|
307
|
+
"""
|
|
308
|
+
response: dict = self._query(payment_id)
|
|
309
|
+
return self._create_payment_query_object(response)
|
|
310
|
+
|
|
311
|
+
def execute_refund(self, payment_id: str, trx_id: str, refund_amount: int, sku: str | None = None, reason: str | None = None) -> RefundExecution:
|
|
312
|
+
"""Executes a refund for a completed payment.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
payment_id: The payment ID from the original payment
|
|
316
|
+
trx_id: The transaction ID from the original payment
|
|
317
|
+
refund_amount: Amount to refund in BDT
|
|
318
|
+
sku: Optional SKU/product identifier
|
|
319
|
+
reason: Optional reason for the refund
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
RefundExecution: Refund transaction details including refund_trx_id and status
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
APIError: If refund fails
|
|
326
|
+
"""
|
|
327
|
+
data = {
|
|
328
|
+
"paymentID": payment_id,
|
|
329
|
+
"trxID": trx_id,
|
|
330
|
+
"amount": str(refund_amount),
|
|
331
|
+
"sku": sku or "not_provided",
|
|
332
|
+
"reason": reason or "not_provided"
|
|
333
|
+
}
|
|
334
|
+
response = self._client.post(url="/tokenized/checkout/payment/refund", headers=self.token.get_headers(), json=data)
|
|
335
|
+
response.raise_for_status()
|
|
336
|
+
response_json = response.json()
|
|
337
|
+
raise_api_exception(response_json)
|
|
338
|
+
return self._create_refund_execution_object(response_json)
|
|
339
|
+
|
|
340
|
+
def search_trx(self, trx_id: str) -> Transaction:
|
|
341
|
+
"""Searches for a transaction by transaction ID.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
trx_id: The transaction ID to search for
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Transaction: Transaction details including amount, status, and timestamps
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
APIError: If transaction not found
|
|
351
|
+
"""
|
|
352
|
+
data = {
|
|
353
|
+
"trxID" : trx_id
|
|
354
|
+
}
|
|
355
|
+
response = self._client.post(url="/tokenized/checkout/general/searchTransaction",headers=self.token.get_headers(), json=data)
|
|
356
|
+
response.raise_for_status()
|
|
357
|
+
response_json = response.json()
|
|
358
|
+
raise_api_exception(response_json)
|
|
359
|
+
return self._create_trx_object(response_json)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class AsyncClient(BaseClient):
|
|
363
|
+
def __init__(self, token: AsyncToken) -> None:
|
|
364
|
+
if not isinstance(token, AsyncToken): # raise error if provided token is not async
|
|
365
|
+
raise TypeError(
|
|
366
|
+
f"AsyncClient requires an AsyncToken instance, got {type(token).__name__} instead. "
|
|
367
|
+
f"Use Token with the synchronous Client class."
|
|
368
|
+
)
|
|
369
|
+
self.token = token
|
|
370
|
+
self._client = HttpxAsyncClient(base_url=token.base_url)
|
|
371
|
+
|
|
372
|
+
async def aclose(self) -> None:
|
|
373
|
+
"""Closes the async HTTP client connection.
|
|
374
|
+
|
|
375
|
+
Should be called when done using the client to clean up resources.
|
|
376
|
+
"""
|
|
377
|
+
await self._client.aclose()
|
|
378
|
+
|
|
379
|
+
async def create_agreement(self, callback_url: str, payer_reference: str) -> AgreementCreation:
|
|
380
|
+
"""Creates a new bKash agreement for tokenized payments.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
callback_url: URL where bKash redirects after user authentication
|
|
384
|
+
payer_reference: Unique reference for the payer
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
AgreementCreation: Agreement creation response with payment_id and bkash_url
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
APIError: If agreement creation fails
|
|
391
|
+
"""
|
|
392
|
+
data = {
|
|
393
|
+
"mode": "0000",
|
|
394
|
+
"callbackURL": callback_url,
|
|
395
|
+
"payerReference": payer_reference,
|
|
396
|
+
}
|
|
397
|
+
response = await self._client.post(url="/tokenized/checkout/create",headers= await self.token.get_headers(), json=data)
|
|
398
|
+
response.raise_for_status()
|
|
399
|
+
response_json = response.json()
|
|
400
|
+
raise_api_exception(response_json)
|
|
401
|
+
return self._create_agreement_object(response_json)
|
|
402
|
+
|
|
403
|
+
async def create_payment(self, callback_url: str, payer_reference: str, amount: int, agreement_id: str | None = None, invoice_number: str | None = None, merchant_association_info: str | None = None) -> PaymentCreation:
|
|
404
|
+
"""Creates a new bKash payment.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
callback_url: URL where bKash redirects after user authentication
|
|
408
|
+
payer_reference: Unique reference for the payer
|
|
409
|
+
amount: Payment amount in BDT
|
|
410
|
+
agreement_id: Optional agreement ID for tokenized payment (enables PIN-only flow)
|
|
411
|
+
invoice_number: Optional merchant invoice number
|
|
412
|
+
merchant_association_info: Optional merchant association information
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
PaymentCreation: Payment creation response with payment_id and bkash_url
|
|
416
|
+
|
|
417
|
+
Raises:
|
|
418
|
+
APIError: If payment creation fails
|
|
419
|
+
"""
|
|
420
|
+
data = {
|
|
421
|
+
"mode": "0011", # mode for url based payment without agreement
|
|
422
|
+
"payerReference": str(payer_reference),
|
|
423
|
+
"callbackURL": callback_url,
|
|
424
|
+
"amount": amount,
|
|
425
|
+
"currency": "BDT",
|
|
426
|
+
"intent": "sale",
|
|
427
|
+
"merchantInvoiceNumber": "0000"
|
|
428
|
+
}
|
|
429
|
+
if agreement_id:
|
|
430
|
+
data["mode"] = "0001" # change mode for agreement payment
|
|
431
|
+
data["agreementID"] = agreement_id
|
|
432
|
+
if invoice_number:
|
|
433
|
+
data["merchantInvoiceNumber"] = invoice_number
|
|
434
|
+
if merchant_association_info:
|
|
435
|
+
data["merchantAssociationInfo"] = merchant_association_info
|
|
436
|
+
response = await self._client.post(url="/tokenized/checkout/create",headers=await self.token.get_headers(), json=data)
|
|
437
|
+
response.raise_for_status()
|
|
438
|
+
response_json = response.json()
|
|
439
|
+
raise_api_exception(response_json)
|
|
440
|
+
return self._create_payment_object(response_json)
|
|
441
|
+
async def _execute(self, payment_id: str) -> dict:
|
|
442
|
+
"""Executes Agreements and Payments via payment_id"""
|
|
443
|
+
data = {
|
|
444
|
+
"paymentID" : payment_id
|
|
445
|
+
}
|
|
446
|
+
response = await self._client.post(url="/tokenized/checkout/execute",headers= await self.token.get_headers(), json=data)
|
|
447
|
+
response.raise_for_status()
|
|
448
|
+
response_json = response.json()
|
|
449
|
+
raise_api_exception(response_json)
|
|
450
|
+
return response_json
|
|
451
|
+
async def execute_payment(self, payment_id: str) -> PaymentExecution:
|
|
452
|
+
"""Executes a payment after user authentication.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
payment_id: The payment ID from create_payment()
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
PaymentExecution: Execution response with transaction details and status
|
|
459
|
+
|
|
460
|
+
Raises:
|
|
461
|
+
APIError: If payment execution fails
|
|
462
|
+
"""
|
|
463
|
+
response_json: dict = await self._execute(payment_id)
|
|
464
|
+
return self._create_payment_execution_object(response_json)
|
|
465
|
+
|
|
466
|
+
async def execute_agreement(self, payment_id: str) -> AgreementExecution:
|
|
467
|
+
"""Executes an agreement after user authentication.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
payment_id: The payment ID from create_agreement()
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
AgreementExecution: Execution response with agreement_id and status
|
|
474
|
+
|
|
475
|
+
Raises:
|
|
476
|
+
APIError: If agreement execution fails
|
|
477
|
+
"""
|
|
478
|
+
response: dict = await self._execute(payment_id)
|
|
479
|
+
return self._create_agreement_execution_object(response)
|
|
480
|
+
|
|
481
|
+
async def _query(self, payment_id: str) -> dict:
|
|
482
|
+
"""queries payments and agreements"""
|
|
483
|
+
data = {
|
|
484
|
+
"paymentID" : payment_id
|
|
485
|
+
}
|
|
486
|
+
response = await self._client.post(url="/tokenized/checkout/payment/status",headers=await self.token.get_headers(), json=data)
|
|
487
|
+
response.raise_for_status()
|
|
488
|
+
response_json = response.json()
|
|
489
|
+
raise_api_exception(response_json)
|
|
490
|
+
return response_json
|
|
491
|
+
|
|
492
|
+
async def query_agreement(self, payment_id: str) -> AgreementQuery:
|
|
493
|
+
"""Queries the status and details of an agreement.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
payment_id: The payment ID from create_agreement()
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
AgreementQuery: Agreement details including status and agreement_id
|
|
500
|
+
|
|
501
|
+
Raises:
|
|
502
|
+
APIError: If query fails
|
|
503
|
+
"""
|
|
504
|
+
response_json: dict = await self._query(payment_id)
|
|
505
|
+
return self._create_agreement_query_object(response_json)
|
|
506
|
+
|
|
507
|
+
async def query_payment(self, payment_id: str) -> PaymentQuery:
|
|
508
|
+
"""Queries the status and details of a payment.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
payment_id: The payment ID from create_payment()
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
PaymentQuery: Payment details including status and transaction information
|
|
515
|
+
|
|
516
|
+
Raises:
|
|
517
|
+
APIError: If query fails
|
|
518
|
+
"""
|
|
519
|
+
response: dict = await self._query(payment_id)
|
|
520
|
+
return self._create_payment_query_object(response)
|
|
521
|
+
|
|
522
|
+
async def execute_refund(self, payment_id: str, trx_id: str, refund_amount: int, sku: str | None = None, reason: str | None = None) -> RefundExecution:
|
|
523
|
+
"""Executes a refund for a completed payment.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
payment_id: The payment ID from the original payment
|
|
527
|
+
trx_id: The transaction ID from the original payment
|
|
528
|
+
refund_amount: Amount to refund in BDT
|
|
529
|
+
sku: Optional SKU/product identifier
|
|
530
|
+
reason: Optional reason for the refund
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Refund: Refund transaction details including refund_trx_id and status
|
|
534
|
+
|
|
535
|
+
Raises:
|
|
536
|
+
APIError: If refund fails
|
|
537
|
+
"""
|
|
538
|
+
data = {
|
|
539
|
+
"paymentID": payment_id,
|
|
540
|
+
"trxID": trx_id,
|
|
541
|
+
"amount": str(refund_amount),
|
|
542
|
+
"sku": sku or "not_provided",
|
|
543
|
+
"reason": reason or "not_provided"
|
|
544
|
+
}
|
|
545
|
+
response = await self._client.post(url="/tokenized/checkout/payment/refund", headers=await self.token.get_headers(), json=data)
|
|
546
|
+
response.raise_for_status()
|
|
547
|
+
response_json = response.json()
|
|
548
|
+
raise_api_exception(response_json)
|
|
549
|
+
return self._create_refund_execution_object(response_json)
|
|
550
|
+
|
|
551
|
+
async def search_trx(self, trx_id: str) -> Transaction:
|
|
552
|
+
"""Searches for a transaction by transaction ID.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
trx_id: The transaction ID to search for
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
Transaction: Transaction details including amount, status, and timestamps
|
|
559
|
+
|
|
560
|
+
Raises:
|
|
561
|
+
APIError: If transaction not found
|
|
562
|
+
"""
|
|
563
|
+
data = {
|
|
564
|
+
"trxID" : trx_id
|
|
565
|
+
}
|
|
566
|
+
response = await self._client.post(url="/tokenized/checkout/general/searchTransaction",headers=await self.token.get_headers(), json=data)
|
|
567
|
+
response.raise_for_status()
|
|
568
|
+
response_json = response.json()
|
|
569
|
+
raise_api_exception(response_json)
|
|
570
|
+
return self._create_trx_object(response_json)
|
pybkash/models.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
class CreationBase:
|
|
2
|
+
def __init__(
|
|
3
|
+
self,
|
|
4
|
+
status_code: str | int,
|
|
5
|
+
status_message: str,
|
|
6
|
+
payment_id: str,
|
|
7
|
+
bkash_url: str,
|
|
8
|
+
callback_url: str,
|
|
9
|
+
success_callback: str,
|
|
10
|
+
failure_callback: str,
|
|
11
|
+
cancel_callback: str,
|
|
12
|
+
) -> None:
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.status_message = status_message
|
|
15
|
+
self.payment_id = payment_id
|
|
16
|
+
self.bkash_url = bkash_url
|
|
17
|
+
self.callback_url = callback_url
|
|
18
|
+
self.success_callback = success_callback
|
|
19
|
+
self.failure_callback = failure_callback
|
|
20
|
+
self.cancel_callback = cancel_callback
|
|
21
|
+
|
|
22
|
+
class StatusMixin:
|
|
23
|
+
status: str
|
|
24
|
+
|
|
25
|
+
def is_complete(self) -> bool:
|
|
26
|
+
return self.status.upper() == "COMPLETED"
|
|
27
|
+
|
|
28
|
+
class ExecutionBase(StatusMixin):
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
status_code: str,
|
|
32
|
+
status_message: str,
|
|
33
|
+
payment_id: str,
|
|
34
|
+
agreement_id: str | None,
|
|
35
|
+
payer_reference: str,
|
|
36
|
+
customer_msisdn: str,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.status_code = status_code
|
|
39
|
+
self.status_message = status_message
|
|
40
|
+
self.payment_id = payment_id
|
|
41
|
+
self.agreement_id = agreement_id
|
|
42
|
+
self.payer_reference = payer_reference
|
|
43
|
+
self.customer_msisdn = customer_msisdn
|
|
44
|
+
|
|
45
|
+
class QueryBase(StatusMixin):
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
status_code: str,
|
|
49
|
+
status_message: str,
|
|
50
|
+
payment_id: str,
|
|
51
|
+
payer_reference: str,
|
|
52
|
+
agreement_id: str | None = None,
|
|
53
|
+
agreement_status: str | None = None,
|
|
54
|
+
agreement_create_time: str | None = None,
|
|
55
|
+
agreement_execute_time: str | None = None,
|
|
56
|
+
verification_status: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self.status_code = status_code
|
|
59
|
+
self.status_message = status_message
|
|
60
|
+
self.payment_id = payment_id
|
|
61
|
+
self.payer_reference = payer_reference
|
|
62
|
+
self.agreement_id = agreement_id
|
|
63
|
+
self.agreement_status = agreement_status
|
|
64
|
+
self.agreement_create_time = agreement_create_time
|
|
65
|
+
self.agreement_execute_time = agreement_execute_time
|
|
66
|
+
self.verification_status = verification_status
|
|
67
|
+
|
|
68
|
+
class PaymentCreation(CreationBase):
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
status_code: int,
|
|
72
|
+
status_message: str,
|
|
73
|
+
payment_id: str,
|
|
74
|
+
bkash_url: str,
|
|
75
|
+
callback_url: str,
|
|
76
|
+
success_callback: str,
|
|
77
|
+
failure_callback: str,
|
|
78
|
+
cancel_callback: str,
|
|
79
|
+
) -> None:
|
|
80
|
+
super().__init__(
|
|
81
|
+
status_code=status_code,
|
|
82
|
+
status_message=status_message,
|
|
83
|
+
payment_id=payment_id,
|
|
84
|
+
bkash_url=bkash_url,
|
|
85
|
+
callback_url=callback_url,
|
|
86
|
+
success_callback=success_callback,
|
|
87
|
+
failure_callback=failure_callback,
|
|
88
|
+
cancel_callback=cancel_callback,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
class AgreementCreation(CreationBase):
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
status_code: str,
|
|
95
|
+
status_message: str,
|
|
96
|
+
payment_id: str,
|
|
97
|
+
bkash_url: str,
|
|
98
|
+
callback_url: str,
|
|
99
|
+
success_callback: str,
|
|
100
|
+
failure_callback: str,
|
|
101
|
+
cancel_callback: str,
|
|
102
|
+
payer_reference: str,
|
|
103
|
+
agreement_status: str,
|
|
104
|
+
agreement_create_time: str,
|
|
105
|
+
) -> None:
|
|
106
|
+
super().__init__(
|
|
107
|
+
status_code=status_code,
|
|
108
|
+
status_message=status_message,
|
|
109
|
+
payment_id=payment_id,
|
|
110
|
+
bkash_url=bkash_url,
|
|
111
|
+
callback_url=callback_url,
|
|
112
|
+
success_callback=success_callback,
|
|
113
|
+
failure_callback=failure_callback,
|
|
114
|
+
cancel_callback=cancel_callback,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self.payer_reference = payer_reference
|
|
118
|
+
self.agreement_status = agreement_status
|
|
119
|
+
self.agreement_create_time = agreement_create_time
|
|
120
|
+
|
|
121
|
+
class RefundExecution(StatusMixin):
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
original_trx_id: str,
|
|
125
|
+
refund_trx_id: str,
|
|
126
|
+
transaction_status: str,
|
|
127
|
+
amount: str,
|
|
128
|
+
currency: str,
|
|
129
|
+
completed_time: str,
|
|
130
|
+
status_code: str,
|
|
131
|
+
status_message: str,
|
|
132
|
+
) -> None:
|
|
133
|
+
self.status = transaction_status # universal status
|
|
134
|
+
self.trx_id = refund_trx_id # universal trx_id
|
|
135
|
+
self.original_trx_id = original_trx_id
|
|
136
|
+
self.refund_trx_id = refund_trx_id
|
|
137
|
+
self.transaction_status = transaction_status
|
|
138
|
+
self.amount = amount
|
|
139
|
+
self.currency = currency
|
|
140
|
+
self.completed_time = completed_time
|
|
141
|
+
self.status_code = status_code
|
|
142
|
+
self.status_message = status_message
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class AgreementExecution(ExecutionBase):
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
status_code: str,
|
|
149
|
+
status_message: str,
|
|
150
|
+
payment_id: str,
|
|
151
|
+
agreement_id: str,
|
|
152
|
+
payer_reference: str,
|
|
153
|
+
customer_msisdn: str,
|
|
154
|
+
agreement_execute_time: str,
|
|
155
|
+
agreement_status: str,
|
|
156
|
+
) -> None:
|
|
157
|
+
super().__init__(
|
|
158
|
+
status_code=status_code,
|
|
159
|
+
status_message=status_message,
|
|
160
|
+
payment_id=payment_id,
|
|
161
|
+
agreement_id=agreement_id,
|
|
162
|
+
payer_reference=payer_reference,
|
|
163
|
+
customer_msisdn=customer_msisdn,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self.status = agreement_status # universal status
|
|
167
|
+
self.agreement_status = agreement_status
|
|
168
|
+
self.agreement_execute_time = agreement_execute_time
|
|
169
|
+
|
|
170
|
+
class PaymentExecution(ExecutionBase):
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
status_code: str,
|
|
174
|
+
status_message: str,
|
|
175
|
+
payment_id: str,
|
|
176
|
+
agreement_id: str | None,
|
|
177
|
+
payer_reference: str,
|
|
178
|
+
customer_msisdn: str,
|
|
179
|
+
trx_id: str,
|
|
180
|
+
amount: str,
|
|
181
|
+
transaction_status: str,
|
|
182
|
+
payment_execute_time: str,
|
|
183
|
+
currency: str,
|
|
184
|
+
intent: str,
|
|
185
|
+
merchant_invoice_number: str,
|
|
186
|
+
) -> None:
|
|
187
|
+
super().__init__(
|
|
188
|
+
status_code=status_code,
|
|
189
|
+
status_message=status_message,
|
|
190
|
+
payment_id=payment_id,
|
|
191
|
+
agreement_id=agreement_id,
|
|
192
|
+
payer_reference=payer_reference,
|
|
193
|
+
customer_msisdn=customer_msisdn,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
self.status = transaction_status # universal status
|
|
197
|
+
self.trx_id = trx_id
|
|
198
|
+
self.transaction_status = transaction_status
|
|
199
|
+
self.amount = amount
|
|
200
|
+
self.payment_execute_time = payment_execute_time
|
|
201
|
+
self.currency = currency
|
|
202
|
+
self.intent = intent
|
|
203
|
+
self.merchant_invoice_number = merchant_invoice_number
|
|
204
|
+
|
|
205
|
+
class AgreementQuery(QueryBase):
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
status_code: str,
|
|
209
|
+
status_message: str,
|
|
210
|
+
payment_id: str,
|
|
211
|
+
agreement_id: str,
|
|
212
|
+
payer_reference: str,
|
|
213
|
+
payer_account: str,
|
|
214
|
+
payer_type: str,
|
|
215
|
+
agreement_status: str,
|
|
216
|
+
agreement_create_time: str,
|
|
217
|
+
agreement_execute_time: str,
|
|
218
|
+
mode: str,
|
|
219
|
+
verification_status: str,
|
|
220
|
+
) -> None:
|
|
221
|
+
super().__init__(
|
|
222
|
+
status_code=status_code,
|
|
223
|
+
status_message=status_message,
|
|
224
|
+
payment_id=payment_id,
|
|
225
|
+
payer_reference=payer_reference,
|
|
226
|
+
agreement_id=agreement_id,
|
|
227
|
+
agreement_status=agreement_status,
|
|
228
|
+
agreement_create_time=agreement_create_time,
|
|
229
|
+
agreement_execute_time=agreement_execute_time,
|
|
230
|
+
verification_status=verification_status,
|
|
231
|
+
)
|
|
232
|
+
self.status = agreement_status
|
|
233
|
+
self.payer_account = payer_account
|
|
234
|
+
self.payer_type = payer_type
|
|
235
|
+
self.mode = mode
|
|
236
|
+
|
|
237
|
+
class PaymentQuery(QueryBase):
|
|
238
|
+
def __init__(
|
|
239
|
+
self,
|
|
240
|
+
status_code: str,
|
|
241
|
+
status_message: str,
|
|
242
|
+
payment_id: str,
|
|
243
|
+
payer_reference: str,
|
|
244
|
+
mode: str,
|
|
245
|
+
payment_create_time: str,
|
|
246
|
+
amount: str,
|
|
247
|
+
currency: str,
|
|
248
|
+
intent: str,
|
|
249
|
+
merchant_invoice: str,
|
|
250
|
+
transaction_status: str,
|
|
251
|
+
verification_status: str,
|
|
252
|
+
agreement_id: str | None,
|
|
253
|
+
agreement_status: str | None,
|
|
254
|
+
agreement_create_time: str | None,
|
|
255
|
+
agreement_execute_time: str | None,
|
|
256
|
+
) -> None:
|
|
257
|
+
super().__init__(
|
|
258
|
+
status_code=status_code,
|
|
259
|
+
status_message=status_message,
|
|
260
|
+
payment_id=payment_id,
|
|
261
|
+
payer_reference=payer_reference,
|
|
262
|
+
agreement_id=agreement_id,
|
|
263
|
+
agreement_status=agreement_status,
|
|
264
|
+
agreement_create_time=agreement_create_time,
|
|
265
|
+
agreement_execute_time=agreement_execute_time,
|
|
266
|
+
verification_status=verification_status,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
self.status = transaction_status
|
|
270
|
+
self.transaction_status = transaction_status
|
|
271
|
+
self.mode = mode
|
|
272
|
+
self.payment_create_time = payment_create_time
|
|
273
|
+
self.amount = amount
|
|
274
|
+
self.currency = currency
|
|
275
|
+
self.intent = intent
|
|
276
|
+
self.merchant_invoice = merchant_invoice
|
|
277
|
+
|
|
278
|
+
class Transaction(StatusMixin):
|
|
279
|
+
def __init__(
|
|
280
|
+
self,
|
|
281
|
+
trx_id: str,
|
|
282
|
+
initiation_time: str,
|
|
283
|
+
completed_time: str,
|
|
284
|
+
transaction_type: str,
|
|
285
|
+
customer_msisdn: str,
|
|
286
|
+
payer_account: str,
|
|
287
|
+
transaction_status: str,
|
|
288
|
+
amount: str,
|
|
289
|
+
currency: str,
|
|
290
|
+
organization_short_code: str,
|
|
291
|
+
status_code: str,
|
|
292
|
+
status_message: str,
|
|
293
|
+
|
|
294
|
+
# following 4 not present in refund transactions
|
|
295
|
+
service_fee: str | None = None,
|
|
296
|
+
payer_type: str | None = None,
|
|
297
|
+
credited_amount: str | None = None,
|
|
298
|
+
max_refundable_amount: str | None = None,
|
|
299
|
+
|
|
300
|
+
# this key is only sent for the refund transcations
|
|
301
|
+
original_trx_amount: str | None = None,
|
|
302
|
+
) -> None:
|
|
303
|
+
self.status = transaction_status
|
|
304
|
+
self.trx_id = trx_id
|
|
305
|
+
self.initiation_time = initiation_time
|
|
306
|
+
self.completed_time = completed_time
|
|
307
|
+
self.transaction_type = transaction_type
|
|
308
|
+
self.customer_msisdn = customer_msisdn
|
|
309
|
+
self.payer_account = payer_account
|
|
310
|
+
self.transaction_status = transaction_status
|
|
311
|
+
self.amount = amount
|
|
312
|
+
self.currency = currency
|
|
313
|
+
self.organization_short_code = organization_short_code
|
|
314
|
+
self.status_code = status_code
|
|
315
|
+
self.status_message = status_message
|
|
316
|
+
|
|
317
|
+
self.service_fee = service_fee
|
|
318
|
+
self.payer_type = payer_type
|
|
319
|
+
self.credited_amount = credited_amount
|
|
320
|
+
self.max_refundable_amount = max_refundable_amount
|
|
321
|
+
|
|
322
|
+
self.original_trx_amount = original_trx_amount
|
|
323
|
+
|
|
324
|
+
|
pybkash/token_manager.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from httpx import post, AsyncClient as HttpxAsyncClient
|
|
2
|
+
from time import time
|
|
3
|
+
from .exception_handlers import raise_api_exception
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseToken:
|
|
7
|
+
"""Base class for bKash token management with shared logic."""
|
|
8
|
+
def __init__(self, username: str, password: str, app_key: str, app_secret: str, sandbox=False) -> None:
|
|
9
|
+
self.base_url = "https://tokenized.pay.bka.sh/v1.2.0-beta"
|
|
10
|
+
if sandbox:
|
|
11
|
+
self.base_url = "https://tokenized.sandbox.bka.sh/v1.2.0-beta"
|
|
12
|
+
|
|
13
|
+
self.username = username
|
|
14
|
+
self.app_key = app_key
|
|
15
|
+
self.timestamp = 0
|
|
16
|
+
self.expires_in = 0
|
|
17
|
+
self.id_token = None
|
|
18
|
+
|
|
19
|
+
self.headers = {
|
|
20
|
+
"username": username,
|
|
21
|
+
"password": password
|
|
22
|
+
}
|
|
23
|
+
self.data = {
|
|
24
|
+
"app_key": app_key,
|
|
25
|
+
"app_secret": app_secret
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def _load_token(self, token: dict) -> None:
|
|
29
|
+
"""Load token data into instance variables."""
|
|
30
|
+
self.id_token = token.get("id_token")
|
|
31
|
+
self.timestamp = token.get("timestamp")
|
|
32
|
+
self.expires_in = token.get("expires_in")
|
|
33
|
+
|
|
34
|
+
def _is_token_valid(self) -> bool:
|
|
35
|
+
"""Check if current token is valid."""
|
|
36
|
+
return self.id_token and not (time() - self.timestamp > self.expires_in)
|
|
37
|
+
|
|
38
|
+
def _process_token_response(self, token_obj: dict) -> dict:
|
|
39
|
+
"""Process raw token response from API."""
|
|
40
|
+
raise_api_exception(token_obj)
|
|
41
|
+
token_obj["expires_in"] -= 10 # keeping a 10 seconds overhead
|
|
42
|
+
token_obj["timestamp"] = time() # adding a key to keep track of when it was fetched
|
|
43
|
+
return token_obj
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Token(BaseToken):
|
|
47
|
+
"""Synchronous bKash token manager."""
|
|
48
|
+
|
|
49
|
+
def _fetch_from_api(self) -> dict:
|
|
50
|
+
"""Fetch a new token from the bKash API."""
|
|
51
|
+
response = post(
|
|
52
|
+
url=f"{self.base_url}/tokenized/checkout/token/grant",
|
|
53
|
+
headers=self.headers,
|
|
54
|
+
json=self.data
|
|
55
|
+
)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
token_obj = response.json()
|
|
58
|
+
return self._process_token_response(token_obj)
|
|
59
|
+
|
|
60
|
+
def get_token_id(self) -> str:
|
|
61
|
+
"""Gets a valid bKash API token ID.
|
|
62
|
+
|
|
63
|
+
Returns a cached token if available, otherwise fetches a new one.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
str: Valid bKash API token ID
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
APIError: If token fetch fails
|
|
70
|
+
"""
|
|
71
|
+
if self._is_token_valid():
|
|
72
|
+
return self.id_token
|
|
73
|
+
|
|
74
|
+
token = self._fetch_from_api()
|
|
75
|
+
self._load_token(token)
|
|
76
|
+
return self.id_token
|
|
77
|
+
|
|
78
|
+
def get_new_token_id(self) -> str:
|
|
79
|
+
"""Forces a fresh token fetch from the bKash API.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
str: New bKash API token ID
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
APIError: If token fetch fails
|
|
86
|
+
"""
|
|
87
|
+
token = self._fetch_from_api()
|
|
88
|
+
self._load_token(token)
|
|
89
|
+
return self.id_token
|
|
90
|
+
|
|
91
|
+
def get_headers(self) -> dict:
|
|
92
|
+
"""Returns authorization headers for bKash API requests.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
dict: Headers with authorization token and X-APP-Key
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
APIError: If token retrieval fails
|
|
99
|
+
"""
|
|
100
|
+
return {
|
|
101
|
+
"authorization": str(self.get_token_id()),
|
|
102
|
+
"X-APP-Key": self.app_key
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class AsyncToken(BaseToken):
|
|
107
|
+
"""Asynchronous bKash token manager."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, username: str, password: str, app_key: str, app_secret: str, sandbox=False) -> None:
|
|
110
|
+
super().__init__(username, password, app_key, app_secret, sandbox)
|
|
111
|
+
self._client = HttpxAsyncClient(base_url=self.base_url)
|
|
112
|
+
|
|
113
|
+
async def aclose(self) -> None:
|
|
114
|
+
"""Closes the async HTTP client connection.
|
|
115
|
+
|
|
116
|
+
Should be called when done using the token manager to clean up resources.
|
|
117
|
+
"""
|
|
118
|
+
await self._client.aclose()
|
|
119
|
+
|
|
120
|
+
async def _fetch_from_api(self) -> dict:
|
|
121
|
+
"""Fetch a new token from the bKash API."""
|
|
122
|
+
response = await self._client.post(
|
|
123
|
+
url="/tokenized/checkout/token/grant",
|
|
124
|
+
headers=self.headers,
|
|
125
|
+
json=self.data
|
|
126
|
+
)
|
|
127
|
+
response.raise_for_status()
|
|
128
|
+
token_obj = response.json()
|
|
129
|
+
return self._process_token_response(token_obj)
|
|
130
|
+
|
|
131
|
+
async def get_token_id(self) -> str:
|
|
132
|
+
"""Gets a valid bKash API token ID.
|
|
133
|
+
|
|
134
|
+
Returns a cached token if available, otherwise fetches a new one.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
str: Valid bKash API token ID
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
APIError: If token fetch fails
|
|
141
|
+
"""
|
|
142
|
+
if self._is_token_valid():
|
|
143
|
+
return self.id_token
|
|
144
|
+
|
|
145
|
+
token = await self._fetch_from_api()
|
|
146
|
+
self._load_token(token)
|
|
147
|
+
return self.id_token
|
|
148
|
+
|
|
149
|
+
async def get_new_token_id(self) -> str:
|
|
150
|
+
"""Forces a fresh token fetch from the bKash API.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
str: New bKash API token ID
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
APIError: If token fetch fails
|
|
157
|
+
"""
|
|
158
|
+
token = await self._fetch_from_api()
|
|
159
|
+
self._load_token(token)
|
|
160
|
+
return self.id_token
|
|
161
|
+
|
|
162
|
+
async def get_headers(self) -> dict:
|
|
163
|
+
"""Returns authorization headers for bKash API requests.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
dict: Headers with authorization token and X-APP-Key
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
APIError: If token retrieval fails
|
|
170
|
+
"""
|
|
171
|
+
return {
|
|
172
|
+
"authorization": str(await self.get_token_id()),
|
|
173
|
+
"X-APP-Key": self.app_key
|
|
174
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pybkash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Pythonic client for the bKash payment gateway. Supports both synchronous and asynchronous usage and covers the entire API surface.
|
|
5
|
+
Keywords: bkash,payments,fintech,api,bangladesh
|
|
6
|
+
Author: Monazir Muhammad Doha
|
|
7
|
+
Author-email: Monazir Muhammad Doha <mmdoha@houndsec.net>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Project-URL: Homepage, https://github.com/itsmmdoha/pybkash
|
|
15
|
+
Project-URL: Issues, https://github.com/itsmmdoha/pybkash/issues
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# pybkash
|
|
19
|
+
|
|
20
|
+
A Pythonic client for the bKash payment gateway. Supports both synchronous and asynchronous usage and covers the entire API surface.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
1. Normal Payment
|
|
25
|
+
2. Agreement Creation
|
|
26
|
+
3. Agreement Payment
|
|
27
|
+
4. Refund
|
|
28
|
+
5. Search Transaction
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install pybkash
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Synchronous Client
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pybkash import Client, Token
|
|
42
|
+
|
|
43
|
+
# First, initialize the token. This will be passed to the Client.
|
|
44
|
+
token = Token(
|
|
45
|
+
username="your_username",
|
|
46
|
+
password="your_password",
|
|
47
|
+
app_key="your_app_key",
|
|
48
|
+
app_secret="your_app_secret",
|
|
49
|
+
sandbox=True # Use False for production
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then, create a client object by passing the token:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
client = Client(token)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
You can now use this client to perform various operations.
|
|
60
|
+
|
|
61
|
+
#### Step 1: Create Payment
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
payment = client.create_payment(
|
|
65
|
+
callback_url="https://yoursite.com/callback",
|
|
66
|
+
payer_reference="CUSTOMER001", # Passing a phone or bKash number pre-populates the wallet number field on the bKash checkout page.
|
|
67
|
+
amount=1000 # Amount in BDT
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This returns a `PaymentCreation` object:
|
|
72
|
+
|
|
73
|
+
* `payment.payment_id` is the ID bKash uses to identify individual payments.
|
|
74
|
+
* `payment.bkash_url` is the payment URL — send the user to this page.
|
|
75
|
+
|
|
76
|
+
After the user has completed the payment:
|
|
77
|
+
|
|
78
|
+
#### Step 2: Execute Payment
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
execution = client.execute_payment(payment.payment_id)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This returns a `PaymentExecution` object.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# Verify completion
|
|
88
|
+
if execution.is_complete():
|
|
89
|
+
print(f"Payment successful! TrxID: {execution.trx_id}")
|
|
90
|
+
else:
|
|
91
|
+
print(f"Payment status: {execution.status}")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Documentation
|
|
95
|
+
|
|
96
|
+
For detailed synchronous and asynchronous usage information, including return types and attributes, see [docs](https://github.com/Itsmmdoha/pybkash/tree/main/docs).
|
|
97
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pybkash/__init__.py,sha256=4Dyl98EJ6WeWPyAZCmetvPDNHA1vFitUUhSjWScZ8sc,189
|
|
2
|
+
pybkash/exception_handlers.py,sha256=vc1s7wV4Hg6V8YUvwEb5pcvayIHAOuayYdR_fQa29K4,365
|
|
3
|
+
pybkash/exceptions.py,sha256=pR5M0ZrArgjl2ZlKbVrA--VOXt1xeDPj9PkmPje9oxs,272
|
|
4
|
+
pybkash/main.py,sha256=DhcM1LmCH6iv0LXNIScCFE-alYRQWSkR0ni0QG7shDI,23491
|
|
5
|
+
pybkash/models.py,sha256=HCbTCHl4l8rJlZTZn4uFIUtsUCHMqEdYiKEZBhIUM68,10254
|
|
6
|
+
pybkash/token_manager.py,sha256=dcBkgsYBnmbQooVvLf1DbsmRN-LjgszcLTmO44DKdR0,5552
|
|
7
|
+
pybkash-0.1.0.dist-info/licenses/LICENSE,sha256=0ESUNlYYDS9EIQOFAAxUgqCvfGebM0vBKwkjXpnGSxo,1078
|
|
8
|
+
pybkash-0.1.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
9
|
+
pybkash-0.1.0.dist-info/METADATA,sha256=H2rfXz68mR73HMJ6hnVdX5hIE5uuSKQ7eTNd_E349XE,2514
|
|
10
|
+
pybkash-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Monazir Muhammad Doha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|