feat(kes): add M-Pesa channel foundations - #635
Conversation
📝 WalkthroughWalkthroughAdds typed KES M-Pesa channel metadata, expands SAFAKEPC into Mobile, Till, and Paybill institutions for KES, formats channel-specific recipient details, updates placeholders, and adds Jest coverage. ChangesKES M-Pesa channel support
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/utils.ts`:
- Around line 170-202: Update formatRecipientInstitutionDisplay to explicitly
return the generic M-PESA label for KES recipients whose institutionCode is
KES_MPESA_INSTITUTION_CODE and whose channel is absent, before falling back to
getInstitutionNameByCode; preserve the existing channel-specific and
accountIdentifier behavior, and add a regression test covering expanded M-Pesa
institutions with a legacy channel-less recipient.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 21495783-2889-4921-960c-62f78eafb869
📒 Files selected for processing (3)
__tests__/kes-mpesa-channel.test.tsapp/types.tsapp/utils.ts
| export function formatRecipientInstitutionDisplay( | ||
| institutionCode: string, | ||
| supportedInstitutions: InstitutionProps[], | ||
| options?: { | ||
| currency?: string; | ||
| channel?: KesMpesaChannel | "" | null; | ||
| accountIdentifier?: string; | ||
| businessNumber?: string | null; | ||
| }, | ||
| ): string { | ||
| const channel = options?.channel; | ||
| const isKesMpesa = | ||
| (options?.currency ?? "").toUpperCase() === "KES" && | ||
| institutionCode === KES_MPESA_INSTITUTION_CODE && | ||
| !!channel; | ||
|
|
||
| if (isKesMpesa && options?.accountIdentifier !== undefined) { | ||
| return formatKesMpesaAccountDisplay( | ||
| options.accountIdentifier, | ||
| channel, | ||
| options.businessNumber, | ||
| ); | ||
| } | ||
|
|
||
| if (isKesMpesa) { | ||
| return getKesMpesaInstitutionLabel(channel); | ||
| } | ||
|
|
||
| return ( | ||
| getInstitutionNameByCode(institutionCode, supportedInstitutions) ?? | ||
| institutionCode | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle channel-less KES M-Pesa recipients explicitly.
When supportedInstitutions contains expandKesMpesaInstitutions(...) output, a legacy recipient without optional channel falls through to getInstitutionNameByCode and is labeled M-PESA (Send Money) because it is the first SAFAKEPC match. Return the generic M-PESA label for KES/SAFAKEPC records with no channel, and add a regression test.
Proposed fix
- const isKesMpesa =
+ const isKesMpesa =
(options?.currency ?? "").toUpperCase() === "KES" &&
- institutionCode === KES_MPESA_INSTITUTION_CODE &&
- !!channel;
+ institutionCode === KES_MPESA_INSTITUTION_CODE;
- if (isKesMpesa && options?.accountIdentifier !== undefined) {
+ if (isKesMpesa && channel && options?.accountIdentifier !== undefined) {
return formatKesMpesaAccountDisplay(
options.accountIdentifier,
channel,
options.businessNumber,
);
}
- if (isKesMpesa) {
+ if (isKesMpesa && channel) {
return getKesMpesaInstitutionLabel(channel);
}
+ if (isKesMpesa) return "M-PESA";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function formatRecipientInstitutionDisplay( | |
| institutionCode: string, | |
| supportedInstitutions: InstitutionProps[], | |
| options?: { | |
| currency?: string; | |
| channel?: KesMpesaChannel | "" | null; | |
| accountIdentifier?: string; | |
| businessNumber?: string | null; | |
| }, | |
| ): string { | |
| const channel = options?.channel; | |
| const isKesMpesa = | |
| (options?.currency ?? "").toUpperCase() === "KES" && | |
| institutionCode === KES_MPESA_INSTITUTION_CODE && | |
| !!channel; | |
| if (isKesMpesa && options?.accountIdentifier !== undefined) { | |
| return formatKesMpesaAccountDisplay( | |
| options.accountIdentifier, | |
| channel, | |
| options.businessNumber, | |
| ); | |
| } | |
| if (isKesMpesa) { | |
| return getKesMpesaInstitutionLabel(channel); | |
| } | |
| return ( | |
| getInstitutionNameByCode(institutionCode, supportedInstitutions) ?? | |
| institutionCode | |
| ); | |
| } | |
| export function formatRecipientInstitutionDisplay( | |
| institutionCode: string, | |
| supportedInstitutions: InstitutionProps[], | |
| options?: { | |
| currency?: string; | |
| channel?: KesMpesaChannel | "" | null; | |
| accountIdentifier?: string; | |
| businessNumber?: string | null; | |
| }, | |
| ): string { | |
| const channel = options?.channel; | |
| const isKesMpesa = | |
| (options?.currency ?? "").toUpperCase() === "KES" && | |
| institutionCode === KES_MPESA_INSTITUTION_CODE; | |
| if (isKesMpesa && channel && options?.accountIdentifier !== undefined) { | |
| return formatKesMpesaAccountDisplay( | |
| options.accountIdentifier, | |
| channel, | |
| options.businessNumber, | |
| ); | |
| } | |
| if (isKesMpesa && channel) { | |
| return getKesMpesaInstitutionLabel(channel); | |
| } | |
| if (isKesMpesa) return "M-PESA"; | |
| return ( | |
| getInstitutionNameByCode(institutionCode, supportedInstitutions) ?? | |
| institutionCode | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/utils.ts` around lines 170 - 202, Update
formatRecipientInstitutionDisplay to explicitly return the generic M-PESA label
for KES recipients whose institutionCode is KES_MPESA_INSTITUTION_CODE and whose
channel is absent, before falling back to getInstitutionNameByCode; preserve the
existing channel-specific and accountIdentifier behavior, and add a regression
test covering expanded M-Pesa institutions with a legacy channel-less recipient.
Description
Noblocks needs to represent Safaricom's Send Money, Till, and Paybill rails separately in the UI even though the Paycrest API exposes all three through the
SAFAKEPCinstitution code.This foundation adds typed KES M-Pesa channels, UI-only virtual institution keys, channel-aware labels and placeholders, recipient display helpers, and optional metadata fields used by verification and saved recipients. The helpers expand only KES M-Pesa; banks, Airtel, and non-KES institutions are unchanged. API submissions continue to use the canonical
SAFAKEPCcode.There are no backend, contract, or database changes in this PR. The UI consumer changes are intentionally split into the next stacked PR so these shared primitives can be reviewed independently.
Testing
npm test -- --runInBand __tests__/kes-mpesa-channel.test.tsCovers virtual expansion, non-KES behavior, channel placeholders, and preview/history formatting.
Developed and tested on macOS with Node.js/Jest.
This change adds test coverage for new/changed/fixed functionality
Checklist
mainBy submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.
Summary by CodeRabbit
New Features
Bug Fixes
Tests