Primitive spec
Mallow
The ledger primitive. Mallow records the finalised financial state behind every Hober Order close, every payment received, and every processor payout — 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 payment lands, the payment-received entry is the immutable record of money in. Compensating actions — refunds, credit notes, 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.
- Bad-debt write-off
- When an invoice will not be paid, an owner writes off all or part of its open balance as a bad debt. It is one balanced journal, debiting bad-debt expense and crediting accounts receivable, and the invoice is marked written off once nothing is left. A reason is required, the amount can never exceed what is still owed, and it is refused on an invoice that is not open or in a closed period. It works the same for an invoice raised here and an opening balance brought over by import. A write-off is a posted entry, not an erasure: voiding the invoice reverses it.
- Journal & LedgerEntry
- A double-entry posting. Every event that moves money creates a balanced
Journalof LedgerEntries with matched debit and credit legs. Source-typed (mallow_invoice_issued,mallow_payment_received,mallow_credit_note_issued,payments_payout,mallow_invoice_voided) for trace-back. - LedgerPostingService
- The single write surface.
postInvoiceIssuedwhen an Invoice issues.postPaymentReceivedwhen a payment lands against it.postRefundandpostCreditNoteIssuedfor refunds and returns.postProcessorPayoutwhen a processor payout is reconciled.postStoredValueMovementfor gift cards and credit. Each method emits well-named, predictable journal entries; UIs and reports read the result. - Processor payouts
- What hit the bank. Each reconciled Payments
ProcessorPayoutposts through the1010 Processor Clearingaccount, so card takings clear out of it when the deposit lands and mismatches show as a clearing balance. - 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 postings flow from service events (Order close, payment received, payout reconciled), not direct HTTP; the surface below is for invoices, credit notes, queries, and admin tooling.
| Method | Path | Purpose |
|---|---|---|
| POST | /mallow/v1/invoices | Create a draft Invoice; add …/lines, then …/issue, record …/payments, or …/void. |
| GET | /mallow/v1/invoices/{id} | Fetch an Invoice with lines and computed totals. |
| GET | /mallow/v1/invoices?customerId=&status= | List invoices by customer or status. |
| POST | /mallow/v1/invoices/{id}/write-off | Write off all or part of an open invoice’s balance as a bad debt. A reason is required; the amount defaults to the whole open balance. |
| POST | /mallow/v1/credit-notes | Draft a compensating CreditNote against an Invoice; …/issue, …/refunds, …/void. |
| GET | /mallow/v1/credit-notes/{id} | Fetch a CreditNote with referenced original lines. |
| GET | /mallow/v1/journals/{id} | Fetch a Journal and its LedgerEntries. /mallow/v1/invoices/{id}/journals lists an Invoice's postings. |
| GET | /mallow/v1/reports/account-balance | Account balance derived from the ledger. /mallow/v1/reports/ar-aging ages receivables. |
| POST / GET | /mallow/v1/accounts | Chart of accounts. /mallow/v1/fiscal-periods manages periods. |
| POST / GET | /mallow/v1/tax-policies | Tax policies and their rates (/mallow/v1/tax-rates). |
| 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 hober_order.closed
{
"orderId": "ord_01JAZB…",
"totalCents": 4500,
"taxCents": 275,
"lines": [ … snapshotted OrderLines … ],
"paymentSummary": [ { "tenderType": "card", "amountCents": 4500, "last4": null } ]
}
// Mallow.LedgerPostingService.postInvoiceIssued runs
Invoice inv_01JAZG… status=issued totalCents=4500
InvoiceLine invl_… sourceLineType=hober_order_line taxCents=92
InvoiceLine invl_… …
LedgerEntry DR Accounts Receivable 4500
LedgerEntry CR Sales Revenue -4225
LedgerEntry CR Sales Tax Payable -275
// Then postPaymentReceived for the card tender
LedgerEntry DR Processor Clearing 4500
LedgerEntry CR Accounts Receivable -4500
How it fits with the rest
flowchart LR
Ho[Hober close] --> M(Mallow)
Pa[Payments received] --> M
Po[Processor payouts] --> M
M -. customer .-> Te[Terminus]
M -- statements --> Sp[Speaker]
Hober triggers Invoice generation on Order close. Payments captures trigger payment-received postings; refunds trigger refund postings; reconciled processor payouts clear the 1010 Processor Clearing account. Terminus is the identity ref for customers on Invoices. Payroll lives in Riose, not here. Speaker sends receipts and statements via consent-gated dispatch.