Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion mpesakit/dynamic_qr_code/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ class DynamicQRGenerateResponse(BaseModel):
json_schema_extra={
"example": {
"ResponseCode": "00",
"RequestID": "16738-27456357-1",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this been changed inside Daraja?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that the Dynamic QR Code documentation is inconsistent. The response example contains a RequestID field, but the documented response schema only includes ResponseCode, ResponseDescription, and QRCode
Thus I mitigated by updating the example.
https://developer.safaricom.co.ke/apis/DynamicQRCode
image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for catching this inconsistency 👍

"ResponseDescription": "QR Code Successfully Generated.",
"QRCode": "iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAIAAAD2HxkiAAAHtElEQVR42...",
}
Expand Down
124 changes: 124 additions & 0 deletions mpesakit/mpesa_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,127 @@ def __init__(
self.ratiba = RatibaService(
http_client=self.http_client, token_manager=self.token_manager
)


def process_stk_callback(self, payload):
"""Process STK Push callback payload.

Validates and parses STK Push simulation callback data.
"""
from mpesakit.mpesa_express.schemas import (
StkPushSimulateCallback
)
return StkPushSimulateCallback.model_validate(payload)

def process_stk_query_callback(self, payload):
"""Process STK Push query response payload.

Validates and parses STK Push query response data.
"""
from mpesakit.mpesa_express.schemas import (
StkPushQueryResponse
)
return StkPushQueryResponse.model_validate(payload)

def process_account_balance_callback(self, payload):
"""Process account balance callback payload.

Validates and parses account balance query result callback.
"""
from mpesakit.account_balance.schemas import (
AccountBalanceResultCallback
)
return AccountBalanceResultCallback.model_validate(payload)

def process_account_balance_timeout(self, payload):
"""Process account balance timeout callback payload.

Validates and parses account balance query timeout notification.
"""
from mpesakit.account_balance.schemas import (
AccountBalanceTimeoutCallback
)
return AccountBalanceTimeoutCallback.model_validate(payload)

def process_b2c_callback(self, payload):
"""Process B2C (Business-to-Customer) callback payload.

Validates and parses B2C payment result callback.
"""
from mpesakit.b2c.schemas import (
B2CResultCallback
)
return B2CResultCallback.model_validate(payload)

def process_b2b_callback(self, payload):
"""Process B2B Express Checkout callback payload.

Validates and parses B2B Express Checkout response data.
"""
from mpesakit.b2b_express_checkout.schemas import (
B2BExpressCheckoutCallback
)
return B2BExpressCheckoutCallback.model_validate(payload)

def process_transactions_callback(self, payload):
"""Process transaction status callback payload.

Validates and parses transaction status query result callback.
"""
from mpesakit.transaction_status.schemas import (
TransactionStatusResultCallback
)
return TransactionStatusResultCallback.model_validate(payload)

def process_bill_manager_callback(self, payload):
"""Process bill manager callback payload.

Validates and parses bill manager payment notification.
"""
from mpesakit.bill_manager.schemas import (
BillManagerPaymentNotificationRequest
)
return BillManagerPaymentNotificationRequest.model_validate(payload)


def process_tax_remittance_callback(self, payload):
"""Process tax remittance callback payload.

Validates and parses tax remittance result callback.
"""
from mpesakit.tax_remittance.schemas import (
TaxRemittanceResultCallback
)
return TaxRemittanceResultCallback.model_validate(payload)

def process_dynamic_qr_code_callback(self, payload):
"""Process dynamic QR code callback payload.

Validates and parses dynamic QR code generation response.
"""
from mpesakit.dynamic_qr_code.schemas import (
DynamicQRGenerateResponse
)
return DynamicQRGenerateResponse.model_validate(payload)

def process_ratiba_service_callback(self, payload):
"""Process Ratiba (Standing Order) callback payload.

Validates and parses M-PESA Ratiba services callback.
"""
from mpesakit.mpesa_ratiba.schemas import (
StandingOrderCallback
)
return StandingOrderCallback.model_validate(payload)

def process_reversal_callback(self, payload):
"""Process transaction reversal callback payload.

Validates and parses transaction reversal result callback.
"""
from mpesakit.reversal.schemas import (
ReversalResultCallback
)
return ReversalResultCallback.model_validate(payload)


255 changes: 255 additions & 0 deletions tests/unit/test_mpesa_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,258 @@ def test_c2b_service_instance(client):
def test_ratiba_service_instance(client):
"""Test that the ratiba service is an instance of RatibaService."""
assert isinstance(client.ratiba, RatibaService)


# Tests for callback processing methods
class TestCallbackProcessing:
"""Tests for MpesaClient callback processing methods."""

def test_process_stk_callback(self, client):
"""Test processing STK Push callback payload."""
payload = {
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{"Name": "Amount", "Value": 1.0},
{"Name": "MpesaReceiptNumber", "Value": "LHG31AA5TX"},
{"Name": "Balance"},
{"Name": "TransactionDate", "Value": 20191219102115},
{"Name": "PhoneNumber", "Value": 254712345678},
]
},
}
}
}
result = client.process_stk_callback(payload)
assert result.Body.stkCallback.MerchantRequestID == "29115-34620561-1"
assert result.Body.stkCallback.ResultCode == 0
assert result.amount == 1.0
assert result.mpesa_receipt_number == "LHG31AA5TX"

def test_process_stk_query_callback(self, client):
"""Test processing STK Push query callback payload."""
payload = {
"MerchantRequestID": "12345",
"CheckoutRequestID": "ws_CO_260520211133524545",
"ResponseCode": 0,
"ResponseDescription": "Success",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
}
result = client.process_stk_query_callback(payload)
assert result.ResultCode == 0
assert result.MerchantRequestID == "12345"

def test_process_account_balance_callback(self, client):
"""Test processing account balance callback payload."""
payload = {
"Result": {
"ResultType": 0,
"ResultCode": 0,
"ResultDesc": "The service request has been processed successfully.",
"OriginatorConversationID": "10571-774651-1",
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
"TransactionID": "LHG31AA5TX",
"ResultParameters": {
"ResultParameter": [
{
"Key": "AccountBalance",
"Value": "580000.00",
}
]
},
"ReferenceData": {
"ReferenceItem": {
"Key": "QueueOfficeNumber",
"Value": "00000",
}
},
}
}
result = client.process_account_balance_callback(payload)
assert result.Result.ResultCode == 0
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"

def test_process_account_balance_timeout(self, client):
"""Test processing account balance timeout callback payload."""
payload = {
"Result": {
"ResultType": 0,
"ResultCode": 2001,
"ResultDesc": "The service request has timed out.",
"OriginatorConversationID": "10571-774651-1",
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
"TransactionID": "LHG31AA5TX",
}
}
result = client.process_account_balance_timeout(payload)
assert result.Result.ResultCode == 2001
assert "timed out" in result.Result.ResultDesc

def test_process_b2c_callback(self, client):
"""Test processing B2C callback payload."""
payload = {
"Result": {
"ResultType": 0,
"ResultCode": 0,
"ResultDesc": "The service request has been processed successfully.",
"OriginatorConversationID": "10571-774651-1",
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
"TransactionID": "LHG31AA5TX",
"ResultParameters": [
{"Key": "TransactionAmount", "Value": "100.00"},
{"Key": "TransactionReceipt", "Value": "LHG31AA5TX"},
{"Key": "B2CRecipientIsLocked", "Value": "false"},
{"Key": "B2CChargesPaidAccountAvailableFunds", "Value": "49900.00"},
{"Key": "B2CUtilityAccountAvailableFunds", "Value": "199900.00"},
{"Key": "TransactionCompletedDateTime", "Value": "31.12.2021 23:59:59"},
{"Key": "B2CRecipientPhoneNumber", "Value": "254712345678"},
],

}
}
result = client.process_b2c_callback(payload)
assert result.Result.ResultCode == 0
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"

def test_process_b2b_callback(self, client):
"""Test processing B2B callback payload."""
payload = {
"resultCode": "0",
"resultDesc": "The service request is processed successfully.",
"amount": "71.0",
"requestId": "404e1aec-19e0-4ce3-973d-bd92e94c8021",
"resultType": "0",
"conversationID": "AG_20230426_2010434680d9f5a73766",
"transactionId": "RDQ01NFT1Q",
"status": "SUCCESS",
}
result = client.process_b2b_callback(payload)
assert result.conversationID == "AG_20230426_2010434680d9f5a73766"
assert result.resultCode == "0"

def test_process_transactions_callback(self, client):
"""Test processing transaction status callback payload."""
payload = {
"Result": {
"ResultType": 0,
"ResultCode": 0,
"ResultDesc": "The service request has been processed successfully.",
"OriginatorConversationID": "10571-774651-1",
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
"TransactionID": "LHG31AA5TX",
"ResultParameters": [
{"Key": "TransactionAmount", "Value": "100.00"},
{"Key": "TransactionStatus", "Value": "Completed"},
{"Key": "TransactionDate", "Value": "31.12.2021 23:59:59"},
{"Key": "ReceiptNo", "Value": "LHG31AA5TX"},
],

}
}
result = client.process_transactions_callback(payload)
assert result.Result.ResultCode == 0
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"

def test_process_bill_manager_callback(self, client):
"""Test processing bill manager callback payload."""
payload = {
"transactionId": "RJB53MYR1N",
"paidAmount": 5000,
"msisdn": "254722000000",
"dateCreated": "2021-10-01",
"accountReference": "BC001",
"shortCode": 456545,
}
result = client.process_bill_manager_callback(payload)
assert result.msisdn == "254722000000"
assert result.paidAmount == 5000

def test_process_tax_remittance_callback(self, client):
"""Test processing tax remittance callback payload."""
payload = {
"Result": {
"ResultType": 0,
"ResultCode": 0,
"ResultDesc": "The service request has been processed successfully.",
"OriginatorConversationID": "10571-774651-1",
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
"TransactionID": "LHG31AA5TX",
"ResultParameters": {
"ResultParameter": [
{"Key": "TransactionAmount", "Value": "100.00"},
{"Key": "TransactionReceipt", "Value": "LHG31AA5TX"},
{"Key": "TransactionDate", "Value": "31.12.2021 23:59:59"},
]
},
}
}
result = client.process_tax_remittance_callback(payload)
assert result.Result.ResultCode == 0
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"

def test_process_dynamic_qr_code_callback(self, client):
"""Test processing dynamic QR code callback payload."""
payload = {
"ResponseCode": "00000000",
"ResponseDescription": "success",
"QRCode": "00000101010101010101",
}
result = client.process_dynamic_qr_code_callback(payload)
assert result.ResponseCode == "00000000"
assert result.ResponseDescription == "success"

def test_process_ratiba_service_callback(self, client):
"""Test processing ratiba service callback payload."""
payload ={
"ResponseHeader": {
"responseRefID": "0acc0239-20fa-4a52-8b9d-9bd64c0465c3",
"requestRefID": "0acc0239-20fa-4a52-8b9d-9bd64c0465c3",
"responseCode": "0",
"responseDescription": "The service request is processed successfully",
},
"ResponseBody": {
"ResponseData": [
{"Name": "TransactionID", "Value": "SC8F2IQMH5"},
{"Name": "responseCode", "Value": "0"},
{"Name": "Status", "Value": "OKAY"},
{"Name": "Msisdn", "Value": "254******867"},
]
},
}

result = client.process_ratiba_service_callback(payload)
assert result.ResponseHeader.requestRefID == "0acc0239-20fa-4a52-8b9d-9bd64c0465c3"
assert any(
item.Name == "TransactionID" and item.Value == "SC8F2IQMH5"
for item in result.ResponseBody.ResponseData
)


def test_process_reversal_callback(self, client):
"""Test processing reversal callback payload."""
payload = {
"Result": {
"ResultType": 0,
"ResultCode": "21",
"ResultDesc": "The service request has been processed successfully.",
"OriginatorConversationID": "10571-774651-1",
"ConversationID": "AN41320161328197f28cc1d183985ef4f1",
"TransactionID": "LHG31AA5TX",
"ResultParameters": {
"ResultParameter": [
{"Key": "TransactionAmount", "Value": "100.00"},
{"Key": "TransactionReceipt", "Value": "LHG31AA5TX"},
{"Key": "TransactionDate", "Value": "31.12.2021 23:59:59"},
]
},
}
}
result = client.process_reversal_callback(payload)
assert result.Result.ResultCode == '21'
assert result.Result.ConversationID == "AN41320161328197f28cc1d183985ef4f1"
Loading