Skip to content

Order to cash (AR)

The complete receivables stack: quote → order → fulfilment → invoice → collection → cash application → revenue.

The rule that governs the whole family:

An invoice is a claim, a payment is cash, and revenue is recognition. They are three different events on three different dates, and a UI that conflates any two of them will misstate something.


1. The lifecycle

Quote → Sales order → Fulfilment → Invoice → Dunning → Payment → Application → Cash
  │         │            │            │                  │           │
  │         │            │            └─ Dr AR / Cr Revenue          │
  │         │            └─ revenue recognition trigger (point-in-time)
  │         └─ backlog; no GL impact
  └─ no GL impact
                                                         └─ Dr Cash / Cr AR

No GL impact until the invoice. Quotes and orders are commitments, not transactions. Showing them in a revenue figure is the most common way a pipeline gets mistaken for performance. SalesOrderTable renders backlog and is visually separated from anything ledger-derived.

States

State Meaning GL
draft Being prepared None
sent Delivered to the customer Dr AR / Cr Revenue (or deferred)
viewed Customer opened it None — but it kills the "never received it" excuse
partially-paid Some cash applied Dr Cash / Cr AR, partial
paid Fully applied —
overdue Past due date, unpaid None; a presentation state
disputed Customer contests it None; blocks dunning
credited Offset by a credit note Dr Revenue (+ Dr Sales tax payable) / Cr AR
written-off Uncollectible Dr Allowance / Cr AR
void Cancelled before it was ever valid Reversal of the original

void and credited are different and the distinction matters. Voiding says the invoice should never have existed; crediting says it existed and is being reversed for a reason. A voided invoice with a payment against it is an error state. InvoiceDetail offers void only while unpaid and within the open period.


2. The invoice

type Invoice = {
  number: string;                  // sequential and unique — many VAT regimes require it
  customerId: string;
  issueDate: Date;
  dueDate: Date;                   // derived from terms; overridable with a reason
  terms: PaymentTerms;
  currency: string;
  lines: InvoiceLine[];
  subtotal: Money;
  discounts: Discount[];
  taxes: TaxLine[];                // per jurisdiction, per rate — never one lump
  total: Money;
  amountPaid: Money;
  amountCredited: Money;
  balanceDue: Money;               // total − paid − credited
  exchangeRate?: { rate: number; asOf: Date };   // if not in functional currency
  poNumber?: string;               // customer's PO — required by many AP departments
  revenueSchedule?: PerformanceObligation[];
};

UI rules:

  • Invoice numbers are sequential and unique. Many VAT regimes require it, and gaps are a standard audit red flag. The UI never lets a user pick a number, and a gap is an audit finding surfaced in InvoiceRegister.
  • balanceDue is the number that matters, and it's the one shown largest. Total is context; balance is the ask.
  • Taxes are itemized by jurisdiction and rate, never a single "Tax" line. A customer disputing tax needs to see which authority and which rate.
  • The customer PO number is prominent when present. An invoice missing the PO an AP department requires will be rejected and the user needs to see that before sending.
  • Foreign-currency invoices show the rate and date used. The receivable is recorded at the transaction-date rate, revalued at period end, and settled at the payment-date rate — three rates, two FX gain/loss events. See multi-entity & FX.

Payment terms

type PaymentTerms =
  | { kind: "net"; days: number }                             // Net 30
  | { kind: "eom"; days: number }                             // Net 30 EOM
  | { kind: "discount"; discountPct: number; discountDays: number; netDays: number }  // 2/10 net 30
  | { kind: "due-on-receipt" }
  | { kind: "installments"; schedule: { date: Date; amount: Money }[] };

2/10 net 30 must be rendered in full, not as a code. "2% discount if paid within 10 days, otherwise due in 30 days" — and the component computes and shows both the discounted amount and the discount deadline, because that's the decision the customer is making.


3. Aging

The core AR view, and the one that ties to the balance sheet.

Bucket Definition Typical treatment
Current Not yet due Normal
1–30 Past due up to a month Reminder
31–60 Escalation
61–90 Collections
90+ Allowance candidate, possible write-off

UI rules:

  • Aging is by due date, not invoice date. Aging from the invoice date makes Net 60 terms look delinquent on day 31. This is a common and embarrassing bug.
  • The aging total ties to the AR control account. Displayed, asserted. A difference means either the aging or the ledger is wrong.
  • Row totals and column totals both foot, and the grand total is proven both ways.
  • Bucket severity uses a sequential lightness ramp, not a red gradient — it survives greyscale and the signal palette. Age is also stated numerically ("47 days past due"), never color alone.
  • Credit balances in AR are shown, not hidden. A customer with a net credit (overpayment, unapplied cash) appears as a negative in the aging and should arguably be reclassified to a liability at period end. The component flags it rather than burying it.
  • Disputed invoices are excluded from dunning but included in aging. They are still receivables.

4. Cash application

Where AR actually breaks. Money arrives; matching it to invoices is the work.

type PaymentApplication = {
  paymentId: string;
  amountReceived: Money;
  applications: { invoiceId: string; amount: Money }[];
  unapplied: Money;              // received − sum(applied)
  writeOff?: { amount: Money; reason: string; account: string };
};

The cases the component must handle — all of them, not just the happy path:

Case Treatment
Exact match One payment, one invoice, same amount. Auto-applied.
Partial payment Applies to one invoice, leaves a balance. Invoice stays partially-paid.
Overpayment Excess becomes unapplied cash — never revenue; a liability once it leaves the customer in a net credit.
Lump sum, many invoices The common case. Apply across invoices; the UI suggests a combination that sums exactly.
Short pay Customer deducts. Either a dispute, an unrecorded credit, or a write-off. Requires a reason.
Payment with no invoice Unapplied cash. Sits on the balance sheet until applied.
Duplicate payment Flagged by amount + customer + date proximity. Refund or apply forward.
Bank fee deducted in transit Received is less than remitted. The difference is a bank charge expense, not a short pay.
FX difference on settlement Realized FX gain/loss. Posted, not written off.

UI rules:

  • Unapplied cash is never revenue. A payment from a customer with open invoices reduces that customer's AR even before it's matched; only a net credit balance (or a prepayment with no receivable) is a liability, reclassified at period end. Products that book it to revenue, or leave a credit balance inside AR, misstate both.
  • The suggested-combination feature is the highest-value interaction in AR. Given a lump sum, propose the set of invoices that sums exactly to it. When multiple combinations exist, show them ranked by oldest-first.
  • A remaining difference always requires a disposition — write-off (with reason and account), leave unapplied, or dispute. The UI never lets a difference silently disappear.
  • Write-off thresholds enforce approval. Small differences auto-write-off to a configured account; anything above the threshold requires an approver.
  • Applying is reversible (unapply), and both actions are audited.

5. Collections and dunning

type DunningStep = {
  offsetDays: number;              // relative to due date; negative = pre-due reminder
  channel: "email" | "letter" | "task" | "call";
  template: string;
  escalateTo?: string;
  pauseIf: ("disputed" | "payment-plan" | "manual-hold")[];
};

UI rules:

  • Disputes pause dunning automatically. Chasing a customer for an invoice they've formally disputed damages the relationship and is the single most common complaint about automated collections.
  • The full communication history is on the customer, not scattered per invoice. A collector needs one timeline.
  • Promise-to-pay is a first-class object with a date and an amount, and a broken promise escalates. "Customer said they'd pay Friday" is the most actionable fact in collections and it belongs in the data model.
  • Payment plans convert one receivable into an installment schedule without changing the total. The aging reflects the plan's due dates, not the original.

6. Credit notes and refunds

Three different things, constantly conflated:

What it does GL
Credit note Reduces what the customer owes Dr Revenue (or contra-revenue) + Dr Sales tax payable / Cr AR
Refund Returns cash already received Dr AR (or liability) / Cr Cash
Write-off Abandons an uncollectible claim Dr Allowance / Cr AR

A write-off is not a credit note. A credit note says the customer doesn't owe it; a write-off says they owe it and won't pay. The revenue treatment and the tax treatment differ, and so does what it tells you about the business. The UI names each distinctly and never offers one where the other is meant.

Credit notes reference their original invoice and cannot exceed its balance without an explicit override.


7. Customer master

type Customer = {
  id: string; name: string; legalName?: string;
  billingAddress: Address; shippingAddress?: Address;
  taxId?: string; taxExemptCertificate?: Document;
  defaultTerms: PaymentTerms;
  creditLimit?: Money;
  currency: string;
  parentCustomerId?: string;       // hierarchy for group-level exposure
  contacts: Contact[];
  status: "active" | "on-hold" | "inactive";
};

UI rules:

  • Credit limit and current exposure shown together, with exposure including unbilled orders. A limit without exposure is decoration.
  • Customer hierarchy matters for exposure. Three subsidiaries of one group are one credit risk, and CustomerStatement supports rolling up.
  • Tax exemption certificates expire. An expired certificate on an active customer is a flagged exception, because invoicing them tax-free after expiry is a liability.
  • On-hold blocks new orders but not collections.

8. Revenue recognition, separated from billing

The most important structural point in this document.

Billing and recognition are independent. You can bill annually and recognize monthly, deliver first and bill later, or bill milestones that don't match delivery. Every combination is normal.

Received/due > Recognized  → contract liability (deferred revenue)
Recognized > Billed        → contract asset (right still conditional)
                             or unbilled receivable (right unconditional)

UI rules:

  • An invoice does not automatically recognize revenue. It creates a receivable. What it credits — revenue or deferred revenue — depends on the performance obligations. InvoiceForm shows which, per line, before posting.
  • The revenue waterfall is derived from obligations, not invoices.
  • Never net contract assets against contract liabilities across contracts. Within a contract the position is presented net (required); across contracts it is not.
  • Full model in schedules & accruals.

9. AR metrics

Metric Formula Watch out
DSO (AR ÷ credit sales) × days in period Distorted by seasonality — prefer the countback method for volatile revenue
DSO (countback) Subtract each period's revenue from AR until exhausted; count the days More robust, harder to explain — show the working
Best possible DSO (Current AR ÷ credit sales) × days The floor DSO could reach with perfect collection
CEI (Opening AR + credit sales − closing total AR) ÷ (opening AR + credit sales − closing current AR) Collections effectiveness; better than DSO for team performance
ADD DSO − best possible DSO Average days delinquent — the actual lateness
Bad debt rate Write-offs ÷ credit sales
% past due Past-due AR ÷ total AR The simplest health signal

Every metric displays its formula, period, and inputs via MetricDefinition. DSO computed two different ways gives two different numbers, and a dashboard that doesn't say which is not comparable to anything.


10. AR component set

Component Purpose
InvoiceForm Create/edit with lines, tax, terms, and the recognition preview
InvoiceDetail Full invoice with payment history, credits, and the audit trail
InvoiceRegister All invoices, filterable, with the gapless-numbering check
AgingTable By bucket, tied to the control account
CashApplication The matching workspace — the highest-value screen in AR
PaymentSuggestion Proposed invoice combinations for a lump sum
UnappliedCashPanel Outstanding customer credits awaiting application
CreditNoteForm Referenced to an invoice, capped at its balance
CollectionsWorklist Prioritized by amount × age × risk, with promise-to-pay
DunningTimeline Communication history per customer
CustomerStatement Printable, per-customer, open items or full activity
CustomerCreditPanel Limit, exposure, hierarchy roll-up
RevenueWaterfall Deferred balance recognizing by future period

Built against this document

The surfaces that implement what this argues — running, computing, or shaped by it.

Told when this spec changes

The docs are the contract the components are built against. When one changes, the component changes with it — and the copy in your repo does not. One email when that happens.

One address, no list rental, unsubscribe in one click.

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.