Primitive spec
Mallow
The ledger primitive. Mallow records the finalised financial state behind every Hober Order close, every Payments settlement, and every payroll cycle — as immutable invoices, credit notes, and double-entry postings. Audit-shaped from day one.
What it owns
Mallow owns the books. Once an Order closes, an Invoice is the immutable record of what was sold, taxed, and tendered. Once a Payments Settlement lands, the cash-receipt entry is the immutable record of money in the bank. Compensating actions — refunds, chargeback losses, voids — create new entries; they never mutate or delete existing ones.
Mallow does not query Payments for processor detail. Cents, currency, and the transaction reference are the only fields that cross the boundary — per the cross-primitive convention. Anything more (last4, processor batch id, dispute reason code) stays in Payments and is fetched separately by UIs that need it.
Concepts
- Invoice
- The immutable record of an Order at close. Lines carry the snapshotted unit price, tax class, and computed line tax. Totals are denormalised. The Invoice is what a CFO reads, an auditor reads, a tax authority reads.
- InvoiceLine
- One line on an Invoice, derived 1:1 from an OrderLine at close. Carries the variant id, quantity, snapshotted price, taxClassId, and the computed taxCents.
- CreditNote
- A compensating Invoice for refunds and returns. References the original Invoice and the original lines being credited. Never mutates the original.
- LedgerEntry
- A double-entry posting. Every event that moves money creates one or more LedgerEntries with matched debit and credit legs. Source-typed (
hober_order,payments_settlement,payments_dispute,payroll_cycle) for trace-back. - LedgerPostingService
- The single write surface.
postSaleon Order close.postPaymentReceivedon Settlement settled.postRefundProcessedon refund settled.postChargebackLosson Dispute lost or accepted. Each method emits well-named, predictable journal entries; UIs and reports read the result. - Payout / Settlement record
- The merchant-side view of what hit the bank, derived from Payments Settlement events. Reconciliation surfaces mismatches between the processor's claimed net and the sum of underlying transactions.
- Employee hours & payroll
- Mallow tracks worked-time records against the Trantor Resource that represents the employee, and payouts against the Terminus Person that is the legal payee. The split mirrors the Trantor/Terminus identity split for humans.
- Stored value
- Gift cards, store credit, account credit, rain checks, and loyalty points as one ledger primitive. A
StoredValueAccountcarries akindand a balance; everyStoredValueTransaction(issue / redeem / adjust / reverse) is append-only and posts a double-entry Journal to a liability account — so an outstanding gift-card balance is real money on the books, not a loose counter. Redeem locks the account row and re-checks balance + idempotency, the same discipline a Booking hold uses. - Loyalty
- A complete accrual-to-redemption loop on top of stored value:
LoyaltyProgram+ CEL-gated earn rules (spend-based accrual with refund clawback, idempotent per order), redemption quotes + ledger-less redeem capped per order, order- and booking-sourcedLoyaltyPunchCardcycles paying store-credit or person-locked percent-off-next-visit rewards, a points-expiry sweep, and tiers — deliberately not a parallel system: the existing Terminus TierEvaluator reads loyalty stats. - Accountant exports & statements
- Getting the books out. A CSV financial export for spreadsheets, a QBO journal CSV that drops into QuickBooks, and PDF account statements (rendered with dompdf) — downloaded from a Demerzel export & statements UI behind per-capability operator gating, with customers pulling their own statements self-service through Mannix.
API surface
Endpoints are versioned under /mallow/v1/. Most writes flow through service events (Order close, Settlement landed), not direct HTTP; the surface below is for queries, manual postings, and admin tooling.
| Method | Path | Purpose |
|---|---|---|
| GET | /mallow/v1/invoices/{id} | Fetch an Invoice with lines and computed totals. |
| GET | /mallow/v1/invoices?customerId=&from=&to= | List invoices by customer, location, or date window. |
| POST | /mallow/v1/credit-notes | Issue a compensating CreditNote against an Invoice. |
| GET | /mallow/v1/credit-notes/{id} | Fetch a CreditNote with referenced original lines. |
| GET | /mallow/v1/ledger?sourceType=&sourceId=&from=&to= | Query LedgerEntries, filtered by source or window. |
| POST | /mallow/v1/ledger/manual | Manual journal entry (admin scope). Audited. |
| GET | /mallow/v1/payouts?from=&to= | Payout history reconciled against Payments Settlements. |
| GET | /mallow/v1/balances?accountCode=&asOf= | Account balance at a point in time, derived from the ledger. |
| POST / GET | /mallow/v1/stored-value-accounts | Open or list stored-value accounts (gift card, store credit, loyalty). |
| POST | /mallow/v1/stored-value-accounts/{id}/issue | Load value into an account; posts the liability journal. |
| POST | /mallow/v1/stored-value-accounts/{id}/redeem | Spend value from an account under a lock; idempotent. |
Example: posting an Order close
When Hober closes an Order, Mallow generates the Invoice and posts the sale entry. The event-driven flow:
// Hober emits order.closed
{
"orderId": "ord_01JAZB…",
"totalCents": 4500,
"taxCents": 275,
"lines": [ … snapshotted OrderLines … ],
"tenders": [ { "paymentsTransactionId": "txn_01JAZF…", "amountCents": 4500 } ]
}
// Mallow.LedgerPostingService.postSale runs
Invoice in_01JAZG… status=issued totalCents=4500
InvoiceLine ilin_… variantId=pvar_burger_single taxCents=92
InvoiceLine ilin_… …
LedgerEntry DR Accounts Receivable 4500
LedgerEntry CR Sales Revenue -4225
LedgerEntry CR Sales Tax Payable -275
// Later, on Payments Settlement landed
LedgerEntry DR Cash 4500
LedgerEntry CR Accounts Receivable -4500
How it fits with the rest
flowchart LR
Ho[Hober close] --> M(Mallow)
Pa[Payments settled] --> M
Disp[Payments dispute] --> M
Payroll[Payroll cycles] --> M
M -. customer / employee .-> Te[Terminus]
M -- statements --> Sp[Speaker]
Hober triggers Invoice generation on Order close. Payments Settlement events trigger cash-receipt postings; refund-leg transactions trigger refund postings; lost or accepted Disputes trigger chargeback-loss postings. Terminus is the identity ref for customers on Invoices and employees on payouts. Trantor Resources are where worked-time records live for employees. Speaker sends receipts and statements via consent-gated dispatch.