The Central Order-Finance Ledger
Validated against classs/CommerceOrdersModule.php and the live schema on 2026-09-06.
Koovira records the money for every module's orders in one place: a two-table double-entry ledger. This is the financial source of truth. Individual module order tables (orders_master, food_orders, hotel_bookings, …) hold the order; the money is mirrored here so finance, refunds, disputes and settlement have a single, consistent view across modules.
| Table | Rows (2026-09-06) | Role |
|---|---|---|
kv_order_finance_master | 63 (re-counted same day; was 62 — grows live during QA) | One row per order (the money header). |
kv_order_finance_entries | 488 (re-counted same day; was 484) | Double-entry lines against a master row (the movements). |
Why it is keyed the way it is (read this first)
Each vertical has its own order table, and its primary key restarts at 1 per table. So an order_id of 7 exists in orders_master, food_orders, hotel_bookings, and more — the same number, different orders. Order IDs collide across modules.
The ledger therefore keys every row by a polymorphic triple:
(module_key, order_table, order_id)
kv_order_finance_master has a lookup on (order_table, order_id) (CommerceOrdersModule::ensureOrderFinanceMaster(), orders_master.php finance methods around :399–460), so a given module order maps to exactly one finance master row. Never look up finance by order_id alone — you will read another module's money.
kv_order_finance_master — the money header
Validated columns: rowId (PK), finance_uid (char(36), unique), module_key varchar(32), order_table varchar(64), order_id, order_uid, order_number, buyer_user_id, seller_user_id, company_id, currency, gross_amount, paid_amount, refunded_amount, status enum(open,paid,cancelled,refunded,closed), kv_billing_uid, wallet_payment_txn_id, meta_json, timestamps.
gross_amount/paid_amount/refunded_amountare aggregates, recomputed from the
entries by refreshOrderFinanceAggregates() (CommerceOrdersModule.php:510) — they are a cache of the entry lines, not an independent source. Trust the entries; the master is their sum.
wallet_payment_txn_idlinks to the wallet transaction that funded the order (wallet-first).kv_billing_uidlinks to the billing/invoice record (kv_billing_*).
kv_order_finance_entries — the double-entry lines
Validated columns: rowId (PK), entry_uid (char(36), unique), finance_id (→ master.rowId), event_type, party, direction, amount, currency, ref_table, ref_id, ref_uid, wallet_txn_id, meta_json, datecreated.
event_typeenum:order_paid,order_cancelled,refund_processed,penalty_applied,
merchant_comp, rider_comp, waiting_charge, adjustment, dispute_opened, dispute_resolved.
partyenum:customer,merchant,rider,platform,franchisee,other— who the
line is for. This is how the platform's commission take, the rider's comp, and the merchant's net are all recorded against one order.
directionenum:debit,credit.ref_table/ref_id/ref_uidpoint at the concrete artifact behind the line
(a wallet txn, a commission row, a dispute row).
Entries are written by recordOrderFinanceEntry() (CommerceOrdersModule.php:462), and the master aggregates are refreshed after each write.
The write path (validated, CommerceX wallet checkout)
From CommerceOrdersModule::checkoutWithWallet() and its finance helpers:
orderFinanceEnabled()(:287) gates on both ledger tables existing — the ledger is written
only when present (defensive; it is present on this DB).
- `ensureOrderFinanceMaster(module_key, order_table='orders_master', order_id, …, grossAmount,
paidAmount, …) (:399`) finds-or-creates the master row for this order.
recordOrderFinanceEntry(financeId, event_type, party, direction, amount, …)(:462) appends
the movement lines (e.g. order_paid / customer / debit; the platform-fee line; the merchant-credit line).
refreshOrderFinanceAggregates(financeId)(:510) recomputes master
gross/paid/refunded/status from the entries.
The actual wallet debit is performed through WalletModule (wallet-first), and its transaction id is threaded into wallet_payment_txn_id / entry wallet_txn_id. Commission is computed by CommissionEngineModule and lands both here (as a platform line) and in commission_transactions.
Relationship to the other money tables
customer wallet ──(debit)──> wallet_transactions ──id──> kv_order_finance_entries.wallet_txn_id
└──> kv_order_finance_master.wallet_payment_txn_id
order (e.g. orders_master) ──(module_key, order_table, order_id)──> kv_order_finance_master
└──< kv_order_finance_entries (party lines)
platform take ──> commission_transactions (also a 'platform' entry line here)
invoice ──> kv_billing_* (via kv_billing_uid)
double-entry books ──> accounting_journal (AccountingEngine)
| Table | Rows (2026-09-06) | Role |
|---|---|---|
wallet_accounts | 7 | one balance per user/company |
wallet_transactions | 142 | every wallet credit/debit |
platform_commission_settings | 11 | commission rate per module |
commission_transactions | 92 | recorded platform take |
kv_billing_master / _invoices / _transactions | 881 / 285 / 870 (2026-09-06 — was 656/215/584) | billing & invoices |
accounting_journal | 2,422 (2026-09-06 — was cited as 2,249 in admin docs; see below) | formal double-entry books |
Known gaps / cautions (validated)
- **CONFIRMED DEFECT (2026-09-06, money-validation pass) — escrow money never reaches
ESCROW_HELD in the derived books.** AccountingEngine::deriveWallet() (classs/AccountingEngine.php:209-259) is the source that rebuilds the reportable ledger from wallet_transactions (module_key wallet rows in accounting_journal are deliberately skipped by deriveLedger() — canonical tables are supposed to cover them). It classifies every row by a hardcoded meta_source string list, and none of its branches ever emits ESCROW_HELD: escrow-tagged sources (classified_escrow, freelancer_escrow_release, generic on-platform spend) land on PAYMENT_GATEWAY_CLEARING or MERCHANT_PAYABLE instead — even though ESCROW_HELD is a real, active row in kv_chart_of_accounts (confirmed present, is_active=1, 2026-09-06) and is read elsewhere (AccountingEngine.php:642, balance-sheet calc). Net effect: the balance sheet's escrow bucket is structurally starved — escrow funds are invisible as escrow in every report deriveLedger() powers. See also wallet.md for the related WalletModule::journalAccountsFor() default-to-revenue defect (a second, independent path to the same "escrow isn't showing up as escrow" symptom).
- Not every module writes here yet. The finance methods live in
CommerceOrdersModule; other
verticals must call the same pattern for their money to appear in the ledger. When documenting a module, confirm whether its order path calls ensureOrderFinanceMaster or only writes its own *_orders table + wallet_transactions. (Historical audits flagged RideMate as settling via wallet_ledger without a commission ledger entry — verify per module before asserting coverage.)
order_items(0 rows) is not the line store for CommerceX. Lines live onorders_master
(one row per line item). See ../20-modules/ecommerce/data-model.md.
Re-validate
php -r 'require "config/baseconfig/config.php"; global $conn;
foreach(["kv_order_finance_master","kv_order_finance_entries","commission_transactions","wallet_transactions"] as $t)
echo $t,"=",$conn->query("SELECT COUNT(*) FROM `$t`")->fetchColumn(),"\n";'