Koovira Dev Docs
40-financial/central-finance-ledger.md

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.

TableRows (2026-09-06)Role
kv_order_finance_master63 (re-counted same day; was 62 — grows live during QA)One row per order (the money header).
kv_order_finance_entries488 (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_amount are 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_id links to the wallet transaction that funded the order (wallet-first).
  • kv_billing_uid links 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_type enum: order_paid, order_cancelled, refund_processed, penalty_applied,

merchant_comp, rider_comp, waiting_charge, adjustment, dispute_opened, dispute_resolved.

  • party enum: 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.

  • direction enum: debit, credit.
  • ref_table / ref_id / ref_uid point 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:

  1. orderFinanceEnabled() (:287) gates on both ledger tables existing — the ledger is written

only when present (defensive; it is present on this DB).

  1. `ensureOrderFinanceMaster(module_key, order_table='orders_master', order_id, …, grossAmount,

paidAmount, …) (:399`) finds-or-creates the master row for this order.

  1. 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).

  1. 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)
TableRows (2026-09-06)Role
wallet_accounts7one balance per user/company
wallet_transactions142every wallet credit/debit
platform_commission_settings11commission rate per module
commission_transactions92recorded platform take
kv_billing_master / _invoices / _transactions881 / 285 / 870 (2026-09-06 — was 656/215/584)billing & invoices
accounting_journal2,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 on orders_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";'