bpay 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.
bpay/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from bpay.client import BPay
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["BPay"]
bpay/client.py ADDED
@@ -0,0 +1,157 @@
1
+ from typing import Any
2
+
3
+ from bpay.providers.bkash.callbacks import (
4
+ parse_agreement_callback,
5
+ )
6
+ from bpay.providers.bkash.payment_callbacks import (
7
+ parse_payment_callback,
8
+ )
9
+ from bpay.providers.registry import (
10
+ PROVIDERS,
11
+ )
12
+ from bpay.schemas.agreement import (
13
+ AgreementResponse,
14
+ CreateAgreementRequest,
15
+ )
16
+ from bpay.schemas.callback import (
17
+ AgreementCallback,
18
+ )
19
+ from bpay.schemas.payment import (
20
+ CreatePaymentRequest,
21
+ PaymentResponse,
22
+ )
23
+ from bpay.schemas.payment_callback import (
24
+ PaymentCallback,
25
+ )
26
+ from bpay.schemas.verification import (
27
+ PaymentVerificationResponse,
28
+ )
29
+
30
+
31
+ class BPay:
32
+ def __init__(
33
+ self,
34
+ provider: str,
35
+ **credentials: Any,
36
+ ) -> None:
37
+ provider_class = (
38
+ PROVIDERS.get(provider)
39
+ )
40
+
41
+ if provider_class is None:
42
+ supported = ", ".join(
43
+ PROVIDERS.keys()
44
+ )
45
+
46
+ raise ValueError(
47
+ f"Unsupported provider: "
48
+ f"{provider}. "
49
+ f"Supported providers: "
50
+ f"{supported}"
51
+ )
52
+
53
+ self.provider_name = (
54
+ provider
55
+ )
56
+
57
+ self.provider = (
58
+ provider_class(
59
+ **credentials
60
+ )
61
+ )
62
+
63
+ async def create_agreement(
64
+ self,
65
+ payload: CreateAgreementRequest,
66
+ ) -> AgreementResponse:
67
+ if not hasattr(
68
+ self.provider,
69
+ "create_agreement",
70
+ ):
71
+ raise NotImplementedError(
72
+ f"{self.provider_name} "
73
+ "does not support "
74
+ "agreement creation"
75
+ )
76
+
77
+ return await (
78
+ self.provider.create_agreement(
79
+ payload
80
+ )
81
+ )
82
+
83
+ async def create_payment(
84
+ self,
85
+ payload: CreatePaymentRequest,
86
+ ) -> PaymentResponse:
87
+ if not hasattr(
88
+ self.provider,
89
+ "create_payment",
90
+ ):
91
+ raise NotImplementedError(
92
+ f"{self.provider_name} "
93
+ "does not support "
94
+ "payment creation"
95
+ )
96
+
97
+ return await (
98
+ self.provider.create_payment(
99
+ payload
100
+ )
101
+ )
102
+
103
+ def parse_agreement_callback(
104
+ self,
105
+ params: dict[str, str],
106
+ ) -> AgreementCallback:
107
+ if (
108
+ self.provider_name
109
+ != "bkash"
110
+ ):
111
+ raise NotImplementedError(
112
+ f"{self.provider_name} "
113
+ "does not support "
114
+ "agreement callbacks yet"
115
+ )
116
+
117
+ return (
118
+ parse_agreement_callback(
119
+ params
120
+ )
121
+ )
122
+
123
+ def parse_payment_callback(
124
+ self,
125
+ params: dict[str, str],
126
+ ) -> PaymentCallback:
127
+ if self.provider_name != "bkash":
128
+ raise NotImplementedError(
129
+ f"{self.provider_name} "
130
+ "does not support "
131
+ "payment callbacks yet"
132
+ )
133
+
134
+ return parse_payment_callback(
135
+ params
136
+ )
137
+
138
+ async def verify_payment(
139
+ self,
140
+ payment_id: str,
141
+ ) -> PaymentVerificationResponse:
142
+ if not hasattr(
143
+ self.provider,
144
+ "verify_payment",
145
+ ):
146
+ raise NotImplementedError(
147
+ f"{self.provider_name} "
148
+ "does not support "
149
+ "payment verification"
150
+ )
151
+
152
+ return await (
153
+ self.provider.verify_payment(
154
+ payment_id
155
+ )
156
+ )
157
+
File without changes
@@ -0,0 +1,6 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class Environment(StrEnum):
5
+ SANDBOX = "sandbox"
6
+ PRODUCTION = "production"
bpay/config.py ADDED
File without changes
bpay/exceptions.py ADDED
@@ -0,0 +1,52 @@
1
+ class BPayError(Exception):
2
+ """Base exception for bpay."""
3
+
4
+
5
+ class AuthenticationError(BPayError):
6
+ def __init__(
7
+ self,
8
+ provider: str,
9
+ message: str,
10
+ provider_code: str | None = None,
11
+ ) -> None:
12
+ self.provider = provider
13
+ self.provider_code = provider_code
14
+ self.message = message
15
+
16
+ super().__init__(self.__str__())
17
+
18
+ def __str__(self) -> str:
19
+ lines = [
20
+ f"[{self.provider} Authentication Error]",
21
+ f"Message: {self.message}",
22
+ ]
23
+
24
+ if self.provider_code:
25
+ lines.append(
26
+ f"Code: {self.provider_code}"
27
+ )
28
+
29
+ return "\n".join(lines)
30
+
31
+
32
+ class AgreementError(
33
+ BPayError
34
+ ):
35
+ """Agreement workflow failed."""
36
+
37
+
38
+ class ProviderAPIError(
39
+ BPayError
40
+ ):
41
+ def __init__(
42
+ self,
43
+ message: str,
44
+ provider_code: (
45
+ str | None
46
+ ) = None,
47
+ ) -> None:
48
+ self.provider_code = (
49
+ provider_code
50
+ )
51
+
52
+ super().__init__(message)
File without changes
bpay/providers/base.py ADDED
@@ -0,0 +1,15 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from bpay.schemas.payment import (
4
+ CreatePaymentRequest,
5
+ PaymentResponse,
6
+ )
7
+
8
+
9
+ class BaseProvider(ABC):
10
+ @abstractmethod
11
+ async def create_payment(
12
+ self,
13
+ payload: CreatePaymentRequest,
14
+ ) -> PaymentResponse:
15
+ pass
File without changes
@@ -0,0 +1,96 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from bpay.exceptions import AgreementError
6
+ from bpay.providers.bkash.auth import (
7
+ BkashAuth,
8
+ )
9
+ from bpay.providers.bkash.constants import (
10
+ BKASH_AGREEMENT_STATUS_MAP,
11
+ )
12
+ from bpay.schemas.agreement import (
13
+ AgreementResponse,
14
+ CreateAgreementRequest,
15
+ )
16
+
17
+
18
+ class BkashAgreement:
19
+ def __init__(
20
+ self,
21
+ auth: BkashAuth,
22
+ base_url: str,
23
+ ) -> None:
24
+ self.auth = auth
25
+ self.base_url = base_url
26
+
27
+ async def create(
28
+ self,
29
+ payload: CreateAgreementRequest,
30
+ ) -> AgreementResponse:
31
+ token = await self.auth.get_token()
32
+
33
+ url = (
34
+ f"{self.base_url}"
35
+ "/tokenized/checkout/create"
36
+ )
37
+
38
+ headers = {
39
+ "Content-Type": "application/json",
40
+ "Authorization": (
41
+ f"Bearer {token}"
42
+ ),
43
+ "X-APP-Key": (
44
+ self.auth.credentials.app_key
45
+ ),
46
+ }
47
+
48
+ request_body = {
49
+ "mode": "0000",
50
+ "payerReference": (
51
+ payload.customer_phone
52
+ ),
53
+ "callbackURL": (
54
+ payload.callback_url
55
+ ),
56
+ }
57
+
58
+ async with httpx.AsyncClient(
59
+ timeout=30
60
+ ) as client:
61
+ response = await client.post(
62
+ url,
63
+ json=request_body,
64
+ headers=headers,
65
+ )
66
+
67
+ response.raise_for_status()
68
+
69
+ data: dict[str, Any] = (
70
+ response.json()
71
+ )
72
+
73
+ if data.get("statusCode") != "0000":
74
+ raise AgreementError(
75
+ f"Agreement creation failed: "
76
+ f"{data}"
77
+ )
78
+
79
+ return AgreementResponse(
80
+ agreement_id="",
81
+ payment_id=str(
82
+ data["paymentID"]
83
+ ),
84
+ checkout_url=str(
85
+ data["bkashURL"]
86
+ ),
87
+ status=(
88
+ BKASH_AGREEMENT_STATUS_MAP[
89
+ str(
90
+ data[
91
+ "agreementStatus"
92
+ ]
93
+ )
94
+ ]
95
+ ),
96
+ )
@@ -0,0 +1,87 @@
1
+ from datetime import UTC, datetime, timedelta
2
+ from typing import Any
3
+
4
+ import httpx
5
+
6
+ from bpay.exceptions import AuthenticationError
7
+ from bpay.providers.bkash.schemas import (
8
+ BkashCredentials,
9
+ BkashTokenResponse,
10
+ )
11
+
12
+
13
+ class BkashAuth:
14
+ BASE_URL = "https://tokenized.sandbox.bka.sh/v1.2.0-beta"
15
+
16
+ def __init__(
17
+ self,
18
+ credentials: BkashCredentials,
19
+ base_url: str
20
+ ) -> None:
21
+ self.credentials = credentials
22
+ self.base_url = base_url
23
+
24
+ self._token: BkashTokenResponse | None = None
25
+ self._expires_at: datetime | None = None
26
+
27
+ async def authenticate(
28
+ self,
29
+ ) -> BkashTokenResponse:
30
+ url = f"{self.base_url}/tokenized/checkout/token/grant"
31
+
32
+ headers = {
33
+ "Content-Type": "application/json",
34
+ "username": self.credentials.username,
35
+ "password": self.credentials.password,
36
+ }
37
+
38
+ payload = {
39
+ "app_key": self.credentials.app_key,
40
+ "app_secret": self.credentials.app_secret,
41
+ }
42
+
43
+ async with httpx.AsyncClient(timeout=30) as client:
44
+ response = await client.post(
45
+ url,
46
+ json=payload,
47
+ headers=headers,
48
+ )
49
+
50
+ response.raise_for_status()
51
+
52
+ data: dict[str, Any] = response.json()
53
+
54
+ if data.get("statusCode") != "0000":
55
+ raise AuthenticationError(
56
+ provider="bKash",
57
+ message=data.get(
58
+ "statusMessage",
59
+ "Authentication failed",
60
+ ),
61
+ provider_code=data.get(
62
+ "statusCode"
63
+ ),
64
+ )
65
+
66
+ return BkashTokenResponse(
67
+ id_token=str(data["id_token"]),
68
+ refresh_token=str(data["refresh_token"]),
69
+ token_type=str(data["token_type"]),
70
+ expires_in=int(data["expires_in"]),
71
+ )
72
+
73
+ async def get_token(self) -> str:
74
+ if (
75
+ self._token is not None
76
+ and self._expires_at is not None
77
+ and datetime.now(UTC) < self._expires_at
78
+ ):
79
+ return self._token.id_token
80
+
81
+ token = await self.authenticate()
82
+
83
+ self._token = token
84
+
85
+ self._expires_at = datetime.now(UTC) + timedelta(seconds=token.expires_in - 60)
86
+
87
+ return token.id_token
@@ -0,0 +1,21 @@
1
+ from bpay.schemas.callback import (
2
+ AgreementCallback,
3
+ )
4
+ from bpay.types import AgreementStatus
5
+
6
+ STATUS_MAP = {
7
+ "success": AgreementStatus.ACTIVE,
8
+ "failure": AgreementStatus.FAILED,
9
+ "cancel": AgreementStatus.CANCELLED,
10
+ }
11
+
12
+
13
+ def parse_agreement_callback(
14
+ params: dict[str, str],
15
+ ) -> AgreementCallback:
16
+ return AgreementCallback(
17
+ payment_id=params["paymentID"],
18
+ status=STATUS_MAP[params["status"]],
19
+ signature=params["signature"],
20
+ agreement_id=params.get("agreementID"),
21
+ )
@@ -0,0 +1,17 @@
1
+ from bpay.types import AgreementStatus
2
+
3
+ BKASH_AGREEMENT_STATUS_MAP = {
4
+ "Initiated": AgreementStatus.INITIATED,
5
+ "Completed": AgreementStatus.ACTIVE,
6
+ "Cancelled": AgreementStatus.CANCELLED,
7
+ "Failure": AgreementStatus.FAILED,
8
+ }
9
+
10
+ BKASH_BASE_URLS = {
11
+ "sandbox": (
12
+ "https://tokenized.sandbox.bka.sh/v1.2.0-beta"
13
+ ),
14
+ "production": (
15
+ "https://tokenized.pay.bka.sh/v1.2.0-beta"
16
+ ),
17
+ }
@@ -0,0 +1,103 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from bpay.exceptions import (
6
+ ProviderAPIError,
7
+ )
8
+ from bpay.providers.bkash.auth import (
9
+ BkashAuth,
10
+ )
11
+ from bpay.schemas.payment import (
12
+ CreatePaymentRequest,
13
+ PaymentResponse,
14
+ )
15
+ from bpay.types import PaymentStatus
16
+
17
+
18
+ class BkashPayment:
19
+ def __init__(
20
+ self,
21
+ auth: BkashAuth,
22
+ base_url: str,
23
+ ) -> None:
24
+ self.auth = auth
25
+ self.base_url = base_url
26
+
27
+ async def create(
28
+ self,
29
+ payload: CreatePaymentRequest,
30
+ ) -> PaymentResponse:
31
+ token = await self.auth.get_token()
32
+
33
+ url = (
34
+ f"{self.base_url}"
35
+ "/tokenized/checkout/create"
36
+ )
37
+
38
+ headers = {
39
+ "Content-Type": "application/json",
40
+ "Authorization": (
41
+ f"Bearer {token}"
42
+ ),
43
+ "X-APP-Key": (
44
+ self.auth.credentials.app_key
45
+ ),
46
+ }
47
+
48
+ request_body = {
49
+ "mode": "0011",
50
+ "callbackURL": (
51
+ payload.callback_url
52
+ ),
53
+ "amount": str(
54
+ payload.amount
55
+ ),
56
+ "currency": (
57
+ payload.currency
58
+ ),
59
+ "intent": (
60
+ payload.intent
61
+ ),
62
+ "merchantInvoiceNumber": (
63
+ payload.merchant_invoice_number
64
+ ),
65
+ }
66
+
67
+ if payload.payer_reference:
68
+ request_body[
69
+ "payerReference"
70
+ ] = (
71
+ payload.payer_reference
72
+ )
73
+
74
+ async with httpx.AsyncClient(
75
+ timeout=30
76
+ ) as client:
77
+ response = await client.post(
78
+ url,
79
+ json=request_body,
80
+ headers=headers,
81
+ )
82
+
83
+ response.raise_for_status()
84
+
85
+ data: dict[str, Any] = (
86
+ response.json()
87
+ )
88
+
89
+ if data.get("statusCode") != "0000":
90
+ raise ProviderAPIError(
91
+ f"Payment creation failed: "
92
+ f"{data}"
93
+ )
94
+
95
+ return PaymentResponse(
96
+ payment_id=str(
97
+ data["paymentID"]
98
+ ),
99
+ checkout_url=str(
100
+ data["bkashURL"]
101
+ ),
102
+ status=PaymentStatus.PENDING,
103
+ )
@@ -0,0 +1,22 @@
1
+ from bpay.schemas.payment_callback import (
2
+ PaymentCallback,
3
+ )
4
+ from bpay.types import PaymentStatus
5
+
6
+ STATUS_MAP = {
7
+ "success": PaymentStatus.SUCCESS,
8
+ "failure": PaymentStatus.FAILED,
9
+ "cancel": PaymentStatus.CANCELLED,
10
+ }
11
+
12
+
13
+ def parse_payment_callback(
14
+ params: dict[str, str],
15
+ ) -> PaymentCallback:
16
+ return PaymentCallback(
17
+ payment_id=params["paymentID"],
18
+ status=STATUS_MAP[
19
+ params["status"]
20
+ ],
21
+ signature=params["signature"],
22
+ )
@@ -0,0 +1,117 @@
1
+ from bpay.config.environments import (
2
+ Environment,
3
+ )
4
+ from bpay.providers.bkash.agreement import (
5
+ BkashAgreement,
6
+ )
7
+ from bpay.providers.bkash.auth import (
8
+ BkashAuth,
9
+ )
10
+ from bpay.providers.bkash.constants import (
11
+ BKASH_BASE_URLS,
12
+ )
13
+ from bpay.providers.bkash.payment import (
14
+ BkashPayment,
15
+ )
16
+ from bpay.providers.bkash.schemas import (
17
+ BkashCredentials,
18
+ )
19
+ from bpay.providers.bkash.verification import (
20
+ BkashVerification,
21
+ )
22
+ from bpay.schemas.agreement import (
23
+ AgreementResponse,
24
+ CreateAgreementRequest,
25
+ )
26
+ from bpay.schemas.payment import (
27
+ CreatePaymentRequest,
28
+ PaymentResponse,
29
+ )
30
+ from bpay.schemas.verification import (
31
+ PaymentVerificationResponse,
32
+ )
33
+
34
+
35
+ class BkashProvider:
36
+ def __init__(
37
+ self,
38
+ username: str,
39
+ password: str,
40
+ app_key: str,
41
+ app_secret: str,
42
+ environment: str = "sandbox",
43
+ ) -> None:
44
+ credentials = (
45
+ BkashCredentials(
46
+ username=username,
47
+ password=password,
48
+ app_key=app_key,
49
+ app_secret=app_secret,
50
+ )
51
+ )
52
+
53
+ environment_enum = (
54
+ Environment(environment)
55
+ )
56
+
57
+ base_url = (
58
+ BKASH_BASE_URLS[
59
+ environment_enum.value
60
+ ]
61
+ )
62
+
63
+ auth = BkashAuth(
64
+ credentials=credentials,
65
+ base_url=base_url,
66
+ )
67
+
68
+ self.agreement_service = (
69
+ BkashAgreement(
70
+ auth=auth,
71
+ base_url=base_url,
72
+ )
73
+ )
74
+
75
+ self.payment_service = (
76
+ BkashPayment(
77
+ auth=auth,
78
+ base_url=base_url,
79
+ )
80
+ )
81
+
82
+ self.verification_service = (
83
+ BkashVerification(
84
+ auth=auth,
85
+ base_url=base_url,
86
+ )
87
+ )
88
+
89
+ async def create_agreement(
90
+ self,
91
+ payload: CreateAgreementRequest,
92
+ ) -> AgreementResponse:
93
+ return await (
94
+ self.agreement_service.create(
95
+ payload
96
+ )
97
+ )
98
+
99
+ async def create_payment(
100
+ self,
101
+ payload: CreatePaymentRequest,
102
+ ) -> PaymentResponse:
103
+ return await (
104
+ self.payment_service.create(
105
+ payload
106
+ )
107
+ )
108
+
109
+ async def verify_payment(
110
+ self,
111
+ payment_id: str,
112
+ ) -> PaymentVerificationResponse:
113
+ return await (
114
+ self.verification_service.verify_payment(
115
+ payment_id
116
+ )
117
+ )
@@ -0,0 +1,15 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class BkashCredentials(BaseModel):
5
+ username: str
6
+ password: str
7
+ app_key: str
8
+ app_secret: str
9
+
10
+
11
+ class BkashTokenResponse(BaseModel):
12
+ id_token: str
13
+ refresh_token: str
14
+ token_type: str
15
+ expires_in: int
@@ -0,0 +1,84 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from bpay.exceptions import (
6
+ ProviderAPIError,
7
+ )
8
+ from bpay.providers.bkash.auth import (
9
+ BkashAuth,
10
+ )
11
+ from bpay.schemas.verification import (
12
+ PaymentVerificationResponse,
13
+ )
14
+ from bpay.types import PaymentStatus
15
+
16
+
17
+ class BkashVerification:
18
+ def __init__(
19
+ self,
20
+ auth: BkashAuth,
21
+ base_url: str,
22
+ ) -> None:
23
+ self.auth = auth
24
+ self.base_url = base_url
25
+
26
+ async def verify_payment(
27
+ self,
28
+ payment_id: str,
29
+ ) -> PaymentVerificationResponse:
30
+ token = await self.auth.get_token()
31
+
32
+ url = (
33
+ f"{self.base_url}"
34
+ "/tokenized/checkout/payment/status"
35
+ )
36
+
37
+ headers = {
38
+ "Authorization": (
39
+ f"Bearer {token}"
40
+ ),
41
+ "X-APP-Key": (
42
+ self.auth.credentials.app_key
43
+ ),
44
+ }
45
+
46
+ payload = {
47
+ "paymentID": payment_id
48
+ }
49
+
50
+ async with httpx.AsyncClient(
51
+ timeout=30
52
+ ) as client:
53
+ response = await client.post(
54
+ url,
55
+ json=payload,
56
+ headers=headers,
57
+ )
58
+
59
+ response.raise_for_status()
60
+
61
+ data: dict[str, Any] = (
62
+ response.json()
63
+ )
64
+
65
+ if data.get("statusCode") != "0000":
66
+ raise ProviderAPIError(
67
+ f"Payment verification failed: "
68
+ f"{data}"
69
+ )
70
+
71
+ return (
72
+ PaymentVerificationResponse(
73
+ payment_id=str(
74
+ data["paymentID"]
75
+ ),
76
+ trx_id=data.get("trxID"),
77
+ amount=float(
78
+ data.get("amount", 0)
79
+ ),
80
+ status=(
81
+ PaymentStatus.SUCCESS
82
+ ),
83
+ )
84
+ )
File without changes
@@ -0,0 +1,5 @@
1
+ from bpay.providers.bkash.provider import BkashProvider
2
+
3
+ PROVIDERS = {
4
+ "bkash": BkashProvider,
5
+ }
File without changes
File without changes
@@ -0,0 +1,15 @@
1
+ from pydantic import BaseModel
2
+
3
+ from bpay.types import AgreementStatus
4
+
5
+
6
+ class CreateAgreementRequest(BaseModel):
7
+ customer_phone: str
8
+ callback_url: str
9
+
10
+
11
+ class AgreementResponse(BaseModel):
12
+ agreement_id: str
13
+ payment_id: str
14
+ checkout_url: str
15
+ status: AgreementStatus
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel
2
+
3
+ from bpay.types import AgreementStatus
4
+
5
+
6
+ class AgreementCallback(BaseModel):
7
+ payment_id: str
8
+ status: AgreementStatus
9
+ signature: str
10
+ agreement_id: str | None = None
@@ -0,0 +1,20 @@
1
+ from pydantic import BaseModel
2
+
3
+ from bpay.types import PaymentStatus
4
+
5
+
6
+ class CreatePaymentRequest(BaseModel):
7
+ amount: float
8
+ callback_url: str
9
+ merchant_invoice_number: str
10
+
11
+ currency: str = "BDT"
12
+ intent: str = "sale"
13
+
14
+ payer_reference: str | None = None
15
+
16
+
17
+ class PaymentResponse(BaseModel):
18
+ payment_id: str
19
+ checkout_url: str
20
+ status: PaymentStatus
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel
2
+
3
+ from bpay.types import PaymentStatus
4
+
5
+
6
+ class PaymentCallback(BaseModel):
7
+ payment_id: str
8
+ status: PaymentStatus
9
+ signature: str
@@ -0,0 +1,12 @@
1
+ from pydantic import BaseModel
2
+
3
+ from bpay.types import PaymentStatus
4
+
5
+
6
+ class PaymentVerificationResponse(
7
+ BaseModel
8
+ ):
9
+ payment_id: str
10
+ trx_id: str | None = None
11
+ amount: float | None = None
12
+ status: PaymentStatus
File without changes
File without changes
@@ -0,0 +1,15 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+
6
+ class HttpTransport:
7
+ def __init__(self) -> None:
8
+ self.client = httpx.AsyncClient(timeout=30)
9
+
10
+ async def post(
11
+ self,
12
+ url: str,
13
+ **kwargs: Any,
14
+ ) -> httpx.Response:
15
+ return await self.client.post(url, **kwargs)
bpay/types.py ADDED
@@ -0,0 +1,21 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class PaymentStatus(StrEnum):
5
+ PENDING = "pending"
6
+ SUCCESS = "success"
7
+ FAILED = "failed"
8
+ CANCELLED = "cancelled"
9
+
10
+
11
+ class RefundStatus(StrEnum):
12
+ PENDING = "pending"
13
+ SUCCESS = "success"
14
+ FAILED = "failed"
15
+
16
+
17
+ class AgreementStatus(StrEnum):
18
+ INITIATED = "initiated"
19
+ ACTIVE = "active"
20
+ FAILED = "failed"
21
+ CANCELLED = "cancelled"
File without changes
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: bpay
3
+ Version: 0.1.0
4
+ Summary: Unified Python SDK for Bangladeshi payment gateways
5
+ Project-URL: Homepage, https://github.com/thatsayon/bpay-python
6
+ Project-URL: Repository, https://github.com/thatsayon/bpay-python
7
+ Project-URL: Issues, https://github.com/thatsayon/bpay-python/issues
8
+ Author: Ayon
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: bangladesh,bkash,nagad,payments,sslcommerz
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: httpx>=0.28.1
22
+ Requires-Dist: pydantic>=2.13.4
23
+ Description-Content-Type: text/markdown
24
+
25
+ # bpay
26
+
27
+ Unified Python SDK for Bangladeshi payment gateways.
28
+
29
+ `bpay` provides a clean, typed, async-first developer experience for integrating Bangladeshi payment providers like bKash, Nagad, and SSLCommerz.
30
+
31
+ ---
32
+
33
+ ## Features
34
+
35
+ - Async-first architecture
36
+ - Typed request & response models
37
+ - Sandbox & production environment support
38
+ - Hosted checkout workflows
39
+ - Agreement/tokenized checkout support
40
+ - Payment callback parsing
41
+ - Payment verification support
42
+ - Provider abstraction layer
43
+ - Modern Python packaging with `uv`
44
+
45
+ ---
46
+
47
+ ## Supported Providers
48
+
49
+ | Provider | Status |
50
+ |---|---|
51
+ | bKash | In Progress |
52
+ | Nagad | Planned |
53
+ | SSLCommerz | Planned |
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install bpay
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Quick Start
66
+
67
+ ### Create a One-Time Payment
68
+
69
+ ```python
70
+ import asyncio
71
+
72
+ from bpay import BPay
73
+ from bpay.config.environments import (
74
+ Environment,
75
+ )
76
+ from bpay.schemas.payment import (
77
+ CreatePaymentRequest,
78
+ )
79
+
80
+
81
+ async def main() -> None:
82
+ client = BPay(
83
+ provider="bkash",
84
+ environment=Environment.SANDBOX,
85
+ username="YOUR_USERNAME",
86
+ password="YOUR_PASSWORD",
87
+ app_key="YOUR_APP_KEY",
88
+ app_secret="YOUR_APP_SECRET",
89
+ )
90
+
91
+ payment = await client.create_payment(
92
+ CreatePaymentRequest(
93
+ amount=100,
94
+ merchant_invoice_number="INV-1001",
95
+ callback_url=(
96
+ "https://yourdomain.com/callback"
97
+ ),
98
+ )
99
+ )
100
+
101
+ print(payment.checkout_url)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ asyncio.run(main())
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Agreement / Tokenized Checkout
111
+
112
+ Agreements allow merchants to create reusable customer payment authorizations for recurring or saved payments.
113
+
114
+ ### Create Agreement
115
+
116
+ ```python
117
+ import asyncio
118
+
119
+ from bpay import BPay
120
+ from bpay.config.environments import (
121
+ Environment,
122
+ )
123
+ from bpay.schemas.agreement import (
124
+ CreateAgreementRequest,
125
+ )
126
+
127
+
128
+ async def main() -> None:
129
+ client = BPay(
130
+ provider="bkash",
131
+ environment=Environment.SANDBOX,
132
+ username="YOUR_USERNAME",
133
+ password="YOUR_PASSWORD",
134
+ app_key="YOUR_APP_KEY",
135
+ app_secret="YOUR_APP_SECRET",
136
+ )
137
+
138
+ agreement = await (
139
+ client.create_agreement(
140
+ CreateAgreementRequest(
141
+ customer_phone="017XXXXXXXX",
142
+ callback_url=(
143
+ "https://yourdomain.com/callback"
144
+ ),
145
+ )
146
+ )
147
+ )
148
+
149
+ print(agreement.checkout_url)
150
+
151
+
152
+ if __name__ == "__main__":
153
+ asyncio.run(main())
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Callback Parsing
159
+
160
+ ### Payment Callback
161
+
162
+ ```python
163
+ from bpay import BPay
164
+
165
+ client = BPay(...)
166
+
167
+ callback = client.parse_payment_callback(
168
+ {
169
+ "paymentID": "PAY123",
170
+ "status": "success",
171
+ "signature": "abc123",
172
+ }
173
+ )
174
+
175
+ print(callback.status)
176
+ ```
177
+
178
+ ---
179
+
180
+ ### Agreement Callback
181
+
182
+ ```python
183
+ from bpay import BPay
184
+
185
+ client = BPay(...)
186
+
187
+ callback = (
188
+ client.parse_agreement_callback(
189
+ {
190
+ "paymentID": "PAY123",
191
+ "agreementID": "AGR123",
192
+ "status": "success",
193
+ "signature": "abc123",
194
+ }
195
+ )
196
+ )
197
+
198
+ print(callback.status)
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Payment Verification
204
+
205
+ ```python
206
+ verification = await client.verify_payment(
207
+ payment_id="PAY123"
208
+ )
209
+
210
+ print(verification.status)
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Environments
216
+
217
+ `bpay` supports both sandbox and production environments.
218
+
219
+ ### Sandbox
220
+
221
+ ```python
222
+ from bpay.config.environments import (
223
+ Environment,
224
+ )
225
+
226
+ Environment.SANDBOX
227
+ ```
228
+
229
+ ### Production
230
+
231
+ ```python
232
+ from bpay.config.environments import (
233
+ Environment,
234
+ )
235
+
236
+ Environment.PRODUCTION
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Development
242
+
243
+ ### Setup
244
+
245
+ ```bash
246
+ uv sync
247
+ ```
248
+
249
+ ### Run Checks
250
+
251
+ ```bash
252
+ make check
253
+ ```
254
+
255
+ ### Run Tests
256
+
257
+ ```bash
258
+ uv run pytest
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Project Structure
264
+
265
+ ```text
266
+ src/bpay/
267
+ ├── client.py
268
+ ├── exceptions.py
269
+ ├── config/
270
+ ├── providers/
271
+ ├── schemas/
272
+ ├── transports/
273
+ └── types.py
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Roadmap
279
+
280
+ - Full bKash recurring payment workflow
281
+ - Nagad integration
282
+ - SSLCommerz integration
283
+ - Refund support
284
+ - Webhook signature verification
285
+ - Automatic retry handling
286
+ - Sync client support
287
+ - Logging middleware
288
+
289
+ ---
290
+
291
+ ## License
292
+
293
+ MIT License
@@ -0,0 +1,36 @@
1
+ bpay/__init__.py,sha256=L4D37QQavc_fb7ub3UdD6nN1V7ke3utYojGV8CGbOe8,72
2
+ bpay/client.py,sha256=yYUpNvyrfLdPUdqrSDxMBmdUoCiyTnw2z7ohn5dUrDQ,3591
3
+ bpay/config.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ bpay/exceptions.py,sha256=eNN79RSgY42tU-RJE4s69ZiR-m7fq1ZJYDhiutoJ_BU,1049
5
+ bpay/types.py,sha256=KblekKPgCT0VW4CpUviw2PvHNDfeP2XX5XTQlCBuT-k,390
6
+ bpay/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ bpay/config/environments.py,sha256=uhIAYD0o9y49_D2SmXhH_oy1TPJV83RHwOejbtvipW0,109
8
+ bpay/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ bpay/providers/base.py,sha256=q2uS8o5o8o6_Jymdc3kf2w1AAr-SGKaNbuLOEeGUNAM,290
10
+ bpay/providers/registry.py,sha256=ArWomtK-7tiLE5i-Mx4M2lhv9dTYSM4SbHfiP51ElDs,101
11
+ bpay/providers/bkash/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ bpay/providers/bkash/agreement.py,sha256=iMYt4lHv7NmqNo7lwAZ3abqjH70K_9waFZ1EJszFdsg,2191
13
+ bpay/providers/bkash/auth.py,sha256=an9grQMvNs-2qM5S33i2LabSGK_ZRSvLNzJfxVhjeCI,2356
14
+ bpay/providers/bkash/callbacks.py,sha256=bFCn-adHn4LdduWCkEEdmKWr_4IToO4wicQXerK4mjs,529
15
+ bpay/providers/bkash/constants.py,sha256=d0Ig-J4aEdIN5dq9FQuQuWBYuiM8IeZIA-1ZVkcBVdg,421
16
+ bpay/providers/bkash/payment.py,sha256=C8UH8dmDcy2FLtUKVLs-isky4kepZjbY-Qa_t5ySDPk,2313
17
+ bpay/providers/bkash/payment_callbacks.py,sha256=1zw-ZWYsfBAWtPE3j7YE17baaYV9NEaj2_0tVlPRIkE,496
18
+ bpay/providers/bkash/provider.py,sha256=x_sBk7k4d7tQ6-OY4PHcUbYqpx_u1U1YCgUe2nkTfAo,2559
19
+ bpay/providers/bkash/schemas.py,sha256=_1uj-nyhYEGQPoNXk19I76TfsuT041RExrxBjN6ELGo,261
20
+ bpay/providers/bkash/verification.py,sha256=LcTcCOcVDB2Kt4nVVjNwuXyNWxvty7BCJNY4HB_eOqI,1860
21
+ bpay/providers/nagad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ bpay/providers/sslcommerz/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ bpay/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ bpay/schemas/agreement.py,sha256=ChuUhHTAiKwomyPWjFdaPEB8Z-WYh3wQqv4erOdVElY,290
25
+ bpay/schemas/callback.py,sha256=Iwn8tnOGBU0_kabeMjI0iYAuur7XrOavox7A4fXiXo8,212
26
+ bpay/schemas/payment.py,sha256=rc1ko6RLZADFslktdPjDf_V3ZKjU_xl70De1ft2rC2Q,379
27
+ bpay/schemas/payment_callback.py,sha256=FGUAHb-xAC-MZqGoDHxaxfKfn62bfTYI3kL9MmFpW9o,170
28
+ bpay/schemas/verification.py,sha256=fwmQUH2hNbQG00JXnLFIE8Cqa-sgGWkK_fdgLIOr5eM,231
29
+ bpay/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ bpay/transports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ bpay/transports/http.py,sha256=39dppdUBvlS59auFh5S16_zBLpTvSQRw_H6YfcjMl4A,298
32
+ bpay/webhooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ bpay-0.1.0.dist-info/METADATA,sha256=JCMUSLo2LVG5_W0Wv_Qcu2su9S4iYraWchXm89eb9ko,5035
34
+ bpay-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
35
+ bpay-0.1.0.dist-info/licenses/LICENSE,sha256=Tci2KBJuPGYHHw0o2VmR8mwmB4145ZIZ6S_Mp21z57o,1061
36
+ bpay-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ayon
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.