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