Skip to content

Feat webhook#139

Merged
RafaelJohn9 merged 4 commits into
Byte-Barn:masterfrom
watersRand:feat_webhook
Jun 24, 2026
Merged

Feat webhook#139
RafaelJohn9 merged 4 commits into
Byte-Barn:masterfrom
watersRand:feat_webhook

Conversation

@watersRand

Copy link
Copy Markdown
Contributor

Description

This PR introduces a unified callback routing feature (MpesaClient.process_callback(payload)) that dynamically detects the signature and structure of incoming M-PESA webhook payloads (e.g., STK Push, Ratiba, B2C, Bill Manager) and automatically forwards them to their respective strongly-typed validation and parsing models.

Closes #133

Motivation:
Reduces cognitive load significantly for developers integrating the SDK. Instead of manually inspecting the payload structure in their web controllers to decide whether to call .process_stk_callback() or .process_ratiba_service_callback(), they can now pass any raw M-PESA JSON payload to a single, intuitive entry point.

Additionally, this PR fixes a documentation example in the Dynamic QR Code Callback section. In alignment with official Safaricom Daraja documentation, the RequestID field is omitted as it is not part of the standard Dynamic QR response schema.

Type of Change

  • New feature (non-breaking change that adds functionality)
  • Refactor (code structure improvements, no new functionality)
  • Tests (addition or improvement of tests)
  • This change requires documentation update

How Has This Been Tested?

  • Unit Tests: Added a new test suite class TestUnifiedCallbackRouting inside tests/unit/test_mpesa_client.py to verify accurate structural sniffing and correct downstream schema generation across distinct payloads (STK Push, Ratiba, B2B, etc.).
  • Regression Testing: Executed all 25+ existing test cases locally to confirm zero breaking changes to explicit parser methods.
  • Bug Fix Verification: Verified the test_process_ratiba_service_callback list-traversal structure passes cleanly without throwing AttributeError.

Checklist

  • My code follows the project's coding style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Additional Context

The sniffing router handles nuanced payload definitions (such as differentiating structural commonalities like Result parameters between Account Balance, Tax, and B2C transactions) by inspect-matching signature keys natively embedded within Safaricom’s standard response parameters.

@RafaelJohn9 RafaelJohn9 left a comment

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.

Hey @watersRand , Thanks for the PR.

Overally this is a good PR, 💯

On process_callback it automates the boring part of Callback handling. We are to make sure we do it here and we do it correctly. That's why we do need strictness and correctness on this.

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
Contributor 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 👍

Comment thread mpesakit/mpesa_client.py Outdated
Comment on lines +103 to +105
# 2. Ratiba Standing Orders ("ResponseHeader" & "ResponseBody")
if "ResponseHeader" in payload and "ResponseBody" in payload:
return self.process_ratiba_service_callback(payload)

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.

maybe we could look for something a little deeper that is inherent to the Ratiba API callback schema.
If there is else we can have irrefutable proof, it's the only schema that has this

@watersRand watersRand Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Apologies for the nested if/else implementation. After further consideration, I believe a generic process_callback() method introduces unnecessary complexity because developers already know which M-PESA callback endpoint they are handling.

I am considering removing process_callback() entirely and instead exposing only the explicit callback processors (e.g. STK Push, B2C, Account Balance, Ratiba, Reversal, etc.), allowing developers to invoke the appropriate parser directly based on the callback they receive.

In the future, I may revisit a unified callback processor using a registry-based factory, decorators, or other metaprogramming techniques to eliminate the conditional logic while preserving maintainability. For now, I believe the explicit approach is simpler, clearer, and easier to maintain.

Comment thread mpesakit/mpesa_client.py Outdated
Comment on lines +111 to +113
# 4. Bill Manager Notification
if "accountReference" in payload and "shortCode" in payload:
return self.process_bill_manager_callback(payload)

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.

same here

maybe we could look for something a little deeper that is inherent to the Bill Manager API callback schema
If there is else we can have irrefutable proof, it's the only schema that has this

Comment thread mpesakit/mpesa_client.py Outdated
Comment on lines +115 to +117
# 5. B2B Express Checkout
if "requestId" in payload and "conversationID" in payload:
return self.process_b2b_callback(payload)

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.

same here

maybe we could look for something a little deeper that is inherent to the B2B Express Checkout API callback schema
If there is else we can have irrefutable proof, it's the only schema that has this

Comment thread mpesakit/mpesa_client.py Outdated
Comment on lines +119 to +121
# 6. STK Query Response
if "CheckoutRequestID" in payload and "MerchantRequestID" in payload and "Body" not in payload:
return self.process_stk_query_callback(payload)

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.

same here

maybe we could look for something a little deeper that is inherent to the STK QueryAPI callback schema
If there is else we can have irrefutable proof, it's the only schema that has this

Comment thread mpesakit/mpesa_client.py Outdated
Comment on lines +139 to +141
# Tax Remittance check
if any(i.get("Key") == "TransactionReceipt" for i in items) and not any(i.get("Key") == "B2CRecipientPhoneNumber" for i in items):
return self.process_tax_remittance_callback(payload)

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.

same here

maybe we could look for something a little deeper that is inherent to the Tax Remmitance API callback schema
If there is else we can have irrefutable proof, it's the only schema that has this

@RafaelJohn9 RafaelJohn9 left a comment

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.

@watersRand , I do agree with you on this, we can remove the function for now, then this PR is good to go 👍

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.

thanks for catching this inconsistency 👍

@RafaelJohn9 RafaelJohn9 left a comment

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.

Awesome!

@RafaelJohn9 RafaelJohn9 merged commit ca214c9 into Byte-Barn:master Jun 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add Webhook response validator inside the facade

2 participants