mpesakit 0.1.2__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.
- mpesakit/B2B_express_checkout/B2B_express_checkout.py +51 -0
- mpesakit/B2B_express_checkout/__init__.py +16 -0
- mpesakit/B2B_express_checkout/schemas.py +127 -0
- mpesakit/B2C/B2C.py +47 -0
- mpesakit/B2C/__init__.py +23 -0
- mpesakit/B2C/schemas.py +339 -0
- mpesakit/B2C_account_top_up/B2C_account_top_up.py +49 -0
- mpesakit/B2C_account_top_up/__init__.py +19 -0
- mpesakit/B2C_account_top_up/schemas.py +316 -0
- mpesakit/C2B/C2B.py +56 -0
- mpesakit/C2B/__init__.py +21 -0
- mpesakit/C2B/schemas.py +234 -0
- mpesakit/__init__.py +0 -0
- mpesakit/account_balance/__init__.py +21 -0
- mpesakit/account_balance/account_balance.py +47 -0
- mpesakit/account_balance/schemas.py +298 -0
- mpesakit/auth/__init__.py +4 -0
- mpesakit/auth/access_token.py +22 -0
- mpesakit/auth/token_manager.py +86 -0
- mpesakit/bill_manager/__init__.py +40 -0
- mpesakit/bill_manager/bill_manager.py +133 -0
- mpesakit/bill_manager/schemas.py +467 -0
- mpesakit/business_buy_goods/__init__.py +19 -0
- mpesakit/business_buy_goods/business_buy_goods.py +47 -0
- mpesakit/business_buy_goods/schemas.py +302 -0
- mpesakit/business_paybill/__init__.py +19 -0
- mpesakit/business_paybill/business_paybill.py +47 -0
- mpesakit/business_paybill/schemas.py +284 -0
- mpesakit/dynamic_qr_code/__init__.py +13 -0
- mpesakit/dynamic_qr_code/dynamic_qr_code.py +49 -0
- mpesakit/dynamic_qr_code/schemas.py +172 -0
- mpesakit/errors.py +49 -0
- mpesakit/http_client/__init__.py +4 -0
- mpesakit/http_client/http_client.py +28 -0
- mpesakit/http_client/mpesa_http_client.py +173 -0
- mpesakit/mpesa_client.py +85 -0
- mpesakit/mpesa_express/__init__.py +28 -0
- mpesakit/mpesa_express/schemas.py +610 -0
- mpesakit/mpesa_express/stk_push.py +64 -0
- mpesakit/mpesa_ratiba/__init__.py +21 -0
- mpesakit/mpesa_ratiba/mpesa_ratiba.py +51 -0
- mpesakit/mpesa_ratiba/schemas.py +320 -0
- mpesakit/reversal/__init__.py +19 -0
- mpesakit/reversal/reversal.py +48 -0
- mpesakit/reversal/schemas.py +295 -0
- mpesakit/services/__init__.py +25 -0
- mpesakit/services/b2b.py +188 -0
- mpesakit/services/b2c.py +124 -0
- mpesakit/services/balance.py +67 -0
- mpesakit/services/bill.py +135 -0
- mpesakit/services/c2b.py +56 -0
- mpesakit/services/dynamic_qr.py +61 -0
- mpesakit/services/express.py +105 -0
- mpesakit/services/ratiba.py +82 -0
- mpesakit/services/reversal.py +67 -0
- mpesakit/services/tax.py +65 -0
- mpesakit/services/transaction.py +78 -0
- mpesakit/tax_remittance/__init__.py +19 -0
- mpesakit/tax_remittance/schemas.py +257 -0
- mpesakit/tax_remittance/tax_remittance.py +47 -0
- mpesakit/transaction_status/__init__.py +25 -0
- mpesakit/transaction_status/schemas.py +325 -0
- mpesakit/transaction_status/transaction_status.py +47 -0
- mpesakit/utils/ip_whitelist.py +56 -0
- mpesakit/utils/phone.py +38 -0
- mpesakit-0.1.2.dist-info/METADATA +226 -0
- mpesakit-0.1.2.dist-info/RECORD +69 -0
- mpesakit-0.1.2.dist-info/WHEEL +4 -0
- mpesakit-0.1.2.dist-info/licenses/LICENSE +73 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""B2BExpressCheckout: Handles M-Pesa B2B Express Checkout API interactions.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to initiate a B2B Express Checkout USSD Push transaction and handle result/timeout notifications
|
|
4
|
+
using the M-Pesa API. Requires a valid access token for authentication and uses the HttpClient for HTTP requests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
from mpesakit.auth import TokenManager
|
|
9
|
+
from mpesakit.http_client import HttpClient
|
|
10
|
+
|
|
11
|
+
from .schemas import (
|
|
12
|
+
B2BExpressCheckoutRequest,
|
|
13
|
+
B2BExpressCheckoutResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class B2BExpressCheckout(BaseModel):
|
|
18
|
+
"""Represents the B2B Express Checkout API client for M-Pesa operations.
|
|
19
|
+
|
|
20
|
+
https://developer.safaricom.co.ke/APIs/B2BExpressCheckout
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
http_client (HttpClient): HTTP client for making requests to the M-Pesa API.
|
|
24
|
+
token_manager (TokenManager): Manages access tokens for authentication.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
http_client: HttpClient
|
|
28
|
+
token_manager: TokenManager
|
|
29
|
+
|
|
30
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
31
|
+
|
|
32
|
+
def ussd_push(
|
|
33
|
+
self, request: B2BExpressCheckoutRequest
|
|
34
|
+
) -> B2BExpressCheckoutResponse:
|
|
35
|
+
"""Initiates a B2B Express Checkout USSD Push transaction.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
request (B2BExpressCheckoutRequest): The B2B Express Checkout request data.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
B2BExpressCheckoutResponse: Response from the M-Pesa API.
|
|
42
|
+
"""
|
|
43
|
+
url = "/v1/ussdpush/get-msisdn"
|
|
44
|
+
headers = {
|
|
45
|
+
"Authorization": f"Bearer {self.token_manager.get_token()}",
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
}
|
|
48
|
+
response_data = self.http_client.post(
|
|
49
|
+
url, json=request.model_dump(mode="json"), headers=headers
|
|
50
|
+
)
|
|
51
|
+
return B2BExpressCheckoutResponse(**response_data)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .schemas import (
|
|
2
|
+
B2BExpressCheckoutRequest,
|
|
3
|
+
B2BExpressCheckoutResponse,
|
|
4
|
+
B2BExpressCheckoutCallback,
|
|
5
|
+
B2BExpressCallbackResponse,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from .B2B_express_checkout import B2BExpressCheckout
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"B2BExpressCheckout",
|
|
12
|
+
"B2BExpressCheckoutRequest",
|
|
13
|
+
"B2BExpressCheckoutResponse",
|
|
14
|
+
"B2BExpressCheckoutCallback",
|
|
15
|
+
"B2BExpressCallbackResponse",
|
|
16
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Schemas for M-PESA B2B Express Checkout APIs."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, ConfigDict, HttpUrl
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class B2BExpressCheckoutRequest(BaseModel):
|
|
8
|
+
"""Request schema for B2B Express Checkout USSD Push."""
|
|
9
|
+
|
|
10
|
+
primaryShortCode: int = Field(
|
|
11
|
+
..., description="Merchant's till (debit party) shortcode/tillNumber."
|
|
12
|
+
)
|
|
13
|
+
receiverShortCode: int = Field(
|
|
14
|
+
..., description="Vendor's paybill (credit party) shortcode."
|
|
15
|
+
)
|
|
16
|
+
amount: int = Field(..., description="Amount to be sent to vendor.")
|
|
17
|
+
paymentRef: str = Field(
|
|
18
|
+
..., description="Reference for the payment (appears in text for merchant)."
|
|
19
|
+
)
|
|
20
|
+
callbackUrl: HttpUrl = Field(
|
|
21
|
+
..., description="Vendor system endpoint for confirmation response."
|
|
22
|
+
)
|
|
23
|
+
partnerName: str = Field(..., description="Vendor's organization friendly name.")
|
|
24
|
+
RequestRefID: str = Field(..., description="Unique identifier for each request.")
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(
|
|
27
|
+
json_schema_extra={
|
|
28
|
+
"example": {
|
|
29
|
+
"primaryShortCode": 123456,
|
|
30
|
+
"receiverShortCode": 654321,
|
|
31
|
+
"amount": 100,
|
|
32
|
+
"paymentRef": "Invoice123",
|
|
33
|
+
"callbackUrl": "http://example.com/result",
|
|
34
|
+
"partnerName": "VendorName",
|
|
35
|
+
"RequestRefID": "550e8400-e29b-41d4-a716-446655440000",
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class B2BExpressCheckoutResponse(BaseModel):
|
|
42
|
+
"""Acknowledgment response schema for B2B Express Checkout USSD Push."""
|
|
43
|
+
|
|
44
|
+
code: str = Field(
|
|
45
|
+
..., description="Shows if the push was successful (0) or failed."
|
|
46
|
+
)
|
|
47
|
+
status: str = Field(..., description="USSD initiation status message.")
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(
|
|
50
|
+
json_schema_extra={
|
|
51
|
+
"example": {
|
|
52
|
+
"code": "0",
|
|
53
|
+
"status": "USSD Initiated Successfully",
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def is_successful(self) -> bool:
|
|
59
|
+
"""Check if the response indicates a successful USSD initiation."""
|
|
60
|
+
return self.code == "0"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class B2BExpressCheckoutCallback(BaseModel):
|
|
64
|
+
"""Callback response schema for B2B Express Checkout USSD Push."""
|
|
65
|
+
|
|
66
|
+
resultCode: str = Field(
|
|
67
|
+
..., description="Status code: 0=success, other=fail/cancelled."
|
|
68
|
+
)
|
|
69
|
+
resultDesc: str = Field(..., description="Description of transaction result.")
|
|
70
|
+
amount: Optional[float] = Field(None, description="Amount initiated for payment.")
|
|
71
|
+
requestId: str = Field(..., description="Unique identifier of the request.")
|
|
72
|
+
paymentReference: Optional[str] = Field(
|
|
73
|
+
None, description="Reference for the payment."
|
|
74
|
+
)
|
|
75
|
+
resultType: Optional[str] = Field(
|
|
76
|
+
None, description="Status code for transaction sent to listener."
|
|
77
|
+
)
|
|
78
|
+
conversationID: Optional[str] = Field(
|
|
79
|
+
None, description="Global unique transaction request ID from M-Pesa."
|
|
80
|
+
)
|
|
81
|
+
transactionId: Optional[str] = Field(
|
|
82
|
+
None, description="Mpesa Receipt No of the transaction."
|
|
83
|
+
)
|
|
84
|
+
status: Optional[str] = Field(
|
|
85
|
+
None, description="Transaction status (SUCCESS/FAILED)."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
model_config = ConfigDict(
|
|
89
|
+
json_schema_extra={
|
|
90
|
+
"example": {
|
|
91
|
+
"resultCode": "0",
|
|
92
|
+
"resultDesc": "The service request is processed successfully.",
|
|
93
|
+
"amount": "71.0",
|
|
94
|
+
"requestId": "404e1aec-19e0-4ce3-973d-bd92e94c8021",
|
|
95
|
+
"resultType": "0",
|
|
96
|
+
"conversationID": "AG_20230426_2010434680d9f5a73766",
|
|
97
|
+
"transactionId": "RDQ01NFT1Q",
|
|
98
|
+
"status": "SUCCESS",
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def is_successful(self) -> bool:
|
|
104
|
+
"""Check if the callback indicates a successful transaction."""
|
|
105
|
+
return self.resultCode == "0"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class B2BExpressCallbackResponse(BaseModel):
|
|
109
|
+
"""Response schema for B2B Express Checkout callback."""
|
|
110
|
+
|
|
111
|
+
ResultCode: int = Field(
|
|
112
|
+
default=0,
|
|
113
|
+
description="Result code (0=Success, other=Failure).",
|
|
114
|
+
)
|
|
115
|
+
ResultDesc: str = Field(
|
|
116
|
+
default="Callback received successfully.",
|
|
117
|
+
description="Result description.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
model_config = ConfigDict(
|
|
121
|
+
json_schema_extra={
|
|
122
|
+
"example": {
|
|
123
|
+
"ResultCode": 0,
|
|
124
|
+
"ResultDesc": "Callback received successfully.",
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
)
|
mpesakit/B2C/B2C.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""B2C: Handles M-Pesa B2C (Business to Customer) API interactions.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to initiate B2C payments using the M-Pesa API.
|
|
4
|
+
Requires a valid access token for authentication and uses the HttpClient for HTTP requests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
from mpesakit.auth import TokenManager
|
|
9
|
+
from mpesakit.http_client import HttpClient
|
|
10
|
+
|
|
11
|
+
from .schemas import (
|
|
12
|
+
B2CRequest,
|
|
13
|
+
B2CResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class B2C(BaseModel):
|
|
18
|
+
"""Represents the B2C API client for M-Pesa Business to Customer operations.
|
|
19
|
+
|
|
20
|
+
https://developer.safaricom.co.ke/APIs/BusinessToCustomerPayment
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
http_client (HttpClient): HTTP client for making requests to the M-Pesa API.
|
|
24
|
+
token_manager (TokenManager): Manages access tokens for authentication.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
http_client: HttpClient
|
|
28
|
+
token_manager: TokenManager
|
|
29
|
+
|
|
30
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
31
|
+
|
|
32
|
+
def send_payment(self, request: B2CRequest) -> B2CResponse:
|
|
33
|
+
"""Initiates a B2C payment request.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
request (B2CRequest): The payment request details.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
B2CResponse: Response from the M-Pesa API after payment initiation.
|
|
40
|
+
"""
|
|
41
|
+
url = "/mpesa/b2c/v3/paymentrequest"
|
|
42
|
+
headers = {
|
|
43
|
+
"Authorization": f"Bearer {self.token_manager.get_token()}",
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
}
|
|
46
|
+
response_data = self.http_client.post(url, json=dict(request), headers=headers)
|
|
47
|
+
return B2CResponse(**response_data)
|
mpesakit/B2C/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .schemas import (
|
|
2
|
+
B2CCommandIDType,
|
|
3
|
+
B2CRequest,
|
|
4
|
+
B2CResponse,
|
|
5
|
+
B2CResultParameter,
|
|
6
|
+
B2CResultMetadata,
|
|
7
|
+
B2CResultCallback,
|
|
8
|
+
B2CTimeoutCallback,
|
|
9
|
+
B2CTimeoutCallbackResponse,
|
|
10
|
+
)
|
|
11
|
+
from .B2C import B2C
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"B2C",
|
|
15
|
+
"B2CCommandIDType",
|
|
16
|
+
"B2CRequest",
|
|
17
|
+
"B2CResponse",
|
|
18
|
+
"B2CResultParameter",
|
|
19
|
+
"B2CResultMetadata",
|
|
20
|
+
"B2CResultCallback",
|
|
21
|
+
"B2CTimeoutCallbackResponse",
|
|
22
|
+
"B2CTimeoutCallback",
|
|
23
|
+
]
|
mpesakit/B2C/schemas.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""This module defines schemas for M-Pesa B2C API requests and responses.
|
|
2
|
+
|
|
3
|
+
It includes models for payment requests, responses, and result notifications.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from mpesakit.utils.phone import normalize_phone_number
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class B2CCommandIDType(str, Enum):
|
|
13
|
+
"""Allowed values for CommandID in B2C payment requests."""
|
|
14
|
+
|
|
15
|
+
SalaryPayment = "SalaryPayment" # Both Registeren and Unregistered M-Pesa users
|
|
16
|
+
BusinessPayment = "BusinessPayment" # Only for registered M-Pesa users
|
|
17
|
+
PromotionPayment = (
|
|
18
|
+
"PromotionPayment" # Congratulatory payments for registered M-Pesa users only
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class B2CRequest(BaseModel):
|
|
23
|
+
"""Request schema for B2C payment initiation.
|
|
24
|
+
|
|
25
|
+
This schema is used to initiate Business-to-Customer (B2C) payments via the Safaricom Daraja API.
|
|
26
|
+
|
|
27
|
+
NOTE: To use this API in production, you must apply for a Bulk Disbursement Account and obtain a Shortcode.
|
|
28
|
+
The Shortcode required here is NOT the same as a Buy Goods Till or Paybill Till Number.
|
|
29
|
+
For more information and to apply for a Bulk Disbursement Account, refer to the official documentation:
|
|
30
|
+
https://developer.safaricom.co.ke/APIs/BusinessToCustomerPayment
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
OriginatorConversationID (str): Unique identifier for the specific request.
|
|
34
|
+
InitiatorName (str): Username used to initiate the request.
|
|
35
|
+
SecurityCredential (str): Encrypted security credential.
|
|
36
|
+
CommandID (str): Type of transaction to perform. Must be a valid B2CCommandIDType.
|
|
37
|
+
Amount (int): Amount to be sent to the customer.
|
|
38
|
+
PartyA (int): Shortcode sending the funds (Bulk Disbursement Account Shortcode).
|
|
39
|
+
PartyB (int): Mobile number receiving the funds (must be a valid Kenyan phone number).
|
|
40
|
+
Remarks (str): Comments for the transaction.
|
|
41
|
+
QueueTimeOutURL (str): URL for timeout notifications.
|
|
42
|
+
ResultURL (str): URL for result notifications.
|
|
43
|
+
Occasion (Optional[str]): Optional occasion for payment.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
OriginatorConversationID: str = Field(
|
|
47
|
+
..., description="Unique identifier for the specific request."
|
|
48
|
+
)
|
|
49
|
+
InitiatorName: str = Field(
|
|
50
|
+
..., description="Username used to initiate the request."
|
|
51
|
+
)
|
|
52
|
+
SecurityCredential: str = Field(..., description="Encrypted security credential.")
|
|
53
|
+
CommandID: str = Field(..., description="Type of transaction to perform.")
|
|
54
|
+
Amount: int = Field(..., description="Amount to be sent to customer.")
|
|
55
|
+
PartyA: int = Field(..., description="Shortcode sending the funds.")
|
|
56
|
+
PartyB: int = Field(..., description="Mobile number receiving the funds.")
|
|
57
|
+
Remarks: str = Field(..., description="Comments for the transaction.")
|
|
58
|
+
QueueTimeOutURL: str = Field(..., description="URL for timeout notifications.")
|
|
59
|
+
ResultURL: str = Field(..., description="URL for result notifications.")
|
|
60
|
+
Occasion: Optional[str] = Field(None, description="Optional occasion for payment.")
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(
|
|
63
|
+
json_schema_extra={
|
|
64
|
+
"example": {
|
|
65
|
+
"OriginatorConversationID": "12345-67890-1",
|
|
66
|
+
"InitiatorName": "testapi",
|
|
67
|
+
"SecurityCredential": "encrypted_credential",
|
|
68
|
+
"CommandID": "BusinessPayment",
|
|
69
|
+
"Amount": 1000,
|
|
70
|
+
"PartyA": 600999,
|
|
71
|
+
"PartyB": 254712345678,
|
|
72
|
+
"Remarks": "Salary for June",
|
|
73
|
+
"QueueTimeOutURL": "https://example.com/timeout",
|
|
74
|
+
"ResultURL": "https://example.com/result",
|
|
75
|
+
"Occasion": "JuneSalary",
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@model_validator(mode="before")
|
|
81
|
+
@classmethod
|
|
82
|
+
def validate(cls, values):
|
|
83
|
+
"""Validate CommandID and URLs."""
|
|
84
|
+
cls._validate_command_id(values)
|
|
85
|
+
cls._validate_partyb(values)
|
|
86
|
+
cls._validate_remarks(values)
|
|
87
|
+
cls._validate_occasion(values)
|
|
88
|
+
return values
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def _validate_occasion(cls, values):
|
|
92
|
+
"""Ensure Occasion is not more than 100 characters."""
|
|
93
|
+
occasion = values.get("Occasion")
|
|
94
|
+
if occasion is not None and len(occasion) > 100:
|
|
95
|
+
raise ValueError("Occasion must not exceed 100 characters.")
|
|
96
|
+
return values
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def _validate_remarks(cls, values):
|
|
100
|
+
"""Ensure Remarks is not more than 100 characters."""
|
|
101
|
+
remarks = values.get("Remarks")
|
|
102
|
+
if remarks is not None and len(remarks) > 100:
|
|
103
|
+
raise ValueError("Remarks must not exceed 100 characters.")
|
|
104
|
+
return values
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def _validate_partyb(cls, values):
|
|
108
|
+
"""Ensure PartyB is a valid Kenyan phone number."""
|
|
109
|
+
partyb = values.get("PartyB")
|
|
110
|
+
normalized = normalize_phone_number(str(partyb))
|
|
111
|
+
if normalized is None:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"PartyB must be a valid Kenyan phone number, got '{partyb}'"
|
|
114
|
+
)
|
|
115
|
+
values["PartyB"] = int(normalized)
|
|
116
|
+
return values
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def _validate_command_id(cls, values):
|
|
120
|
+
"""Ensure CommandID is a valid B2CCommandIDType value."""
|
|
121
|
+
command_id = values.get("CommandID")
|
|
122
|
+
valid_ids = [e.value for e in B2CCommandIDType]
|
|
123
|
+
if command_id not in valid_ids:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"CommandID must be one of {valid_ids}, got '{command_id}'"
|
|
126
|
+
)
|
|
127
|
+
return values
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class B2CResponse(BaseModel):
|
|
131
|
+
"""Response schema for B2C payment initiation."""
|
|
132
|
+
|
|
133
|
+
ConversationID: Optional[str] = Field(
|
|
134
|
+
..., description="Unique ID for the payment request."
|
|
135
|
+
)
|
|
136
|
+
OriginatorConversationID: Optional[str] = Field(
|
|
137
|
+
..., description="ID for tracking the request."
|
|
138
|
+
)
|
|
139
|
+
ResponseCode: str | int = Field(..., description="Status code, 0 means success.")
|
|
140
|
+
ResponseDescription: str = Field(..., description="Status message.")
|
|
141
|
+
|
|
142
|
+
model_config = ConfigDict(
|
|
143
|
+
json_schema_extra={
|
|
144
|
+
"example": {
|
|
145
|
+
"ConversationID": "AG_20170717_00006c6f7f5b8b6b1a62",
|
|
146
|
+
"OriginatorConversationID": "12345-67890-1",
|
|
147
|
+
"ResponseCode": "0",
|
|
148
|
+
"ResponseDescription": "Accept the service request successfully.",
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def is_successful(self) -> bool:
|
|
154
|
+
"""Return True if ResponseCode indicates success (e.g., '0', '00000000')."""
|
|
155
|
+
code = str(self.ResponseCode)
|
|
156
|
+
return code.strip("0") == "" and code != ""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class B2CResultParameter(BaseModel):
|
|
160
|
+
"""Parameter item in B2C result notification."""
|
|
161
|
+
|
|
162
|
+
Key: str = Field(..., description="Parameter name.")
|
|
163
|
+
Value: str | int | float = Field(..., description="Parameter value.")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class B2CResultMetadata(BaseModel):
|
|
167
|
+
"""Metadata for B2C result notification."""
|
|
168
|
+
|
|
169
|
+
ResultType: int = Field(..., description="Type of result (0=Success, 1=Failure).")
|
|
170
|
+
ResultCode: int = Field(..., description="Result code (0=Success).")
|
|
171
|
+
ResultDesc: str = Field(..., description="Result description.")
|
|
172
|
+
OriginatorConversationID: str = Field(
|
|
173
|
+
..., description="Originator conversation ID."
|
|
174
|
+
)
|
|
175
|
+
ConversationID: str = Field(..., description="Conversation ID.")
|
|
176
|
+
TransactionID: Optional[str] = Field(
|
|
177
|
+
None, description="M-Pesa transaction ID (if successful)."
|
|
178
|
+
)
|
|
179
|
+
ResultParameters: Optional[list[B2CResultParameter]] = Field(
|
|
180
|
+
None, description="List of result parameters."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
model_config = ConfigDict(
|
|
184
|
+
json_schema_extra={
|
|
185
|
+
"example": {
|
|
186
|
+
"ResultType": 0,
|
|
187
|
+
"ResultCode": 0,
|
|
188
|
+
"ResultDesc": "The service request is processed successfully.",
|
|
189
|
+
"OriginatorConversationID": "12345-67890-1",
|
|
190
|
+
"ConversationID": "AG_20170717_00006c6f7f5b8b6b1a62",
|
|
191
|
+
"TransactionID": "LKXXXX1234",
|
|
192
|
+
"ResultParameters": [
|
|
193
|
+
{"Key": "TransactionAmount", "Value": 1000},
|
|
194
|
+
{"Key": "TransactionReceipt", "Value": "LKXXXX1234"},
|
|
195
|
+
],
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def __init__(self, **data):
|
|
201
|
+
"""Initialize B2CResultMetadata and cache parameters as a dictionary."""
|
|
202
|
+
super().__init__(**data)
|
|
203
|
+
|
|
204
|
+
self._parameters_dict = {
|
|
205
|
+
param.Key: param.Value for param in self.ResultParameters or []
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def transaction_amount(self) -> Optional[int | float]:
|
|
210
|
+
"""Return the TransactionAmount value if present."""
|
|
211
|
+
return self._parameters_dict.get("TransactionAmount")
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def transaction_receipt(self) -> Optional[str]:
|
|
215
|
+
"""Return the TransactionReceipt value if present."""
|
|
216
|
+
return self._parameters_dict.get("TransactionReceipt")
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def recipient_is_registered(self) -> Optional[bool]:
|
|
220
|
+
"""Return True if B2CRecipientIsRegisteredCustomer is 'Y', False if 'N', None if missing."""
|
|
221
|
+
val = self._parameters_dict.get("B2CRecipientIsRegisteredCustomer")
|
|
222
|
+
if val == "Y":
|
|
223
|
+
return True
|
|
224
|
+
if val == "N":
|
|
225
|
+
return False
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def receiver_party_public_name(self) -> Optional[str]:
|
|
230
|
+
"""Return the ReceiverPartyPublicName value if present."""
|
|
231
|
+
return self._parameters_dict.get("ReceiverPartyPublicName")
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def transaction_completed_datetime(self) -> Optional[str]:
|
|
235
|
+
"""Return the TransactionCompletedDateTime value if present."""
|
|
236
|
+
return self._parameters_dict.get("TransactionCompletedDateTime")
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def charges_paid_account_available_funds(self) -> Optional[float]:
|
|
240
|
+
"""Return the B2CChargesPaidAccountAvailableFunds value if present."""
|
|
241
|
+
return self._parameters_dict.get("B2CChargesPaidAccountAvailableFunds")
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def utility_account_available_funds(self) -> Optional[float]:
|
|
245
|
+
"""Return the B2CUtilityAccountAvailableFunds value if present."""
|
|
246
|
+
return self._parameters_dict.get("B2CUtilityAccountAvailableFunds")
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def working_account_available_funds(self) -> Optional[float]:
|
|
250
|
+
"""Return the B2CWorkingAccountAvailableFunds value if present."""
|
|
251
|
+
return self._parameters_dict.get("B2CWorkingAccountAvailableFunds")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class B2CResultCallback(BaseModel):
|
|
255
|
+
"""Schema for B2C result notification sent to ResultURL."""
|
|
256
|
+
|
|
257
|
+
Result: B2CResultMetadata = Field(..., description="Result metadata.")
|
|
258
|
+
|
|
259
|
+
model_config = ConfigDict(
|
|
260
|
+
json_schema_extra={
|
|
261
|
+
"example": {
|
|
262
|
+
"Result": {
|
|
263
|
+
"ResultType": 0,
|
|
264
|
+
"ResultCode": 0,
|
|
265
|
+
"ResultDesc": "The service request is processed successfully.",
|
|
266
|
+
"OriginatorConversationID": "12345-67890-1",
|
|
267
|
+
"ConversationID": "AG_20170717_00006c6f7f5b8b6b1a62",
|
|
268
|
+
"TransactionID": "LKXXXX1234",
|
|
269
|
+
"ResultParameters": [
|
|
270
|
+
{"Key": "TransactionAmount", "Value": 1000},
|
|
271
|
+
{"Key": "TransactionReceipt", "Value": "LKXXXX1234"},
|
|
272
|
+
],
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class B2CResultCallbackResponse(BaseModel):
|
|
280
|
+
"""Schema for response to B2C result callback."""
|
|
281
|
+
|
|
282
|
+
ResultCode: int = Field(
|
|
283
|
+
default=0, description="Result code (0=Success, other=Failure)."
|
|
284
|
+
)
|
|
285
|
+
ResultDesc: str = Field(
|
|
286
|
+
default="Result received and processed successfully.",
|
|
287
|
+
description="Result description.",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
model_config = ConfigDict(
|
|
291
|
+
json_schema_extra={
|
|
292
|
+
"example": {
|
|
293
|
+
"ResultCode": 0,
|
|
294
|
+
"ResultDesc": "Result received and processed successfully.",
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class B2CTimeoutCallback(BaseModel):
|
|
301
|
+
"""Schema for B2C timeout notification sent to QueueTimeOutURL."""
|
|
302
|
+
|
|
303
|
+
Result: B2CResultMetadata = Field(..., description="Result metadata.")
|
|
304
|
+
|
|
305
|
+
model_config = ConfigDict(
|
|
306
|
+
json_schema_extra={
|
|
307
|
+
"example": {
|
|
308
|
+
"Result": {
|
|
309
|
+
"ResultType": 0,
|
|
310
|
+
"ResultCode": 0,
|
|
311
|
+
"ResultDesc": "The service request is processed successfully.",
|
|
312
|
+
"OriginatorConversationID": "12345-67890-1",
|
|
313
|
+
"ConversationID": "AG_20170717_00006c6f7f5b8b6b1a62",
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class B2CTimeoutCallbackResponse(BaseModel):
|
|
321
|
+
"""Schema for response to B2C timeout callback."""
|
|
322
|
+
|
|
323
|
+
ResultCode: int = Field(
|
|
324
|
+
default=0,
|
|
325
|
+
description="Result code (0=Success, other=Failure).",
|
|
326
|
+
)
|
|
327
|
+
ResultDesc: str = Field(
|
|
328
|
+
default="Timeout notification received and processed successfully.",
|
|
329
|
+
description="Result description.",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
model_config = ConfigDict(
|
|
333
|
+
json_schema_extra={
|
|
334
|
+
"example": {
|
|
335
|
+
"ResultCode": 0,
|
|
336
|
+
"ResultDesc": "Timeout notification received and processed successfully.",
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""B2CAccountTopUp: Handles M-Pesa B2C Account Topup API interactions.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to initiate a B2C Account Topup transaction and handle result/timeout notifications
|
|
4
|
+
using the M-Pesa API. Requires a valid access token for authentication and uses the HttpClient for HTTP requests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
from mpesakit.auth import TokenManager
|
|
9
|
+
from mpesakit.http_client import HttpClient
|
|
10
|
+
|
|
11
|
+
from .schemas import (
|
|
12
|
+
B2CAccountTopUpRequest,
|
|
13
|
+
B2CAccountTopUpResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class B2CAccountTopUp(BaseModel):
|
|
18
|
+
"""Represents the B2C Account TopUp API client for M-Pesa operations.
|
|
19
|
+
|
|
20
|
+
https://developer.safaricom.co.ke/APIs/B2CAccountTopUp
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
http_client (HttpClient): HTTP client for making requests to the M-Pesa API.
|
|
24
|
+
token_manager (TokenManager): Manages access tokens for authentication.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
http_client: HttpClient
|
|
28
|
+
token_manager: TokenManager
|
|
29
|
+
|
|
30
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
31
|
+
|
|
32
|
+
def topup(self, request: B2CAccountTopUpRequest) -> B2CAccountTopUpResponse:
|
|
33
|
+
"""Initiates a B2C Account TopUp transaction.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
request (B2CAccountTopUpRequest): The B2C Account TopUp request data.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
B2CAccountTopUpResponse: Response from the M-Pesa API.
|
|
40
|
+
"""
|
|
41
|
+
url = "/mpesa/b2b/v1/paymentrequest"
|
|
42
|
+
headers = {
|
|
43
|
+
"Authorization": f"Bearer {self.token_manager.get_token()}",
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
}
|
|
46
|
+
response_data = self.http_client.post(
|
|
47
|
+
url, json=request.model_dump(mode="json"), headers=headers
|
|
48
|
+
)
|
|
49
|
+
return B2CAccountTopUpResponse(**response_data)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .schemas import (
|
|
2
|
+
B2CAccountTopUpRequest,
|
|
3
|
+
B2CAccountTopUpResponse,
|
|
4
|
+
B2CAccountTopUpCallback,
|
|
5
|
+
B2CAccountTopUpCallbackResponse,
|
|
6
|
+
B2CAccountTopUpTimeoutCallback,
|
|
7
|
+
B2CAccountTopUpTimeoutCallbackResponse,
|
|
8
|
+
)
|
|
9
|
+
from .B2C_account_top_up import B2CAccountTopUp
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"B2CAccountTopUp",
|
|
13
|
+
"B2CAccountTopUpRequest",
|
|
14
|
+
"B2CAccountTopUpResponse",
|
|
15
|
+
"B2CAccountTopUpCallback",
|
|
16
|
+
"B2CAccountTopUpCallbackResponse",
|
|
17
|
+
"B2CAccountTopUpTimeoutCallback",
|
|
18
|
+
"B2CAccountTopUpTimeoutCallbackResponse",
|
|
19
|
+
]
|