last conformance run: 10/10 scenarios pass · 26 acts mapped 2026-08-23 23:48 UTC
| Version | 0.1-draft |
| Status | curated draft — not yet frozen; implementation conformance not yet enforced |
| Curator | Peter Varga (single editor; changes arrive as spec PRs, separate from implementation PRs) |
| Calibration | between 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.
---
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:
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:
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.
| Entity | Lifecycle |
|---|---|
| quote | draft → sent → accepted (raises a draft order) — or expired (terminal) |
| order | draft → confirmed → (partially fulfilled…) → closed — or cancelled (terminal, only if nothing fulfilled) |
| fulfilment | immutable once recorded (a fact, not a workflow object) |
| invoice | open → paid — or void (terminal, only if nothing applied) |
| credit note | open → settled (fully applied and/or refunded) |
| payment | no states — a fact with a derived unapplied balance |
| return | immutable 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.
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).
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.
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.
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.
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).
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.
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.
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.
The heart of the spec. Doctrine strings in implementations should quote these.
close_short may extinguish it, with a reason.Named views the area must answer without the caller assembling rows. Shapes are freedom; content is contract.
| Read model | Must answer |
|---|---|
order | lines with ordered/fulfilled/billed quantities, linked fulfilments & invoices, open + unbilled value, available next actions with refusal reasons |
customer_position | credit limit, open AR, committed (confirmed-unbilled), unapplied cash & credits, available credit, hold state |
invoice | lines with tax, applications received, open balance |
backorders | all open confirmed quantities, by item and by customer — the "what do we owe whom" view |
ar_aging | open 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_cash | payments and credit notes with remaining balances |
customer_statement | one 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.
| Contract | Freedom |
|---|---|
| the 18 acts, their required args, their refusal semantics | extension commands (own prefix), extra optional args |
| lifecycles in §3, all 21 invariants | schema, storage, id formats (prefixes) |
| read model content (§6) | read model shape, pagination, extra views |
| credit gate exists at confirm; exposure ≥ open AR + committed | exact formula extras (offsets), override/approval flows |
| tax captured & frozen per line | tax rate determination |
| bill-from-fulfilment | invoicing cadence/consolidation |
| refusal text = one sentence, with numbers | wording style beyond that |
An implementation conforms when:
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).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" } }
] }
| Item | Why deferred, not rejected |
|---|---|
| Down payments / deposits | Real 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-currency | Column exists; the hard part is revaluation and realized FX on application. Whole-area concern, own spec revision. |
| Price lists / customer pricing | Today: 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 billing | Different vertical (subscription), different lifecycle. |
| Consignment, drop-ship | Stock-ownership semantics beyond one-warehouse calibration. |
| Promise-to-pay / collections notes | Valuable, but it is CRM-on-AR; the dunning worklist (§6) is the hook it will hang from. |
| Per-line delivery trace on the document | Invoice 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 entries | The journal is revenue-side by design until items carry cost. Costing method choice (FIFO vs average) is a real design decision, not a column. |
---
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.
| Event | Entry |
|---|---|
| invoice issued | DR Accounts Receivable · CR Sales Revenue (goods net) · CR Service Revenue (services net, by the item's stocked flag) · CR Sales Tax Payable (tax) |
| payment received | DR Cash · CR Customer Deposits |
| payment applied | DR 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 applied | DR Customer Credits · CR Accounts Receivable |
| refund recorded | DR 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.
| Act | Kind | Required |
|---|---|---|
create_quote | write | customer lines |
send_quote | write | quote |
accept_quote | write | quote |
create_order | write | customer lines |
amend_order | write | order lines |
confirm_order | write | order |
cancel_order | write | order reason |
close_short | write | order reason |
fulfil_order | write | order |
invoice_fulfilled | write | order |
void_invoice | write | invoice reason |
record_return | write | order lines reason |
create_credit_note | write | customer amount kind reason |
write_off | write | invoice reason |
record_payment | write | customer amount |
apply_payment | write | payment invoice |
apply_credit | write | credit_note invoice |
refund | write | source_type source_id amount |
order | read | order |
customer_position | read | customer |
invoice | read | invoice |
backorders | read | — |
ar_aging | read | — |
unapplied_cash | read | — |
customer_statement | read | customer from to |
journal | read | — |
create_customer | environment | name |
create_item | environment | sku name unit_price |
receive_stock | environment | item qty |
set_price | environment | item unit_price |
set_credit_limit | environment | customer credit_limit reason |
hold_customer | environment | customer reason |
release_customer | environment | customer reason |
set_company_profile | environment | name |
| Id | Invariant |
|---|---|
INV-1 | Money is integer minor units, everywhere. |
INV-2 | Both prices survive: list price and charged price on every line. |
INV-3 | Documents don't mutate past draft except through defined acts. |
INV-4 | Bill what was fulfilled, never what was ordered. |
INV-5 | One credit gate, at confirmation; exposure counts open AR plus committed. |
INV-6 | Stock never goes negative; only the stock owner's API changes it. |
INV-7 | Short fulfilment leaves the remainder open; only close_short extinguishes it. |
INV-8 | Recording cash is not applying cash; unapplied cash is a valid state. |
INV-9 | Document numbers are sequential and never reused. |
INV-10 | Void only when untouched; anything applied-against is corrected by credit note. |
INV-11 | The credit note is the only downward correction past draft. |
INV-12 | Restock only what physically returns sellable. |
INV-13 | Cash is not transferable between customers. |
INV-14 | Tax is frozen at issuance. |
INV-15 | Due dates derive from terms at issuance. |
INV-16 | Write-offs are reasoned, visible credit acts. |
INV-17 | Refusals name the numbers. |
INV-18 | One sentence per rule, on every surface. |
INV-19 | Prices freeze when a document is raised from another. |
INV-20 | Closure is derived, not declared. |
INV-21 | Every write is a logged command with an actor; reads are never logged. |
INV-22 | The books produce documents; they never send them. |
INV-23 | Every journal entry balances — debits equal credits, derived from the books, never entered. |
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.
create_customer, create_item, create_item, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_quote | ok total=380000 | |
| 2 | send_quote | ok | |
| 3 | accept_quote | ok status="draft" total=380000 | |
| 4 | confirm_order | ok status="confirmed" | |
| 5 | fulfil_order | refused 5 on hand, 10 needed | INV-6: refuse rather than go negative; refusal names the numbers (INV-17) |
| 6 | fulfil_order | ok open_qty=5 status="confirmed" | INV-7: short fulfilment leaves 5 open; order does NOT advance to fulfilled |
| 7 | invoice_fulfilled | ok total=340000 status="open" | INV-4: 340000 = what was fulfilled, not the 380000 ordered |
| 8 | invoice_fulfilled | refused Nothing has shipped | |
| 9 | record_payment | ok | no remittance advice yet — INV-8, sits unapplied |
| 10 | apply_payment | refused only $2000.00 left | |
| 11 | apply_payment | ok | invoice remains open for the 140000 difference — short-pay stays visible |
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_order | ok status="draft" total=250000 | creating a draft consumes nothing — the gate is at confirmation only |
| 2 | confirm_order | refused Short by $1500.00 | |
| 3 | create_order | ok | |
| 4 | confirm_order | ok | 50000 fits inside the 100000 limit |
| 5 | create_order | ok | |
| 6 | confirm_order | refused committed $500.00 | INV-5: the 50000 confirmed-but-uninvoiced on SO-0002 counts as exposure — a gate that only reads invoices would wave this through |
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_order | ok | |
| 2 | confirm_order | ok | |
| 3 | fulfil_order | ok | |
| 4 | invoice_fulfilled | ok total=100000 | |
| 5 | record_payment | ok | |
| 6 | apply_payment | ok | |
| 7 | void_invoice | refused applied | INV-10: money moved; the record must keep saying so |
| 8 | record_return | ok restocked_qty=2 credit_total=30000 | INV-12: stock gains 2, not 3; the damaged unit is recorded but never sellable |
| 9 | apply_credit | ok | invoice open balance now 100000 - 60000 - 30000 = 10000 |
| 10 | write_off | ok invoice_status="paid" | INV-16: visible, reasoned, a credit act — never a deletion. INV-20: paid is derived once fully applied |
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_order | ok status="draft" total=30000 | |
| 2 | amend_order | ok total=50000 | a draft is a conversation — replace the lines freely, the log keeps every version |
| 3 | close_short | refused no open remainder | close_short is for confirmed orders with an unfulfilled tail — a draft has nothing committed to abandon |
| 4 | confirm_order | ok status="confirmed" | |
| 5 | customer_position | ok committed=50000 open_balance=0 | INV-5: the moment of confirmation, the order's value counts against credit — before any invoice exists |
| 6 | amend_order | refused Only a draft order can be amended | INV-3: what was confirmed is the fact the credit gate evaluated. Growth is a new order; shrink is close_short |
| 7 | cancel_order | ok status="cancelled" | cancellation is only possible while nothing has been fulfilled — after that, the honest path is bill and credit-note |
| 8 | customer_position | ok committed=0 | cancelling releases the committed exposure; the credit line breathes again |
| 9 | confirm_order | refused Only a draft order can be confirmed | |
| 10 | order | ok status="cancelled" | INV-9 in spirit: the cancelled order stays on the books, visible, number and lines intact — nothing is ever deleted |
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_order | ok total=100000 | order totals are net — tax is a billing fact, not an ordering fact, because the rate belongs to the moment of issuance |
| 2 | confirm_order | ok | |
| 3 | fulfil_order | ok open_qty=0 | |
| 4 | invoice_fulfilled | ok subtotal=100000 tax_total=8750 total=108750 | INV-14: 8.75% snapshotted per line at issuance. If the rate changes tomorrow, this invoice never moves |
| 5 | record_payment | ok | |
| 6 | apply_payment | ok | |
| 7 | invoice | ok status="open" open=58750 | short-paid and honestly so: the invoice stays open for the difference — resolved by credit note if justified, dunning if not, never fudged |
| 8 | ar_aging | ok 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 |
| 9 | customer_statement | ok opening=0 closing=58750 | balance-forward: opening + invoices − credits − payments = closing. The monthly document, minus the envelope |
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_order | ok total=150000 | |
| 2 | confirm_order | ok | |
| 3 | fulfil_order | refused 5 on hand, 12 needed | INV-6: never silently reduce the ordered quantity — ship what exists and keep the remainder visible |
| 4 | fulfil_order | ok open_qty=7 | |
| 5 | backorders | ok length=1 | INV-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 |
| 6 | close_short | ok open_qty=0 status="shipped" | the honest death: the debt of goods is extinguished with a reason, committed credit exposure is released |
| 7 | backorders | ok length=0 | |
| 8 | invoice_fulfilled | ok 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 |
| 9 | record_payment | ok | the customer paid against the original order value — overpayment is real life, not an error state |
| 10 | apply_payment | ok applied="$625.00" | omitted amount applies the smaller remaining side; the extra $375 stays as unapplied cash, visible (INV-8) |
| 11 | refund | refused only $375.00 unapplied | INV-17: bounded and the refusal names the bound — you cannot refund money that already settled an invoice |
| 12 | refund | ok source_remaining="$0.00" | records that the money went back; actually moving it is out of scope, like every outbound act |
| 13 | unapplied_cash | ok length=0 | clean end state: nothing owed in goods, nothing owed in cash, and every step of getting here is in the log |
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_order | ok | |
| 2 | confirm_order | ok | |
| 3 | fulfil_order | ok | |
| 4 | invoice_fulfilled | ok total=40000 | |
| 5 | void_invoice | ok status="void" | INV-10: nothing was applied yet, so void is allowed — the quantities go back to billable and the mistake never billed anyone |
| 6 | invoice_fulfilled | ok id="INV-0002" total=40000 | INV-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 |
| 7 | record_payment | ok | |
| 8 | apply_payment | ok | |
| 9 | void_invoice | refused applied | INV-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 |
| 10 | create_credit_note | ok status="open" total=5000 | INV-11: the universal downward correction, with its own open balance and a mandatory reason that will be read back |
| 11 | apply_credit | ok applied="$50.00" | |
| 12 | apply_payment | ok | |
| 13 | invoice | ok status="paid" open=0 | INV-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 |
create_customer, create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_quote | ok total=40000 | quoted at the list price of the day: 100.00/unit |
| 2 | send_quote | ok | |
| 3 | set_price | ok unit_price=12000 previous_price=10000 | the list price moves while the quote is out — a logged act, because WHEN it moved is part of the record |
| 4 | accept_quote | ok total=40000 | INV-19: the customer accepted 100.00/unit, so that is what the order carries — the whole reason the quote existed |
| 5 | create_order | ok total=12000 | a fresh document takes the new list price — the freeze protects raised documents, it does not fossilize the catalog |
| 6 | confirm_order | ok | |
| 7 | fulfil_order | ok | |
| 8 | invoice_fulfilled | ok total=40000 | INV-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 |
| 9 | confirm_order | ok | |
| 10 | fulfil_order | ok | |
| 11 | invoice_fulfilled | ok total=12000 | |
| 12 | record_payment | ok | Blue Yonder pays their invoice |
| 13 | apply_payment | refused not transferable | INV-13: INV-0001 is Adventure Works' invoice. However tempting the tidy-up, cash stays with the customer who paid it |
| 14 | create_credit_note | ok | |
| 15 | apply_credit | refused not transferable | INV-13 covers credit too: Adventure Works' credit note cannot settle Blue Yonder's invoice |
| 16 | apply_payment | ok | applied where it belongs; INV-0002 derives paid (INV-20) |
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_order | ok total=250000 | |
| 2 | confirm_order | refused Short by $1500.00 | INV-5: the gate said no and named the numbers. What happens next is a human decision, not an agent maneuver |
| 3 | set_credit_limit | ok credit_limit=300000 previous_limit=100000 | the 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 |
| 4 | confirm_order | ok status="confirmed" | same order, same gate, new facts. The log now shows: refusal, approval with reason, confirmation — the whole story |
| 5 | hold_customer | ok on_hold=1 | a hold is for judgment the formula cannot see. It outranks the limit entirely |
| 6 | hold_customer | refused Already on credit hold | |
| 7 | create_order | ok | drafts are still fine — a hold blocks NEW exposure, which begins at confirmation |
| 8 | confirm_order | refused is on credit hold | plenty of room under the new limit, and it does not matter: the hold wins |
| 9 | customer_position | ok on_hold=1 committed=250000 | the already-confirmed SO-0001 is untouched — we honor what we committed to; we just take on nothing new |
| 10 | release_customer | ok on_hold=0 | the release carries its own reason — "who released it and why" is exactly the question that gets asked later |
| 11 | confirm_order | ok status="confirmed" | the limit applies again and the small order clears it. Refusal, hold, release, confirmation: all in the trail, all with reasons |
set_company_profile, create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_order | ok total=400000 | order totals are net — tax belongs to the moment of issuance |
| 2 | confirm_order | ok status="confirmed" | |
| 3 | fulfil_order | ok | |
| 4 | invoice_fulfilled | ok total=430000 tax_total=30000 | |
| 5 | invoice | ok 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 |
| 6 | record_payment | ok | |
| 7 | apply_payment | ok | |
| 8 | journal | ok balanced=true entry_count=3 | INV-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 |