Financial System
- JikoXpress Financial System Architecture
- JikoXpress Pro — Payment, Subscription & Business Model Architecture
- JikoXpress Pro — Offer Engine Architecture
- JikoXpress Pro — Financial Engine Flow
JikoXpress Financial System Architecture
JikoXpress Pro — Financial System Architecture
Version 2.0 | April 2026 | QBIT SPARK CO LIMITED Refined architecture based on full design sessions — all scenarios, edge cases, and decisions captured.
Table of Contents
- Overview & Core Principles
- Platform Context
- Two Financial Worlds
- Treasury Design
- Wallet System
- Chart of Accounts
- Money Splitting
- Escrow System
- Payment Channels
- Full Money Journey — All Scenarios
- Subscription Payments
- Settlements & Payouts
- Refunds & Cancellations
- Admin Withdrawal
- PSP Reconciliation
- Reporting
- Safety Rules & Integrity Checks
- Decision Log
1. Overview & Core Principles
JikoXpress financial system is a unified internal accounting engine — a proper ledger that tracks every single money movement on the platform, regardless of amount (even TZS 0.00), channel, or purpose.
Core Principles
| Principle | Description |
|---|---|
| Every event is a transaction | Cash, mobile money, wallet, kitchen wallet, TZS 0 promo orders — all recorded |
| Double entry accounting | Every debit has a credit. Books always balance |
| Single source of truth | Internal ledger is authoritative — not the external PSP |
| Dynamic money splitting | Splits are passed as parameters, not hardcoded |
| Escrow is a flag | Any transaction can be held — decided at runtime by the caller |
| Everyone has one wallet | Same JikoXpress account can be customer, kitchen owner, and rider — one wallet |
| Counter orders stay outside | POS/cash/kitchen custom payments never enter the treasury pool |
| Platform earns on online only | Service fee applies only to App and WhatsApp orders |
2. Platform Context
JikoXpress Pro connects customers with local chefs and restaurants (like Toasty for East Africa). The platform has:
- 7 sales channels: POS, Kiosk, Table QR, Mobile App, WhatsApp, Drive-Through, Direct Counter
- 4 subscription tiers: STARTER, GROWING, PROFESSIONAL, ENTERPRISE
- Multiple fulfillment types: Dine-in, Pickup, Delivery, Drive-Through
- East Africa first: Built for M-Pesa, Tigo Pesa, Airtel Money via Selcom, Azampay
Who Are the Actors?
Every person on the platform has one account and one wallet — regardless of their role:
Same JikoXpress account can be:
├── Customer — orders food via App/WhatsApp
├── Kitchen owner — runs a kitchen on the platform
└── Rider — delivers orders for kitchens
One person. One wallet. All roles.
3. Two Financial Worlds
Not all money flows through JikoXpress. There are two separate worlds:
┌──────────────────────────────────────────────────────┐
│ WORLD 1 — JikoXpress Pool │
│ │
│ App orders, WhatsApp orders │
│ Money flows through our PSP accounts │
│ Treasury tracks everything │
│ Platform earns service fee here │
└──────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ WORLD 2 — Kitchen's Own World │
│ │
│ POS cash, card, kitchen custom payments │
│ Money goes directly to kitchen's physical till │
│ or kitchen's own mobile money account │
│ Treasury never sees this money │
│ Platform earns ZERO here (covered by subscription) │
└──────────────────────────────────────────────────────┘
Which Channels Belong to Which World?
| Channel | World | Service Fee | Treasury Involvement |
|---|---|---|---|
| Mobile App | JikoXpress Pool | Yes | Full |
| JikoXpress Pool | Yes | Full | |
| POS — Cash | Kitchen's World | No | None |
| POS — Card | Kitchen's World | No | None |
| POS — Kitchen Wallet | Kitchen's World | No | None |
| POS — Kitchen Custom (Lipa Voda etc) | Kitchen's World | No | None |
| Kiosk — Cash | Kitchen's World | No | None |
| Table QR — Cash | Kitchen's World | No | None |
| Drive-Through — Cash | Kitchen's World | No | None |
The rule: Platform only earns a service fee when it brings the customer. Counter/automation channels are covered by the kitchen's subscription fee — not per-order cuts.
4. Treasury Design
The treasury is the central bank of JikoXpress. It is a proper ledger using double entry accounting — every money movement has two sides (debit and credit), books always balance.
What Double Entry Means
Money never appears or disappears — it always moves from somewhere to somewhere.
Example: Customer pays TZS 10,000 via Mpesa
Money arrives at Selcom (we now have it):
DEBIT ASSET_PSP_SELCOM 10,000
Money is split:
CREDIT LIABILITY_WALLETS 9,000 (kitchen's cut — we owe them)
CREDIT REVENUE_SERVICE_FEE 1,000 (we earned this)
Total debits = Total credits = TZS 10,000 ✅
Treasury Buckets — What the Money Is
At any moment, every shilling in the treasury is tagged to a bucket:
ASSET_PSP_SELCOM — physical money sitting at Selcom
ASSET_PSP_AZAMPAY — physical money sitting at Azampay
ASSET_ESCROW — held money, belongs to nobody yet
LIABILITY_WALLETS — belongs to ALL users (customers, kitchen owners, riders)
this is their money — we owe it back on demand
LIABILITY_SETTLEMENTS — payout requests being processed, on the way out
REVENUE_SERVICE_FEE — platform's cut from online orders (OURS)
REVENUE_SUBSCRIPTION — monthly/yearly kitchen fees (OURS)
EXPENSE_REFUNDS — money returned to customers
The Golden Safety Rule
ASSET_PSP_SELCOM + ASSET_PSP_AZAMPAY + ASSET_ESCROW
>=
LIABILITY_WALLETS + LIABILITY_SETTLEMENTS
If this breaks → crisis alert immediately. Platform is spending money it doesn't own.
5. Wallet System
User Wallet (Personal)
Every registered JikoXpress user gets one wallet automatically on account creation.
- One wallet per person, regardless of role
- Used to: top up, pay orders, receive earnings (kitchen revenue, delivery fees), withdraw to Mpesa
- Lives under
LIABILITY_WALLETSin treasury — it's the user's money, not the platform's
Kitchen Wallet (Kitchen-Level)
A completely separate system — not the platform wallet.
- Issued by the kitchen to their own loyal customers
- Kitchen manages it independently (deposits, deductions, balance)
- Has its own ledger — kitchen's books, not platform's
- Platform only records "paid via KITCHEN_WALLET" in the transaction — no treasury movement
Wallet vs Treasury
| Level | What It Sees |
|---|---|
| Treasury | LIABILITY_WALLETS = TZS 5,000,000 (total of all wallets combined) |
| Wallet level | Kibuti = TZS 45,000 / Mama Lishe = TZS 230,000 / John rider = TZS 80,000 |
Treasury sees the total obligation. Individual wallets explain the breakdown.
Wallet Transaction Ledger
Every wallet movement creates a record with:
type— TOPUP, ORDER_PAYMENT, ORDER_EARNING, DELIVERY_EARNING, WITHDRAWAL, SUBSCRIPTION_PAYMENTdirection— CREDIT (in) or DEBIT (out)amountbalanceBeforeandbalanceAfter— full audit trail, history reconstructable at any pointreferenceId— order ID, payout ID etc
6. Chart of Accounts
ASSETS (what platform has / controls)
├── ASSET_PSP_SELCOM Physical money at Selcom
├── ASSET_PSP_AZAMPAY Physical money at Azampay
└── ASSET_ESCROW Held money — no owner yet
LIABILITIES (what platform owes — never touch without permission)
├── LIABILITY_WALLETS All user wallets combined
└── LIABILITY_SETTLEMENTS Payout requests being processed
REVENUE (platform's own earnings)
├── REVENUE_SERVICE_FEE Cut from App/WhatsApp orders
└── REVENUE_SUBSCRIPTION Kitchen monthly/yearly fees
EXPENSE (money platform spent)
└── EXPENSE_REFUNDS Refunds sent back to customers
EQUITY (platform net worth)
├── EQUITY_CAPITAL Admin deposits / investments into platform
└── EQUITY_RETAINED_EARNINGS Accumulated platform profits
P&L Formula
Platform Net Profit = REVENUE_SERVICE_FEE + REVENUE_SUBSCRIPTION - EXPENSE_REFUNDS
Admin can only withdraw from Net Profit — never from ASSETS covering LIABILITIES
7. Money Splitting
When an online order is paid, the total is split into buckets dynamically. The caller (checkout service) passes the split parameters — the financial service just executes. No business logic in the financial service.
Split Buckets per Online Order
| Split Type | Goes To | Notes |
|---|---|---|
| Menu revenue | Kitchen owner's wallet | Main food items |
| Packaging fee | Kitchen owner's wallet | Kitchen sells packaging too |
| Delivery fee | Rider's wallet | Only on delivery orders |
| Service fee | REVENUE_SERVICE_FEE |
App/WhatsApp only |
Service Fee — Configurable Refundability
Controlled by a flag in the platform config:
service.fee.refundable = true → service fee goes into escrow with the rest
on cancel → full refund including service fee
service.fee.refundable = false → service fee goes to REVENUE immediately on confirmation
on cancel → only kitchen/rider money refunded
platform keeps its cut
Early stage → set true to build customer trust.
Mature stage → flip to false to protect platform revenue.
No code change needed — one config change.
8. Escrow System
Escrow is a dynamic flag, not a hardcoded rule. The checkout service decides whether to hold money based on the order context, and passes this to the financial service.
How It Works
Every transaction has:
holdInEscrow: true/falseescrowReleaseCondition: DELIVERY_CONFIRMED | PICKUP_CODE_CONFIRMED | null
Financial service does exactly what it's told. It doesn't know WHY — the checkout layer knows that.
Default Business Rules (Set by Checkout, Not Financial Service)
| Fulfillment Type | Payment Channel | Hold in Escrow? | Release Condition |
|---|---|---|---|
| Delivery | Mobile Money | Yes | DELIVERY_CONFIRMED |
| Delivery | Platform Wallet | Yes | DELIVERY_CONFIRMED |
| Pickup | Mobile Money | Yes | PICKUP_CODE_CONFIRMED |
| Pickup | Platform Wallet | Yes | PICKUP_CODE_CONFIRMED |
| Dine-in | Any online | No | Immediate |
| Any | Cash | No (cash is physical) | Immediate |
Rules can change without touching the financial service — just update the checkout layer logic.
9. Payment Channels
| Channel | Hits Treasury? | External PSP API? | Escrow Possible? | Service Fee? |
|---|---|---|---|---|
| CASH | No | No | No | No |
| KITCHEN_CUSTOM (Lipa Voda etc) | No | No | No | No |
| KITCHEN_WALLET | No | No | No | No |
| MOBILE_MONEY (USSD) | Yes | Yes (Selcom/Azampay) | Yes | If online channel |
| PLATFORM_WALLET | Yes (internal) | No | Yes | If online channel |
| CARD | Yes | Yes (card gateway) | Yes | If online channel |
10. Full Money Journey — All Scenarios
Chapter 1 — Counter Orders (World 2 — No Treasury Involvement)
Scenario 1.1 — POS Cash, Any Fulfillment
Customer pays TZS 10,000 cash at counter
→ Order recorded ✅
→ Transaction logged (channel: CASH) ✅
→ Receipt generated ✅
→ Money physically in kitchen till 💰
→ Treasury: nothing moves ❌
→ Platform earns: TZS 0 (covered by subscription)
Kitchen sales report shows: CASH += TZS 10,000
Scenario 1.2 — POS Kitchen Custom Payment (Lipa Voda, HaloPesa)
Customer pays TZS 15,000 via Lipa Voda at counter
→ Order recorded ✅
→ Transaction logged (channel: KITCHEN_CUSTOM) ✅
→ Money goes to kitchen's own Lipa Voda account 💰
→ Treasury: nothing moves ❌
→ Platform earns: TZS 0
Kitchen sales report shows: KITCHEN_CUSTOM += TZS 15,000
Scenario 1.3 — POS Kitchen Wallet Payment
Loyal customer has TZS 20,000 in kitchen wallet
Orders food TZS 8,000, pays from kitchen wallet
→ Order recorded ✅
→ Transaction logged (channel: KITCHEN_WALLET) ✅
→ Kitchen wallet balance -= TZS 8,000 (kitchen manages independently)
→ Treasury: nothing moves ❌
→ Platform earns: TZS 0
Kitchen sales report shows: KITCHEN_WALLET += TZS 8,000
Scenario 1.4 — Kiosk, Table QR, Drive-Through (Cash)
Same as above — counter level, no treasury, no service fee. Order recorded, money stays in kitchen's physical world.
Chapter 2 — Online Orders (World 1 — Treasury Involved)
Scenario 2.1 — App Order, Pickup, Mobile Money (USSD)
Total: TZS 12,000
Menu: TZS 10,500
Packaging: TZS 500
Service fee: TZS 1,000
STEP 1 — Payment initiated:
Checkout session created, inventory held
USSD prompt sent to customer via Selcom API
Customer confirms on phone
Selcom webhook fires
STEP 2 — Money arrives, held in escrow (pickup not yet confirmed):
DEBIT ASSET_PSP_SELCOM 12,000
CREDIT ASSET_ESCROW 12,000
STEP 3 — Order confirmed, kitchen notified, kitchen prepares
STEP 4 — Customer arrives, shows pickup code, confirmed:
DEBIT ASSET_ESCROW 12,000
CREDIT LIABILITY_WALLETS 11,000 (kitchen owner)
CREDIT REVENUE_SERVICE_FEE 1,000 (platform)
Kitchen wallet += TZS 11,000 ✅
Platform revenue += TZS 1,000 ✅
Scenario 2.2 — App Order, Delivery, Mobile Money
Total: TZS 18,000
Menu: TZS 12,000
Packaging: TZS 1,000
Delivery fee: TZS 4,000
Service fee: TZS 1,000
STEP 1 — Money arrives, full amount held in escrow:
DEBIT ASSET_PSP_SELCOM 18,000
CREDIT ASSET_ESCROW 18,000
STEP 2 — Order confirmed, kitchen prepares, rider picks up
STEP 3 — Delivery confirmed:
DEBIT ASSET_ESCROW 18,000
CREDIT LIABILITY_WALLETS 12,000 (kitchen — menu + packaging)
CREDIT LIABILITY_WALLETS 4,000 (rider — delivery fee)
CREDIT REVENUE_SERVICE_FEE 1,000 (platform)
Kitchen wallet += TZS 12,000 ✅
Rider wallet += TZS 4,000 ✅
Platform += TZS 1,000 ✅
Scenario 2.3 — App Order, Delivery, JikoXpress Wallet Payment
Customer has TZS 30,000 in JikoXpress wallet
Pays TZS 18,000 from wallet — NO PSP API call, purely internal
STEP 1 — Wallet debited, goes to escrow:
DEBIT LIABILITY_WALLETS 18,000 (customer wallet decreases)
CREDIT ASSET_ESCROW 18,000 (held)
STEP 2 — Delivery confirmed, escrow releases:
DEBIT ASSET_ESCROW 18,000
CREDIT LIABILITY_WALLETS 12,000 (kitchen)
CREDIT LIABILITY_WALLETS 4,000 (rider)
CREDIT REVENUE_SERVICE_FEE 1,000 (platform)
No money entered or left the pool — purely internal redistribution ✅
Scenario 2.4 — App Order, Dine-in, Mobile Money
Dine-in — no escrow needed (no delivery/pickup risk)
DEBIT ASSET_PSP_SELCOM 11,000
CREDIT LIABILITY_WALLETS 10,000 (kitchen)
CREDIT REVENUE_SERVICE_FEE 1,000 (platform)
Immediate split. No escrow. ✅
Scenario 2.5 — WhatsApp Order, Pickup, Mobile Money
Same flow as Scenario 2.1. Channel recorded as WHATSAPP. Service fee applies. Escrow holds until pickup code confirmed.
Scenario 2.6 — Split Payment (Multiple Methods)
Order total: TZS 20,000
Customer pays TZS 10,000 from wallet
Customer pays TZS 10,000 via USSD
Two TransactionEntities — both linked to same order, both held in escrow
DEBIT LIABILITY_WALLETS 10,000 (wallet portion)
DEBIT ASSET_PSP_SELCOM 10,000 (USSD portion)
CREDIT ASSET_ESCROW 20,000 (total held)
On release → same split logic, total amount split proportionally ✅
Scenario 2.7 — Platform Offer / Promo Applied
Customer orders TZS 10,000 via App
Platform covers delivery fee TZS 2,000 (free delivery offer)
Customer pays TZS 8,000
Two TransactionEntities:
1. Customer: TZS 8,000 via USSD → escrow
2. Platform: TZS 2,000 subsidy (paidBy = PLATFORM) → escrow
On delivery confirmed:
DEBIT ASSET_ESCROW 10,000
CREDIT LIABILITY_WALLETS 8,000 (kitchen)
CREDIT LIABILITY_WALLETS 2,000 (rider — full fee covered)
CREDIT REVENUE_SERVICE_FEE 500 (platform still earns cut on food amount)
PlatformOfferUsageEntity created — budget tracked
If budgetUsed >= budget → offer auto-deactivated ✅
Scenario 2.8 — TZS 0.00 Free Order (Full Promo)
Platform gives customer completely free order
TransactionEntity still created:
amount: 0.00
channel: PLATFORM
holdInEscrow: false
Still recorded. Kitchen notified. Order flows normally.
No money moves. Event is captured. ✅
Chapter 3 — Wallet Operations
Scenario 3.1 — User Tops Up Wallet
Kibuti tops up TZS 50,000 via Mpesa
DEBIT ASSET_PSP_SELCOM 50,000
CREDIT LIABILITY_WALLETS 50,000 (belongs to Kibuti)
Kibuti wallet += TZS 50,000 ✅
Scenario 3.2 — Anyone Requests Payout to Mpesa
Kitchen owner (or rider, or customer) withdraws TZS 30,000
STEP 1 — Money earmarked:
DEBIT LIABILITY_WALLETS 30,000
CREDIT LIABILITY_SETTLEMENTS 30,000 (being processed)
STEP 2 — JikoXpress calls Selcom API, sends to Mpesa:
DEBIT LIABILITY_SETTLEMENTS 30,000
CREDIT ASSET_PSP_SELCOM 30,000 (money leaves)
Money arrives on Mpesa ✅
Same flow for customers, kitchen owners, riders
Scenario 3.3 — Payout Fails
Selcom API returns failure (wrong number, system down etc)
DEBIT LIABILITY_SETTLEMENTS 30,000 (earmark reversed)
CREDIT LIABILITY_WALLETS 30,000 (money back in wallet)
User notified. They can retry. No money lost. ✅
11. Subscription Payments
Kitchen owners pay monthly or yearly. They choose how to pay.
Scenario 11.1 — Subscription via Wallet
Mama Lishe pays TZS 15,000/month from wallet
Pure internal — no PSP:
DEBIT LIABILITY_WALLETS 15,000
CREDIT REVENUE_SUBSCRIPTION 15,000
Kitchen wallet -= TZS 15,000
Platform revenue += TZS 15,000 ✅
Scenario 11.2 — Subscription via Mobile Money
DEBIT ASSET_PSP_SELCOM 15,000
CREDIT REVENUE_SUBSCRIPTION 15,000
Platform revenue += TZS 15,000 ✅
Renewal Logic
On renewal date:
1. Check if owner chose wallet AND has sufficient balance
2. If yes → auto deduct (Scenario 11.1)
3. If no balance OR chose mobile money → send payment prompt
4. If payment fails → grace period (configurable, e.g. 3 days)
5. If still unpaid after grace → kitchen suspended
12. Settlements & Payouts
Any user can request a payout at any time — customers, kitchen owners, riders. Same mechanism for all.
Rules
- Amount cannot exceed available wallet balance
- Minimum payout amount configurable in platform settings
- Processing time depends on external PSP
- Failed payouts → money returns to wallet automatically
- Full audit trail — who requested, when, destination, result
13. Refunds & Cancellations
Scenario 13.1 — Order Cancelled, Money in Escrow, service.fee.refundable=true
TZS 18,000 in escrow, full refund including service fee
DEBIT ASSET_ESCROW 18,000
CREDIT ASSET_PSP_SELCOM 18,000
Selcom API → refund to customer Mpesa
Platform earns nothing ✅
Scenario 13.2 — Order Cancelled, service.fee.refundable=false
Platform keeps service fee
DEBIT ASSET_ESCROW 18,000
CREDIT ASSET_PSP_SELCOM 17,000 (customer refund minus fee)
CREDIT REVENUE_SERVICE_FEE 1,000 (platform keeps)
Scenario 13.3 — Order Cancelled, Paid via Wallet
DEBIT ASSET_ESCROW 18,000
CREDIT LIABILITY_WALLETS 18,000 (back to customer wallet)
No PSP call. Pure internal. Instant refund to wallet ✅
Scenario 13.4 — Post-Completion Refund (Dispute)
Order delivered, escrow already released, kitchen already paid
Rare — dispute/complaint scenario. Manual admin action required.
DEBIT LIABILITY_WALLETS 10,000 (kitchen wallet reduced)
DEBIT EXPENSE_REFUNDS 10,000
CREDIT ASSET_PSP_SELCOM 10,000 (refund to customer)
Full audit trail. SUPER_ADMIN approval required.
14. Admin Withdrawal
Platform accumulates profit from service fees and subscriptions. Admin can withdraw profit only.
Rules
- Only
SUPER_ADMINcan request - Requires second
SUPER_ADMINapproval - Can only withdraw from net profit (REVENUE minus EXPENSE minus already withdrawn)
- Can never withdraw from escrow — belongs to customers
- Can never withdraw from
LIABILITY_WALLETS— belongs to users - System enforces hard limit — no override
Withdrawal Calculation
Available to withdraw =
REVENUE_SERVICE_FEE
+ REVENUE_SUBSCRIPTION
- EXPENSE_REFUNDS
- previously_withdrawn
If requested amount <= available → allow ✅
If requested amount > available → reject hard ❌
Journal Entry
DEBIT EQUITY_RETAINED_EARNINGS 500,000
CREDIT ASSET_PSP_SELCOM 500,000
Selcom API → transfers to admin Mpesa ✅
15. PSP Reconciliation
The Rule
Selcom actual balance >= ASSET_PSP_SELCOM ✅ (normal — other money may be there too)
Selcom actual balance < ASSET_PSP_SELCOM ❌ CRISIS — money is missing
The Selcom account may receive money from other business activities unrelated to JikoXpress. That is expected and normal. The check only validates that JikoXpress-tracked money is fully covered.
Daily Reconciliation Process
1. Pull Selcom transaction log for the day
2. Match each incoming payment to a TransactionEntity by reference/amount
3. Flag unmatched payments (other sources — expected, not a problem)
4. Flag any JikoXpress transaction with no matching Selcom record (problem)
5. Alert if ASSET_PSP_SELCOM > Selcom actual balance (crisis)
16. Reporting
Treasury Snapshot (Platform View)
WHAT WE HAVE:
At Selcom: ASSET_PSP_SELCOM balance
At Azampay: ASSET_PSP_AZAMPAY balance
In escrow: ASSET_ESCROW balance
WHAT WE OWE:
To all users (wallets): LIABILITY_WALLETS balance
Payouts in progress: LIABILITY_SETTLEMENTS balance
WHAT WE EARNED:
Service fees: REVENUE_SERVICE_FEE balance
Subscriptions: REVENUE_SUBSCRIPTION balance
Net profit: REVENUE_SERVICE_FEE + REVENUE_SUBSCRIPTION - EXPENSE_REFUNDS
Kitchen Owner Sales Report
Reads from orders + wallet transactions — NOT treasury directly:
SALES REPORT — Mama Lishe Kitchen | April 2026
──────────────────────────────────────────────
CHANNEL BREAKDOWN:
App orders TZS 150,000 (in your JikoXpress wallet)
WhatsApp orders TZS 80,000 (in your JikoXpress wallet)
Cash (POS) TZS 200,000 (collected physically — in your till)
Lipa Voda (POS) TZS 50,000 (in your Lipa Voda account)
TOTAL SALES: TZS 480,000
──────────────────────────────────────────────
JIKOXPRESS WALLET:
Available balance TZS 230,000 (ready to withdraw)
Individual Wallet Statement
MY WALLET — Kibuti | April 2026
──────────────────────────────────────────────────────
Current Balance: TZS 45,000
──────────────────────────────────────────────────────
DATE DESCRIPTION IN OUT BALANCE
Apr 23 Top up via Mpesa 50,000 50,000
Apr 23 Order #47 — Mama Lishe 5,000 45,000
Apr 22 Order #31 earnings 8,500 53,500
Apr 21 Withdrawal to Mpesa 15,000 38,500
Apr 20 Subscription — April 15,000 53,500
──────────────────────────────────────────────────────
Same wallet, same statement format — whether customer, kitchen owner, or rider. Transaction type field enables per-role breakdown reports.
How Treasury Gets Breakdown by Role
Total rider earnings in wallets =
sum(WalletTransactionEntity where type = DELIVERY_EARNING and not withdrawn)
Total kitchen earnings in wallets =
sum(WalletTransactionEntity where type = ORDER_EARNING and not withdrawn)
Treasury sees LIABILITY_WALLETS = TZS 5,000,000.
Wallet transaction types explain the breakdown underneath it.
17. Safety Rules & Integrity Checks
Primary Safety Check (Runs Continuously)
ASSET_PSP_SELCOM + ASSET_PSP_AZAMPAY + ASSET_ESCROW
>=
LIABILITY_WALLETS + LIABILITY_SETTLEMENTS
If false → ALERT IMMEDIATELY 🚨
Immutability Rules
JournalEntryEntity— never updated, never deleted. Append-only ledger.TransactionEntity— status only moves forward:PENDING → COMPLETED → REFUNDED. Never backwards.WalletTransactionEntity— never deleted.balanceBeforeandbalanceAfteron every record. History reconstructable at any point in time.
Escrow Integrity Check
ASSET_ESCROW balance = sum of all EscrowEntity where status = HELD
If mismatch → discrepancy alert 🚨
Admin Withdrawal Hard Limit
Withdrawal amount <= net profit available
System rejects if exceeded. No override. No exceptions.
Access Control
| Action | Who |
|---|---|
| View treasury | SUPER_ADMIN |
| Request payout | Any user (own wallet only) |
| Approve admin withdrawal | Second SUPER_ADMIN |
| View platform P&L | SUPER_ADMIN |
| View own wallet | Any user |
| Manual post-completion refund | SUPER_ADMIN + approval |
| Create platform offers | SUPER_ADMIN |
18. TODO — Pending Design Decisions
These are confirmed features that need to be designed when we reach the relevant service/module.
TODO-001 — Kitchen Delivery Configuration
Where: Kitchen settings / Kitchen config entity What: Kitchen owner needs ability to configure their delivery model:
deliveryModel:
PLATFORM_RIDER — use JikoXpress riders (default)
OWN_RIDER — kitchen has own boda, delivers themselves
NO_DELIVERY — kitchen does not offer delivery
absorbDeliveryFee: true/false
true — kitchen covers delivery cost for customer (free delivery for customer)
false — customer pays delivery fee (default)
deliveryCoverageRange: Integer (km)
— only absorb delivery fee for orders within this range
— e.g. 3 = free delivery within 3km, customer pays beyond that
Impact on splits:
PLATFORM_RIDER+absorbDeliveryFee=true→ kitchen split reduced, rider still paid, customer sees free deliveryOWN_RIDER→ no rider split, no DisbursementEntity for rider, kitchen keeps full amount- Checkout service reads kitchen config and builds splits accordingly
- Financial service just executes whatever splits it receives — no business logic inside
Delivery fee deduction logic (checkout service responsibility):
- Kitchen delivery subsidy is deducted from kitchen earnings on that specific order — no pre-loaded balance required
- Checkout service calculates net kitchen earning BEFORE sending splits to financial service:
kitchenNet = kitchenEarning - deliveryFee - If
kitchenNet < 0(delivery fee exceeds kitchen earning on that order): → subsidy auto-disabled for this order → customer pays delivery fee normally → no debt created, no blocking - Financial service receives already-calculated net splits — executes blindly
SplitFundedBy.KITCHENon delivery split records that kitchen funded it (for reporting)- No minimum wallet balance required — self-regulating per order
19. Decision Log
| Decision | Choice | Rationale |
|---|---|---|
| Tax handling | Dropped for V1 | Reduces complexity. Kitchen owner responsible for TRA compliance. Add later if legal pressure arises. |
| Escrow rule location | Dynamic flag from checkout caller | Financial service stays dumb — just executes. Business logic lives in checkout. Decoupled. |
| Service fee refundability | Config flag in properties file | One switch changes behavior platform-wide. No code change needed. |
| Separate wallets per role | No — one wallet per person | Same account = customer, owner, rider. No artificial separation by role. |
| Treasury breakdown per role | Not in treasury — in wallet transaction types | Treasury sees total obligation. Breakdown via WalletTransactionEntity.type queries. |
| Full accounting vs simple ledger | Proper double-entry ledger, not QuickBooks | Platform holds other people's money — can't skip proper tracking. Kept fit-for-purpose, not over-engineered. |
| PSP accounts in chart | Yes — one per PSP | Need to know where physical money sits. Enables clean reconciliation. |
| Counter orders in treasury | No | Cash/kitchen custom never touch JikoXpress PSP. Platform is just a tool. Covered by subscription. |
| Admin withdrawal restriction | Revenue/profit only | Can never touch escrow or user wallet liabilities. Non-negotiable safety rule. |
| Payout eligibility | Any user | Customers, kitchen owners, riders — same wallet system, same payout flow. Role irrelevant. |
| Subscription payment method | User's choice | Flexibility. Auto-deduct from wallet if chosen and balance sufficient. Fallback to mobile money prompt. |
| Kitchen wallet | Completely separate from platform | Kitchen manages independently. Platform records usage only. No treasury involvement. |
QBIT SPARK CO LIMITED | JikoXpress Pro | April 2026 Internal financial architecture document — confidential
JikoXpress Pro — Payment, Subscription & Business Model Architecture
| Property | Value |
|---|---|
| Version | 1.0 |
| Status | Draft |
| Created | April 2026 |
| Purpose | Development Guide — Payment, Subscription & Delivery Model |
Executive Summary
JikoXpress Pro operates a three-engine business model designed specifically for the East African food market. Unlike generic SaaS platforms, JikoXpress earns from three distinct but complementary revenue streams — each designed to align platform success with kitchen success.
Part 1: The Three-Engine Business Model
1.1 Overview
ENGINE 1 — Platform Subscription
Kitchen pays monthly to use JikoXpress management tools
POS, KDS, menu, staff, devices, reports — the full platform
ENGINE 2 — Marketplace Commission
JikoXpress brings external customers to the kitchen
Platform earns a cut on those orders only
ENGINE 3 — Delivery Revenue Share
Customer pays delivery fee
JikoXpress shares majority with Dasher, keeps margin
1.2 Revenue Stream Breakdown
| Stream | Who Pays | When | Amount | Applies To |
|---|---|---|---|---|
| Subscription | Kitchen | Monthly | Plan-based | All kitchens |
| Marketplace Commission | Kitchen (deducted) | Per order | 10% of food value | App & WhatsApp orders only |
| Delivery Margin | Customer (via delivery fee) | Per delivery | 30% of delivery fee | All Dasher deliveries |
1.3 What Each Order Type Earns JikoXpress
| Channel | Fulfillment | Subscription ✓ | Commission | Delivery Margin |
|---|---|---|---|---|
| POS | Dine-in | ✅ | ❌ | ❌ |
| POS | Pickup | ✅ | ❌ | ❌ |
| POS | Delivery (Dasher) | ✅ | ❌ | ✅ |
| Kiosk | Dine-in / Pickup | ✅ | ❌ | ❌ |
| Table QR | Dine-in | ✅ | ❌ | ❌ |
| App | Pickup | ✅ | ✅ | ❌ |
| App | Delivery (Dasher) | ✅ | ✅ | ✅ |
| Pickup | ✅ | ✅ | ❌ | |
| Delivery (Dasher) | ✅ | ✅ | ✅ |
App + Dasher Delivery = all three engines firing simultaneously. Highest value order type.
1.4 The Core Principle
Subscription is the stability engine — predictable, recurring. Commission is the growth engine — scales with kitchen volume. Delivery is the experience engine — owns the customer journey end-to-end.
STARTER is free forever — not charity. It is a customer acquisition tool. STARTER kitchens still generate commission revenue on app orders and processing margin on every M-Pesa transaction. Every kitchen on STARTER that takes 50 app orders/month earns JikoXpress real money without paying a subscription.
Part 2: Subscription Plans
2.1 Plan Tiers
STARTER — Free Forever
Price: TZS 0 / month
Billing: None
Trial: Not applicable — free forever
Target: Home chefs, street vendors, side hustle cooks
Included:
Limits:
GROWING — Mid Tier
Price: TZS X/month (monthly) or TZS X/week (weekly)
Billing: Monthly or Weekly — kitchen chooses
Trial: Covered by 3-day PROFESSIONAL trial on signup
Target: Small restaurants, busy food stalls, cafes, food trucks
Included (everything in STARTER plus):
Limits:
- 1,000 orders/month
- 3 staff accounts
- 1 location
Weekly billing option exists because a food stall thinking "TZS 50,000/month" hesitates. "TZS 12,500/week" maps to how small East African businesses think about cash flow.
PROFESSIONAL — Full Feature
Price: TZS XX/month or TZS XX/year (annual = 2 months free)
Billing: Monthly or Annual — kitchen chooses
Trial: Covered by 3-day trial on signup
Target: Full-service restaurants, fast food, bars, hotel restaurants
Included (everything in GROWING plus):
- Kitchen stations
- KDS (Kitchen Display System)
- Drive-through support
- Tabs (pay-later for dine-in)
- Table management
- Up to 15 staff accounts
- Roles & permissions + discount limits
- Advanced reports + customer insights
- Priority support
Limits:
- Unlimited orders
- 15 staff accounts
- 1 location
Annual billing for PROFESSIONAL is a strong retention tool. Once they've paid a year upfront, churn almost disappears.
ENTERPRISE — Custom
Price: Custom (negotiated offline)
Billing: Annual or manual — admin-activated
Trial: No trial — demo + custom onboarding instead
Target: Restaurant chains, franchises, hotel groups, cloud kitchens
Included (everything in PROFESSIONAL plus):
- Multi-location support
- Central menu management
- Consolidated cross-location reports
- API access for integrations
- Inventory management
- White-label option
- Dedicated support + SLA guarantees
- Custom integrations
Limits: Unlimited everything
2.2 Billing Cycle Options by Plan
| Plan | Weekly | Monthly | Annual |
|---|---|---|---|
| STARTER | ❌ | ❌ | ❌ |
| GROWING | ✅ | ✅ | ❌ |
| PROFESSIONAL | ❌ | ✅ | ✅ |
| ENTERPRISE | ❌ | ❌ | Custom |
Part 3: The 3-Day Trial System
3.1 How It Works
Every new kitchen gets one automatic 3-day trial on PROFESSIONAL — not GROWING.
Why PROFESSIONAL specifically? Show them the ceiling. KDS, stations, tabs, drive-through. If they trial GROWING, they never see the most powerful features and undervalue the platform. Show them the best, then they decide where to settle.
Kitchen registers
↓
System offers: "Try PROFESSIONAL free for 3 days — no payment needed"
↓
├── Accept → status: TRIALING, plan: PROFESSIONAL, trialEndsAt: now + 3 days
│ No payment info required
│
└── Skip → lands directly on STARTER
3.2 Trial Expiry Flow
Trial day 3 ends
↓
├── Kitchen entered payment method → auto-converts to PROFESSIONAL ACTIVE
│ First charge hits immediately
│ New billing cycle starts
│
└── No payment method → drops silently to STARTER
Notification: "Your trial ended. Upgrade anytime to get back."
No disruption — kitchen still operational on STARTER
3.3 Trial Rules
- One trial per account ever — automatic, system-enforced
- Admin can manually grant re-trial — for re-engagement, churned kitchens, sales tool
- No trial for ENTERPRISE — they get a demo + custom onboarding instead
- Trial is always PROFESSIONAL — never GROWING, never ENTERPRISE
3.4 The Re-Trial (Admin Tool)
Admin dashboard → Kitchen profile → [Grant Re-Trial]
↓
Admin selects duration (3, 7, or 14 days)
↓
Kitchen gets push notification: "Good news! You've been given X days on PROFESSIONAL"
↓
Same flow as original trial — expires or converts
This is a powerful churn recovery and sales tool. Use it for:
- Kitchens that churned due to payment failure (not dissatisfaction)
- Enterprise prospects evaluating the platform
- Re-engaging inactive STARTER kitchens
Part 4: Subscription Lifecycle — All Scenarios
4.1 New Kitchen — Full Flow
SIGNUP
↓
3-day PROFESSIONAL trial offered
↓
[Accept trial] → TRIALING
↓
Day 3: trial expires
↓
[Has payment method] → ACTIVE on chosen paid plan
[No payment method] → STARTER (free forever)
↓
Monthly auto-renewal cycle begins
4.2 Upgrade Flow
From STARTER → GROWING or PROFESSIONAL:
Kitchen hits a limit OR tries a locked feature
↓
In-context upgrade prompt appears (not redirect to settings)
Shown right where they hit the wall
↓
"You've reached 20 menu items. Upgrade to GROWING for unlimited."
[Upgrade Now]
↓
Kitchen selects billing cycle (weekly/monthly for GROWING, monthly/annual for PRO)
↓
Pays via M-Pesa / card
↓
Plan activates IMMEDIATELY
↓
Feature they were trying to use → now available
System continues where they left off — no restart
In-context upgrade triggers:
| Trigger | Message |
|---|---|
| 21st menu item | "You've reached 20 items. Upgrade to GROWING for unlimited." |
| 80 orders (STARTER) | "20 orders left this month. Upgrade to avoid interruption." |
| Enable POS | "POS is available on GROWING and above." |
| Enable KDS | "KDS is available on PROFESSIONAL and above." |
| Enable tabs | "Tabs are available on PROFESSIONAL and above." |
| Add 4th staff | "You've reached your staff limit. Upgrade to add more." |
From GROWING → PROFESSIONAL: Same flow. Charge full PROFESSIONAL price. New billing cycle starts on upgrade date. No proration in v1 — simpler, cleaner.
4.3 Downgrade Flow
Kitchen requests downgrade (e.g. PROFESSIONAL → GROWING)
↓
System shows what they will LOSE on the downgrade date:
"On [date] you will lose access to:
- Kitchen Stations
- KDS displays
- Tabs (pay later)
- Drive-through
- Staff accounts above 3"
↓
Save flow — system offers:
"Stay on PROFESSIONAL for 1 more month at 50% off?"
[Accept offer] [Proceed with downgrade]
↓
[Accept] → offer applied, subscription continues at 50%
[Proceed] → downgrade SCHEDULED for end of current billing period
↓
Until that date: still on PROFESSIONAL, full access
↓
On renewal date: switches to GROWING, charged GROWING price
What happens to data above the new plan's limits?
Data is NEVER deleted. It is soft-locked:
| Resource | On Downgrade to GROWING | On Re-upgrade |
|---|---|---|
| Kitchen stations | Archived (not deleted) | Restored instantly |
| KDS config | Archived | Restored instantly |
| Staff above 3 | Suspended (can't login) | Reactivated |
| Tabs history | Read-only | Fully accessible |
4.4 Cancellation Flow
Kitchen requests cancellation
↓
System shows impact:
"Your subscription ends on [date].
After that you'll move to STARTER (free forever).
Your data is safe — menus, orders, history all preserved."
↓
Save flow:
"Stay for 1 more month at 50% off?"
[Accept] [Cancel anyway]
↓
[Cancel anyway] → cancellation SCHEDULED for end of current period
↓
Access continues until period ends — no immediate disruption
↓
On expiry: status → CANCELLED, plan falls to STARTER
Kitchen still operational — just on free tier
Cancellation ≠ lockout. They keep STARTER forever.
4.5 Payment Failure & Dunning Flow
Renewal date arrives
↓
Auto-charge attempt → FAILS
↓
status: PAST_DUE
↓
Day 0: Push notification + WhatsApp:
"Your payment didn't go through. Tap to retry."
Silent retry attempt
Day 1: Silent retry
Day 3: Push + WhatsApp (stronger):
"⚠️ Your subscription payment failed again.
Update your payment method to keep full access."
Silent retry attempt
Day 5: Silent retry
Day 7: Push + WhatsApp (urgent):
"⚠️ Final notice — access suspending tomorrow.
Tap to update payment and stay active."
↓
Day 8: status: SUSPENDED
Kitchen enters LIMITED MODE
↓
During suspension (7 days):
- Can log in ✅
- Can view menu, history, reports ✅
- CANNOT accept new orders ❌
- CANNOT process payments ❌
- Banner shown everywhere: "Renew to resume taking orders"
- Push + WhatsApp every 2 days
↓
Day 15: status: CANCELLED
Plan falls to STARTER
Full STARTER access restored
Paid features soft-locked (data preserved)
Limited mode during suspension is critical. A fully locked-out owner panics and churns permanently. A visible-but-restricted owner is motivated to pay and comes back.
4.6 Re-subscription After Cancellation / Failure
Kitchen wants to re-subscribe
↓
Selects plan + billing cycle
↓
Pays
↓
ACTIVE immediately
↓
All archived data restored:
- Stations restored
- KDS config restored
- Staff reactivated
- Old menu items restored (including any over-limit ones)
↓
"Welcome back! Everything is right where you left it." 🎉
Part 5: Limit Enforcement & Data Preservation
5.1 The Core Principle
JikoXpress never deletes kitchen data due to plan changes. Ever.
Data is soft-locked. Access is restricted. But everything is preserved and restores instantly on upgrade.
5.2 What Happens When Kitchen Drops to STARTER
Kitchen has 200 items → drops to STARTER
↓
System auto-selects 20 most recently active items
Selection priority:
1. Items with orders in last 30 days (sorted by order count)
2. Fill remaining with most recently created items
3. Never auto-select items owner already marked unavailable
↓
Those 20 → status: ACTIVE (kitchen immediately live — no downtime)
Remaining 180 → status: OVER_LIMIT_INACTIVE (preserved, not orderable)
↓
Owner notified:
"You're on STARTER. We've kept your 20 most active items live.
Swap them anytime from your menu settings."
↓
Owner can swap at any time:
Deactivate 1 active → activate 1 locked
Always exactly 20 active. Kitchen always live.
Kitchen never goes offline due to a plan change.
5.3 Menu Item Status Model
ACTIVE → visible, orderable, counts toward limit
UNAVAILABLE → owner manually turned off (out of stock)
OVER_LIMIT_INACTIVE → exists but locked due to plan limit
ARCHIVED → owner soft-deleted
OVER_LIMIT_INACTIVE is distinct from UNAVAILABLE — different reason, different resolution path. One is owner choice, the other resolves automatically on upgrade.
5.4 Limit Enforcement Across All Resources
| Resource | STARTER | On Drop — System Action | Kitchen Impact |
|---|---|---|---|
| Menu items | 20 | Auto-pick 20 most active, rest OVER_LIMIT_INACTIVE | None — stays live with 20 |
| Staff accounts | 1 | Extra staff suspended (can't login) | Owner still operational |
| Orders/month | 100 | Counter resets monthly, past orders preserved | Future orders blocked at 100 |
| Table QR | 1 table | Extra QR codes deactivated, first one stays | 1 table still works |
| Locations | 1 | Extra locations suspended, not deleted | Primary location works |
5.5 Usage-Based Upgrade Prompts
STARTER → GROWING triggers:
- Orders this month > 80 (approaching 100 limit)
- Menu items > 15 (approaching 20 limit)
- Customer tried to pay by card (not available on STARTER)
- Multiple staff login attempts
GROWING → PROFESSIONAL triggers:
- Orders this month > 800 (approaching 1,000 limit)
- Staff maxed at 3
- Kitchen searched for: tabs, stations, KDS, drive-through
- Average prep time > 15 minutes
PROFESSIONAL → ENTERPRISE triggers:
- Kitchen asked about second location
- Searched for: multi-location, franchise, API
- Monthly orders consistently > 5,000
Part 6: Feature Access Control
6.1 Three-Layer Access Model
Every feature check in the system goes through three layers:
Layer 1 — Subscription Status
ACTIVE or TRIALING → proceed to Layer 2
PAST_DUE → proceed (grace, still accessible)
SUSPENDED → BLOCK (show renew prompt)
CANCELLED → STARTER features only
Layer 2 — Plan Features
Feature in this plan? → proceed to Layer 3
Feature not in plan? → BLOCK (show upgrade prompt)
Layer 3 — Usage Limits
Usage under limit? → ALLOW
Usage at limit? → BLOCK (show upgrade prompt)
All three layers checked by one service. Business logic never knows which layer blocked — it just gets true or false.
// Single call anywhere in codebase
entitlementService.hasAccess(kitchenId, Feature.KDS_DISPLAY);
entitlementService.canPerform(kitchenId, Action.CREATE_ORDER);
limitService.canAddMenuItem(kitchenId);
6.2 Feature → Plan Mapping
CORE (STARTER — always on):
basic_menu, order_management, mobile_notifications,
basic_payments, operating_hours, order_history_30d,
table_qr_single, mobile_app_channel
CHANNELS (GROWING+):
pos_access, kiosk_channel, table_qr_unlimited,
whatsapp_bot, receipt_printer, kitchen_printer
PAYMENTS (GROWING+):
card_payments, cash_handling, kitchen_wallet
KITCHEN OPS (PROFESSIONAL+):
stations, kds_display, expeditor_mode, drive_through, tabs,
table_management, staff_roles_permissions
INSIGHTS (PROFESSIONAL+):
advanced_reports, customer_insights, order_history_full
SCALE (ENTERPRISE):
multi_location, central_menu, consolidated_reports,
api_access, white_label, inventory_management
Part 7: Transaction Split Architecture
7.1 The Two Flags That Drive Every Split
boolean chargeCommission =
order.channel == APP || order.channel == WHATSAPP;
boolean chargeDeliveryMargin =
order.fulfillmentType == DELIVERY &&
order.deliveryProvider == JIKOXPRESS_DASHERS;
These two flags determine exactly how money is split on every order. No hardcoding per kitchen. Universal.
7.2 Split Scenarios
Scenario 1 — POS Dine-in (subscription only, no per-order split)
Food value: TZS 15,000
Kitchen gets: TZS 15,000 (100%)
Platform earns: TZS 0 per order
(earns via monthly subscription separately)
Processing margin: TZS 75 (0.5% on transaction)
Scenario 2 — App Pickup (commission applies)
Food value: TZS 15,000
Commission (10%): TZS 1,500 → REVENUE_MARKETPLACE_COMMISSION
Kitchen gets: TZS 13,500 → kitchen settlement
Processing margin: TZS 75 → REVENUE_PROCESSING_MARGIN
Scenario 3 — App + Dasher Delivery (all splits)
Food value: TZS 15,000
Delivery fee: TZS 2,500 (see delivery fee formula)
Total collected: TZS 17,500
↓
Commission (10%): TZS 1,500 → REVENUE_MARKETPLACE_COMMISSION
Kitchen gets: TZS 13,500 → kitchen settlement
Dasher earning (70%): TZS 1,750 → DasherWalletEntity
Platform delivery (30%): TZS 750 → REVENUE_DELIVERY_MARGIN
Processing margin: TZS 75 → REVENUE_PROCESSING_MARGIN
Platform total earned: TZS 2,325
Kitchen nets: TZS 13,500
Dasher gets: TZS 1,750
7.3 TransactionSplitEntity Records Per Order
Every order that involves money generates split records:
Order #47 — App + Dasher Delivery
SplitRecord 1:
type: KITCHEN_EARNING
amount: TZS 13,500
destination: kitchen_settlement_ledger
SplitRecord 2:
type: PLATFORM_COMMISSION
amount: TZS 1,500
destination: REVENUE_MARKETPLACE_COMMISSION
SplitRecord 3:
type: DASHER_EARNING
amount: TZS 1,750
destination: dasher_wallet
SplitRecord 4:
type: PLATFORM_DELIVERY_MARGIN
amount: TZS 750
destination: REVENUE_DELIVERY_MARGIN
SplitRecord 5:
type: PROCESSING_MARGIN
amount: TZS 75
destination: REVENUE_PROCESSING_MARGIN
Total splits always equal total collected. Double-entry stays balanced.
7.4 Revenue Ledger Accounts
REVENUE_SUBSCRIPTION_FEES ← monthly plan payments
REVENUE_MARKETPLACE_COMMISSION ← 10% on app/WhatsApp orders
REVENUE_DELIVERY_MARGIN ← 30% of delivery fee
REVENUE_PROCESSING_MARGIN ← 0.5% spread on all transactions
LIABILITY_DASHER_WALLETS ← accumulated Dasher earnings (owed to drivers)
LIABILITY_KITCHEN_SETTLEMENTS ← kitchen earnings pending payout
ASSET_DELIVERY_POOL ← delivery fees collected, not yet split
Part 8: Delivery System & Fee Model
8.1 JikoXpress Dashers — Internal Fleet
JikoXpress owns the delivery operation. No third-party providers in v1.
Why own the fleet:
- Control the full customer experience end-to-end
- Own the delivery margin (not shared with Bolt/Uber)
- Dasher quality directly reflects on JikoXpress brand
- Build competitive advantage — "Dashers are faster and cheaper"
8.2 Delivery Fee Formula
The customer delivery fee is calculated from the Dasher cost outward, not arbitrarily:
Dasher earning = baseFee + (pricePerKm × distanceKm)
Delivery fee = Dasher earning × (1 + platformMarginRate)
Final fee = round to nearest TZS 100
Config values (adjustable by admin):
baseFee: TZS 1,000
pricePerKm: TZS 150
platformMarginRate: 0.30 (30%)
roundingUnit: 100
Examples:
1km: Dasher TZS 1,150 → Fee TZS 1,495 → Customer pays TZS 1,500
3km: Dasher TZS 1,450 → Fee TZS 1,885 → Customer pays TZS 1,900
5km: Dasher TZS 1,750 → Fee TZS 2,275 → Customer pays TZS 2,300
8km: Dasher TZS 2,200 → Fee TZS 2,860 → Customer pays TZS 2,900
8.3 Delivery Fee Split
Customer pays delivery fee
↓
Dasher share: 70% of fee (always)
Platform share: 30% of fee
Minimum Dasher: TZS 1,000 (floor — protects very short trips)
8.4 Dasher Payout
Dasher completes delivery
↓
Earning added to DasherWalletEntity instantly
↓
Dasher sees real-time balance in app
↓
Daily or on-demand withdrawal to:
M-Pesa / Tigo Pesa / Airtel Money
Part 9: Dasher App Psychology & UX Flow
9.1 The Three Questions Every Driver Asks
When an order pops, every Dasher makes a split-second decision based on exactly three things:
1. How much will I earn?
2. How far do I have to go?
3. Is it worth my time?
Every screen must answer these three questions instantly. Nothing else matters at the moment of decision.
9.2 Screen 1 — Order Notification (The Pop-up)
┌─────────────────────────────────────────┐
│ 🛵 NEW DELIVERY │
│ │
│ TZS 1,750 │ ← earnings FIRST, biggest font
│ your earning │
│ │
│ ├── 📍 0.8km to kitchen │ ← distance to pickup
│ └── 🏠 2.3km to customer │ ← distance to drop-off
│ │
│ Est. 18 mins total │ ← time commitment
│ │
│ ████████████░░░ 12s │ ← countdown timer
│ │
│ [ACCEPT ORDER] │
└─────────────────────────────────────────┘
Design decisions:
- Earnings shown first, largest — this is what drives acceptance
- No restaurant name — prevents bias (driver shouldn't reject based on kitchen)
- No customer name — privacy
- 12-second timer — sweet spot (5s = panic, 30s = overthinking)
- Single button — no friction on accept
Timer expiry behavior:
No response in 12s → auto-decline for this Dasher
→ Order sent to next nearest available Dasher
→ No penalty for first miss
→ 3 consecutive misses → system marks Dasher as "away" automatically
9.3 Screen 2 — After Accept (Full Details Unlock)
┌─────────────────────────────────────────┐
│ ✅ Order Accepted! │
│ │
│ PICK UP │
│ Mama Lishe Downtown │
│ Msimbazi Street, Dar │
│ 📍 0.8km from you │
│ [NAVIGATE] │
│ │
│ ───────────────────────────────────── │
│ ORDER SUMMARY │
│ 1× Ugali + Samaki │
│ 1× Pilau │
│ ───────────────────────────────────── │
│ │
│ DROP OFF │
│ Mlimani City Area, Near Gate 2 │
│ 📍 2.3km from kitchen │
│ [NAVIGATE] │
│ │
│ Your earning: TZS 1,750 │
│ │
│ [ARRIVED AT KITCHEN] │
└─────────────────────────────────────────┘
9.4 Screen 3 — At Kitchen (Pickup Verification)
┌─────────────────────────────────────────┐
│ 📦 PICK UP │
│ │
│ Show this to kitchen staff: │
│ │
│ ┌─────────────────────────────────┐ │
│ │ │ │
│ │ ORDER #47 │ │
│ │ Dasher: John M. │ │
│ │ │ │
│ │ ████████████████ │ │
│ │ (QR code) │ │
│ └─────────────────────────────────┘ │
│ │
│ Kitchen scans → confirms → you proceed │
│ │
│ [FOOD PICKED UP] │
└─────────────────────────────────────────┘
QR verification prevents wrong Dasher picking up wrong order. Kitchen scans, confirms, Dasher proceeds.
9.5 Screen 4 — En Route to Customer
┌─────────────────────────────────────────┐
│ 🏍️ EN ROUTE │
│ │
│ Delivering to: │
│ Mlimani City Area, Near Gate 2 │
│ │
│ Customer note: │
│ "Call when you arrive, gate is locked" │
│ │
│ 📞 Call Customer │
│ │
│ [NAVIGATE] │
│ │
│ [ARRIVED AT CUSTOMER] │
└─────────────────────────────────────────┘
9.6 Screen 5 — Delivery Complete (The Reward Screen)
┌─────────────────────────────────────────┐
│ 🎉 Delivery Complete! │
│ │
│ TZS 1,750 added to your wallet │
│ │
│ Today's earnings: TZS 8,750 │
│ Deliveries today: 5 │
│ │
│ ───────────────────────────────────── │
│ │
│ Keep it up! 3 more deliveries │
│ to hit your daily bonus 🔥 │
│ │
│ [GO ONLINE] │
└─────────────────────────────────────────┘
The daily bonus progress is deliberate gamification — keeps Dashers online longer and drives supply during peak hours.
9.7 Daily Bonus Structure (Dasher Retention)
8 deliveries/day → TZS 2,000 bonus
12 deliveries/day → TZS 5,000 bonus
15 deliveries/day → TZS 8,000 bonus + "Top Dasher" badge
Dashers chase the bonus. You get supply when you need it most.
Part 10: Dasher Delivery Status Flow
ORDER PLACED
↓
FINDING_DASHER → System dispatches to nearest available Dasher
↓
DASHER_ASSIGNED → Dasher accepted, heading to kitchen
↓
DASHER_AT_KITCHEN → Dasher arrived, awaiting pickup verification
↓
PICKED_UP → QR scanned, food confirmed, en route to customer
↓
IN_TRANSIT → Real-time location tracking active
↓
DASHER_AT_CUSTOMER → Arrived at drop-off location
↓
DELIVERED → Order complete, Dasher earning posted to wallet
Failure states:
NO_DASHER_AVAILABLE → Alert admin, manual assign or fallback
DASHER_CANCELLED → Re-dispatch to next available Dasher
DELIVERY_FAILED → Escalate, refund flow triggered
Part 11: Offer System
11.1 Offer Types
Offers can be applied to subscriptions or per-order:
| Type | How It Works | Example |
|---|---|---|
| PERCENT_DISCOUNT | % off subscription price | 30% off for 3 months |
| FIXED_DISCOUNT | Fixed amount off | TZS 10,000 off first month |
| FREE_MONTHS | N months free | 2 months free on annual plan |
| TRIAL_EXTENSION | Extend trial duration | +7 days on trial |
| SAVE_OFFER | Shown on cancel/downgrade | Stay 1 month at 50% off |
11.2 Save Flow Offer (Churn Prevention)
Kitchen requests cancel or downgrade
↓
System shows save offer:
"Stay on PROFESSIONAL for 1 month at 50% off?"
[Accept] [Proceed anyway]
↓
[Accept] → offer applied, billed at 50% next cycle, full price after
[Proceed] → scheduled for end of cycle
11.3 Offer Rules
- Offers have budget limits (max total discount)
- Offers have expiry dates
- Offers have usage limits (per kitchen, global)
- One active offer per subscription at a time
- Offers track how many billing cycles they've been applied
- When cycles run out, full price resumes automatically
Part 12: Entity Overview
12.1 Subscription Package
subscription/
├── entity/
│ ├── SubscriptionPlanEntity ← plan catalog (STARTER, GROWING, PRO, ENTERPRISE)
│ ├── PlanFeatureEntity ← plan → feature mapping
│ ├── PlanLimitEntity ← plan → limit mapping
│ ├── PlanPricingEntity ← plan → price per billing cycle
│ ├── SubscriptionEntity ← per-kitchen live subscription
│ ├── SubscriptionUsageEntity ← current period usage tracking
│ ├── SubscriptionPaymentEntity ← each billing event
│ ├── SubscriptionTrialEntity ← trial record (one per account)
│ └── SubscriptionScheduledChangeEntity ← pending downgrade/cancel
│
├── enums/
│ ├── SubscriptionStatus ← TRIALING, ACTIVE, PAST_DUE, SUSPENDED, CANCELLED
│ ├── BillingCycle ← WEEKLY, MONTHLY, ANNUAL, CUSTOM
│ └── PlanTier ← STARTER, GROWING, PROFESSIONAL, ENTERPRISE
│
└── service/
├── EntitlementService ← the access gate (most critical)
├── LimitService ← usage limit enforcement
├── SubscriptionService ← lifecycle management
└── SubscriptionBillingService ← renewals, dunning, trial expiry
12.2 Dasher Package
dasher_service/
├── driver/ ← profiles, onboarding, status, ratings
├── dispatch/ ← order assignment, routing, fallback
├── tracking/ ← real-time location, ETA
├── earnings/ ← DasherWalletEntity, per-trip records
└── payout/ ← settlement to M-Pesa / Tigo / Airtel
12.3 Financial Package (delivery additions)
wallet/
├── CustomerWalletEntity
├── KitchenWalletEntity
└── DasherWalletEntity ← Dasher accumulated earnings
ledger/
├── LedgerAccountEntity (includes delivery-specific accounts)
└── JournalEntryEntity
Part 13: The Business Model in One View
┌─────────────────────────────────────────────────────────────────┐
│ JIKOXPRESS EARNS FROM │
├─────────────────────────────────────────────────────────────────┤
│ │
│ EVERY KITCHEN (subscription) │
│ └── Monthly fee for using the management platform │
│ STARTER: TZS 0 | GROWING: TZS X | PRO: TZS XX │
│ │
│ EVERY APP / WHATSAPP ORDER (commission) │
│ └── 10% of food value on orders JikoXpress generated │
│ Kitchen's own customers (POS/Kiosk) = zero commission │
│ │
│ EVERY DASHER DELIVERY (delivery margin) │
│ └── 30% of delivery fee on all Dasher deliveries │
│ Dasher gets 70% — "we share with you, not take from you" │
│ │
│ EVERY TRANSACTION (processing margin) │
│ └── ~0.5% spread on all M-Pesa / card transactions │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ STARTER kitchen, 50 app orders/month, avg TZS 10,000: │
│ Commission: 50 × 10,000 × 10% = TZS 50,000/month │
│ Subscription: TZS 0 │
│ Platform earns TZS 50,000 from a "free" kitchen │
│ │
│ PROFESSIONAL kitchen, 500 app orders + 100 Dasher deliveries: │
│ Subscription: TZS 150,000 │
│ Commission: 500 × 10,000 × 10% = TZS 500,000 │
│ Delivery: 100 × 2,000 × 30% = TZS 60,000 │
│ Platform earns TZS 710,000/month from one kitchen │
│ │
└─────────────────────────────────────────────────────────────────┘
Appendix A: Subscription Status Reference
| Status | Kitchen Can Login | Can Take Orders | Paid Features | Action Required |
|---|---|---|---|---|
| TRIALING | ✅ | ✅ | ✅ (PROFESSIONAL) | None — enjoy trial |
| ACTIVE | ✅ | ✅ | ✅ (per plan) | None |
| PAST_DUE | ✅ | ✅ | ✅ (grace) | Update payment |
| SUSPENDED | ✅ | ❌ | ❌ | Pay to resume |
| CANCELLED | ✅ | ✅ (STARTER) | ❌ (STARTER only) | Re-subscribe |
Appendix B: Dunning Timeline Reference
| Day | Event | Kitchen Status | Action |
|---|---|---|---|
| 0 | Payment fails | PAST_DUE | Push + WhatsApp + retry |
| 1 | Retry | PAST_DUE | Silent retry |
| 3 | Still failing | PAST_DUE | Push + WhatsApp + retry |
| 5 | Retry | PAST_DUE | Silent retry |
| 7 | Final warning | PAST_DUE | Push + WhatsApp + retry |
| 8 | All retries exhausted | SUSPENDED | Limited mode |
| 15 | Grace period over | CANCELLED | Falls to STARTER |
Appendix C: Decision Log
| Decision | Choice | Rationale |
|---|---|---|
| STARTER pricing | Free forever | Acquisition tool — earns via commission anyway |
| Trial plan | PROFESSIONAL (not GROWING) | Show the ceiling — max value drives conversion |
| Trial duration | 3 days | Short enough to create urgency, long enough to experience value |
| Trial recurrence | Once automatic + admin re-grant | Prevents abuse, keeps re-engagement tool |
| Upgrade timing | Immediate | Kitchen gets value now, better experience |
| Downgrade timing | End of cycle | Fair, reduces panic cancellations |
| Failed payment lockout | Soft lock (limited mode) | Hard lockout causes permanent churn |
| Data on downgrade | Soft-lock, never delete | Re-upgrade restores everything — retention lever |
| Auto-select on limit drop | 20 most recently active | Kitchen stays live immediately, no downtime |
| Commission scope | App + WhatsApp only | Fair — charge for demand we generated |
| Delivery model | Internal Dashers only (v1) | Own the margin, own the experience |
| Delivery fee basis | Dasher cost + 30% margin | Fee always covers Dasher, platform never loses |
| Dasher share | 70% of delivery fee | Fair split, drives Dasher supply |
| Dasher notification | Earnings first, 12s timer | Psychology — answer the 3 questions instantly |
| Dunning channel | Push + WhatsApp (not email) | East Africa mobile-first — WhatsApp beats email |
| Service fee on pickup/dine-in | No | Price sensitivity — avoid customer leakage to WhatsApp direct |
| Delivery fee from customer | Yes | Universal expectation, accepted without friction |
End of Document JikoXpress Pro — Payment, Subscription & Business Model Architecture v1.0
JikoXpress Pro — Offer Engine Architecture
Version 3.0 | May 2026 | QBIT SPARK CO LIMITED Full offer engine design — psychology, architecture, user journeys, UI flows, and competitive positioning. Updated: Three discount layers defined. Menu discount as Layer 1. Offer governance and loss prevention added. Implementation prerequisites defined — offer engine builds only after core order flow is stable. No code. No schema. Pure design thinking.
Table of Contents
- Why Offers Win or Kill a Platform
- The Psychology Behind Offers
- The Three Offer Worlds
- The Three Discount Layers — The Full Picture
- Phase Strategy — What We Build Now vs Later
- The Coupon Engine — Phase 1 Architecture
- Offer Types — Deep Dive
- The Rule Engine
- Coupon Scope — What a Coupon Can Target
- Delivery Configuration vs Offers
- Offer Governance & Loss Prevention
- Referral System — Built on Coupon Engine
- B2B — Kitchen Subscription Offers
- Notification & Preference System
- User Journeys — All Levels
- UI Ideas & Flow Concepts
- Where JikoXpress Wins
- Financial Impact of Offers
- Entity Overview
- Implementation Prerequisites
- Decision Log
1. Why Offers Win or Kill a Platform
Jumia failed in food not because of bad tech — they failed because customers had no reason to stay loyal. An offer engine done wrong either burns money with zero return, or is so stingy users ignore it entirely. JikoXpress needs to get this exactly right.
The three ways platforms lose with offers:
TOO GENEROUS → burns budget, users only order when there's a deal, unsustainable
TOO STINGY → users don't feel valued, go to competitor
TOO COMPLEX → users don't understand the deal, ignore it entirely
The winning formula:
RIGHT OFFER → RIGHT PERSON → RIGHT TIME → IRRESISTIBLE
This document designs exactly that — starting with the simplest, most powerful tool: the coupon code.
2. The Psychology Behind Offers
Understanding why humans respond to offers is the foundation of the entire engine. Every offer type maps to a specific psychological trigger.
2.1 Loss Aversion — The Most Powerful Trigger
Humans feel the pain of losing TZS 2,000 more than the pleasure of gaining TZS 2,000.
How JikoXpress uses it:
- "Your coupon expires tonight" → time pressure creates urgency
- "You have TZS 1,500 in your wallet — don't let it go to waste" → loss framing
- Showing crossed-out delivery fee → TZS
1,500→ FREE
2.2 Reciprocity — I Got Something, Now I Owe You
When someone gives us something, we feel compelled to give back.
How JikoXpress uses it:
- Referral coupon gives new user free delivery → they feel obligated to complete the order
- Kitchen gives free item coupon → customer feels loyalty to that kitchen
- First order deal → customer feels obligated to return
2.3 Social Proof — Others Are Doing It
Humans look at what others do to decide what to do.
How JikoXpress uses it:
- "127 people used this coupon today" on the offer card
- Referral system shows friend already uses the platform
2.4 Scarcity — Limited Makes It Valuable
We want what is running out.
How JikoXpress uses it:
- "Only 14 redemptions left for this coupon"
- "This kitchen's lunch special ends at 2pm"
- Coupon with totalUseLimit shows remaining count
2.5 Identity & Reward — I Earned This
We value things more when we feel we worked for them or were specially chosen.
How JikoXpress uses it:
- Birthday coupon — platform "remembered" you, feels personal
- Referral reward — you helped a friend, you get rewarded
- Kitchen coupon on flyer — feels exclusive, like insider access
2.6 Anchoring — The Original Price Matters
Humans judge value based on the first number they see.
How JikoXpress uses it:
- Always show original price crossed out → TZS
8,000→ TZS 6,000 - Show "You saved TZS 2,000" at order confirmation
- Delivery fee shown then zeroed → feels better than just "Free delivery"
3. The Four Offer Worlds
JikoXpress operates four completely separate offer worlds. Each has different actors, different funding, different purpose, and different implementation timing. Understanding these worlds is the foundation of the entire offer system.
┌─────────────────────────────────────────────────────────────────┐
│ WORLD 1: MENU ITEM OFFER │
│ │
│ Actor → Kitchen Owner (menu management) │
│ Funded by → Kitchen (absorbed in their margin) │
│ Purpose → Permanent or semi-permanent price reduction │
│ on a specific menu item │
│ Trigger → None — always active while item is active │
│ Code → No code needed │
│ Channels → ALL channels, always, no restriction │
│ Phase 1 → ✅ BUILD NOW (part of menu management) │
│ Examples → Pilau TZS ~~8,000~~ → TZS 7,500 │
│ Juice TZS ~~3,000~~ → TZS 2,500 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ WORLD 2: KITCHEN COUPON │
│ │
│ Actor → Kitchen Owner (offer dashboard) │
│ Funded by → Kitchen (deducted from their settlement) │
│ Purpose → Time-limited promotions to attract customers, │
│ boost slow hours, push specific items │
│ Trigger → Customer enters code at checkout │
│ Code → Yes — e.g. MAMA20, FRIDAY10, OPENDAY │
│ Channels → All channels (configurable per coupon) │
│ Phase 1 → ✅ BUILD NOW (after core order flow stable) │
│ Examples → 15% off this weekend, free delivery Friday, │
│ TZS 1,000 off orders above TZS 8,000 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ WORLD 3: PLATFORM COUPON │
│ │
│ Actor → JikoXpress Admin │
│ Funded by → Platform (marketing cost, EXPENSE_OFFER_SUBSIDY) │
│ Purpose → Customer acquisition, retention, market growth │
│ Trigger → Customer enters code at checkout │
│ Code → Yes — e.g. JIKO20, WELCOME50, FREESHIP │
│ Also: system-generated referral codes (REF-*) │
│ Channels → All channels (configurable per coupon) │
│ Phase 1 → ✅ BUILD NOW (after core order flow stable) │
│ Examples → 20% off first order, free delivery new users, │
│ referral reward coupons │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ WORLD 4: AUTO / MANUAL PLATFORM OFFERS │
│ │
│ Actor → JikoXpress Admin (defines rules) │
│ System (fires automatically based on triggers) │
│ Funded by → Platform │
│ Purpose → Targeted offers fired by system events or │
│ scheduled by admin — no code needed by customer │
│ Trigger → System event (birthday, first session, schedule) │
│ OR admin manually pushes to eligible users │
│ Code → No — applies transparently at checkout │
│ Channels → All channels │
│ Phase 1 → ⏳ NOT YET — Phase 2 only │
│ Examples → Free delivery this whole week (manual, admin │
│ sets it and it runs for all orders) │
│ Birthday offer — 30% off on user's birthday │
│ Welcome offer — free delivery first order ever │
│ Happy hour — 15% off between 3pm–5pm daily │
│ B2B trial — PROFESSIONAL plan free 30 days │
└─────────────────────────────────────────────────────────────────┘
Critical rule: These four worlds never mix financially. Each has its own cost center, its own accounting entry, its own audit trail. A kitchen coupon cost never touches the platform marketing budget. A platform coupon never reduces what a kitchen earns.
How the Four Worlds Relate to the Three Discount Layers
Layer 1 (menu discount) ←→ World 1 (menu item offer)
Layer 2 (coupon code) ←→ World 2 + World 3 (kitchen + platform coupons)
Layer 3 (auto-offer) ←→ World 4 (auto / manual platform offers)
Same concept, two views. The layers describe the checkout calculation sequence. The worlds describe who owns and funds each type of offer.
4. The Three Discount Layers — The Full Picture
This is the most important framing in the entire document. JikoXpress has three completely separate discount layers. They are independent, they serve different purposes, and they must never be confused with each other — not in design, not in code, not in financial accounting.
┌─────────────────────────────────────────────────────────────────────────┐
│ LAYER 1 — MENU ITEM DISCOUNT │
│ │
│ What it is: A price reduction set directly on a menu item │
│ Who sets it: Kitchen owner (in menu management) │
│ How: Fixed amount off OR percent off the base price │
│ Trigger: None — always active while item is active │
│ Channels: ALL channels (App, WhatsApp, Counter, Kiosk) │
│ Lifespan: No expiry — kitchen edits manually to change │
│ Budget: None — kitchen accepts the reduced earnings │
│ Code needed: No │
│ Financial: Changes the item's selling price │
│ Baked into the order subtotal before anything else │
│ │
│ Example: │
│ Pilau base price: TZS 8,000 │
│ Menu discount: TZS 500 (fixed) │
│ Selling price: TZS 7,500 ← this is now THE price │
│ Shown as: TZS ~~8,000~~ → TZS 7,500 on all channels │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ LAYER 2 — COUPON CODE │
│ │
│ What it is: A promotional code applied at checkout │
│ Who sets it: Admin (platform coupon) or Kitchen owner (kitchen coupon)│
│ How: Customer enters a code at checkout │
│ Trigger: Customer action — manual code entry │
│ Channels: All channels (can be restricted per coupon config) │
│ Lifespan: Defined startDate → endDate, auto-expires │
│ Budget: Hard cap — auto-deactivates when exhausted │
│ Code needed: Yes — e.g. JIKO20, MAMA15, REF-GRACE7X │
│ Financial: Applied on top of already-discounted subtotal (Layer 1) │
│ Platform coupon → platform absorbs cost │
│ Kitchen coupon → deducted from kitchen settlement │
│ │
│ Example: │
│ Order subtotal (after menu discounts): TZS 15,000 │
│ Coupon JIKO20 (20% off): - TZS 3,000 │
│ Customer pays: TZS 12,000 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ LAYER 3 — PLATFORM AUTO-OFFER (Phase 2 only) │
│ │
│ What it is: An offer applied automatically by the system │
│ Who sets it: Admin defines rules, system fires automatically │
│ How: System checks eligibility on every checkout │
│ Trigger: System — based on user profile, order history, date │
│ Channels: All or specific channels │
│ Lifespan: Defined, auto-expires │
│ Budget: Hard cap │
│ Code needed: No — transparent to customer, just applies │
│ Financial: Applied after Layer 1 and Layer 2 │
│ Platform always absorbs cost │
│ │
│ Examples: │
│ Birthday offer — 30% off on user's birthday, auto-applied │
│ Welcome offer — free delivery on first order, auto-applied │
│ ⚠ NOT BUILT IN PHASE 1 │
└─────────────────────────────────────────────────────────────────────────┘
4.1 How the Three Layers Interact at Checkout
The layers are resolved in strict order. Each layer is independent — a change in one does not affect the others.
CHECKOUT CALCULATION SEQUENCE
══════════════════════════════════════════════════════
Step 1 — LAYER 1: Apply menu item discounts
For each item in cart:
selling price = base price - menu discount (if any)
Order subtotal = sum of all selling prices
···············································
Result: discounted subtotal
e.g. TZS 15,000 (already reflects item discounts)
···············································
Step 2 — Calculate delivery fee
From kitchen distance tiers or flat fee
e.g. TZS 1,500
Step 3 — Kitchen delivery subsidy active?
YES → delivery fee = 0, kitchen billed at settlement
NO → delivery fee stays
Step 4 — LAYER 2: Customer entered a coupon?
YES → validate coupon → apply on discounted subtotal
NO → skip
···············································
Result: coupon discount deducted
e.g. JIKO20 20% off TZS 15,000 = - TZS 3,000
New total: TZS 12,000
···············································
Step 5 — LAYER 3: Platform auto-offer eligible? (Phase 2)
YES → apply platform offer
NO → skip
Step 6 — Final total shown to customer
Subtotal (after layer 1): TZS 15,000
Coupon JIKO20 (layer 2): - TZS 3,000
Delivery: TZS 1,500
─────────────────────────────────────────
You pay: TZS 13,500
Savings shown: TZS 3,500 (item discounts + coupon)
4.2 The Clean Separation Rule
Layer 1 (menu discount)
→ Is a PRICE. Not a promotion. Not tracked as offer usage.
→ No budget. No rules. No code. No audit trail as offer.
→ Kitchen absorbs it silently in their margin.
Layer 2 (coupon)
→ Is a PROMOTION. Tracked. Has budget. Has rules. Has audit trail.
→ Goes through the full coupon engine.
→ Creates CouponRedemptionEntity, updates budgetUsed, perUserCount.
Layer 3 (auto-offer, phase 2)
→ Is a PLATFORM PROMOTION. System-triggered.
→ Full audit trail. Platform absorbs cost.
→ Only applies if no coupon was entered (coupon wins).
4.3 Channel Behaviour Per Layer
App WhatsApp Counter Kiosk
─── ──────── ─────── ─────
Layer 1 (menu) ✅ ✅ ✅ ✅ always, no config
Layer 2 (coupon) ✅ ✅ ✅ ✅ configurable per coupon
Layer 3 (auto) ✅ ✅ ✅ ✅ phase 2
5. Phase Strategy — What We Build Now vs Later
This is the most important section for implementation planning.
Why Coupons First is the Right Decision
Auto-applied offers require: Coupons require:
→ User profiling → User enters a code
→ Order history queries at checkout → System validates
→ Real-time eligibility on every session → Done
→ Complex targeting logic
→ Easy to get wrong silently
Coupons give you 80% of the value at 20% of the complexity. More importantly — coupons give you market feedback before you build more. You learn which offer types actually drive orders in Tanzania before over-engineering.
Phase 1 — Build Now
✅ Coupon engine
Admin creates platform coupons
Kitchen owner creates kitchen coupons
Full rule engine behind every coupon
Full scope targeting (whole kitchen, menu item, delivery)
All channels (App, WhatsApp, Counter, Kiosk)
✅ Referral system
Built on top of coupon engine
System generates unique referral coupon per user
Referrer reward via wallet credit after referee completes first order
✅ B2B subscription offers
Manual — admin grants trial or discount via subscription admin panel
No separate offer engine needed
Subscription system handles it directly
Phase 2 — After Real User Data
⏳ Auto-applied offers
Birthday offers (daily scheduled job)
Welcome offer for new users (auto-apply on first checkout)
New user window offers
⏳ Automated B2B offer engine
System detects kitchen growth signals
Auto-triggers trial offer
Tracks acceptance and conversion
⏳ Advanced offer types
Flash sales with countdown
Happy hour triggers
Community offers
The Key Mindset
Phase 1 coupon engine is NOT a simplified version.
It IS the full engine — rules, scope, budget, limits, all of it.
The only thing missing is the auto-apply trigger.
When Phase 2 comes → you add AUTO_APPLIED redemption type
and write the trigger logic. The engine itself doesn't change.
5. The Coupon Engine — Phase 1 Architecture
5.1 The Full Flow
COUPON CREATED (Admin or Kitchen Owner)
Code set (e.g. JIKO20)
Offer type set (FREE_DELIVERY, PERCENT_DISCOUNT, etc.)
Scope set (whole kitchen, menu item, delivery only)
Rules attached (first order, min amount, time window, etc.)
Budget + limits set
Lifespan set (startDate → endDate)
│
▼
COUPON ACTIVE
Discoverable in kitchen deal badges (if kitchen coupon)
Shareable by admin via WhatsApp / SMS / social
Printable by kitchen on physical flyers
│
▼
CUSTOMER ENTERS CODE AT CHECKOUT
│
▼
VALIDATION LAYER (atomic — no side effects yet)
├── Does this code exist?
├── Is the coupon ACTIVE?
├── Is the channel eligible?
├── Does the order scope match? (right kitchen? right item?)
├── Does customer pass ALL rules?
├── Is totalBudget remaining?
├── Has user hit perUserLimit?
├── Is dailyUseLimit still available?
│
├── ANY FAIL → clear error shown, no money moved
│
└── ALL PASS →
│
▼
SLOT LOCKED (prevents race condition on last redemption)
│
▼
DISCOUNT COMPUTED + SHOWN TO CUSTOMER
│
▼
PAYMENT PROCESSED
│
├── PAYMENT SUCCESS →
│ CouponRedemptionEntity created
│ CouponUsageEntity updated (perUser count)
│ budgetUsed updated
│ totalUsed updated
│ Auto-deactivate check (budget or limit exhausted?)
│
└── PAYMENT FAILED →
Slot released
Coupon still available
Customer can retry
5.2 Validation is Atomic
Validation happens before payment. If validation fails, nothing has happened — no money, no records, no side effects. This is non-negotiable.
5.3 One Coupon Per Order — Phase 1
One coupon code per order. No stacking. Ever. Phase 1.
This is a deliberate decision, not a limitation. Here is the full reasoning:
Financial clarity:
One coupon → discount calculation is clean and predictable
Two coupons → edge cases multiply:
Do they stack on original price or already-discounted price?
What if one is platform-funded and one is kitchen-funded?
What if FREE_DELIVERY + PERCENT_DISCOUNT both apply?
→ Financial math becomes ambiguous fast
Abuse prevention:
User finds two 20% off coupons → stacks them → 40% off
Platform or kitchen bleeds money on one order
One coupon → cost is controlled and predictable
Market learning:
You don't yet know which coupon types resonate in Tanzania
Run one coupon per order → collect data → understand behavior
Then decide if stacking makes sense and for which combinations
The rule at checkout:
Customer enters first code → validated and applied, shown in summary
Customer enters second code → first code is REPLACED by second
customer sees new discount
only the last entered code applies at payment
Customer removes code → order returns to full price
Last code entered wins. Customer can change their mind freely — but only one applies when they place the order.
Phase 2 — Controlled Stacking (when data justifies it):
One platform coupon + one kitchen coupon → potentially allowed (different funders)
Two platform coupons → never allowed
Two kitchen coupons → never allowed
FREE_DELIVERY + PERCENT_DISCOUNT → potentially allowed (different targets)
Two PERCENT_DISCOUNT coupons → never allowed
This is future thinking only. Build it when kitchens are running coupons constantly and users are actively requesting it.
5.4 Validation Response — Always Specific
✅ VALID → show discount amount, "JIKO20 applied ✓"
❌ NOT_FOUND → "This code doesn't exist"
❌ EXPIRED → "This offer has ended"
❌ NOT_YET_ACTIVE → "This offer starts on [date]"
❌ ALREADY_USED → "You've already used this code"
❌ LIMIT_REACHED → "This offer is fully redeemed"
❌ BUDGET_EXHAUSTED→ "This offer is no longer available"
❌ WRONG_KITCHEN → "This code is only valid at [Kitchen Name]"
❌ WRONG_ITEM → "This code only applies to [Item Name]"
❌ MIN_NOT_MET → "Minimum order TZS [X] required"
❌ WRONG_DAY → "This offer is only valid on [days]"
❌ WRONG_TIME → "This offer is only valid between [time] – [time]"
❌ NOT_FIRST_ORDER → "This offer is for first-time orders only"
❌ NOT_NEW_USER → "This offer is for new users only"
❌ WRONG_CHANNEL → "This code is not valid on this channel"
Never a vague "invalid code." Always tell the user exactly why. This is UX that builds trust.
6. Offer Types — Deep Dive
6.1 FREE_DELIVERY
Customer pays zero delivery fee. The delivery cost is absorbed by whoever owns the coupon.
Platform coupon → platform pays rider from treasury (EXPENSE_OFFER_SUBSIDY)
Kitchen coupon → deducted from kitchen earnings at settlement
This is different from:
- Kitchen self-delivery setting → kitchen uses own riders, JikoXpress not involved (not a coupon)
- Kitchen delivery subsidy setting → standing "cover all delivery this week" config (not a coupon)
FREE_DELIVERY coupon is promotional — has rules, budget, lifespan, code. Always goes through the coupon engine.
Psychology: #1 checkout abandonment reason is delivery fee. Removing it with a code converts hesitant users instantly.
6.2 PERCENT_DISCOUNT
X% off the order subtotal. Optional maximum cap.
Example: 20% off, max TZS 5,000
Order TZS 12,000 → discount TZS 2,400 (20%) → customer pays TZS 9,600
Order TZS 30,000 → discount capped at TZS 5,000 → customer pays TZS 25,000
Almost always paired with MIN_ORDER_AMOUNT rule to protect margin.
Psychology: Anchoring. Customer calculates savings themselves, feels smart.
6.3 FIXED_DISCOUNT
Flat TZS amount off. Simpler to market than percent.
Example: TZS 2,000 off orders above TZS 8,000
"TZS 2,000 OFF" on a banner → more readable than "20% off up to TZS 5,000"
Psychology: Simplicity. Customer knows exactly what they save before doing math.
6.4 FREE_ITEM
Coupon: FREEJUICE
Offer: Get one Mango Juice free with any main course
Scope: SPECIFIC_MENU_ITEM (menuItemId = juice item)
Rule: MIN_ORDER_AMOUNT = 8,000 (must order a main course worth this)
Kitchen use case — push a new menu item by making it free for first 100 orders. Sampling strategy that works in physical markets too (print flyers, drive digital orders).
Psychology: Reciprocity. Free gift creates obligation to order again.
7. The Rule Engine
Every coupon can have multiple eligibility rules. All rules must pass (AND logic) for the coupon to apply. Rules are stored as separate records linked to the coupon — new rule types can be added in future without changing the coupon structure.
7.1 Phase 1 Rules
┌──────────────────────────────┬──────────────────────────────────────────────┐
│ Rule Type │ What It Checks │
├──────────────────────────────┼──────────────────────────────────────────────┤
│ FIRST_ORDER │ User has zero completed orders ever │
│ FIRST_ORDER_AT_KITCHEN │ User has zero completed orders at this │
│ │ specific kitchen │
│ MIN_ORDER_AMOUNT │ Order subtotal >= configured amount (TZS) │
│ NEW_USER_DAYS │ User registered within last N days │
│ VALID_TIME_WINDOW │ Current time is between start and end time │
│ VALID_DAYS_OF_WEEK │ Today is one of the configured days │
│ MAX_USES_PER_USER │ User redemption count < configured limit │
│ ELIGIBLE_CHANNEL │ Order channel is in the allowed list │
└──────────────────────────────┴──────────────────────────────────────────────┘
7.2 How Rules Compose — Real Examples
"Welcome coupon — new user, first order ever"
FIRST_ORDER
NEW_USER_DAYS = 7
"Lunch special — weekdays only, 12pm–2pm, above TZS 10,000"
VALID_DAYS_OF_WEEK = [MON, TUE, WED, THU, FRI]
VALID_TIME_WINDOW = 12:00 – 14:00
MIN_ORDER_AMOUNT = 10,000
"New customer promo — first time at this kitchen, above TZS 8,000, weekend"
FIRST_ORDER_AT_KITCHEN
MIN_ORDER_AMOUNT = 8,000
VALID_DAYS_OF_WEEK = [FRI, SAT]
"Simple discount — no restrictions, anyone, any time"
(no rules attached — just budget and lifespan limits)
7.3 Future Rule Types — Phase 2
MIN_ORDERS_COMPLETED → "Must have ordered at least 5 times total"
REFERRED_USER → "Must have come via a referral link"
BIRTHDAY_MONTH → "User's birthday is this month"
SPECIFIC_PAYMENT_METHOD → "Must pay via mobile money"
REPEAT_CUSTOMER → "Has ordered from this kitchen before"
Adding a new rule = new enum value + new validation handler. Zero schema changes.
8. Coupon Scope — What a Coupon Can Target
Every coupon has a scope that defines which orders and items it applies to.
┌─────────────────────┬───────────────────────────────────────────────────┐
│ Scope │ Meaning │
├─────────────────────┼───────────────────────────────────────────────────┤
│ ALL_KITCHENS │ Platform coupon — works at any kitchen │
│ SPECIFIC_KITCHEN │ Only at one named kitchen │
│ WHOLE_KITCHEN │ Any item from this kitchen qualifies │
│ SPECIFIC_MENU_ITEM │ Only this specific item gets the discount │
│ DELIVERY_ONLY │ Only the delivery fee is affected │
│ │ Food price unchanged │
└─────────────────────┴───────────────────────────────────────────────────┘
Scope + Offer Type Combinations
DELIVERY_ONLY + FREE_DELIVERY → classic free shipping coupon
SPECIFIC_MENU_ITEM + FREE_ITEM → free specific item
WHOLE_KITCHEN + PERCENT_DISCOUNT → % off everything at this kitchen
ALL_KITCHENS + FIXED_DISCOUNT → TZS X off any order anywhere
SPECIFIC_KITCHEN + FREE_DELIVERY → partnership deal with one kitchen
10. Delivery Configuration vs Offers
This is the most important separation in the delivery + offer system. Three things look similar on the surface but are fundamentally different systems.
┌───────────────────────────┬─────────────────────────────────────────────┐
│ Concept │ What It Is │
├───────────────────────────┼─────────────────────────────────────────────┤
│ Kitchen Self-Delivery │ Kitchen uses own riders. │
│ (KitchenDeliveryConfig) │ JikoXpress NOT involved at all. │
│ │ NOT a coupon. NOT financial. │
│ │ Just a capability flag. │
├───────────────────────────┼─────────────────────────────────────────────┤
│ Kitchen Delivery Subsidy │ "I cover all delivery fees this week." │
│ (KitchenDeliverySubsidy) │ JikoXpress still dispatches riders. │
│ │ NOT a coupon. Standing config with │
│ │ lifespan. No code. No rules. │
├───────────────────────────┼─────────────────────────────────────────────┤
│ FREE_DELIVERY Coupon │ Promotional. Has code, rules, budget, │
│ (CouponEntity) │ lifespan. Platform or kitchen funded. │
│ │ Goes through full coupon engine. │
└───────────────────────────┴─────────────────────────────────────────────┘
Distance-Based Delivery Pricing
Kitchen can configure delivery fee by distance — this is kitchen config, not a coupon:
Kitchen X delivery tiers:
0 – 2 km → Free delivery
2 – 5 km → TZS 1,000
5 – 10 km → TZS 2,500
Above 10km → Not deliverable
Priority Chain at Delivery Checkout
When a customer places a delivery order, system resolves in this exact order:
Step 1: Does kitchen handle own delivery?
YES → customer uses kitchen's own system, JikoXpress out entirely
NO → continue
Step 2: Is customer within kitchen's max delivery radius?
NO → reject (cannot deliver to this location)
YES → continue
Step 3: Calculate base delivery fee from distance tiers (or flat fee)
Step 4: Is there an active Kitchen Delivery Subsidy for this order?
YES → fee = 0, kitchen billed at settlement
NO → continue
Step 5: Did customer enter a FREE_DELIVERY coupon that passed validation?
YES → fee = 0, coupon owner (platform or kitchen) absorbs cost
NO → continue
Step 6: Customer pays the calculated delivery fee
11. Offer Governance & Loss Prevention
This section exists because offers, done without controls, will bleed money silently. Every control here is deliberate — designed to prevent the three failure modes: overspending, abuse, and accidental duplication.
11.1 Who Can Trigger What — Strict Lane Separation
┌─────────────────────┬──────────────────────────────────────────────────┐
│ Actor │ What They Can Create / Trigger │
├─────────────────────┼──────────────────────────────────────────────────┤
│ JikoXpress Admin │ Platform coupons (any type, any kitchen scope) │
│ │ Platform free delivery offers │
│ │ Referral reward configuration │
│ │ B2B subscription trials (manual) │
├─────────────────────┼──────────────────────────────────────────────────┤
│ Kitchen Owner │ Kitchen coupons (their kitchen only) │
│ │ Menu item discounts (their items only) │
│ │ Kitchen delivery subsidy (their orders only) │
├─────────────────────┼──────────────────────────────────────────────────┤
│ System (automated) │ Referral coupon generation (on user request) │
│ │ Auto-deactivation when budget/limit exhausted │
│ │ Auto-expiry at endDate │
│ │ Phase 2 only: auto-offer triggering │
└─────────────────────┴──────────────────────────────────────────────────┘
Nobody crosses their lane. Admin cannot touch kitchen pricing. Kitchen cannot touch platform offers. System only does what it is explicitly programmed to do.
11.2 The Six Hard Controls
These controls are non-negotiable. Every coupon must have all applicable controls set before it can go active.
Control 1 — Hard Budget Cap
Every coupon must have a totalBudget set.
When budgetUsed reaches totalBudget → coupon auto-deactivates immediately.
No human intervention needed. No override possible.
No open-ended "unlimited budget" coupons allowed in the system.
Platform coupon budget = marketing cost line (admin accountable)
Kitchen coupon budget = kitchen's own money (they feel the pain directly)
Control 2 — Hard Time Boundary
Every coupon must have startDate and endDate.
System auto-expires at endDate — midnight, no exceptions.
No open-ended coupons. No "runs until we decide to stop it."
Admin must set an end date before publishing.
Control 3 — Per-User Limit
perUserLimit prevents one person from emptying the budget alone.
Default: 1 use per user unless explicitly set higher.
System checks CouponUsageEntity before every redemption.
Control 4 — Daily Redemption Limit
dailyUseLimit = max redemptions across all users per day.
Even if budget remains, daily cap protects against day-1 rush abuse.
Spreads the cost over the full lifespan rather than burning in hours.
Control 5 — Total Redemption Limit
totalUseLimit = max redemptions ever, regardless of budget.
Useful for "first 100 customers only" style campaigns.
Whichever hits first (budget or redemption limit) auto-deactivates.
Control 6 — Duplicate Offer Warning
When admin creates a new coupon, system checks:
Is there already an ACTIVE coupon of the same type
covering the same scope?
Example: FREE_DELIVERY coupon already active for ALL_KITCHENS?
→ System warns: "A free delivery coupon (FREESHIP) is already running.
Creating another will double your committed delivery budget.
Deactivate FREESHIP first, or confirm you want both active."
Admin must explicitly confirm before publishing the duplicate.
This prevents accidental budget doubling — the most common admin mistake.
11.3 The Real-Time Governance Dashboard (Admin)
Admin sees a live view of all active offer costs:
┌──────────────────────────────────────────────────────────┐
│ Active Offers — Live Overview │
│ │
│ Total active coupons: 4 │
│ Total budget committed: TZS 1,200,000 │
│ Total burned today: TZS 187,000 │
│ Projected burn by end dates: TZS 820,000 │
│ │
│ ⚠ At risk (>80% budget used): │
│ JIKO20 — 84% used — TZS 32,000 remaining │
│ │
│ COUPON TYPE USED BUDGET STATUS │
│ JIKO20 20% off 168× 84% used ACTIVE ● │
│ FREESHIP Free delivery 43× 43% used ACTIVE ● │
│ REF-* Referral 89× 62% used ACTIVE ● │
│ MAMA15 Kitchen 15% 31× 78% used ACTIVE ● │
└──────────────────────────────────────────────────────────┘
You see the bleeding before it kills you.
11.4 How Conflict Is Actually Prevented
The real-world conflict scenarios and how each is handled:
SCENARIO 1: Two free delivery coupons active simultaneously
FREESHIP (platform) active
Admin creates FREERIDE (platform) — same scope
→ Duplicate warning fires before publishing
→ Admin must confirm or deactivate FREESHIP first
→ Even if both go active, one coupon per order rule means
customer only uses one — no double cost per order
→ But total budget is double what was intended — warning saves this
SCENARIO 2: Platform coupon + kitchen coupon same order
JIKO20 (platform 20% off) — customer enters this
MAMA15 (kitchen 15% off) — customer tries to add this
→ Second code replaces first (our one-coupon rule)
→ Customer chooses which one to use
→ No stacking, no conflict, no double cost
SCENARIO 3: Menu discount + coupon
Item has TZS 500 menu discount already (Layer 1)
Customer applies JIKO20 coupon (Layer 2)
→ Coupon applies to the already-discounted subtotal
→ Both discounts apply — by design, no conflict
→ Menu discount absorbed by kitchen margin
→ Coupon cost absorbed by platform or kitchen (whoever owns it)
→ Separate cost centers, no confusion
SCENARIO 4: Budget exhausted mid-day
FREESHIP budget TZS 200,000
Order 87 pushes budgetUsed to TZS 200,000
→ Auto-deactivates immediately
→ Order 88 tries FREESHIP → "This offer is no longer available"
→ No over-spending. Ever.
11.5 Kitchen-Level Protection
Kitchen owners also need protection from their own offers:
Kitchen creates MAMA15 — 15% off, no budget cap set
→ System REQUIRES a budget before publishing
"Set a maximum budget to protect your earnings"
Kitchen's dashboard shows real-time:
MAMA15 used 31× · TZS 28,500 subsidized · Budget: 71% remaining
Estimated end: Sunday (at current rate)
At 90% budget → kitchen gets a warning notification:
"Your MAMA15 coupon budget is almost used up.
Add more budget or let it auto-stop when exhausted."
12. Referral System — Built on Coupon Engine
The referral system is not a separate engine. It sits on top of the coupon engine. A referral is a system-generated coupon with special attributes.
10.1 How It Works
Customer A (Grace) wants to refer a friend
Grace goes to "Refer a Friend" section
System generates a unique referral coupon: REF-GRACE7X
│
▼
Grace shares code/link with Zawadi via WhatsApp, SMS, or any channel
│
▼
Zawadi registers on JikoXpress
Zawadi places her first order
Zawadi enters REF-GRACE7X at checkout
│
▼
Coupon engine validates:
✓ Code exists and is a referral code (REF- prefix)
✓ Zawadi is a new user (NEW_USER_DAYS rule)
✓ This is Zawadi's first order (FIRST_ORDER rule)
✓ Code not yet used (perUserLimit = 1, totalUseLimit = 1)
✓ Grace cannot refer herself (device/phone check)
│
▼
Offer applied: FREE_DELIVERY on Zawadi's first order
│
▼
Order completes
│
├── Zawadi already got free delivery (applied at checkout)
│
└── ReferralRewardEntity created for Grace
Status: PENDING → UNLOCKED
TZS 2,000 wallet credit → Grace's wallet
Push notification: "Zawadi placed her first order! TZS 2,000 added to your wallet 💰"
10.2 Referral Coupon Properties
Code format → REF-[shortcode] (e.g. REF-GRACE7X)
Generated by → system (not human-readable marketing code)
Offer type → FREE_DELIVERY (referee gets free delivery on first order)
Rules attached → FIRST_ORDER + NEW_USER_DAYS
perUserLimit → 1 (referee can only use one referral code)
totalUseLimit → 1 (each referral code is single-use)
Funded by → Platform (marketing cost)
linkedReferrer → Grace's userId (for reward tracking)
10.3 Referral Entities
ReferralEntity
referrerId → Grace
refereeId → Zawadi (set when Zawadi registers with the link)
couponCode → REF-GRACE7X
status → PENDING | COMPLETED | REWARDED | VOIDED
ReferralRewardEntity
referralId → links to ReferralEntity
beneficiary → REFERRER (Grace) or REFEREE (Zawadi)
rewardType → WALLET_CREDIT | OFFER_APPLIED
amount → TZS 2,000 (Grace) / TZS 1,500 delivery value (Zawadi)
status → PENDING | UNLOCKED | PAID | EXPIRED
10.4 Anti-Abuse Rules
Self-referral check → Grace cannot use her own code
Device fingerprint → same device cannot be both referrer and referee
Phone number check → same number cannot register twice
Reward only on complete → Zawadi must complete order (not just register)
10.5 Financial Impact
Cost per referral:
Zawadi's free delivery subsidy: TZS 1,500 → EXPENSE_OFFER_SUBSIDY
Grace's wallet credit: TZS 2,000 → EXPENSE_REFERRAL_REWARD
Total platform cost: TZS 3,500
vs. acquiring a new user via digital ads in Tanzania:
Estimated CAC via ads: TZS 15,000 – 25,000+
Referral CAC: TZS 3,500 with two happy, engaged users.
13. B2B — Kitchen Subscription Offers
Why B2B Is Not in the Coupon Engine (Phase 1)
Coupon engine target → customers (end users placing food orders)
B2B target → kitchen owners (business operators)
B2B currency → subscription months, plan access
B2B logic → lives in the subscription system
Building a separate B2B coupon engine in phase 1 is overengineering. The subscription system already has everything needed.
Phase 1 — Manual Admin Actions
Admin handles B2B directly from the subscription admin panel:
FREE TRIAL
Admin selects kitchen → "Grant free trial"
Selects plan (PROFESSIONAL) and duration (30 days)
SubscriptionEntity updated:
plan = PROFESSIONAL
trialEndDate = today + 30 days
isTrial = true
Kitchen owner notified via WhatsApp
Day 28 → system reminder to kitchen owner
Day 30 → auto-revert to previous plan if not upgraded
DISCOUNTED UPGRADE
Admin selects kitchen → "Apply discount"
Sets discount percent and duration (N billing cycles)
Subscription billing uses discounted rate for those cycles
Normal rate resumes after
LOYALTY REWARD
Admin manually adjusts kitchen's subscription rate
"You've been with us 12 months — 20% off forever"
Admin note recorded for audit trail
Phase 2 — Automated B2B Engine
When you have enough kitchens that manual management doesn't scale:
System detects growth signals (order volume threshold)
Auto-triggers trial offer to qualifying kitchens
Tracks acceptance, trial usage, conversion rate
Full B2B offer engine with targeting rules
14. Notification & Preference System
12.1 User Notification Preferences
Every customer controls their offer notification preferences:
Master toggle: Notify me for offers [ON / OFF]
If ON — what types do you want?
☑ Free delivery deals
☑ Discounts & coupons
☑ New kitchen promotions
☑ Birthday offers
☐ Flash sales
☑ Referral rewards
One UserOfferPreferenceEntity per user. System only notifies users who opted in to the relevant category.
12.2 Notification Triggers
TRIGGER ACTION
─────────────────────────────── ──────────────────────────────────────────────
New platform coupon published → notify all eligible opted-in users
New kitchen coupon published → notify users who ordered from this kitchen
and opted in
Referral reward unlocked → notify referrer "TZS 2,000 added to wallet"
Coupon about to expire → 24hr warning if user viewed but didn't redeem
B2B trial offer granted → WhatsApp to kitchen owner
B2B trial ending soon → reminder 2 days before trial ends
12.3 Notification Channels
Push notification → app users
WhatsApp message → WhatsApp channel users (highest open rate in Tanzania)
SMS → fallback for users without WhatsApp or app
In-app banner → shown next time user opens app
15. User Journeys — All Levels
13.1 New Customer — Platform Coupon (Admin Drops Code on WhatsApp)
[JikoXpress admin posts in a WhatsApp group]:
"New on JikoXpress? Use code JIKO20 — 20% off your first order! 🎉"
Maria sees the message, downloads the app, registers
│
▼
Maria browses kitchens, adds items (TZS 10,000 food)
│
▼
Checkout → Maria types: JIKO20
│
▼
System validates:
✓ Code exists
✓ Coupon is ACTIVE
✓ Channel: App (eligible)
✓ FIRST_ORDER rule → Maria has 0 completed orders ✓
✓ NEW_USER_DAYS = 7 → Maria registered today ✓
✓ Budget remaining
✓ perUserLimit not hit
│
▼
Checkout updates:
Food total: TZS 10,000
20% discount: - TZS 2,000
You pay: TZS 8,000
"JIKO20 applied — You saved TZS 2,000 ✓"
│
▼
Maria pays TZS 8,000. Order placed. Order arrives.
│
▼
Order confirmation: "You saved TZS 2,000 today 🎉
Share JikoXpress with a friend — you BOTH get a reward. [Share]"
│
▼
Maria immediately shares her referral code.
New user acquired. Referral loop started.
13.2 Regular Customer — Kitchen Coupon from Physical Flyer
[Juma walks past a flyer in Mbeya town]
Flyer: "Order from Mama Lishe online — use MAMA15 for 15% off this weekend 🍽"
│
▼
Juma opens JikoXpress (app or WhatsApp), finds Mama Lishe, adds items (TZS 12,000)
│
▼
Checkout → types: MAMA15
│
▼
System validates:
✓ Code exists
✓ Coupon is ACTIVE, scope = SPECIFIC_KITCHEN (Mama Lishe) ✓
✓ VALID_DAYS_OF_WEEK = [FRI, SAT] → today is Saturday ✓
✓ MIN_ORDER_AMOUNT = 8,000 → order is TZS 12,000 ✓
✓ perUserLimit = 2 → Juma has used 0 times ✓
│
▼
Checkout updates:
Food: TZS 12,000
15% discount: - TZS 1,800
You pay: TZS 10,200
"MAMA15 applied — You saved TZS 1,800 ✓"
│
▼
Order placed.
Kitchen Mama Lishe receives:
Gross: TZS 12,000 (full food amount)
Less offer subsidy:- TZS 1,800 (kitchen funded this)
Net settlement: TZS 10,200
Platform earns service fee on original TZS 12,000.
Physical flyer → digital order. Tanzania market unlocked.
13.3 New User — Referral Coupon
[Grace is a happy JikoXpress user]
Grace goes to "Refer a Friend" → gets code REF-GRACE7X
Grace sends it to her sister Zawadi via WhatsApp
│
▼
Zawadi registers on JikoXpress
Zawadi browses, adds items (TZS 9,000 food, delivery TZS 1,500)
│
▼
Checkout → Zawadi enters: REF-GRACE7X
│
▼
System validates:
✓ Referral code exists and is valid
✓ Zawadi is a new user ✓
✓ This is Zawadi's first order ✓
✓ Code not yet used ✓
✓ Grace ≠ Zawadi (self-referral check passed) ✓
│
▼
Offer: FREE_DELIVERY applied
Food: TZS 9,000
Delivery: ~~TZS 1,500~~ → FREE ✓
You pay: TZS 9,000
"Welcome gift — Free delivery on your first order 🎁"
│
▼
Zawadi pays TZS 9,000. Order completes.
│
▼
System creates ReferralRewardEntity for Grace
Status: PENDING → UNLOCKED
TZS 2,000 credited to Grace's wallet
│
▼
Grace gets push notification:
"Your sister Zawadi placed her first order! TZS 2,000 added to your wallet 💰"
│
▼
Grace uses wallet credit on her next order.
Two users engaged. Total platform cost: TZS 3,500.
13.4 Kitchen Owner — Creating a Weekend Promo Coupon
[Mama Lishe logs into kitchen dashboard — Thursday afternoon]
Goes to Offers → clicks [+ New Coupon]
│
▼
Form:
Coupon code: MAMA15 (she types this — her brand, memorable)
Offer type: PERCENT_DISCOUNT
Discount: 15%
Applies to: Whole kitchen
Rules:
Min order: TZS 8,000
Valid days: Friday, Saturday
Time window: All day (no time restriction)
Budget: TZS 40,000 (won't spend more than this)
Per user limit: 2 uses
Runs: This Friday → Sunday night
│
▼
Coupon goes ACTIVE
Deal badge appears on Mama Lishe's kitchen card in the app
│
▼
Kitchen dashboard shows real-time:
"MAMA15 — Used 18× today · TZS 28,500 subsidized · Budget: 29% remaining"
│
▼
Sunday midnight → coupon auto-expires
Kitchen sees performance report:
Total orders from coupon: 31
Total subsidy paid: TZS 40,000 (budget fully used, auto-deactivated)
New customers acquired: 14 first-time orders at this kitchen
Kitchen owner has full control, zero surprises.
13.5 Kitchen Owner — Receiving B2B Trial Offer (Manual Phase 1)
[Kitchen Bora Bora — 4 months on STARTER, growing fast]
Admin notices 3x order volume growth in last 60 days
Admin goes to subscription admin panel
Selects Bora Bora → [Grant Free Trial]
Plan: PROFESSIONAL
Duration: 30 days
Note: "Growth milestone reward"
│
▼
Admin sends WhatsApp to Bora Bora owner:
"Habari! Your kitchen is growing fast 🚀
We're giving you FREE access to PROFESSIONAL plan for 30 days.
Enjoy: unlimited menu items, advanced analytics, priority support.
No action needed — it's already active. Try it!"
│
▼
Day 28 → system auto-sends reminder to kitchen:
"Your free PROFESSIONAL trial ends in 2 days.
Upgrade to keep your features — TZS 45,000/month.
Reply to talk to our team."
│
▼
Kitchen has grown accustomed to PROFESSIONAL features → upgrades.
16. UI Ideas & Flow Concepts
14.1 Home Screen — Coupon Discovery
┌─────────────────────────────────────────────┐
│ JikoXpress 🔔 👤 │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 🎉 Use code JIKO20 — 20% off │ │
│ │ your order today! │ │
│ │ [Copy Code] │ │
│ └─────────────────────────────────────┘ │
│ │
│ 🔥 Kitchens With Deals │
│ ┌────────────┐ ┌────────────┐ │
│ │ [Photo] │ │ [Photo] │ │
│ │ Mama Lishe │ │ Bora Bora │ │
│ │ 🏷 MAMA15 │ │ 🚚 Free │ │
│ │ 15% OFF │ │ delivery │ │
│ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────┘
Deal badges are visible before clicking into a kitchen. Value is shown upfront.
14.2 Checkout — Coupon Entry & Applied State
┌──────────────────────────────────────┐
│ Order Summary │
│ │
│ Ugali na Nyama TZS 7,000 │
│ Pilau TZS 5,000 │
│ ─────────────────────────────────── │
│ Subtotal TZS 12,000 │
│ Delivery TZS 1,500 │
│ │
│ ┌──────────────────────────────┐ │
│ │ 🏷 Enter coupon code │ │
│ │ [MAMA15 ] [APPLY] │ │
│ └──────────────────────────────┘ │
│ │
│ ✅ MAMA15 applied! │
│ Discount: - TZS 1,800 │
│ ─────────────────────────────────── │
│ Total TZS 10,200 │
│ │
│ 💚 You saved TZS 1,800 today! │
│ │
│ [Place Order — TZS 10,200] │
└──────────────────────────────────────┘
Savings celebrated before payment. Positive reinforcement at the most important moment.
14.3 Order Confirmation — Savings + Referral CTA
┌──────────────────────────────────────┐
│ ✅ Order Confirmed! │
│ │
│ Estimated delivery: 30 minutes │
│ │
│ ┌──────────────────────────────┐ │
│ │ 💰 You saved TZS 1,800 │ │
│ │ with code MAMA15 │ │
│ └──────────────────────────────┘ │
│ │
│ 🎁 Know someone who'd love this? │
│ Share your referral code and │
│ you BOTH get a reward. │
│ Your code: REF-MARIA3K │
│ [Share Now] │
└──────────────────────────────────────┘
Every confirmation is a referral opportunity. Strike when satisfaction is highest.
14.4 Coupon Validation — Error State
┌──────────────────────────────────────┐
│ [MAMA15 ] [APPLY] │
│ │
│ ❌ This offer is only valid on │
│ Fridays and Saturdays. │
│ Come back this weekend! │
└──────────────────────────────────────┘
Specific error. Not just "invalid." And crucially — "come back this weekend" keeps the user engaged for a future order.
14.5 Kitchen Dashboard — Coupon Management
┌──────────────────────────────────────┐
│ My Coupons [+ New] │
│ │
│ ● MAMA15 — Weekend Special ACTIVE │
│ 15% off · Fri–Sat │
│ Used 18× · Budget: 29% left │
│ Expires Sunday │
│ [Pause] [Edit] [Stats] │
│ │
│ ○ OPENDAY — Grand Opening EXPIRED │
│ 20% off · 3 days │
│ Used 87× · Budget fully used │
│ New customers: 41 │
│ [Duplicate as new] │
└──────────────────────────────────────┘
Performance visible at a glance. Kitchen owners see ROI clearly.
14.6 Offer Notification Preferences
┌──────────────────────────────────────┐
│ ← Offer Notifications │
│ │
│ Get notified about deals │
│ ●────────────────── ON │
│ │
│ What interests you? │
│ │
│ 🚚 Free Delivery deals ☑ │
│ 🏷 Discounts & coupons ☑ │
│ 🆕 New kitchen promos ☑ │
│ 🎂 Birthday offers ☑ │
│ 🎁 Referral rewards ☑ │
│ ⚡ Flash sales ☐ │
│ │
│ [Save] │
└──────────────────────────────────────┘
User in control. Builds trust. Reduces unsubscribes.
17. Where JikoXpress Wins
15.1 The Tanzania-Specific Advantages
Physical coupon → digital order bridge
Kitchen prints MAMA15 on a flyer, sticks it at a bus station, a market stall, a university notice board. Customer orders on WhatsApp or app using that code. No other food platform in Tanzania does this cleanly. This is the entire informal economy brought into the digital platform.
WhatsApp as a first-class coupon channel
Admin drops JIKO20 in a WhatsApp group. Kitchen shares MAMA15 in their neighborhood group. These codes work on WhatsApp ordering directly. The channel Tanzanians already use becomes a marketing and ordering channel simultaneously.
Kitchen empowerment Kitchens are not passive suppliers. They create their own coupons, set their own rules, control their own budget. They become invested in JikoXpress success because JikoXpress invests in their success.
Referral at TZS 3,500 CAC Acquiring users via digital ads in Tanzania costs TZS 15,000–25,000+. A referral costs TZS 3,500 and gives you two engaged users. This is how JikoXpress grows without burning investor money.
15.2 What Jumia Got Wrong — What JikoXpress Fixes
Jumia's mistake JikoXpress answer
──────────────────────────────────── ──────────────────────────────────────────
App-only offers → Coupons work on App + WhatsApp + Counter
No physical coupon bridge → Kitchen prints codes on flyers
Generic "save money" messaging → Specific errors, specific savings shown
No kitchen marketing tools → Kitchen dashboard with full coupon control
No referral program → Structured referral built on coupon engine
Burned budget on wrong users → Rule engine filters eligible users precisely
15.3 The Retention Loop
Customer discovers JikoXpress via referral code or admin coupon
│
▼
First order → coupon gives value → positive experience
│
▼
Refers a friend → wallet credit → uses wallet → orders again
│
▼
Favorite kitchen runs a weekend promo → orders again
│
▼
Habit formed → orders without needing a coupon
│
▼
Becomes organic referrer
The coupon engine's real job is not discounts. Its job is to convert a first-time user into a habitual customer before they even notice it happened.
18. Financial Impact of Offers
16.1 Platform Coupon — Financial Flow
Platform coupon applied (e.g. JIKO20 — 20% off):
Customer pays: TZS 8,000 (discounted)
Platform absorbs: TZS 2,000 (the discount)
Kitchen receives: TZS 10,000 (full original amount — always)
Journal:
DEBIT EXPENSE_OFFER_SUBSIDY 2,000
CREDIT ASSET_PSP_SELCOM 2,000
Platform service fee earned on original TZS 10,000 — not discounted amount.
Platform marketing cost = TZS 2,000.
16.2 Kitchen Coupon — Financial Flow
Kitchen coupon applied (e.g. MAMA15 — 15% off TZS 12,000 order):
Customer pays: TZS 10,200 (discounted)
Kitchen absorbs: TZS 1,800 (they chose to discount)
Kitchen receives: TZS 10,200 net (after subsidy deduction at settlement)
Settlement:
Kitchen gross: TZS 12,000
Less: coupon subsidy: - TZS 1,800
Kitchen net: TZS 10,200
Platform service fee earned on original TZS 12,000.
Platform earns same regardless of kitchen's promotional decisions.
16.3 FREE_DELIVERY Coupon — Financial Flow
FREE_DELIVERY coupon applied:
Customer pays: food total only (e.g. TZS 9,000)
Platform absorbs: delivery fee (e.g. TZS 1,500) if platform coupon
Kitchen absorbs: delivery fee if kitchen coupon (deducted at settlement)
Rider receives: full delivery fee as normal
Platform coupon journal:
DEBIT EXPENSE_OFFER_SUBSIDY 1,500
CREDIT ASSET_PSP_SELCOM 1,500
16.4 Budget Controls — Automatic
Every coupon with a budget has automatic controls:
budgetUsed reaches 80% of budgetTotal → admin / kitchen notification
budgetUsed reaches 100% → coupon auto-deactivates immediately
totalUsed reaches totalUseLimit → coupon auto-deactivates immediately
No manual intervention needed.
No over-spending possible.
16.5 Referral Financial Impact
Per referral:
Referee free delivery: TZS 1,500 → EXPENSE_OFFER_SUBSIDY
Referrer wallet credit: TZS 2,000 → EXPENSE_REFERRAL_REWARD
Total platform cost: TZS 3,500
Industry digital ad CAC: TZS 15,000 – 25,000+
Referral CAC: TZS 3,500 with two engaged users
19. Entity Overview
No code. No schema. Just entities and their relationships.
CouponEntity
→ defines the coupon (code, type, owner, scope, discount config, budget, dates)
→ offerOwner: PLATFORM | KITCHEN
→ has many CouponRuleEntity
→ has many CouponUsageEntity (per user tracking)
→ has many CouponRedemptionEntity (per order)
CouponRuleEntity
→ one rule per record (ruleType + ruleValue)
→ many rules per coupon, ALL must pass (AND logic)
CouponUsageEntity
→ one record per user per coupon
→ tracks how many times this user used this coupon
→ prevents per-user limit violations
CouponRedemptionEntity
→ one record per order that used a coupon
→ stores exact discount applied, amount subsidized
→ funded by: PLATFORM or KITCHEN
→ linked to both coupon and order
KitchenDeliveryConfig
→ deliveryHandledBy: JIKOXPRESS | KITCHEN_SELF
→ deliveryPricingModel: FLAT_FEE | DISTANCE_BASED | FREE_ALWAYS
→ maxDeliveryRadiusKm
→ has many KitchenDeliveryTierEntity
KitchenDeliveryTierEntity
→ fromKm, toKm, fee, isDeliverable
→ child of KitchenDeliveryConfig
KitchenDeliverySubsidyEntity
→ standing delivery cost coverage (not a coupon)
→ coverageType: ALL_ORDERS | SPECIFIC_ORDERS
→ startDate, endDate, status: ACTIVE | EXPIRED | CANCELLED
UserOfferPreferenceEntity
→ one per user
→ notificationsEnabled (master toggle)
→ preferredTypes: list of OfferNotificationType
ReferralEntity
→ referrerId, refereeId
→ couponCode (the REF- code generated)
→ status: PENDING | COMPLETED | REWARDED | VOIDED
ReferralRewardEntity
→ referralId (links to ReferralEntity)
→ beneficiary: REFERRER | REFEREE
→ rewardType: WALLET_CREDIT | OFFER_APPLIED
→ amount, status: PENDING | UNLOCKED | PAID | EXPIRED
20. Implementation Prerequisites
The offer engine is designed. It is complete. Do not build it yet.
This is not because the design is incomplete — it is because offers touch every financial layer of the platform. Building offers while those layers are still moving creates financial bugs that are hard to trace and expensive to fix.
The offer engine implementation begins only when all of the following are green:
PREREQUISITE WHY IT MATTERS FOR OFFERS
──────────────────────────────────────── ─────────────────────────────────────────────
✅ Order lifecycle complete Offer redemption is tied to order status.
(checkout → confirmed → delivered) Can't track "order completed" for FIRST_ORDER
rule or referral reward unlock without this.
✅ Payment splitting working Platform coupons debit EXPENSE_OFFER_SUBSIDY.
(kitchen vs platform vs rider) Kitchen coupons deduct from settlement.
If splitting is wrong, offer accounting is wrong.
✅ Kitchen settlement working Kitchen coupon subsidy is deducted at settlement.
(earnings, deductions, payouts) Can't deduct what hasn't been calculated.
✅ Journal entries flowing correctly Every offer redemption creates journal entries.
If double-entry bookkeeping isn't stable,
offer entries will pollute the ledger.
✅ User order history queryable FIRST_ORDER rule queries: "has this user ever
completed an order?"
FIRST_ORDER_AT_KITCHEN queries per kitchen.
Needs reliable order history.
✅ Wallet system working Referral rewards credit the referrer's wallet.
Wallet must exist and be transactionally safe
before referral rewards are issued.
✅ Menu item management stable Layer 1 (menu discount) is set on menu items.
Menu item CRUD must be stable before adding
discount fields to it.
When all seven are confirmed stable and tested in a real environment — open this document and begin entity design and implementation.
The design will not change significantly. The thinking is done. Build the foundation first.
21. Decision Log
| Decision | Choice | Reason |
|---|---|---|
| Three discount layers | Layer 1 (menu), Layer 2 (coupon), Layer 3 (auto-offer phase 2) | Each serves a different purpose, different actor, different financial impact. Must never be confused. |
| Menu discount channels | All channels, always | A price is a price. Not a promotion. Channel restriction makes no sense for item pricing. |
| Menu discount lifespan | None in phase 1 — kitchen edits manually | Adding lifespan blurs the line between menu pricing and the coupon engine. Keep layers clean. |
| Menu discount financial | Absorbed in kitchen margin, not tracked as offer | It is a price reduction, not a promotional cost. No CouponRedemptionEntity created. |
| Phase 1 redemption | Coupon codes only | 80% of value, 20% complexity. Market feedback before building auto-apply. |
| Auto-applied offers | Phase 2 | Requires user profiling and eligibility checks on every checkout. Build when you have data. |
| Rule engine | Separate CouponRuleEntity table |
Future-proof. New rule types added as enum values without schema changes. |
| Referral implementation | System-generated coupon on top of coupon engine | Reuses validation engine. Not a separate system. Clean separation of concerns. |
| B2B phase 1 | Manual via subscription admin panel | Not enough kitchens to justify automation. Admin handles manually. |
| Coupon codes on all channels | Yes, default | Physical flyer → WhatsApp/app/counter bridge is a real Tanzania market advantage. |
| Coupon stacking | One coupon per order, phase 1 | Financial math stays clean. Prevents abuse. Last code entered replaces previous. Phase 2 may allow controlled stacking when market data justifies it. |
| Kitchen always gets full amount | Yes (platform coupons) | Platform subsidy is platform's marketing cost. Kitchen is not penalized. |
| Platform earns on original price | Yes (kitchen coupons too) | Kitchen's promotional decision does not affect platform service fee. |
| Delivery config separation | Three distinct systems | Self-delivery config, subsidy standing instruction, and FREE_DELIVERY coupon are fundamentally different. Must never mix. |
| Distance-based delivery | Kitchen config tiers | Pricing decision by kitchen, resolved at checkout before coupons are checked. |
| Validation specificity | Always specific error message | "Come back this weekend" keeps user engaged. Vague errors kill conversion. |
| Budget auto-deactivation | Yes, at 100% usage | No over-spending possible. No manual intervention needed. |
| Budget required | Mandatory before publishing any coupon | No open-ended coupons. Every coupon has a financial ceiling. |
| Duplicate offer warning | Yes — system warns before admin publishes duplicate type/scope | Most common admin mistake. Prevents accidental budget doubling. |
| Referral CAC | TZS 3,500 target | TZS 1,500 (referee delivery) + TZS 2,000 (referrer wallet). Far below digital ad CAC. |
| Offer engine implementation timing | Only after 7 core prerequisites are stable | Offers touch every financial layer. Building on unstable foundation creates untraceable financial bugs. |
QBIT SPARK CO LIMITED | JikoXpress Pro | May 2026 Internal offer engine architecture — confidential
JikoXpress Pro — Financial Engine Flow
QBIT SPARK CO LIMITED | April 2026 | Internal Architecture Reference
1. Architecture Overview
The financial engine is a layered stack. Each layer only talks to the layer directly below it. No layer skips levels.
┌─────────────────────────────────────────────────────┐
│ DOMAIN LAYER (callers) │
│ CheckoutSessionService SubscriptionBillingService│
└──────────────────────┬──────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────┐
│ OrderPaymentService │
│ builds splits, decides escrow, calls TransactionSvc│
└──────────────────────┬──────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────┐
│ TransactionService │
│ records PaymentTransaction, creates splits, │
│ manages EscrowEntity, calls Ledger + Wallet │
└────────┬─────────────────────────┬──────────────────┘
│ │
┌────────▼────────┐ ┌───────────▼───────────────────┐
│ LedgerService │ │ WalletService │
│ double-entry │ │ credit/debit + WalletTxn │
│ journal entries│ │ balanceBefore/balanceAfter │
└────────▲────────┘ └───────────────────────────────┘
│
┌────────┴──────────────────────────────────────────┐
│ PSP Layer │
│ PspGatewayResolver → SnippeGateway │
│ PspCollectionService DisbursementService │
│ SnippeWebhookService │
└───────────────────────────────────────────────────┘
2. The Two Financial Worlds
| World | Channels | Treasury Involved | Platform Earns |
|---|---|---|---|
| World 1 — JikoXpress Pool | App, WhatsApp | Yes — full ledger | Commission + delivery margin |
| World 2 — Kitchen's Own | POS cash, kitchen custom, kitchen wallet | No — zero treasury | Zero per order (covered by subscription) |
Rule: If money never touches Snippe, it never touches the ledger.
3. Chart of Accounts
ASSETS — what platform physically controls
ASSET_PSP_SNIPPE money sitting at Snippe
ASSET_ESCROW held money, belongs to nobody yet
LIABILITIES — what platform owes, never touch without permission
LIABILITY_WALLETS all user wallets combined
LIABILITY_SETTLEMENTS payouts in flight, on the way out
REVENUE — platform's own earnings
REVENUE_SUBSCRIPTION_FEES kitchen plan payments
REVENUE_MARKETPLACE_COMMISSION 10% on App/WhatsApp orders
REVENUE_DELIVERY_MARGIN 30% of delivery fee
REVENUE_PROCESSING_MARGIN 0.5% on PSP transactions
EXPENSES
EXPENSE_REFUNDS money returned to customers
EQUITY
EQUITY_CAPITAL admin investments
EQUITY_RETAINED_EARNINGS accumulated profits — admin withdraws from here only
Golden Rule (checked continuously):
ASSET_PSP_SNIPPE + ASSET_ESCROW >= LIABILITY_WALLETS + LIABILITY_SETTLEMENTS
If this breaks → crisis alert immediately.
4. Money Flow — All Scenarios
4.1 Wallet Topup (Customer adds money)
Trigger: Customer initiates topup via mobile money
Customer phone
│
▼
[SnippeGateway.initiateCollection()]
│ POST /v1/payments → Snippe
│
▼
PspCollectionEntity created (status: PROCESSING)
│
│ ... customer enters PIN ...
│
▼
Snippe fires webhook: payment.completed
│
▼
[SnippeWebhookController]
│ verifies HMAC signature
│ idempotency guard via PspCollectionLogEntity
│
▼
[PspCollectionService.onPaymentCompleted()]
│
├── LedgerService.writeEntry()
│ DEBIT ASSET_PSP_SNIPPE 50,000
│ CREDIT LIABILITY_WALLETS 50,000
│
├── WalletService.credit()
│ wallet.balance += 50,000
│ WalletTransactionEntity (type: TOPUP, balanceBefore, balanceAfter)
│
└── PspCollectionEntity (status: COMPLETED)
End state: Money at Snippe. Customer wallet credited. Ledger balanced.
4.2 App Order — Delivery — Mobile Money (Full escrow flow)
Order total: TZS 18,000
Customer pays via USSD
│
▼
[PspCollectionService.initiateCollection()]
│ PspCollectionEntity (status: PROCESSING)
│
▼
Snippe webhook: payment.completed
│
▼
[PspCollectionService.onPaymentCompleted()]
│ LedgerService.writeEntry()
│ DEBIT ASSET_PSP_SNIPPE 18,000
│ CREDIT LIABILITY_WALLETS 18,000 ← temporary, will move to escrow
│
▼ (collection type = ORDER_PAYMENT, wallet NOT credited here)
│
▼
[OrderPaymentService.pay()]
│ builds splits:
│ KITCHEN_REVENUE → kitchen 13,000 (PENDING)
│ DELIVERY_FEE → rider 2,800 (PENDING)
│ DELIVERY_FEE → platform 1,200 (PENDING)
│ SERVICE_FEE → platform 1,000 (PENDING)
│
▼
[TransactionService.record()]
│
├── LedgerService.writeEntry()
│ DEBIT ASSET_PSP_SNIPPE 18,000
│ CREDIT ASSET_ESCROW 18,000 ← money held
│
├── PaymentTransactionEntity (holdInEscrow: true)
├── TransactionSplitEntity × 4 (status: PENDING)
└── EscrowEntity (status: HELD, condition: DELIVERY_CONFIRMED)
... kitchen prepares, rider picks up, delivers ...
▼
Rider confirms delivery
│
▼
[OrderPaymentService.confirmDelivery()]
│
▼
[TransactionService.releaseEscrow()]
│
├── LedgerService.writeEntry()
│ DEBIT ASSET_ESCROW 18,000
│ CREDIT LIABILITY_WALLETS 13,000 (kitchen)
│ CREDIT LIABILITY_WALLETS 2,800 (rider)
│ CREDIT REVENUE_DELIVERY_MARGIN 1,200
│ CREDIT REVENUE_MARKETPLACE_COMMISSION 1,000
│
├── WalletService.credit(kitchenId, 13,000, ORDER_EARNING)
├── WalletService.credit(riderId, 2,800, DELIVERY_EARNING)
│
├── TransactionSplitEntity × 4 (status: CREDITED)
└── EscrowEntity (status: RELEASED)
End state: Kitchen wallet +13,000. Rider wallet +2,800. Platform revenue +2,200. Ledger balanced.
4.3 App Order — Dine-in — Mobile Money (Immediate split, no escrow)
Order total: TZS 11,000
- Kitchen: 10,000
- Commission: 1,000
Customer pays via USSD
│
▼
Snippe webhook: payment.completed
│
▼
[TransactionService.record()] holdInEscrow: false
│
├── LedgerService.writeEntry()
│ DEBIT ASSET_PSP_SNIPPE 11,000
│ CREDIT LIABILITY_WALLETS 10,000 (kitchen)
│ CREDIT REVENUE_MARKETPLACE_COMMISSION 1,000
│
├── WalletService.credit(kitchenId, 10,000, ORDER_EARNING)
│
├── TransactionSplitEntity × 2 (status: CREDITED immediately)
└── No EscrowEntity created
End state: Money flows straight through. No holding. Kitchen credited immediately.
4.4 App Order — Pickup — Platform Wallet (Internal payment)
Order total: TZS 12,000
Customer pays from JikoXpress wallet
│ No PSP API call
│
▼
[TransactionService.record()] channel: PLATFORM_WALLET
│
├── WalletService.debit(customerId, 12,000, ORDER_PAYMENT)
│
├── LedgerService.writeEntry()
│ DEBIT LIABILITY_WALLETS 12,000 ← customer wallet reduced
│ CREDIT ASSET_ESCROW 12,000 ← held for pickup
│
├── EscrowEntity (status: HELD, condition: PICKUP_CODE_CONFIRMED)
└── TransactionSplitEntity × N (status: PENDING)
... customer arrives, shows pickup code ...
▼
[OrderPaymentService.confirmPickup()]
│
▼
[TransactionService.releaseEscrow()]
│
└── Same release flow as delivery — splits credited, escrow released
End state: Pure internal redistribution. No money entered or left the Snippe pool.
4.5 POS Cash Order (World 2 — Counter order)
Customer pays cash at counter
│
▼
[TransactionService.record()] channel: CASH
│
├── writeCollectionJournal() → returns null ← no journal entry
├── PaymentTransactionEntity created (for reporting only)
└── No splits, no escrow, no ledger movement
End state: Order recorded. Money physically in kitchen till. Treasury untouched. Platform earns zero per order.
4.6 Order Cancellation — Money in Escrow — PSP Payment
Order cancelled
│
▼
[OrderPaymentService.cancelOrder()]
│
▼
[TransactionService.refundEscrow()]
│
├── LedgerService.writeEntry()
│ DEBIT ASSET_ESCROW 18,000
│ CREDIT ASSET_PSP_SNIPPE 18,000 ← back to Snippe for external refund
│
├── TransactionSplitEntity × N (status: REVERSED)
├── EscrowEntity (status: REFUNDED)
└── PaymentTransactionEntity (status: REFUNDED)
External refund to customer Mpesa handled by PSP layer separately
4.7 Order Cancellation — Wallet Payment
Order cancelled, paid via wallet
│
▼
[TransactionService.refundEscrow()]
│
├── LedgerService.writeEntry()
│ DEBIT ASSET_ESCROW 12,000
│ CREDIT LIABILITY_WALLETS 12,000 ← straight back to customer wallet
│
├── WalletService.credit(customerId, 12,000, REFUND)
└── Instant. No PSP call. No waiting.
4.8 Subscription Payment — Wallet
Kitchen pays monthly subscription from wallet
│
▼
[SubscriptionBillingService.chargeViaWallet()]
│
├── WalletService.debit(accountId, 15,000, SUBSCRIPTION_PAYMENT)
│
└── LedgerService.writeEntry()
DEBIT LIABILITY_WALLETS 15,000
CREDIT REVENUE_SUBSCRIPTION_FEES 15,000
4.9 Subscription Payment — Mobile Money
Kitchen pays via Mpesa
│
▼
Snippe webhook: payment.completed
│
▼
[SubscriptionBillingService.onPaymentCompleted()]
│
└── LedgerService.writeEntry()
DEBIT ASSET_PSP_SNIPPE 15,000
CREDIT REVENUE_SUBSCRIPTION_FEES 15,000
4.10 User Withdrawal (Customer, Kitchen Owner, or Rider)
User requests withdrawal of TZS 30,000
│
▼
[DisbursementService.initiate()]
│
├── hasSufficientBalance() check
│
├── WalletService.debit(accountId, 30,000, WITHDRAWAL)
│
├── LedgerService.writeEntry() ← money earmarked
│ DEBIT LIABILITY_WALLETS 30,000
│ CREDIT LIABILITY_SETTLEMENTS 30,000
│
├── DisbursementRequestEntity (status: PENDING)
│
└── SnippeGateway.initiatePayout()
POST /v1/payouts/send → Snippe
... Snippe processes ...
▼
Snippe webhook: payout.completed
│
▼
[DisbursementService.onPayoutCompleted()]
│
├── LedgerService.writeEntry() ← money physically left
│ DEBIT LIABILITY_SETTLEMENTS 30,000
│ CREDIT ASSET_PSP_SNIPPE 30,000
│
└── DisbursementRequestEntity (status: COMPLETED)
End state: User wallet debited. Money left Snippe. Ledger balanced.
4.11 Withdrawal Failed
Snippe webhook: payout.failed
│
▼
[DisbursementService.onPayoutFailed()]
│
├── LedgerService.writeEntry() ← earmark reversed
│ DEBIT LIABILITY_SETTLEMENTS 30,000
│ CREDIT LIABILITY_WALLETS 30,000
│
├── WalletService.credit(accountId, 30,000, REVERSAL)
│
└── DisbursementRequestEntity (status: FAILED, walletRefunded: true)
End state: User wallet restored. No money lost.
4.12 Withdrawal Reversed (After Completion)
Snippe webhook: payout.reversed
│ money came back to Snippe after successful payout
│
▼
[DisbursementService.onPayoutReversed()]
│
├── LedgerService.writeEntry()
│ DEBIT ASSET_PSP_SNIPPE 30,000 ← money back at Snippe
│ CREDIT LIABILITY_WALLETS 30,000 ← returned to user
│
├── WalletService.credit(accountId, 30,000, REVERSAL)
│
└── DisbursementRequestEntity (status: REVERSED, walletRefunded: true)
4.13 Platform Offer / Subsidy Applied
Customer pays TZS 8,000. Platform covers TZS 2,000 delivery fee.
Two TransactionEntities for same order:
1. Customer: 8,000 via USSD → escrow
2. Platform: 2,000 subsidy (paidBy: PLATFORM) → escrow
On delivery confirmed:
LedgerService.writeEntry()
DEBIT ASSET_ESCROW 10,000
CREDIT LIABILITY_WALLETS 8,000 (kitchen)
CREDIT LIABILITY_WALLETS 1,400 (rider — 70% of 2,000)
CREDIT REVENUE_DELIVERY_MARGIN 600 (platform — 30% of 2,000)
CREDIT REVENUE_MARKETPLACE_COMMISSION X (commission on food amount)
SplitFundedBy.PLATFORM on delivery split — recorded for reporting
5. Idempotency Guards
Every entry point is protected against duplicates:
| Layer | Guard Mechanism |
|---|---|
| PSP Collection | PspCollectionLogEntity.providerTransid unique constraint |
| PSP Disbursement | PspDisbursementLogEntity.pspEventId unique constraint |
| Collection initiation | PspCollectionEntity.idempotencyKey unique constraint |
| Disbursement initiation | DisbursementRequestEntity.idempotencyKey unique constraint |
| Wallet debit | WalletEntity @Version optimistic lock |
| Ledger account update | LedgerAccountEntity @Version optimistic lock |
If a duplicate webhook arrives → DataIntegrityViolationException on log save → silently skipped.
6. Safety Rules
Golden Rule (continuous check)
ASSET_PSP_SNIPPE + ASSET_ESCROW >= LIABILITY_WALLETS + LIABILITY_SETTLEMENTS
Escrow Integrity Check (nightly)
ASSET_ESCROW balance = SUM(EscrowEntity WHERE status = HELD)
Balance Drift Check (nightly)
For each LedgerAccountEntity:
stored balance = recalculated from JournalEntryLines
if mismatch → alert immediately
Wallet Refund Guard
DisbursementRequestEntity.walletRefunded = true
prevents double-refund on concurrent failure callbacks
7. Disbursement Channel Lifecycle
User adds channel (phone or bank account)
│
▼
DisbursementChannelEntity
status: ACTIVE
nameVerified: false ← placeholder until Snippe name lookup available
otpVerified: true ← no OTP for now
isPrimary: true ← if first channel
│
▼
User initiates withdrawal
│
▼
DisbursementChannelService.getChannel()
channel.isUsable() check:
status == ACTIVE
otpVerified == true
deletedAt == null
│
▼
DisbursementService.initiate() proceeds
8. Data Entities — Quick Reference
| Entity | Purpose |
|---|---|
LedgerAccountEntity |
Chart of accounts — one per LedgerAccountCode |
JournalEntryEntity |
One entry per money event — append only, never updated |
JournalEntryLineEntity |
Individual debit/credit lines per journal entry |
WalletEntity |
Per-user balance + status |
WalletTransactionEntity |
Every wallet movement with balanceBefore/balanceAfter |
PspCollectionEntity |
Incoming payment request lifecycle |
PspCollectionLogEntity |
Raw webhook log — idempotency guard |
DisbursementRequestEntity |
Outgoing payout lifecycle |
DisbursementChannelEntity |
User's saved payout destinations |
PspDisbursementLogEntity |
Raw payout webhook log — idempotency guard |
PaymentTransactionEntity |
Business-level payment record per order |
TransactionSplitEntity |
Split breakdown per transaction |
EscrowEntity |
Held money with release condition |
9. What Triggers What — Event Map
Customer pays via USSD
└── Snippe webhook: payment.completed
└── SnippeWebhookService.handle()
└── PspCollectionService.onPaymentCompleted()
├── WALLET_TOPUP → WalletService.credit()
└── ORDER_PAYMENT → OrderPaymentService notified
Rider confirms delivery
└── OrderPaymentService.confirmDelivery()
└── TransactionService.releaseEscrow()
├── LedgerService.writeEntry()
└── WalletService.credit() × N recipients
Order cancelled
└── OrderPaymentService.cancelOrder()
└── TransactionService.refundEscrow()
├── LedgerService.writeEntry()
└── WalletService.credit() if wallet payment
User withdraws
└── DisbursementService.initiate()
├── WalletService.debit()
├── LedgerService.writeEntry()
└── SnippeGateway.initiatePayout()
└── Snippe webhook: payout.completed / payout.failed / payout.reversed
└── DisbursementService.onPayout*()
├── LedgerService.writeEntry()
└── WalletService.credit() if failed/reversed
QBIT SPARK CO LIMITED | JikoXpress Pro | Financial Engine Reference | April 2026 Internal document — confidential