Specification · solo @ 0.3

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

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

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

ItemWhy deferred, not rejected
Credit notes / corrections on paid invoicesVoid covers the unpaid case; applied-cash unwinding needs its own design pass.
Recurring invoicesReal freelancer need; add with demand, not speculation.
Currency conversion / FX0.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 pricingRates 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 / interestTerms 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

ActKindRequired
draft_invoicewritecustomer lines
update_draftwriteinvoice
issue_invoicewriteinvoice
void_invoicewriteinvoice reason
record_paymentwritecustomer amount
apply_paymentwritepayment invoice
invoicereadinvoice
documentreadinvoice
outstandingreadnone
statementreadcustomer
setupreadnone
draft_reminderwriteinvoice subject body
reminder_outcomewritereminder outcome
remindersreadnone
create_customerenvironmentname
set_company_profileenvironmentname
update_customerenvironmentcustomer

Invariants

IdInvariant
S-1Money is integer cents, everywhere.
S-2An issued invoice is immutable — mistakes are void-and-reissue, on the record.
S-3The seller and bill-to blocks freeze at issuance; issuing without a profile or a client address is refused with the guide sentence.
S-4Numbers are sequential and never reused; a voided number stays burned.
S-5Invoice timing is the freelancer's agreement — the system records, never gatekeeps terms.
S-6No invented facts: amounts, dates and names come from the person or stay empty.
S-7Documents are produced, never sent; the doc link views one document (DRAFT-stamped until issued) and nothing more.
S-8Every write is a logged act with an actor; refusals are logged too.
S-9Journal derivation balances: issue to AR/Revenue, receipt to Cash/Deposits, application to Deposits/AR.
S-10An invoice carries exactly one currency from the company's set, a payment too; cash never crosses currencies; sums are per currency.
S-11Tax 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-12Numbers follow the company's format (sequence token, optional year token) and stay sequential and never reused within it.
S-13A 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

The whole product in one sitting. S-1 cents, S-3 seller frozen at issue, S-5 the terms are the agreement, S-7 the doc link is produced not sent.
Environment: set_company_profile, create_customer
#ActExpectWhy
1draft_invoiceok status="draft" total=545000S-1: 17 × $250.00 + $1,200.00 = $5,450.00, all integer cents. S-5: net-30 is THEIR agreement — recorded, not judged
2update_draftok total=582500drafts are worksheets — add the forgotten line, totals recompute
3issue_invoiceok status="issued" seller_name="Vega Consulting" seller_as_issued=trueS-3: the seller block froze from the profile; the doc link now exists for the freelancer to SEND THEMSELVES (S-7)
4record_paymentok
5apply_paymentok
6outstandingok count=1 total_open=282500partial payment leaves the honest remainder on the morning list
7statementok closing_balance=282500

immutability, burned numbers, and the refusal that teaches pass

S-2 issued is immutable, S-3 the no-profile refusal IS the interactive guide, S-4 void burns the number, S-8 refusals are logged.
Environment: create_customer
#ActExpectWhy
1draft_invoiceok status="draft" total=90000qty defaults to 1; drafting needs no profile — issuing does
2issue_invoicerefused No company details yetS-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
3set_company_profileok
4issue_invoiceok status="issued" seller_name="Nora Kis Design"
5update_draftrefused Only a draft can be editedS-2: issued is immutable — the refusal names the path (void-and-reissue)
6void_invoiceok status="void"S-4: INV-0001 stays burned; the doc link dies with it
7draft_invoiceok id="INV-0002"the next number, never the old one

New Zealand: GST registered, NZD, tax invoice pass

S-11: the company's tax scheme decides. Registered at 15% -> every line defaults to GST, the document prints TAX INVOICE and the GST number. S-10: the invoice carries NZD from the profile without being asked. S-1: 15% of $2,500.00 is 37500 cents, integer.
Environment: set_company_profile, create_customer
#ActExpectWhy
1draft_invoiceok 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
2update_draftok subtotal=300000 tax_total=37500 total=337500a line may opt out (tax_rate_bp 0) — zero-rated work exists; the default is a default, not a mandate
3issue_invoiceok status="issued" tax_invoice=true tax_id_label="GST No." seller_as_issued=trueS-3: the tax scheme froze with the seller block — flipping registration later never reprints this document
4record_paymentok currency="NZD"
5apply_paymentok
6outstandingok count=0

unregistered business, two currencies, custom numbering pass

S-11: not registered -> a taxed line is refused with the sentence naming who can change that. S-10: an invoice carries one currency from the company's set, a payment too, and cash never crosses currencies. S-12: the number format is the company's; F-{NNN} yields F-001.
Environment: set_company_profile, create_customer
#ActExpectWhy
1draft_invoicerefused not registeredS-11: the business is not registered; the write is refused and says the owner sets tax_registered if that changed
2draft_invoicerefused not one of this business's currenciesS-10: EUR is not in the set (USD, CZK)
3draft_invoiceok id="F-001" currency="CZK" total=1500000 tax_total=0S-12: numbered by the company's format
4issue_invoiceok status="issued" tax_invoice=false
5record_paymentok currency="USD"currency omitted -> the company default, USD
6apply_paymentrefused cannot cross currenciesS-10: a USD payment does not settle a CZK invoice, whatever the numbers say
7record_paymentok currency="CZK"
8apply_paymentok
9invoiceok status="paid" open=0
10draft_invoiceok 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

The guide's contract. setup names the next question in order; each answer is written as it comes; tax must be answered, a default is not an answer; the space is ready when the required six exist, and the first invoice follows.
#ActExpectWhy
1setupok ready_to_invoice=false next="name"nothing set: the first question is the name
2set_company_profileok
3setupok next="country"
4set_company_profileok
5setupok next="currency"
6set_company_profileok
7setupok next="tax"tax is open until answered — the unregistered default does not count
8set_company_profileok tax_decided=true
9setupok next="address"
10set_company_profileok
11setupok next="payment_instructions" ready_to_invoice=false
12set_company_profileok
13setupok ready_to_invoice=true next="tax_id"ready; what remains is optional
14create_customerok
15draft_invoiceok currency="NZD" total=180000
16issue_invoiceok status="issued" tax_invoice=falsean unregistered business issues a plain INVOICE

reminders: drafted, checked, sent by a person pass

S-13 a reminder is a draft the freelancer sends; its figures are the invoice's own, and a send is recorded only with the day it went. The scenario's clock stands at 2099-01-15; the invoice was issued on 2098-12-01 on net 30, so it is two weeks overdue.
Environment: set_company_profile, create_customer
#ActExpectWhy
1draft_invoiceok
2issue_invoiceok
3record_paymentok
4apply_paymentok
5draft_invoiceok
6draft_reminderrefused only an issued invoiceS-13 a draft is not owed yet
7draft_reminderrefused never add a feeS-13 no amount the invoice does not have
8draft_reminderrefused Say what is owed
9draft_reminderrefused placeholder
10draft_reminderok status="draft" stage=1S-13 the open amount, the total and what was paid are the invoice's own figures
11draft_reminderrefused already waiting
12reminder_outcomerefused Say when it went
13reminder_outcomerefused has not happened yet
14reminder_outcomeok status="sent" sent_at="2099-01-15"
15draft_reminderrefused less than 7 daysS-13 another within the week needs a reason
16draft_reminderok stage=2
17reminder_outcomerefused needs a reason
18reminder_outcomeok status="discarded"
19remindersok 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.