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 ADDED
@@ -0,0 +1,6 @@
1
+ from .main import Client, AsyncClient
2
+ from .token_manager import Token, AsyncToken
3
+ from .exceptions import APIError
4
+
5
+ __all__ = ['Token', 'AsyncToken', 'Client', 'AsyncClient', 'APIError']
6
+
@@ -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
@@ -0,0 +1,8 @@
1
+ class APIError(Exception):
2
+ def __init__(self, status_code: str, message: str):
3
+ self.status_code = status_code
4
+ self.message = message
5
+ super().__init__(message)
6
+
7
+ def __str__(self) -> str:
8
+ return f"[{self.status_code}] {self.message}"
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
+
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.