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.
Files changed (69) hide show
  1. mpesakit/B2B_express_checkout/B2B_express_checkout.py +51 -0
  2. mpesakit/B2B_express_checkout/__init__.py +16 -0
  3. mpesakit/B2B_express_checkout/schemas.py +127 -0
  4. mpesakit/B2C/B2C.py +47 -0
  5. mpesakit/B2C/__init__.py +23 -0
  6. mpesakit/B2C/schemas.py +339 -0
  7. mpesakit/B2C_account_top_up/B2C_account_top_up.py +49 -0
  8. mpesakit/B2C_account_top_up/__init__.py +19 -0
  9. mpesakit/B2C_account_top_up/schemas.py +316 -0
  10. mpesakit/C2B/C2B.py +56 -0
  11. mpesakit/C2B/__init__.py +21 -0
  12. mpesakit/C2B/schemas.py +234 -0
  13. mpesakit/__init__.py +0 -0
  14. mpesakit/account_balance/__init__.py +21 -0
  15. mpesakit/account_balance/account_balance.py +47 -0
  16. mpesakit/account_balance/schemas.py +298 -0
  17. mpesakit/auth/__init__.py +4 -0
  18. mpesakit/auth/access_token.py +22 -0
  19. mpesakit/auth/token_manager.py +86 -0
  20. mpesakit/bill_manager/__init__.py +40 -0
  21. mpesakit/bill_manager/bill_manager.py +133 -0
  22. mpesakit/bill_manager/schemas.py +467 -0
  23. mpesakit/business_buy_goods/__init__.py +19 -0
  24. mpesakit/business_buy_goods/business_buy_goods.py +47 -0
  25. mpesakit/business_buy_goods/schemas.py +302 -0
  26. mpesakit/business_paybill/__init__.py +19 -0
  27. mpesakit/business_paybill/business_paybill.py +47 -0
  28. mpesakit/business_paybill/schemas.py +284 -0
  29. mpesakit/dynamic_qr_code/__init__.py +13 -0
  30. mpesakit/dynamic_qr_code/dynamic_qr_code.py +49 -0
  31. mpesakit/dynamic_qr_code/schemas.py +172 -0
  32. mpesakit/errors.py +49 -0
  33. mpesakit/http_client/__init__.py +4 -0
  34. mpesakit/http_client/http_client.py +28 -0
  35. mpesakit/http_client/mpesa_http_client.py +173 -0
  36. mpesakit/mpesa_client.py +85 -0
  37. mpesakit/mpesa_express/__init__.py +28 -0
  38. mpesakit/mpesa_express/schemas.py +610 -0
  39. mpesakit/mpesa_express/stk_push.py +64 -0
  40. mpesakit/mpesa_ratiba/__init__.py +21 -0
  41. mpesakit/mpesa_ratiba/mpesa_ratiba.py +51 -0
  42. mpesakit/mpesa_ratiba/schemas.py +320 -0
  43. mpesakit/reversal/__init__.py +19 -0
  44. mpesakit/reversal/reversal.py +48 -0
  45. mpesakit/reversal/schemas.py +295 -0
  46. mpesakit/services/__init__.py +25 -0
  47. mpesakit/services/b2b.py +188 -0
  48. mpesakit/services/b2c.py +124 -0
  49. mpesakit/services/balance.py +67 -0
  50. mpesakit/services/bill.py +135 -0
  51. mpesakit/services/c2b.py +56 -0
  52. mpesakit/services/dynamic_qr.py +61 -0
  53. mpesakit/services/express.py +105 -0
  54. mpesakit/services/ratiba.py +82 -0
  55. mpesakit/services/reversal.py +67 -0
  56. mpesakit/services/tax.py +65 -0
  57. mpesakit/services/transaction.py +78 -0
  58. mpesakit/tax_remittance/__init__.py +19 -0
  59. mpesakit/tax_remittance/schemas.py +257 -0
  60. mpesakit/tax_remittance/tax_remittance.py +47 -0
  61. mpesakit/transaction_status/__init__.py +25 -0
  62. mpesakit/transaction_status/schemas.py +325 -0
  63. mpesakit/transaction_status/transaction_status.py +47 -0
  64. mpesakit/utils/ip_whitelist.py +56 -0
  65. mpesakit/utils/phone.py +38 -0
  66. mpesakit-0.1.2.dist-info/METADATA +226 -0
  67. mpesakit-0.1.2.dist-info/RECORD +69 -0
  68. mpesakit-0.1.2.dist-info/WHEEL +4 -0
  69. 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)
@@ -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
+ ]
@@ -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
+ ]