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.

Status: Live. Double-entry posting end to end: the chart of accounts and tax defaults seed at onboarding, invoices settle from POS close, refunds post compensating journals, multi-rate tax posts exactly what was charged, and accountants get CSV / QBO-journal / PDF-statement exports. Since October 1, 2026 an owner can write an unpaid invoice off as a bad debt.

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 Journal of 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. postInvoiceIssued when an Invoice issues. postPaymentReceived when a payment lands against it. postRefund and postCreditNoteIssued for refunds and returns. postProcessorPayout when a processor payout is reconciled. postStoredValueMovement for 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 ProcessorPayout posts through the 1010 Processor Clearing account, 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 StoredValueAccount carries a kind and a balance; every StoredValueTransaction (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-sourced LoyaltyPunchCard cycles 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.

Mallow endpoints in the Foundation API reference OpenAPI 3.1 schema for Mallow with request/response shapes, parameters, and a try-it client.
MethodPathPurpose
POST/mallow/v1/invoicesCreate 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-offWrite 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-notesDraft 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-balanceAccount balance derived from the ledger. /mallow/v1/reports/ar-aging ages receivables.
POST / GET/mallow/v1/accountsChart of accounts. /mallow/v1/fiscal-periods manages periods.
POST / GET/mallow/v1/tax-policiesTax policies and their rates (/mallow/v1/tax-rates).
POST / GET/mallow/v1/stored-value-accountsOpen or list stored-value accounts (gift card, store credit, loyalty).
POST/mallow/v1/stored-value-accounts/{id}/issueLoad value into an account; posts the liability journal.
POST/mallow/v1/stored-value-accounts/{id}/redeemSpend 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.