Skip to content

feat(kes): add M-Pesa channel foundations - #635

Open
chibie wants to merge 1 commit into
mainfrom
feat/kes-mpesa-foundations
Open

feat(kes): add M-Pesa channel foundations#635
chibie wants to merge 1 commit into
mainfrom
feat/kes-mpesa-foundations

Conversation

@chibie

@chibie chibie commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

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 SAFAKEPC institution 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 SAFAKEPC code.

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.ts

  • Covers 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

  • I have added documentation and tests for new/changed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main

By submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

  • New Features

    • Added support for separate KES M-Pesa channels: Send Money, Till, and Paybill.
    • Added channel-specific account placeholders and recipient display formatting.
    • Added support for Paybill business numbers and channel details in verification and transaction history.
  • Bug Fixes

    • Improved institution matching to recognize both standard and interface-specific identifiers.
  • Tests

    • Added coverage for KES channel expansion, non-KES behavior, placeholders, labels, and display formatting.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

KES M-Pesa channel support

Layer / File(s) Summary
Channel data contracts
app/types.ts
Adds KesMpesaChannel and carries channel and Paybill business-number data through institution, form, recipient, verification, and transaction-history types.
Expansion and display utilities
app/utils.ts, __tests__/kes-mpesa-channel.test.ts
Expands SAFAKEPC for KES, adds channel labels and UI keys, formats account and institution displays, updates placeholders, and tests KES-only behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: 5ran6

Poem

I’m a bunny with channels three,
Mobile, Till, Paybill—hop with glee.
Labels bloom and placeholders shine,
Tests keep every hop in line.
🐇 M-Pesa paths now neatly agree!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding KES M-Pesa channel foundations.
Description check ✅ Passed The description covers purpose, impact, API behavior, testing, environment, and checklist items, with no major missing required section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 29b85b1 and dca3a01.

📒 Files selected for processing (3)
  • __tests__/kes-mpesa-channel.test.ts
  • app/types.ts
  • app/utils.ts

Comment thread app/utils.ts
Comment on lines +170 to +202
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
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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.

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