Specification · o2c @ 0.2

o2c — Order to Cash · Area Specification

last conformance run: 10/10 scenarios pass · 26 acts mapped 2026-08-23 23:48 UTC

SpecActsInvariantsScenarios

Spec

Version0.1-draft
Statuscurated draft — not yet frozen; implementation conformance not yet enforced
CuratorPeter Varga (single editor; changes arrive as spec PRs, separate from implementation PRs)
Calibrationbetween toy and SAP: usable by a real SMB, honest about what is deferred

This document is the contract all implementations of the o2c area share. It specifies business acts, invariants, lifecycles, and required read models — never screens, schemas, or workflows. Anything two reasonable implementations might do differently is marked freedom; everything else is contract. Scenario files under scenarios/ are the executable part: an implementation that passes them, provides the acts, and violates no invariant conforms.

---

1. Calibration: who this is for

A company of roughly 5–200 people selling goods and/or services on account: distributors, wholesalers, field-service firms, light assembly. One legal entity, one currency, one warehouse. Customers buy on credit terms, pay by ACH/check/wire/card, sometimes short-pay, sometimes return goods, occasionally never pay. The company has a bookkeeper or controller, not an AR department.

The reference points, so the line is honest:

2. Scope

In scope (contract): quotation → order (credit-gated) → fulfilment (partial-friendly, for goods and services) → billing (from fulfilment) → credits & returns → cash (receipt, application, refund, write-off) → the read models a controller needs.

Deferred (§9): down payments/deposits, early-payment discounts, multi-currency, recurring billing, consignment, drop-ship, price lists as first-class objects, promise-to-pay tracking.

Rejected — permanent non-goals for this area:

3. Entities and lifecycles

Implementations own their schemas. The spec fixes only the entities' existence, their lifecycle states, and the transitions — because acts and invariants are written against them.

EntityLifecycle
quotedraft → sent → accepted (raises a draft order) — or expired (terminal)
orderdraft → confirmed → (partially fulfilled…) → closed — or cancelled (terminal, only if nothing fulfilled)
fulfilmentimmutable once recorded (a fact, not a workflow object)
invoiceopen → paid — or void (terminal, only if nothing applied)
credit noteopen → settled (fully applied and/or refunded)
paymentno states — a fact with a derived unapplied balance
returnimmutable once recorded; linked to a credit note

closed on an order is derived: every line fully fulfilled and fully billed (or closed short). There is no "close order" act (INV-20).

Master data (customer, item, stock) is expected from the environment — in the reference implementation, the core module. o2c requires: customers with terms, credit limit and hold flag; items with list price and a stocked/service distinction; a stock quantity that o2c can check and deplete only through the owner's API. Changing a credit limit and holding/releasing a customer must themselves be logged acts of the environment.

4. The acts

Acts are named abstractly (confirm_order); an implementation exposes each as a command under its own prefix (reference: o2c_confirm_order). A conforming module publishes a map act → command. Required arguments are contract (agents and scenario files depend on them); implementations may add optional arguments freely. Every act's refusals must satisfy INV-17 (name the numbers) and INV-18 (same sentence on every surface).

4.1 Quoting

create_quote (customer, lines[item, qty, unit_price?], valid_until?) Commits nothing: no stock reserved, no credit consumed. Omitted unit_price takes the list price; an explicit one is a visible discount (INV-2). Freedom: quote revisions/versions, approval before send.

send_quote (quote) — records that it went out (no outbound side effect). Only from draft.

accept_quote (quote, po_ref?) — records the customer's acceptance and raises a draft order carrying the quoted prices, frozen (INV-19). Refused for expired quotes and quotes already accepted. Acceptance is the customer's act; confirmation (credit) is ours.

4.2 Orders

create_order (customer, lines, po_ref?) — direct order without a quote, for repeat and phone business. Negotiated pricing should have gone through a quote.

amend_order (order, lines) — replace/add/remove lines while draft only. After confirmation the only permitted shrink is close_short; growth is a new order. This keeps "what was confirmed" a fixed fact the credit gate evaluated (INV-3).

confirm_order (order) — the credit gate, and the only one (INV-5). Exposure = open AR + value confirmed-but-uninvoiced. Refused when the customer is on hold or the limit is short — naming order value, limit, open AR, committed value, and the shortfall. Freedom: whether unapplied cash/credits offset exposure; approval-override flows (which must themselves be logged acts).

cancel_order (order, reason) — only while nothing has been fulfilled. After that, the path is fulfil/bill/credit-note (INV-11).

close_short (order, lines?, reason) — cancel the open remainder of a partially fulfilled order (all lines or named ones). This is how backorders die honestly: the customer is no longer owed the difference, and the aging of open quantity stops. Releases committed credit exposure.

4.3 Fulfilment

fulfil_order (order, lines?[order_line, qty], carrier?, tracking?) Records fulfilment against a confirmed order; omitting lines fulfils everything open. Goods lines deplete stock through the stock owner's API and are refused (per line, naming on-hand vs needed) rather than going negative; service lines mark work performed and touch no stock (INV-6). Partial fulfilment is normal and leaves the remainder open — never silently reduce the ordered quantity (INV-7). Reference implementation calls this ship; the spec name is fulfil because services never see a truck.

4.4 Billing

invoice_fulfilled (order) — one invoice for everything fulfilled and not yet billed on the order. We bill what was fulfilled, never what was ordered (INV-4) — this is a spec decision, not an Odoo-style policy toggle, because it is what keeps every invoice line traceable to a fulfilment and AR reconcilable. Due date derives from the customer's terms at issuance (INV-15). Tax per line is computed and frozen here (INV-14). Freedom: invoicing cadence (per fulfilment, daily batch, monthly consolidated per customer) — provided each invoice line still traces to fulfilment.

void_invoice (invoice, reason) — only while nothing is applied to it; releases the billed quantities back to billable. Anything already paid against is corrected by credit note, never voided (INV-10). The number is never reused (INV-9).

4.5 Credits and returns

record_return (order or invoice ref, lines[qty, restock: bool], reason) Records goods coming back. Restocking increments stock through the owner's API only for lines flagged restock — damaged goods physically exist but never re-enter sellable stock (INV-12). Produces (or links to) a credit note for the returned value.

create_credit_note (customer, ref: invoice|return|none, lines or amount, reason, kind: correction|return|goodwill|write_off) The universal downward correction (INV-11): pricing errors, short-pay resolution, returns, goodwill. A credit note is an AR instrument with its own open balance — it can be applied to invoices like a payment, or refunded. Standalone credit notes (no invoice ref) are allowed but require a reason; freedom: approval thresholds by kind and amount.

write_off (invoice, reason) — closes an uncollectible open balance as a visible, reasoned credit act (kind write_off) — never a deletion, never silent (INV-16). Freedom: the small-balance auto-threshold; who may write off above it.

4.6 Cash

record_payment (customer, amount, method?, reference?, received_at?) Records money received. Recording and applying are separate acts (INV-8): a payment with no remittance advice sits as unapplied cash, a correct and visible state. Never guess an application to tidy the aging.

apply_payment (payment, invoice, amount?) / apply_credit (credit_note, invoice, amount?) Bounded on both sides — never more than the instrument has left, never more than the invoice still owes — with refusals naming both numbers (INV-17). Cross-customer application is refused (INV-13). Omitted amount applies the smaller remaining side. A short-paid invoice stays open for the difference; the difference is resolved (credit note if justified, dunning if not), never fudged.

refund (payment or credit_note, amount, method?, reference?) — records money returned against an unapplied balance. Records the fact; moving the money is out of scope.

Act count and module budget

18 write acts + 7 read models ≈ the 25-tool budget. That is deliberate pressure: an implementation adding many extension commands should split (e.g. sales vs AR) into two modules mounted together, rather than breach the budget.

5. Invariants

The heart of the spec. Doctrine strings in implementations should quote these.

  1. INV-1 Money is integer minor units, everywhere. A float is a bug, not a rounding choice.
  2. INV-2 Both prices survive. Every document line preserves list price and charged price; a discount is always derivable, never implicit.
  3. INV-3 Documents don't mutate past draft except through defined acts. There is no "edit" on a confirmed order or issued invoice.
  4. INV-4 Bill what was fulfilled, never what was ordered. Every invoice line traces to a fulfilment line.
  5. INV-5 One credit gate, at confirmation. Exposure counts open AR plus confirmed-but-uninvoiced value. Routing around a refusal (splitting, early invoicing, quiet limit raises) is a human decision, logged as its own act.
  6. INV-6 Stock never goes negative, and only the stock owner's API changes it. Service lines never touch stock.
  7. INV-7 Short fulfilment leaves the remainder open. The open quantity is the record that the customer is still owed; only close_short may extinguish it, with a reason.
  8. INV-8 Recording cash ≠ applying cash. Unapplied cash is a valid, visible, first-class state.
  9. INV-9 Document numbers are sequential and never reused. A void keeps its number and its lines. Nothing is ever deleted.
  10. INV-10 Void only when untouched. An invoice with any application is corrected by credit note, because the money moved and the record must keep saying so.
  11. INV-11 The credit note is the only downward correction for anything past draft.
  12. INV-12 Restock only what physically returns sellable. Damaged returns are recorded but never re-enter stock.
  13. INV-13 Cash is not transferable between customers. Applications stay within one customer's instruments and invoices.
  14. INV-14 Tax is frozen at issuance. Each invoice line carries rate and amount as charged; later rate changes never touch issued documents. (Rate determination is freedom; capture is contract.)
  15. INV-15 Due dates derive from terms at issuance; changing a customer's terms never retro-adjusts issued invoices.
  16. INV-16 Write-offs are reasoned, visible credit acts — never deletions, never silent.
  17. INV-17 Refusals name the numbers. "Short by $1,500 (limit … less …)" — written to be shown to a person, verbatim.
  18. INV-18 One sentence per rule. The greyed button's tooltip, the next-actions reason, and the thrown refusal are the same string.
  19. INV-19 Prices freeze when a document is raised from another (quote → order); list price changes never rewrite live documents.
  20. INV-20 Closure is derived, not declared. Orders close when fully fulfilled+billed (or closed short); invoices become paid when fully applied. No manual "mark as done".
  21. INV-21 Every write is a logged command with an actor; refusals are logged too. Reads are never logged.

6. Required read models

Named views the area must answer without the caller assembling rows. Shapes are freedom; content is contract.

Read modelMust answer
orderlines with ordered/fulfilled/billed quantities, linked fulfilments & invoices, open + unbilled value, available next actions with refusal reasons
customer_positioncredit limit, open AR, committed (confirmed-unbilled), unapplied cash & credits, available credit, hold state
invoicelines with tax, applications received, open balance
backordersall open confirmed quantities, by item and by customer — the "what do we owe whom" view
ar_agingopen balances bucketed current/1-30/31-60/61-90/90+ by due date, with invoice-level detail — net of nothing (unapplied cash shown separately, INV-8)
unapplied_cashpayments and credit notes with remaining balances
customer_statementone customer, period-bounded: opening balance, invoices, credits, payments, closing balance — the document a controller sends monthly (rendering is out of scope; content is not)

The dunning worklist is ar_aging detail sorted by overdue severity with contact data — a view over required data, listed here so no implementation forgets why aging exists.

7. Contract vs freedom — summary

ContractFreedom
the 18 acts, their required args, their refusal semanticsextension commands (own prefix), extra optional args
lifecycles in §3, all 21 invariantsschema, storage, id formats (prefixes)
read model content (§6)read model shape, pagination, extra views
credit gate exists at confirm; exposure ≥ open AR + committedexact formula extras (offsets), override/approval flows
tax captured & frozen per linetax rate determination
bill-from-fulfilmentinvoicing cadence/consolidation
refusal text = one sentence, with numberswording style beyond that

8. Conformance

An implementation conforms when:

  1. Acts — it publishes an act → command map covering §4, each with the required args.
  2. Scenarios — it passes every file under scenarios/. Scenario steps name acts, not commands; the runner resolves them through the map. Steps assert ok (with expected fields) or refused (with a substring the message must contain — refusals are contract).
  3. Invariants — the area contract test asserts what is mechanically checkable (INV-1, 9, 18, 21 and the parity/ownership/namespace gates already in test/contract.test.js); the rest are enforced by scenarios and review.

Scenario file shape (executable; see scenarios/*.json):

{ "name": "…", "spec": "o2c@0.1",
  "steps": [
    { "act": "confirm_order", "args": { "order": "SO-0001" },
      "expect": { "ok": true, "include": { "status": "confirmed" } } },
    { "act": "confirm_order", "args": { "order": "SO-0002" },
      "expect": { "refused": "Short by" } }
  ] }

9. Deferred — with reasons

ItemWhy deferred, not rejected
Down payments / depositsReal SMB need (50% upfront is common), but it drags in liability semantics — money held against undelivered goods — and collides with INV-4. Even Odoo's model (deposit as a pseudo-product, reconciled by credit note) is notoriously messy. Needs its own design pass; a workaround exists today (record_payment → unapplied cash, applied at invoicing).
Early-payment discounts (2/10 net 30)Doubles cash-application complexity (settlement below face value is correct, not short-pay). Add only with real demand.
Multi-currencyColumn exists; the hard part is revaluation and realized FX on application. Whole-area concern, own spec revision.
Price lists / customer pricingToday: freedom (implementations may resolve prices however they like; INV-2 keeps discounts visible). First-class price objects only when rules need sharing across surfaces.
Recurring billingDifferent vertical (subscription), different lifecycle.
Consignment, drop-shipStock-ownership semantics beyond one-warehouse calibration.
Promise-to-pay / collections notesValuable, but it is CRM-on-AR; the dunning worklist (§6) is the hook it will hang from.
Per-line delivery trace on the documentInvoice lines carry order_line_id; the delivery linkage that PROVES "we bill what shipped" exists in the data flow but is not yet printed on the document. Needs invoice_line → delivery_line, a schema change with backfill questions.
Item cost / COGS entriesThe journal is revenue-side by design until items carry cost. Costing method choice (FIFO vs average) is a real design decision, not a column.

---

10. 0.2 additions — documents and the journal

Three gaps between "the books are correct" and "a business can run its month on them".

Company profile (environment obligation, §3 family). An invoice document needs a seller. set_company_profile records the business's own identity — name, address, tax id, payment instructions, a footer note — as a logged environment act with patch semantics. One profile per workspace; it is master data, not configuration.

The invoice is the document (read model, extends §6 invoice). The invoice read model now carries everything a printable document needs: the seller block, the bill-to identity, lines with both prices and tax, application state, and the seller's payment instructions. There is no separate "render" act — reading the invoice IS obtaining the document, on every surface. The seller block is FROZEN onto the invoice at issuance (INV-19's spirit): changing the company profile never reprints history — the document forever shows the identity and bank details in force when it was issued (seller_as_issued: true). Only invoices issued before a profile existed fall back to the live profile, and say so.

document. It is branding, not a fact: it is not part of the frozen seller block, so re-branding re-brands past documents while their names, addresses and amounts stay exactly as issued. Stored as the image itself (PNG/JPEG/SVG/WebP, 300 KB cap), never hotlinked; set from a file or fetched once from a public https URL.

The journal (read model). journal derives double-entry lines from the books over a date range — nothing is posted, nothing is stored; it is a projection for handing to the ledger system of record (QuickBooks/Xero import, or an accountant's CSV). Fixed account names: Accounts Receivable, Sales Revenue, Service Revenue, Sales Tax Payable, Cash, Customer Deposits, Customer Credits, Sales Returns & Allowances, Bad Debt Expense.

EventEntry
invoice issuedDR Accounts Receivable · CR Sales Revenue (goods net) · CR Service Revenue (services net, by the item's stocked flag) · CR Sales Tax Payable (tax)
payment receivedDR Cash · CR Customer Deposits
payment appliedDR Customer Deposits · CR Accounts Receivable
credit note issued (return/correction/goodwill)DR Sales Returns & Allowances · CR Customer Credits
credit note issued (write_off)DR Bad Debt Expense · CR Accounts Receivable
credit appliedDR Customer Credits · CR Accounts Receivable
refund recordedDR Customer Deposits or Customer Credits (by source) · CR Cash

Known limit, stated plainly: this is a projection, not a general ledger, and it is the REVENUE SIDE of the books only. Items carry no cost, so there are no COGS or inventory entries — margin and the inventory account belong to the ledger of record. No chart of accounts, no manual journals, no close. The ledger of record stays wherever it is; Saybooks is the operating layer in front of it.

---

Change log: 0.1-draft (2026-08-22) — initial curation, extracted from the reference implementation and calibrated against SMB practice (QuickBooks-class gaps, Odoo invoicing policies, standard AR/dunning/write-off practice). 0.2 (2026-08-23) — company profile environment act, invoice-as-document read model, derived journal read model; INV-22, INV-23. 0.2.1 (2026-09-04) — company logo: branding, not frozen.

Acts

ActKindRequired
create_quotewritecustomer lines
send_quotewritequote
accept_quotewritequote
create_orderwritecustomer lines
amend_orderwriteorder lines
confirm_orderwriteorder
cancel_orderwriteorder reason
close_shortwriteorder reason
fulfil_orderwriteorder
invoice_fulfilledwriteorder
void_invoicewriteinvoice reason
record_returnwriteorder lines reason
create_credit_notewritecustomer amount kind reason
write_offwriteinvoice reason
record_paymentwritecustomer amount
apply_paymentwritepayment invoice
apply_creditwritecredit_note invoice
refundwritesource_type source_id amount
orderreadorder
customer_positionreadcustomer
invoicereadinvoice
backordersread
ar_agingread
unapplied_cashread
customer_statementreadcustomer from to
journalread
create_customerenvironmentname
create_itemenvironmentsku name unit_price
receive_stockenvironmentitem qty
set_priceenvironmentitem unit_price
set_credit_limitenvironmentcustomer credit_limit reason
hold_customerenvironmentcustomer reason
release_customerenvironmentcustomer reason
set_company_profileenvironmentname

Invariants

IdInvariant
INV-1Money is integer minor units, everywhere.
INV-2Both prices survive: list price and charged price on every line.
INV-3Documents don't mutate past draft except through defined acts.
INV-4Bill what was fulfilled, never what was ordered.
INV-5One credit gate, at confirmation; exposure counts open AR plus committed.
INV-6Stock never goes negative; only the stock owner's API changes it.
INV-7Short fulfilment leaves the remainder open; only close_short extinguishes it.
INV-8Recording cash is not applying cash; unapplied cash is a valid state.
INV-9Document numbers are sequential and never reused.
INV-10Void only when untouched; anything applied-against is corrected by credit note.
INV-11The credit note is the only downward correction past draft.
INV-12Restock only what physically returns sellable.
INV-13Cash is not transferable between customers.
INV-14Tax is frozen at issuance.
INV-15Due dates derive from terms at issuance.
INV-16Write-offs are reasoned, visible credit acts.
INV-17Refusals name the numbers.
INV-18One sentence per rule, on every surface.
INV-19Prices freeze when a document is raised from another.
INV-20Closure is derived, not declared.
INV-21Every write is a logged command with an actor; reads are never logged.
INV-22The books produce documents; they never send them.
INV-23Every journal entry balances — debits equal credits, derived from the books, never entered.

Scenarios

Each scenario is a file of acts with expected outcomes. ok means the act must succeed with the listed fields; refused means the act must be refused with a sentence containing the listed text. Refusals are contract.

partial fulfilment, bill what shipped, apply cash pass

The spine. Exercises INV-4 (bill from fulfilment), INV-7 (short fulfilment stays open), INV-8 (unapplied cash), INV-20 (derived states).
Environment: create_customer, create_item, create_item, create_item
#ActExpectWhy
1create_quoteok total=380000
2send_quoteok
3accept_quoteok status="draft" total=380000
4confirm_orderok status="confirmed"
5fulfil_orderrefused 5 on hand, 10 neededINV-6: refuse rather than go negative; refusal names the numbers (INV-17)
6fulfil_orderok open_qty=5 status="confirmed"INV-7: short fulfilment leaves 5 open; order does NOT advance to fulfilled
7invoice_fulfilledok total=340000 status="open"INV-4: 340000 = what was fulfilled, not the 380000 ordered
8invoice_fulfilledrefused Nothing has shipped
9record_paymentokno remittance advice yet — INV-8, sits unapplied
10apply_paymentrefused only $2000.00 left
11apply_paymentokinvoice remains open for the 140000 difference — short-pay stays visible

the credit gate refuses and explains pass

INV-5 (one gate, counts committed value), INV-17 (numbers in the refusal), INV-21 (the refusal is logged).
Environment: create_customer, create_item
#ActExpectWhy
1create_orderok status="draft" total=250000creating a draft consumes nothing — the gate is at confirmation only
2confirm_orderrefused Short by $1500.00
3create_orderok
4confirm_orderok50000 fits inside the 100000 limit
5create_orderok
6confirm_orderrefused committed $500.00INV-5: the 50000 confirmed-but-uninvoiced on SO-0002 counts as exposure — a gate that only reads invoices would wave this through

returns, credit notes, and the honest ends of an invoice pass

INV-10 (void only untouched), INV-11 (credit note is the correction), INV-12 (restock only sellable), INV-16 (write-off is a reasoned credit act).
Environment: create_customer, create_item
#ActExpectWhy
1create_orderok
2confirm_orderok
3fulfil_orderok
4invoice_fulfilledok total=100000
5record_paymentok
6apply_paymentok
7void_invoicerefused appliedINV-10: money moved; the record must keep saying so
8record_returnok restocked_qty=2 credit_total=30000INV-12: stock gains 2, not 3; the damaged unit is recorded but never sellable
9apply_creditokinvoice open balance now 100000 - 60000 - 30000 = 10000
10write_offok invoice_status="paid"INV-16: visible, reasoned, a credit act — never a deletion. INV-20: paid is derived once fully applied

drafts bend, confirmed documents do not pass

INV-3 (no mutation past draft), INV-5 (committed exposure), INV-20 (derived states). Amendment, cancellation, and the exact moment an order stops being editable.
Environment: create_customer, create_item
#ActExpectWhy
1create_orderok status="draft" total=30000
2amend_orderok total=50000a draft is a conversation — replace the lines freely, the log keeps every version
3close_shortrefused no open remainderclose_short is for confirmed orders with an unfulfilled tail — a draft has nothing committed to abandon
4confirm_orderok status="confirmed"
5customer_positionok committed=50000 open_balance=0INV-5: the moment of confirmation, the order's value counts against credit — before any invoice exists
6amend_orderrefused Only a draft order can be amendedINV-3: what was confirmed is the fact the credit gate evaluated. Growth is a new order; shrink is close_short
7cancel_orderok status="cancelled"cancellation is only possible while nothing has been fulfilled — after that, the honest path is bill and credit-note
8customer_positionok committed=0cancelling releases the committed exposure; the credit line breathes again
9confirm_orderrefused Only a draft order can be confirmed
10orderok status="cancelled"INV-9 in spirit: the cancelled order stays on the books, visible, number and lines intact — nothing is ever deleted

tax is frozen at issuance; the statement adds up pass

INV-14 (tax snapshot), INV-15 (due date from terms), INV-8 (statement shows payments, not applications). The billing math a controller will re-check by hand exactly once.
Environment: create_customer, create_item
#ActExpectWhy
1create_orderok total=100000order totals are net — tax is a billing fact, not an ordering fact, because the rate belongs to the moment of issuance
2confirm_orderok
3fulfil_orderok open_qty=0
4invoice_fulfilledok subtotal=100000 tax_total=8750 total=108750INV-14: 8.75% snapshotted per line at issuance. If the rate changes tomorrow, this invoice never moves
5record_paymentok
6apply_paymentok
7invoiceok status="open" open=58750short-paid and honestly so: the invoice stays open for the difference — resolved by credit note if justified, dunning if not, never fudged
8ar_agingok total="$587.50"the aging is net of nothing (INV-8): unapplied cash lives in its own view, so this number is what customers actually owe
9customer_statementok opening=0 closing=58750balance-forward: opening + invoices − credits − payments = closing. The monthly document, minus the envelope

backorders die honestly, and overpayment goes home pass

INV-7 (the open quantity is the record of what is owed), INV-8, INV-20 (closure derived). close_short, the backorder view, and refunding unapplied cash.
Environment: create_customer, create_item
#ActExpectWhy
1create_orderok total=150000
2confirm_orderok
3fulfil_orderrefused 5 on hand, 12 neededINV-6: never silently reduce the ordered quantity — ship what exists and keep the remainder visible
4fulfil_orderok open_qty=7
5backordersok length=1INV-7: the 7 open units ARE the record that Proseware is still owed. This view is where backorders wait for fulfilment or an honest death
6close_shortok open_qty=0 status="shipped"the honest death: the debt of goods is extinguished with a reason, committed credit exposure is released
7backordersok length=0
8invoice_fulfilledok total=62500 order_status="closed"INV-4 and INV-20: billed for the 5 fulfilled, never the 12 ordered — and with nothing open and everything billed, closure derives itself
9record_paymentokthe customer paid against the original order value — overpayment is real life, not an error state
10apply_paymentok applied="$625.00"omitted amount applies the smaller remaining side; the extra $375 stays as unapplied cash, visible (INV-8)
11refundrefused only $375.00 unappliedINV-17: bounded and the refusal names the bound — you cannot refund money that already settled an invoice
12refundok source_remaining="$0.00"records that the money went back; actually moving it is out of scope, like every outbound act
13unapplied_cashok length=0clean end state: nothing owed in goods, nothing owed in cash, and every step of getting here is in the log

the two corrections: void for the untouched, credit note for everything else pass

INV-9 (numbers never reused), INV-10 (void only untouched), INV-11 (credit note is THE downward correction), INV-20. The difference between erasing a mistake and correcting one.
Environment: create_customer, create_item
#ActExpectWhy
1create_orderok
2confirm_orderok
3fulfil_orderok
4invoice_fulfilledok total=40000
5void_invoiceok status="void"INV-10: nothing was applied yet, so void is allowed — the quantities go back to billable and the mistake never billed anyone
6invoice_fulfilledok id="INV-0002" total=40000INV-9: the replacement is INV-0002. INV-0001 keeps its number, its lines and its void status forever — numbers are never reused, nothing is deleted
7record_paymentok
8apply_paymentok
9void_invoicerefused appliedINV-10 again, from the other side: money moved against this invoice, so the record must keep saying so. The correction is now a credit note
10create_credit_noteok status="open" total=5000INV-11: the universal downward correction, with its own open balance and a mandatory reason that will be read back
11apply_creditok applied="$50.00"
12apply_paymentok
13invoiceok status="paid" open=0INV-20: 40000 billed = 20000 + 15000 cash + 5000 credit; fully applied, so paid derived itself. The void, the credit and both applications are all in the trail

prices stay quoted, cash stays home pass

INV-19 (prices freeze when a document is raised from another) and INV-13 (cash and credit are not transferable between customers). The two invariants nothing else exercises — this scenario exists because the evidence map said so.
Environment: create_customer, create_customer, create_item
#ActExpectWhy
1create_quoteok total=40000quoted at the list price of the day: 100.00/unit
2send_quoteok
3set_priceok unit_price=12000 previous_price=10000the list price moves while the quote is out — a logged act, because WHEN it moved is part of the record
4accept_quoteok total=40000INV-19: the customer accepted 100.00/unit, so that is what the order carries — the whole reason the quote existed
5create_orderok total=12000a fresh document takes the new list price — the freeze protects raised documents, it does not fossilize the catalog
6confirm_orderok
7fulfil_orderok
8invoice_fulfilledok total=40000INV-19 held all the way to billing; INV-2 kept both prices on the line, so the 20.00/unit discount-vs-current-list is derivable, never invisible
9confirm_orderok
10fulfil_orderok
11invoice_fulfilledok total=12000
12record_paymentokBlue Yonder pays their invoice
13apply_paymentrefused not transferableINV-13: INV-0001 is Adventure Works' invoice. However tempting the tidy-up, cash stays with the customer who paid it
14create_credit_noteok
15apply_creditrefused not transferableINV-13 covers credit too: Adventure Works' credit note cannot settle Blue Yonder's invoice
16apply_paymentokapplied where it belongs; INV-0002 derives paid (INV-20)

the credit gate's human side: limits, holds, releases pass

INV-5's other half. The gate refuses; a person with authority answers — by raising the limit or by holding the account — and every one of those answers is a logged act with a reason, never a workaround. Environment acts obligated by spec §3.
Environment: create_customer, create_item
#ActExpectWhy
1create_orderok total=250000
2confirm_orderrefused Short by $1500.00INV-5: the gate said no and named the numbers. What happens next is a human decision, not an agent maneuver
3set_credit_limitok credit_limit=300000 previous_limit=100000the answer the refusal pointed to: the limit changes HERE, as its own logged act with a reason — never by splitting the order or invoicing early
4confirm_orderok status="confirmed"same order, same gate, new facts. The log now shows: refusal, approval with reason, confirmation — the whole story
5hold_customerok on_hold=1a hold is for judgment the formula cannot see. It outranks the limit entirely
6hold_customerrefused Already on credit hold
7create_orderokdrafts are still fine — a hold blocks NEW exposure, which begins at confirmation
8confirm_orderrefused is on credit holdplenty of room under the new limit, and it does not matter: the hold wins
9customer_positionok on_hold=1 committed=250000the already-confirmed SO-0001 is untouched — we honor what we committed to; we just take on nothing new
10release_customerok on_hold=0the release carries its own reason — "who released it and why" is exactly the question that gets asked later
11confirm_orderok status="confirmed"the limit applies again and the small order clears it. Refusal, hold, release, confirmation: all in the trail, all with reasons

the document and the journal: produced, never sent; balanced, never entered pass

INV-22 and INV-23. The invoice read IS the document — seller block, payment instructions and all — and the journal is a balanced derivation the ledger system imports. Nothing here transmits anything.
Environment: set_company_profile, create_customer, create_item
#ActExpectWhy
1create_orderok total=400000order totals are net — tax belongs to the moment of issuance
2confirm_orderok status="confirmed"
3fulfil_orderok
4invoice_fulfilledok total=430000 tax_total=30000
5invoiceok seller_name="Fabrikam Supply Co." payment_instructions="ACH to First Federal 026009593 / 5550123. Reference the invoice number."INV-22: the invoice read carries the whole document — seller identity and how to pay. Producing it is a read; SENDING it is the operator's act, outside the system, on purpose
6record_paymentok
7apply_paymentok
8journalok balanced=true entry_count=3INV-23: issue, receipt, application — three entries, each balanced, derived from the books. Revenue 4000.00 net + 300.00 tax payable; cash in through deposits, deposits onto the receivable. Nothing was posted; the ledger of record imports this