Skip to content

Daraja 3.0 customer payments: Dynamic QR, B2B Express CheckOut, Pull Transactions - #64

Merged
newtonmunene99 merged 16 commits into
devfrom
feature/customer-payments_20261007
Oct 7, 2026
Merged

newtonmunene99 merged 16 commits into
devfrom
feature/customer-payments_20261007

Conversation

@newtonmunene99

Copy link
Copy Markdown
Owner

Adds three Daraja 3.0 products. None of them needs an initiator.

  • Dynamic QR: mpesa.qr.generate returns a QR code as a base64 PNG.
  • B2B Express CheckOut: mpesa.b2b.expressCheckout sends a USSD prompt to a merchant's till to pay your paybill. The new parseExpressCheckoutCallback reads its result.
  • Pull Transactions: mpesa.pullTransactions.register, query and all. They list the last 48 hours of C2B payments to a shortcode. all is an async iterator over every page.

Transport: ctx.post takes an optional per-call success rule for ResponseCode. Pull Transactions uses it to accept 1000 and 1001. The default is still all zeros, so the existing APIs are unchanged.

Sandbox runs

  • Dynamic QR works live and returned ResponseCode "00" with no request ID.
  • Express CheckOut was refused with a 401. Pull Transactions register and query were refused with 401.001. In each case the token worked for QR, so the sandbox app most likely lacks these products.
  • The refusals are pinned as fixtures. The request shapes and parsers follow the portal samples.
  • Each docs page says what was tested live.

Docs: new pages for Dynamic QR and Pull Transactions, and an Express CheckOut section on the B2B page. Also updated: the sidebar, README, exports reference, callbacks, limits, errors, configuration, certificates and migration pages.

Changeset: minor. Together with the disbursements changeset, the next release is 4.0.0-alpha.1.

Checks: vp check, vp test (476 tests), vp pack, test:dist, lint:pkg and docs:build all pass.

@newtonmunene99

Copy link
Copy Markdown
Owner Author

@claude review

@claude

claude Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Claude finished @newtonmunene99's task in 24s —— View job


Review of #64: Daraja 3.0 customer payments

I read the src/ changes (QR, Express CheckOut, Pull Transactions, the ctx.post success option and formatEatDateTime). I skimmed the docs and fixtures but didn't read them line by line, and I didn't run the test suite, so the PR's own check results are unverified by me. Overall the code is clean and consistent with the existing APIs. The default ResponseCode behaviour is unchanged, and the validation and error paths are well covered. I have no blocking issues, but there are a few things worth a look.

Worth checking

  1. pullTransactions.all may throw after yielding everything (src/apis/pull-transactions.ts, all). The loop only stops when a page comes back empty or has no new IDs. Your own docs say Daraja answers "no transactions" with an HTTP 500 in some cases. If the real last request (offset equal to the total count) returns that 500 rather than a 1001 body, the iterator rejects after yielding every transaction. A consumer's for await would then fail on a fully successful run. I can't tell from the sandbox fixtures which one Daraja sends, since register and query were both refused with 401.001. Options:

    • Treat that specific 500 as end-of-data when offset > 0.
    • Stop early when a page has fewer rows than the first page.
    • Keep the current behaviour, but call it out as a known limitation in docs/apis/pull-transactions.md.
  2. b2b.expressCheckout returns a non-zero code instead of throwing (src/apis/b2b.ts, around lines 288-306). The comment explains this: the acknowledgement uses code, not ResponseCode, so the transport check doesn't apply. It's still inconsistent with every other method, where a rejected request throws DarajaApiError. A caller who does await mpesa.b2b.expressCheckout(...) and moves on would treat a refused push as sent. Either throw on a non-zero code or put a prominent warning in the JSDoc and docs. The JSDoc currently says only that the acknowledgement confirms the prompt was sent.

  3. ISO date regex vs. Date parsing (readIsoDate). The regex accepts +0300 with no colon. new Date('2020-08-05T10:13:00+0300') is not guaranteed to parse in ISO form on every engine. When it fails, isDate rejects it and the whole query throws ValidationError, which loses the page. Normalising ±hhmm to ±hh:mm before constructing the Date would avoid that.

Minor

  • readTransactions reports issues under transactions[index], where index is the position after .flat(). That can differ from the position in Daraja's nested array.
  • In all, the .filter(... && seen.add(...)) line relies on Set.add returning the set to be truthy. It works, but an explicit loop or !seen.has(id) && (seen.add(id), true) would read more clearly.
  • parseExpressCheckoutCallback uses amountCents! after throwIfAny. It's safe, but restructuring so the type narrows would remove the assertion.
  • Unit coverage of the sandbox-refusal fixtures is good. A test where the final page returns the documented 500 would cover item 1 whichever way you decide.

Looks good otherwise: the per-call success rule is a small, backward-compatible change, register correctly checks ResponseStatus itself, and time.ts removes duplication cleanly.

A non-zero or missing acknowledgement code now throws DarajaApiError, like every other method, so a refused push can't be mistaken for a sent one.
…sets

all() stops after a page shorter than the first, avoiding the request for an empty page that Daraja may answer with HTTP 500. A trxDate offset without a colon is rewritten to +hh:mm before parsing.
@newtonmunene99
newtonmunene99 merged commit e65588d into dev Oct 7, 2026
4 checks passed
@newtonmunene99
newtonmunene99 deleted the feature/customer-payments_20261007 branch October 7, 2026 14:20
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.

1 participant