solo — Freelancer Invoicing · Area Specification
last conformance run: 6/6 scenarios pass · 14 acts mapped 2026-10-07 20:15 UTC
Spec
Status: 0.3. The fourth area, deliberately tiny. 0.3 adds payment reminders: drafts for an overdue invoice that the freelancer sends from their own mailbox.
Solo is NOT o2c-lite — it is a different calibration, extracted from a different life. o2c bills what shipped because a warehouse must; a freelancer has no warehouse, no fulfilment, and no credit committee. What they have is an agreement with a client and a need for clean, immutable invoices and an honest picture of who owes what. Solo records exactly that.
1. Calibration: who this is for
One person (or a very small team) selling their own work: consultants, developers, designers, trades. A handful of clients, invoices with free-text lines, payment by whatever was agreed. Any currency the company names, one per invoice (0.2). The chat agent is often the ONLY interface — the doctrine is written as an interactive guide, and refusals steer the conversation, not just the tool call.
2. Scope
In: clients (core customer, reused), drafting and issuing invoices, voiding with reasons, recording and applying payments, outstanding and statements, the printable document with a shareable link, journal derivation for the accountant. 0.2: the company's country, currency set, tax scheme and numbering format, so a New Zealand GST-registered business and a US consultant billing in CZK both get a correct document without touching the code.
Out (§9): orders/fulfilment (that is o2c), credit gating (the freelancer IS the credit authority), currency conversion, recurring invoices, sending anything anywhere.
3. Entities and lifecycles
- client — core's customer, as-is, carrying a billing address and tax id (0.1.1). Terms live in the human agreement; the record keeps the default terms note for convenience, never enforces them. A client without a billing address cannot be issued to: the document would be incomplete, and the refusal says what to ask for.
- company profile (core) — the seller block plus, at 0.2, the country (how dates and amounts print), the currency set with a default, the tax scheme (label, default rate, registered or not, how the tax id is captioned) and the numbering format. Owner-only.
- invoice — draft → issued → paid | void. A draft is a worksheet: lines are free text (description · qty · rate, optional per-line tax rate and reference), a currency from the company's set, an optional subject line, editable until issued. Issuing assigns nothing new — the number existed from the draft — but freezes everything: the seller block from the company profile, the bill-to block from the client, the amounts, the due date. Paid is derived from applications. Void requires a reason and burns the number forever.
- payment — recorded as received (unapplied is a real state), applied to invoices with bounds on both sides, refusals naming the numbers.
- document link — the draft mints a per-invoice token;
/doc/<space>/<token>renders the real document, stamped DRAFT until issued (0.1.1: the preview a person looks at before the point of no return, so nobody hand-rolls a document). At issue the same link becomes the invoice — the link the freelancer forwards to their client — and<token>.pdfis the same document as a file (issued invoices only). Void does not kill the link: the document stays readable and printable, stamped VOID, because a record you cannot read is not a record;openis 0 on a void invoice so no reader has to remember the status.
- reminder — a payment reminder for an overdue invoice: a subject and a body the agent drafts in the freelancer's voice, its stage (first, second, final, from how many already went), the open amount it was written against. draft → sent (with the day it actually went, from the person) | discarded (with a reason). Saybooks never sends it; mailto opens it in the freelancer's own mail, addressed and filled in.
4. The acts
Writes (8): draft_invoice, update_draft, issue_invoice, void_invoice, record_payment, apply_payment, draft_reminder, reminder_outcome. Reads (6): invoice, document, outstanding (0.3: with reminders sent, the last one's date and the next stage), statement, setup (0.2: what the company profile still needs and the next question, in order), reminders. Environment (core, existing): create_customer, update_customer, set_company_profile.
5. Invariants
- S-1 Money is integer cents, everywhere.
- S-2 An issued invoice is immutable. Mistakes are void-and-reissue, on the record; a draft is the only editable state.
- S-3 The seller block and the bill-to block freeze at issuance. Changing company or client details never reprints history. Issuing without a company profile, or to a client without a billing address, is refused — and the refusal tells the agent to gather the details conversationally, one question at a time.
- S-4 Numbers are sequential and never reused. A voided number stays burned.
- S-5 Invoice timing is the freelancer's agreement — ahead of the work, partial, or after. The system records; it never gatekeeps terms.
- S-6 No invented facts: amounts, dates, names come from the person or stay empty.
- S-7 Documents are produced, never sent; payments are recorded, never moved. The document link is a capability to view one document, nothing more.
- S-8 Every write is a logged act with an actor; refusals are logged too.
- S-9 Journal derivation balances (o2c INV-23 family): issue → AR / Revenue (+ Tax Payable), receipt → Cash / Deposits, application → Deposits / AR. Amounts post in the invoice's currency; nothing converts.
- S-10 An invoice carries exactly one currency, from the company's set; a payment too. Cash never crosses currencies: applying a USD payment to a CZK invoice is refused whatever the numbers say. Sums are per currency; a grand total exists only when there is one.
- S-11 Tax follows the company's scheme, frozen with the seller block at issue. Not registered: no line may carry tax, the write is refused naming who can change that. Registered: every line defaults to the company's rate, a line may opt out at 0, the document prints TAX INVOICE, the tax name and rate, and the tax id under its own caption.
- S-12 Numbers follow the company's format — a sequence token, optionally a year token that restarts the sequence each year — and remain sequential and never reused within it (S-4).
- S-13 A reminder is a draft a person sends. Only an issued invoice past its due date with money open gets one, and one draft per invoice waits at a time. Its figures are the invoice's own: it names the invoice and the amount open, and any other amount it mentions must be the invoice's total or what was paid, so no fee or interest nobody agreed can appear; a placeholder is refused. A send is recorded only as the person reports it, with the day it went, which cannot be in the future or before the draft. Another reminder within seven days of the last one sent needs a reason. Saybooks never sends it (S-7).
6. Required read models
invoice (the whole document: lines, seller and bill-to snapshots, applied/open, the doc link), document (0.1.1: the rendered page as a picture, DRAFT-stamped until issued, and the PDF file once issued — so an agent looks at the real render before issue and hands over the file after), outstanding (issued-unpaid, oldest first, days overdue, totals), statement (one client's invoices and payments, chronological, closing balance).
7. Contract vs freedom
Contract: draft-only mutability, seller freeze, burned numbers, bounds on application, produce-never-send, one currency per invoice, the tax scheme's refusals. Freedom: the numbering format itself, which currencies and rates a business names (capturing them is ours; deciding them is the freelancer's and their accountant's), how the agent phrases the guide.
8. Conformance (scenarios)
01 the first invoice: profile → client → draft → issue (doc fields present) → partial payment → apply → outstanding shows the remainder · 02 immutability: update after issue refused; issue without profile refused with the guide sentence; void with reason; number burned; paid-invoice void refused · 03 New Zealand: GST registered at 15%, NZD from the profile, default tax on every line with one zero-rated line, TAX INVOICE at issue, paid in NZD · 04 unregistered business with USD and CZK: taxed line refused, unknown currency refused, F-{NNN} numbering, a USD payment refused against the CZK invoice, the CZK one applied · 05 guided setup: a fresh space, setup names each next question in order, tax must be answered, ready after the six required facts, first invoice issued · 06 reminders: an invoice partly paid and overdue; a reminder on a draft refused; a late fee, a missing amount and a placeholder refused; the first reminder drafted, a second draft refused while it waits; a send refused without its date and for a date not yet come; recorded as sent; another within the week refused, then drafted with a reason; a discard refused without a reason, then discarded.
9. Deferred — with reasons
| Item | Why deferred, not rejected |
|---|---|
| Credit notes / corrections on paid invoices | Void covers the unpaid case; applied-cash unwinding needs its own design pass. |
| Recurring invoices | Real freelancer need; add with demand, not speculation. |
| Currency conversion / FX | 0.2 bills in any currency the company names, one per invoice, and never converts. Reporting across currencies in one figure needs rates, a source for them, and a policy — not this area's. |
| Tax-inclusive pricing | Rates are entered exclusive of tax. Some markets quote inclusive; it doubles the arithmetic paths and needs its own scenarios. |
| Sending (email the client) | The doc link makes it one step away, which is exactly why the boundary must be crossed deliberately or not at all. |
| Late fees / interest | Terms live in the agreement; automating them is policy the system refuses to own at 0.1. |
---
Change log: 0.3 (2026-10-07) — payment reminders as drafts the freelancer sends, from Aryeh Munk's phase 1 list ("Follow up with customers AR"). 0.1-draft (2026-09-02) — drafted with Peter's refocus from an o2c door to a freelancer invoice generator; the interactive-guide doctrine and S-5 came from that conversation. 0.1.1 (2026-09-04) — client address + tax id, bill-to block frozen at issue, preview link from the first draft (DRAFT-stamped), document read returning the rendered page and the PDF; void invoices stay readable (VOID-stamped) with open = 0. 0.2 (2026-09-05) — company country, currency set with per-invoice currency, tax scheme with registration (S-10, S-11), numbering format (S-12), per-line refs and a subject line; prompted by a New Zealand freelancer's sample invoice and Peter's CZK/USD billing.
Acts
| Act | Kind | Required |
|---|---|---|
draft_invoice | write | customer lines |
update_draft | write | invoice |
issue_invoice | write | invoice |
void_invoice | write | invoice reason |
record_payment | write | customer amount |
apply_payment | write | payment invoice |
invoice | read | invoice |
document | read | invoice |
outstanding | read | none |
statement | read | customer |
setup | read | none |
draft_reminder | write | invoice subject body |
reminder_outcome | write | reminder outcome |
reminders | read | none |
create_customer | environment | name |
set_company_profile | environment | name |
update_customer | environment | customer |
Invariants
| Id | Invariant |
|---|---|
S-1 | Money is integer cents, everywhere. |
S-2 | An issued invoice is immutable — mistakes are void-and-reissue, on the record. |
S-3 | The seller and bill-to blocks freeze at issuance; issuing without a profile or a client address is refused with the guide sentence. |
S-4 | Numbers are sequential and never reused; a voided number stays burned. |
S-5 | Invoice timing is the freelancer's agreement — the system records, never gatekeeps terms. |
S-6 | No invented facts: amounts, dates and names come from the person or stay empty. |
S-7 | Documents are produced, never sent; the doc link views one document (DRAFT-stamped until issued) and nothing more. |
S-8 | Every write is a logged act with an actor; refusals are logged too. |
S-9 | Journal derivation balances: issue to AR/Revenue, receipt to Cash/Deposits, application to Deposits/AR. |
S-10 | An invoice carries exactly one currency from the company's set, a payment too; cash never crosses currencies; sums are per currency. |
S-11 | Tax follows the company's scheme, frozen at issue: unregistered lines cannot carry tax; registered lines default to the rate and the document prints TAX INVOICE. |
S-12 | Numbers follow the company's format (sequence token, optional year token) and stay sequential and never reused within it. |
S-13 | A reminder is a draft a person sends: only for an overdue invoice with money open, one waiting at a time, its figures the invoice's own, a send recorded only with the day it went, another within seven days only with a reason. |
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.
the first invoice: profile, client, draft, issue, partial payment pass
set_company_profile, create_customer| # | Act | Expect | Why |
|---|---|---|---|
| 1 | draft_invoice | ok status="draft" total=545000 | S-1: 17 × $250.00 + $1,200.00 = $5,450.00, all integer cents. S-5: net-30 is THEIR agreement — recorded, not judged |
| 2 | update_draft | ok total=582500 | drafts are worksheets — add the forgotten line, totals recompute |
| 3 | issue_invoice | ok status="issued" seller_name="Vega Consulting" seller_as_issued=true | S-3: the seller block froze from the profile; the doc link now exists for the freelancer to SEND THEMSELVES (S-7) |
| 4 | record_payment | ok | |
| 5 | apply_payment | ok | |
| 6 | outstanding | ok count=1 total_open=282500 | partial payment leaves the honest remainder on the morning list |
| 7 | statement | ok closing_balance=282500 |
immutability, burned numbers, and the refusal that teaches pass
create_customer| # | Act | Expect | Why |
|---|---|---|---|
| 1 | draft_invoice | ok status="draft" total=90000 | qty defaults to 1; drafting needs no profile — issuing does |
| 2 | issue_invoice | refused No company details yet | S-3: the refusal is the GUIDE — it tells the agent exactly what to gather, one question at a time. This is the interactive-onboarding mechanism, as a testable sentence |
| 3 | set_company_profile | ok | |
| 4 | issue_invoice | ok status="issued" seller_name="Nora Kis Design" | |
| 5 | update_draft | refused Only a draft can be edited | S-2: issued is immutable — the refusal names the path (void-and-reissue) |
| 6 | void_invoice | ok status="void" | S-4: INV-0001 stays burned; the doc link dies with it |
| 7 | draft_invoice | ok id="INV-0002" | the next number, never the old one |
New Zealand: GST registered, NZD, tax invoice pass
set_company_profile, create_customer| # | Act | Expect | Why |
|---|---|---|---|
| 1 | draft_invoice | ok status="draft" currency="NZD" subtotal=250000 tax_total=37500 total=287500 tax_invoice=true tax_label="GST" | no tax_rate_bp on the lines: the registered default (15%) applies; the currency came from the profile |
| 2 | update_draft | ok subtotal=300000 tax_total=37500 total=337500 | a line may opt out (tax_rate_bp 0) — zero-rated work exists; the default is a default, not a mandate |
| 3 | issue_invoice | ok status="issued" tax_invoice=true tax_id_label="GST No." seller_as_issued=true | S-3: the tax scheme froze with the seller block — flipping registration later never reprints this document |
| 4 | record_payment | ok currency="NZD" | |
| 5 | apply_payment | ok | |
| 6 | outstanding | ok count=0 |
unregistered business, two currencies, custom numbering pass
set_company_profile, create_customer| # | Act | Expect | Why |
|---|---|---|---|
| 1 | draft_invoice | refused not registered | S-11: the business is not registered; the write is refused and says the owner sets tax_registered if that changed |
| 2 | draft_invoice | refused not one of this business's currencies | S-10: EUR is not in the set (USD, CZK) |
| 3 | draft_invoice | ok id="F-001" currency="CZK" total=1500000 tax_total=0 | S-12: numbered by the company's format |
| 4 | issue_invoice | ok status="issued" tax_invoice=false | |
| 5 | record_payment | ok currency="USD" | currency omitted -> the company default, USD |
| 6 | apply_payment | refused cannot cross currencies | S-10: a USD payment does not settle a CZK invoice, whatever the numbers say |
| 7 | record_payment | ok currency="CZK" | |
| 8 | apply_payment | ok | |
| 9 | invoice | ok status="paid" open=0 | |
| 10 | draft_invoice | ok id="F-002" currency="USD" | the sequence continues in the company's format; currency omitted -> USD |
guided setup: a fresh space becomes ready to invoice one question at a time pass
| # | Act | Expect | Why |
|---|---|---|---|
| 1 | setup | ok ready_to_invoice=false next="name" | nothing set: the first question is the name |
| 2 | set_company_profile | ok | |
| 3 | setup | ok next="country" | |
| 4 | set_company_profile | ok | |
| 5 | setup | ok next="currency" | |
| 6 | set_company_profile | ok | |
| 7 | setup | ok next="tax" | tax is open until answered — the unregistered default does not count |
| 8 | set_company_profile | ok tax_decided=true | |
| 9 | setup | ok next="address" | |
| 10 | set_company_profile | ok | |
| 11 | setup | ok next="payment_instructions" ready_to_invoice=false | |
| 12 | set_company_profile | ok | |
| 13 | setup | ok ready_to_invoice=true next="tax_id" | ready; what remains is optional |
| 14 | create_customer | ok | |
| 15 | draft_invoice | ok currency="NZD" total=180000 | |
| 16 | issue_invoice | ok status="issued" tax_invoice=false | an unregistered business issues a plain INVOICE |
reminders: drafted, checked, sent by a person pass
set_company_profile, create_customer| # | Act | Expect | Why |
|---|---|---|---|
| 1 | draft_invoice | ok | |
| 2 | issue_invoice | ok | |
| 3 | record_payment | ok | |
| 4 | apply_payment | ok | |
| 5 | draft_invoice | ok | |
| 6 | draft_reminder | refused only an issued invoice | S-13 a draft is not owed yet |
| 7 | draft_reminder | refused never add a fee | S-13 no amount the invoice does not have |
| 8 | draft_reminder | refused Say what is owed | |
| 9 | draft_reminder | refused placeholder | |
| 10 | draft_reminder | ok status="draft" stage=1 | S-13 the open amount, the total and what was paid are the invoice's own figures |
| 11 | draft_reminder | refused already waiting | |
| 12 | reminder_outcome | refused Say when it went | |
| 13 | reminder_outcome | refused has not happened yet | |
| 14 | reminder_outcome | ok status="sent" sent_at="2099-01-15" | |
| 15 | draft_reminder | refused less than 7 days | S-13 another within the week needs a reason |
| 16 | draft_reminder | ok stage=2 | |
| 17 | reminder_outcome | refused needs a reason | |
| 18 | reminder_outcome | ok status="discarded" | |
| 19 | reminders | ok count=2 drafts=0 |
These pages are generated from the files in specs/ in the repository; scenarios are executed by src/conformance.js on every build. Written and maintained by Peter Varga, who also runs Portland AI Works. github.com/alviso/saybooks, AGPL-3.0.