POS payment methods — architecture
Problem
Kenyan retail rarely uses one M-Pesa setup. Typical patterns:
| Tenant profile | What they use |
|---|---|
| Kiosk | One bank paybill (e.g. Equity 247247) + account ref → SMS on owner phone |
| Shop | Buy Goods till or own paybill |
| Supermarket | Per-branch paybill/till; STK at some branches only |
| Petrol station | Many outlets × many Buy Goods tills (one per pump lane) |
Payments must be configured independently, saved per block, and scoped to branches where needed. Checkout should only show what the tenant activated.
Layers
┌─────────────────────────────────────────────────────────────┐
│ Admin: Integrations → POS payment settings │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Manual rules│ │ POS channels│ │ Daraja/KCB/ │ each with │
│ │ [Save] │ │ [Save] │ │ Equity Save │ own save │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ pos_payment_settings (org-wide) │
│ • manual defaults (txn code, verify-on-phone) │
│ • mpesa_channel_options[] — payment acceptors (JSON) │
│ • daraja / kcb / equity credentials (encrypted) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ POS checkout (branch-aware) │
│ Filter channels: branch_id null OR branch_id = current │
│ Show only enabled options; cashier picks one sub-chip │
└─────────────────────────────────────────────────────────────┘
Payment acceptor (channel option row)
Each row in M-Pesa options at POS is a payment acceptor:
| Field | Purpose |
|---|---|
id |
Stable key at checkout (supports multiple Equity/Co-op paybills) |
type |
Flow template (bank_paybill, manual_buy_goods, stk_paybill, …) |
label |
Cashier-facing name |
enabled |
Show at checkout when true |
branch_id |
null = all branches; set = that branch only |
bank_name |
e.g. Equity Bank, SACCO name |
paybill_number |
Lipa na M-Pesa paybill (247247, 400200, …) |
account_number |
6-digit (or shorter) account ref mapped to tenant bank account |
till_number |
Buy Goods till |
txn_code_required |
Override org manual rule (optional) |
allow_verify_without_code |
Override org manual rule (optional) |
Dominant flow: bank paybill (manual)
- Owner adds Bank paybill (Lipa na M-Pesa) → Equity 247247 + account ref.
- Saves POS channels section only.
- Sets Active; disables unused STK/Daraja options.
- Cashier at checkout sees chip Equity Paybill 247247.
- Customer pays on phone → owner SMS → cashier enters M-Pesa code (or marks verified-on-phone if allowed).
No STK API, no Daraja credentials required for this flow.
Section saves
| Section | Route | Persists |
|---|---|---|
| Manual M-Pesa | PATCH …/sections/manual |
Global manual rules |
| POS channels | PATCH …/sections/channels |
mpesa_channel_options |
| Daraja | PATCH …/sections/daraja |
Daraja env + credentials |
| KCB Buni | PATCH …/sections/kcb |
KCB env + credentials |
| Equity Jenga | PATCH …/sections/equity |
Equity env + credentials |
Tenants configure only the sections they need.
Branch scenarios
| Scenario | Configuration |
|---|---|
| 3 branches, different paybills | 3 bank_paybill rows, each with branch_id |
| Org-wide Equity paybill | 1 row, branch_id empty |
| Petrol: 20 outlets × 4 tills | 80 manual_buy_goods rows with outlet branch_id + till_number |
| HQ STK + branch manual only | Daraja section saved once; branch rows manual-only |
Checkout resolves CurrentBranch::id() and filters acceptors.
Activation at POS
- Per acceptor:
enabledcheckbox in channel builder. - Payment method catalog:
pos_payment_methods.is_active(Settings → payment methods) for cash/card/bank. - Both must pass for M-Pesa family codes.
Future (out of scope — not implemented)
These were explicitly deferred in the branch-aware payment methods plan. They are not small polish items; each is a separate feature if requested later.
| Item | Why deferred |
|---|---|
| Bulk till import (CSV) | Petrol chains with 80+ tills need a dedicated import UX and validation pipeline |
pos_payment_acceptors table |
JSON array handles ~200 acceptors; relational model only if orgs exceed that scale |
| Per-branch Daraja shortcodes | Enterprise: different Safaricom shortcodes per outlet (credentials today are org-wide) |
| Auto-match SMS/C2B to checkout | Match incoming payment notifications to open POS carts by till/paybill/account ref |
Small polish (shipped with section saves)
- Per-section PATCH only — monolithic
PUT /pos/payments/settingsremoved - Hash scroll to saved section (
#ppay-channels, etc.) after redirect - Section isolation tests for all five PATCH endpoints