🦊 FoxyInvoice

Chapter 05 β€” The domain: invoices, tax, and money math

The "domain" is the part of the software that would be true even if you ran the business on paper: what an invoice is, when tax applies, how a quote becomes a bill. Get this layer right and every interface on top β€” web, public link, export file β€” is just a window onto the same truth. This chapter tours FoxyInvoice's domain logic, which is where most of the genuine engineering lives.

The invoice state machine

stateDiagram-v2
    [*] --> Draft: create
    Draft --> Sent: send (email / share link)
    Sent --> Paid: payment recorded
    Sent --> Partial: partial payment
    Partial --> Paid: remainder recorded
    Sent --> Overdue: due date passes
    Overdue --> Paid: payment recorded
    Draft --> Void: discard
    Sent --> Void: cancel
    note right of Sent
        Quotes: same machine, type=Quote.
        Accept β†’ converts to a real
        Invoice (clone + void quote)
    end note

Small on purpose. Every extra state is a branch in fifty UIs and a question for every customer. Overdue isn't stored so much as derived: a daily worker flags Sent invoices past due (which also triggers reminders β€” +3 and +14 days, plus a pre-due nudge three days before). Credit notes exist as a third type for refunds/adjustments.

Two invariants protect this machine:

  1. Totals are server property. The SPA shows a live preview explicitly labeled "server-confirmed on save" β€” but no number the browser sends is ever trusted. Every mutation (edit a line, change a discount) triggers full recomputation from the line items up through the tax engine. The client lying about a total simply has no effect.
  2. Edit windows. Invoices lock InvoiceEditWindowHours (default 24h) after certain transitions β€” accounting systems don't like history being quietly rewritten.

The tax engine: nexus, jurisdictions, and a waterfall

US sales tax is the reason this product has a "tax engine" and not a tax_rate column. The rules in one paragraph: whether you collect sales tax for a sale depends on where your business has nexus (a presence triggering tax duties), what jurisdiction the client is in, and what kind of thing you sold β€” service vs good, taxable or exempt, sometimes with a rate override.

The engine models exactly that, evaluated per line item:

  1. Is the client tax-exempt? β†’ zero tax, keep the line total.
  2. Do you have nexus in the client's jurisdiction? β†’ no: don't collect.
  3. Is a rate known for that jurisdiction? β†’ no: can't collect.
  4. Is there a taxability rule for this product type in this jurisdiction? β†’ use it (including rate overrides); otherwise fall back to the product's default taxability.
  5. Apply: lineTotal Γ— rate, rounded to cents. Never floats.

Jurisdictions carry component breakdowns (state/county/city) for reporting; a tax liability report sums what you owe where. The whole calculator is a pure function β€” no I/O β€” which makes it the most tested code in the repo.

Quotes that convert

A quote is an invoice with type=Quote and the same math. Conversion clones header + lines into a real Draft invoice (new number from the tenant's sequence, atomically reserved with SELECT … FOR UPDATE so two concurrent creates can't collide), then voids the quote so it can't convert twice. The client-facing version (Chapter 12): a share link with Accept / Decline buttons β€” accept runs this exact conversion server-side and emails the owner. One code path for button and menu.

Getting paid: two directions

Recurring invoices: templates with a schedule; a worker generates Draft invoices on cadence, runs them through the same tax engine, and can auto-send. Because it reuses the create path, every invariant applies for free.

Exports: the accountant is the audience

An invoicing product that can't hand off to the accountant is a toy. Three formats ship today β€” CSV (spreadsheets), QuickBooks IIF (balanced TRNS/SPL entries β€” debits and credits must sum to zero or QuickBooks rejects the file), and Tally XML for India (voucher envelopes where every voucher must balance). An export profile per workspace holds country, tax registration number, its locale-aware label (EIN, GSTIN, VAT No…), and fiscal-year start β€” so a PDF invoice in India shows GSTIN: 27ABCDE… without anyone configuring "labels." Export code is where you learn accountants are a format problem more than a math problem.

Money, one more time

Every amount is (decimal, currency); cross-currency arithmetic throws; totals are aggregates of per-line decimals rounded once at the edges (subtot→tax→total each rounded to cents, in a fixed order — rounding order is an API contract with your accountant).


Recap. A small state machine guarded by server-owned totals, a pure per-line tax waterfall driven by nexus and jurisdiction, conversion instead of duplication, machines-suggest-humans-confirm for inbound money, and exports designed for the person who'll actually read them.

Next: Chapter 06 β€” Accounts, hosting, DNS, email: from zero to a domain β€” buying the pieces, wiring them, and the mail-routing storm that taught us DNS respect.