How an invoice gets built
When a schedule fires, the contract is just a set of line items and a customer. This post is about the step that turns that into an actual invoice: where the line items come from, how the invoice number is assigned without ever colliding or skipping, and why every figure on the document is computed by plain code that produces the same answer every single time.
Key takeaways
- When a schedule fires, the builder loads the contract, gathers its line items, and assembles one complete invoice in plain code.
- The invoice number comes from an atomic counter in DynamoDB — it never collides and never skips, even on a retry.
- The builder is idempotent: building the same contract-and-cycle twice produces the same invoice, never a duplicate in the ledger.
- Bedrock is optional and only ever polishes a line’s description — it never sees or sets a quantity, a price, or a total.
- The finished invoice and its full workings are written to the ledger before anything is rendered or sent.
What the builder receives
A schedule firing hands the builder almost nothing — a contract id and the cycle date it’s billing for. That deliberate thinness is what makes the build trustworthy: everything the invoice contains is read fresh from the current contract, not carried over from last month’s document. The builder loads the contract from the mirrored store, reads its line items, its customer, and its jurisdiction, and from there does the same handful of steps every time — gather, number, compute, record — with no branch that depends on anything outside the contract and the rules doc.
The first step is gathering the lines. A retainer is often a single line (“Monthly retainer”, quantity 1, the agreed price). Most contracts have a few: a base retainer, a couple of add-on services, perhaps a usage line the contract pulls from a counter. Each line has a description, a quantity, and a unit price. The builder also checks whether anything changed part-way through this cycle — a line added, removed, or repriced since the last invoice — because those are the lines that need proration. The arithmetic of that is Part 4; here the point is simply that the builder collects every line that belongs on this invoice, including the prorated catch-up lines, before it adds anything up.
The invoice number
An invoice number has two hard requirements that fight each other: it must be unique, and for most tax regimes it must be sequential with no gaps. A naive “read the highest number, add one, write it back” breaks the moment two builds run close together or a retry re-enters — you get a collision or a skip. So the number comes from a single counter item in DynamoDB, incremented with an atomic update that returns the new value in one operation. Two builds can never be handed the same number, and the sequence never jumps. The format itself — the prefix, the width, whether it resets each year — comes from the numbering rule in the settings doc, so INV-2026-0148 is just the counter dressed in the format you chose.
The number is only stamped onto an invoice that is actually going to be issued. If a build is abandoned before it commits — a contract turns out to be paused, say — no number is drawn, so the sequence doesn’t develop a hole for an invoice that never existed.
Idempotent by design
Two things can ask the builder to build the same invoice: a retry after a transient failure, and the daily catch-up sweep from Part 2. Both are good behaviours — you want the system to try again rather than silently drop a bill — but they mean the builder has to be safe to run twice. It is, because the ledger record is keyed by the contract id and the cycle it bills. Before the builder commits a new invoice it does a conditional write on that key: if an invoice already exists for this contract and this cycle, the write is rejected and the builder simply returns the one that’s already there. A retry can never produce two invoices for the same month, and the sweep can only ever fill a genuine gap. The single risk in recurring billing — billing the same period twice — is closed at the database, not in fragile application logic.
Where the model is allowed, and where it isn’t
This system can use Bedrock, but only in one narrow, optional place: turning a terse contract line into a clearer description. A contract might store a line as “Apr retainer — phase 2”, and a contract that opts in can have the model expand that into “Monthly retainer, April 2026 — Phase 2 delivery and support” for the printed invoice. That is the entire remit. The model never sees a price, never sets a quantity, never picks a tax rate, and never computes a total. Every figure on the invoice is plain Python operating on the contract’s own numbers. Switch Bedrock off entirely and the invoice still renders correctly — the descriptions are just the raw contract text. Keeping the model strictly on the wording side of the line is what lets the arithmetic be audited and trusted; a number you can’t reproduce by hand has no place on an invoice.
By the time the builder finishes, the ledger holds a complete, numbered invoice with every line, the tax, the total, and the exact workings behind each figure. Nothing has been rendered or sent yet — recording comes first, so an invoice can never go out without a matching record. Turning that record into a PDF in someone’s inbox is Part 5; the proration and tax arithmetic it depends on is Part 4.
Why this shape
- Build from the contract, not last month. Everything on the invoice is read fresh, so a stale price can’t survive.
- Numbers come from an atomic counter. Unique and gap-free, even under retries and concurrent builds.
- Record before you render. The ledger write commits first, so no invoice can be sent without a matching record.
- Idempotent on contract-and-cycle. Building the same period twice returns the same invoice — never a duplicate.
- The model stays on the wording. Bedrock is optional and touches descriptions only; every figure is plain, reproducible code.