Skip to content

Commit ca214c9

Browse files
authored
Merge pull request #139 from watersRand/feat_webhook
Feat webhook
2 parents 0dcb946 + a8c2a61 commit ca214c9

3 files changed

Lines changed: 379 additions & 1 deletion

File tree

mpesakit/dynamic_qr_code/schemas.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ class DynamicQRGenerateResponse(BaseModel):
158158
json_schema_extra={
159159
"example": {
160160
"ResponseCode": "00",
161-
"RequestID": "16738-27456357-1",
162161
"ResponseDescription": "QR Code Successfully Generated.",
163162
"QRCode": "iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAIAAAD2HxkiAAAHtElEQVR42...",
164163
}

mpesakit/mpesa_client.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,127 @@ def __init__(
8787
self.ratiba = RatibaService(
8888
http_client=self.http_client, token_manager=self.token_manager
8989
)
90+
91+
92+
def process_stk_callback(self, payload):
93+
"""Process STK Push callback payload.
94+
95+
Validates and parses STK Push simulation callback data.
96+
"""
97+
from mpesakit.mpesa_express.schemas import (
98+
StkPushSimulateCallback
99+
)
100+
return StkPushSimulateCallback.model_validate(payload)
101+
102+
def process_stk_query_callback(self, payload):
103+
"""Process STK Push query response payload.
104+
105+
Validates and parses STK Push query response data.
106+
"""
107+
from mpesakit.mpesa_express.schemas import (
108+
StkPushQueryResponse
109+
)
110+
return StkPushQueryResponse.model_validate(payload)
111+
112+
def process_account_balance_callback(self, payload):
113+
"""Process account balance callback payload.
114+
115+
Validates and parses account balance query result callback.
116+
"""
117+
from mpesakit.account_balance.schemas import (
118+
AccountBalanceResultCallback
119+
)
120+
return AccountBalanceResultCallback.model_validate(payload)
121+
122+
def process_account_balance_timeout(self, payload):
123+
"""Process account balance timeout callback payload.
124+
125+
Validates and parses account balance query timeout notification.
126+
"""
127+
from mpesakit.account_balance.schemas import (
128+
AccountBalanceTimeoutCallback
129+
)
130+
return AccountBalanceTimeoutCallback.model_validate(payload)
131+
132+
def process_b2c_callback(self, payload):
133+
"""Process B2C (Business-to-Customer) callback payload.
134+
135+
Validates and parses B2C payment result callback.
136+
"""
137+
from mpesakit.b2c.schemas import (
138+
B2CResultCallback
139+
)
140+
return B2CResultCallback.model_validate(payload)
141+
142+
def process_b2b_callback(self, payload):
143+
"""Process B2B Express Checkout callback payload.
144+
145+
Validates and parses B2B Express Checkout response data.
146+
"""
147+
from mpesakit.b2b_express_checkout.schemas import (
148+
B2BExpressCheckoutCallback
149+
)
150+
return B2BExpressCheckoutCallback.model_validate(payload)
151+
152+
def process_transactions_callback(self, payload):
153+
"""Process transaction status callback payload.
154+
155+
Validates and parses transaction status query result callback.
156+
"""
157+
from mpesakit.transaction_status.schemas import (
158+
TransactionStatusResultCallback
159+
)
160+
return TransactionStatusResultCallback.model_validate(payload)
161+
162+
def process_bill_manager_callback(self, payload):
163+
"""Process bill manager callback payload.
164+
165+
Validates and parses bill manager payment notification.
166+
"""
167+
from mpesakit.bill_manager.schemas import (
168+
BillManagerPaymentNotificationRequest
169+
)
170+
return BillManagerPaymentNotificationRequest.model_validate(payload)
171+
172+
173+
def process_tax_remittance_callback(self, payload):
174+
"""Process tax remittance callback payload.
175+
176+
Validates and parses tax remittance result callback.
177+
"""
178+
from mpesakit.tax_remittance.schemas import (
179+
TaxRemittanceResultCallback
180+
)
181+
return TaxRemittanceResultCallback.model_validate(payload)
182+
183+
def process_dynamic_qr_code_callback(self, payload):
184+
"""Process dynamic QR code callback payload.
185+
186+
Validates and parses dynamic QR code generation response.
187+
"""
188+
from mpesakit.dynamic_qr_code.schemas import (
189+
DynamicQRGenerateResponse
190+
)
191+
return DynamicQRGenerateResponse.model_validate(payload)
192+
193+
def process_ratiba_service_callback(self, payload):
194+
"""Process Ratiba (Standing Order) callback payload.
195+
196+
Validates and parses M-PESA Ratiba services callback.
197+
"""
198+
from mpesakit.mpesa_ratiba.schemas import (
199+
StandingOrderCallback
200+
)
201+
return StandingOrderCallback.model_validate(payload)
202+
203+
def process_reversal_callback(self, payload):
204+
"""Process transaction reversal callback payload.
205+
206+
Validates and parses transaction reversal result callback.
207+
"""
208+
from mpesakit.reversal.schemas import (
209+
ReversalResultCallback
210+
)
211+
return ReversalResultCallback.model_validate(payload)
212+
213+

tests/unit/test_mpesa_client.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,258 @@ def test_c2b_service_instance(client):
8989
def test_ratiba_service_instance(client):
9090
"""Test that the ratiba service is an instance of RatibaService."""
9191
assert isinstance(client.ratiba, RatibaService)
92+
93+
94+
# Tests for callback processing methods
95+
class TestCallbackProcessing:
96+
"""Tests for MpesaClient callback processing methods."""
97+
98+
def test_process_stk_callback(self, client):
99+
"""Test processing STK Push callback payload."""
100+
payload = {
101+
"Body": {
102+
"stkCallback": {
103+
"MerchantRequestID": "29115-34620561-1",
104+
"CheckoutRequestID": "ws_CO_191220191020363925",
105+
"ResultCode": 0,
106+
"ResultDesc": "The service request is processed successfully.",
107+
"CallbackMetadata": {
108+
"Item": [
109+
{"Name": "Amount", "Value": 1.0},
110+
{"Name": "MpesaReceiptNumber", "Value": "LHG31AA5TX"},
111+
{"Name": "Balance"},
112+
{"Name": "TransactionDate", "Value": 20191219102115},
113+
{"Name": "PhoneNumber", "Value": 254712345678},
114+
]
115+
},
116+
}
117+
}
118+
}
119+
result = client.process_stk_callback(payload)
120+
assert result.Body.stkCallback.MerchantRequestID == "29115-34620561-1"
121+
assert result.Body.stkCallback.ResultCode == 0
122+
assert result.amount == 1.0
123+
assert result.mpesa_receipt_number == "LHG31AA5TX"
124+
125+
def test_process_stk_query_callback(self, client):
126+
"""Test processing STK Push query callback payload."""
127+
payload = {
128+
"MerchantRequestID": "12345",
129+
"CheckoutRequestID": "ws_CO_260520211133524545",
130+
"ResponseCode": 0,
131+
"ResponseDescription": "Success",
132+
"ResultCode": 0,
133+
"ResultDesc": "The service request is processed successfully.",
134+
}
135+
result = client.process_stk_query_callback(payload)
136+
assert result.ResultCode == 0
137+
assert result.MerchantRequestID == "12345"
138+
139+
def test_process_account_balance_callback(self, client):
140+
"""Test processing account balance callback payload."""
141+
payload = {
142+
"Result": {
143+
"ResultType": 0,
144+
"ResultCode": 0,
145+
"ResultDesc": "The service request has been processed successfully.",
146+
"OriginatorConversationID": "10571-774651-1",
147+
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
148+
"TransactionID": "LHG31AA5TX",
149+
"ResultParameters": {
150+
"ResultParameter": [
151+
{
152+
"Key": "AccountBalance",
153+
"Value": "580000.00",
154+
}
155+
]
156+
},
157+
"ReferenceData": {
158+
"ReferenceItem": {
159+
"Key": "QueueOfficeNumber",
160+
"Value": "00000",
161+
}
162+
},
163+
}
164+
}
165+
result = client.process_account_balance_callback(payload)
166+
assert result.Result.ResultCode == 0
167+
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"
168+
169+
def test_process_account_balance_timeout(self, client):
170+
"""Test processing account balance timeout callback payload."""
171+
payload = {
172+
"Result": {
173+
"ResultType": 0,
174+
"ResultCode": 2001,
175+
"ResultDesc": "The service request has timed out.",
176+
"OriginatorConversationID": "10571-774651-1",
177+
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
178+
"TransactionID": "LHG31AA5TX",
179+
}
180+
}
181+
result = client.process_account_balance_timeout(payload)
182+
assert result.Result.ResultCode == 2001
183+
assert "timed out" in result.Result.ResultDesc
184+
185+
def test_process_b2c_callback(self, client):
186+
"""Test processing B2C callback payload."""
187+
payload = {
188+
"Result": {
189+
"ResultType": 0,
190+
"ResultCode": 0,
191+
"ResultDesc": "The service request has been processed successfully.",
192+
"OriginatorConversationID": "10571-774651-1",
193+
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
194+
"TransactionID": "LHG31AA5TX",
195+
"ResultParameters": [
196+
{"Key": "TransactionAmount", "Value": "100.00"},
197+
{"Key": "TransactionReceipt", "Value": "LHG31AA5TX"},
198+
{"Key": "B2CRecipientIsLocked", "Value": "false"},
199+
{"Key": "B2CChargesPaidAccountAvailableFunds", "Value": "49900.00"},
200+
{"Key": "B2CUtilityAccountAvailableFunds", "Value": "199900.00"},
201+
{"Key": "TransactionCompletedDateTime", "Value": "31.12.2021 23:59:59"},
202+
{"Key": "B2CRecipientPhoneNumber", "Value": "254712345678"},
203+
],
204+
205+
}
206+
}
207+
result = client.process_b2c_callback(payload)
208+
assert result.Result.ResultCode == 0
209+
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"
210+
211+
def test_process_b2b_callback(self, client):
212+
"""Test processing B2B callback payload."""
213+
payload = {
214+
"resultCode": "0",
215+
"resultDesc": "The service request is processed successfully.",
216+
"amount": "71.0",
217+
"requestId": "404e1aec-19e0-4ce3-973d-bd92e94c8021",
218+
"resultType": "0",
219+
"conversationID": "AG_20230426_2010434680d9f5a73766",
220+
"transactionId": "RDQ01NFT1Q",
221+
"status": "SUCCESS",
222+
}
223+
result = client.process_b2b_callback(payload)
224+
assert result.conversationID == "AG_20230426_2010434680d9f5a73766"
225+
assert result.resultCode == "0"
226+
227+
def test_process_transactions_callback(self, client):
228+
"""Test processing transaction status callback payload."""
229+
payload = {
230+
"Result": {
231+
"ResultType": 0,
232+
"ResultCode": 0,
233+
"ResultDesc": "The service request has been processed successfully.",
234+
"OriginatorConversationID": "10571-774651-1",
235+
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
236+
"TransactionID": "LHG31AA5TX",
237+
"ResultParameters": [
238+
{"Key": "TransactionAmount", "Value": "100.00"},
239+
{"Key": "TransactionStatus", "Value": "Completed"},
240+
{"Key": "TransactionDate", "Value": "31.12.2021 23:59:59"},
241+
{"Key": "ReceiptNo", "Value": "LHG31AA5TX"},
242+
],
243+
244+
}
245+
}
246+
result = client.process_transactions_callback(payload)
247+
assert result.Result.ResultCode == 0
248+
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"
249+
250+
def test_process_bill_manager_callback(self, client):
251+
"""Test processing bill manager callback payload."""
252+
payload = {
253+
"transactionId": "RJB53MYR1N",
254+
"paidAmount": 5000,
255+
"msisdn": "254722000000",
256+
"dateCreated": "2021-10-01",
257+
"accountReference": "BC001",
258+
"shortCode": 456545,
259+
}
260+
result = client.process_bill_manager_callback(payload)
261+
assert result.msisdn == "254722000000"
262+
assert result.paidAmount == 5000
263+
264+
def test_process_tax_remittance_callback(self, client):
265+
"""Test processing tax remittance callback payload."""
266+
payload = {
267+
"Result": {
268+
"ResultType": 0,
269+
"ResultCode": 0,
270+
"ResultDesc": "The service request has been processed successfully.",
271+
"OriginatorConversationID": "10571-774651-1",
272+
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
273+
"TransactionID": "LHG31AA5TX",
274+
"ResultParameters": {
275+
"ResultParameter": [
276+
{"Key": "TransactionAmount", "Value": "100.00"},
277+
{"Key": "TransactionReceipt", "Value": "LHG31AA5TX"},
278+
{"Key": "TransactionDate", "Value": "31.12.2021 23:59:59"},
279+
]
280+
},
281+
}
282+
}
283+
result = client.process_tax_remittance_callback(payload)
284+
assert result.Result.ResultCode == 0
285+
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"
286+
287+
def test_process_dynamic_qr_code_callback(self, client):
288+
"""Test processing dynamic QR code callback payload."""
289+
payload = {
290+
"ResponseCode": "00000000",
291+
"ResponseDescription": "success",
292+
"QRCode": "00000101010101010101",
293+
}
294+
result = client.process_dynamic_qr_code_callback(payload)
295+
assert result.ResponseCode == "00000000"
296+
assert result.ResponseDescription == "success"
297+
298+
def test_process_ratiba_service_callback(self, client):
299+
"""Test processing ratiba service callback payload."""
300+
payload ={
301+
"ResponseHeader": {
302+
"responseRefID": "0acc0239-20fa-4a52-8b9d-9bd64c0465c3",
303+
"requestRefID": "0acc0239-20fa-4a52-8b9d-9bd64c0465c3",
304+
"responseCode": "0",
305+
"responseDescription": "The service request is processed successfully",
306+
},
307+
"ResponseBody": {
308+
"ResponseData": [
309+
{"Name": "TransactionID", "Value": "SC8F2IQMH5"},
310+
{"Name": "responseCode", "Value": "0"},
311+
{"Name": "Status", "Value": "OKAY"},
312+
{"Name": "Msisdn", "Value": "254******867"},
313+
]
314+
},
315+
}
316+
317+
result = client.process_ratiba_service_callback(payload)
318+
assert result.ResponseHeader.requestRefID == "0acc0239-20fa-4a52-8b9d-9bd64c0465c3"
319+
assert any(
320+
item.Name == "TransactionID" and item.Value == "SC8F2IQMH5"
321+
for item in result.ResponseBody.ResponseData
322+
)
323+
324+
325+
def test_process_reversal_callback(self, client):
326+
"""Test processing reversal callback payload."""
327+
payload = {
328+
"Result": {
329+
"ResultType": 0,
330+
"ResultCode": "21",
331+
"ResultDesc": "The service request has been processed successfully.",
332+
"OriginatorConversationID": "10571-774651-1",
333+
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
334+
"TransactionID": "LHG31AA5TX",
335+
"ResultParameters": {
336+
"ResultParameter": [
337+
{"Key": "TransactionAmount", "Value": "100.00"},
338+
{"Key": "TransactionReceipt", "Value": "LHG31AA5TX"},
339+
{"Key": "TransactionDate", "Value": "31.12.2021 23:59:59"},
340+
]
341+
},
342+
}
343+
}
344+
result = client.process_reversal_callback(payload)
345+
assert result.Result.ResultCode == '21'
346+
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"

0 commit comments

Comments
 (0)