Commit Graph

5 Commits (6b8412f0c394981c091fdba767fc2defe3627124)

Author SHA1 Message Date
nexxo cd54212b34 Issue an invoice from what somebody bought, and freeze it there
tests / pest (push) Failing after 7m32s Details
tests / assets (push) Successful in 25s Details
tests / release (push) Has been skipped Details
One invoice per purchase rather than per order: a customer who buys a plan and
two add-ons in one go has bought once, and three invoices for one purchase is
three times the paperwork for the same money. Each order becomes a line, which
is also how the add-ons get listed individually.

Everything the document says is copied into the snapshot at the moment the
number is assigned — issuer, recipient, lines, rate, the reason for the rate.
A test changes the company name and the customer name afterwards and asserts
the invoice still says what it said. That is the whole reason no PDF is stored.

The number and the invoice commit in one transaction. A number taken and then
lost to a failure is a gap in a series that must not have one, and the refusal
test asserts that a rejected invoice leaves the counter where it was.

It refuses outright until the company details are complete. An invoice without
a registered name, an address or a VAT number is not a valid invoice here, and
issuing one consumes a number that can never be handed out again.

VAT comes from TaxTreatment, which already existed and already handles a
verified EU VAT ID correctly — including the reverse-charge note, without which
a zero-rated invoice says nothing about why no VAT was charged.

The billing address is carried as the lines it was written as. It is one
free-text field today, and guessing which line is the postcode would put it
where the street belongs, on a document nobody can correct afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 02:09:05 +02:00
nexxo 4646880b90 Print every line at full price and take the discount off once, in euros
tests / pest (push) Failing after 7m40s Details
tests / assets (push) Successful in 22s Details
tests / release (push) Has been skipped Details
Second correction to the same block, and this one came from the right place: a
line reading 202,50 beside "Rabatt 10 %" does not say whether the discount is
in that number or still to come. It was unreadable, and unreadable on the one
figure a customer checks.

Lines are at full price now. Discounts are still entered per line — that is
where they belong and where somebody decides them — but they are summed and
shown once, in euros, under a subtotal that the "Gesamt netto" column adds up
to. Subtotal, one subtraction, VAT, total: a sum anybody can follow without
being told which figures are already adjusted.

The gross line-total column is gone and a gross UNIT price takes its place.
Full-price gross totals would sum to 315,60 against a total of 288,60 — the
same contradiction in a different column. Nobody adds up unit prices, and a
consumer reading this wants to know what one of the things costs.

lineTotal() and adjustmentLabel() went with it. Nothing calls them any more,
and a helper kept for a layout that no longer exists is the next person's
wrong turn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 01:59:56 +02:00
nexxo d21e4f0eb2 Put a discount on the line it belongs to, so the columns add up
tests / pest (push) Failing after 8m5s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
With one discount applied under the table, the gross column summed to 315,60
against a total of 284,04. Nothing was miscalculated — the discount simply came
after — but a reader who adds up a column found a document that does not agree
with itself, and no way to tell which figure was the real one. On an invoice
that is not a presentation detail.

A discount or surcharge now sits on its line, percentage or fixed amount, and
is inside that line's total. The label under the line is built from the same
value that did the arithmetic, so the words cannot end up describing a
different number than the one printed beside them.

The invoice-wide adjustment stays in the arithmetic for anything that genuinely
applies to the whole document, but nothing uses it by default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 01:52:16 +02:00
nexxo f8c923b48a Stop the invoice printing my own indentation
tests / pest (push) Failing after 8m3s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
TCPDF takes leading whitespace in HTML literally, so every line of the address
and every line description came out indented by however far the Blade template
happened to be nested — and the one line that had no indentation, the VAT
number, sat flush against the margin looking like the mistake. It was the only
correct one.

Also puts the title and the invoice number in one block. As an h1 with a
paragraph beneath it, TCPDF gave the heading a margin of its own and the two
drifted a centimetre apart.

Found by rendering the page and looking at it, which is the only way any of
this is ever found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 01:45:55 +02:00
nexxo c1896e70fc Render an invoice, with the arithmetic held to integer cents
tests / pest (push) Failing after 7m9s Details
tests / assets (push) Successful in 24s Details
tests / release (push) Has been skipped Details
TCPDF 6, deliberately, after installing 7 and finding out what it is. Version 7
is not a newer TCPDF; it is a compatibility shim over tc-lib-pdf whose own
mapping table lists 115 of 293 methods as stubs, and whose font package ships
no font files at all — the first render died on a missing helvetica.json. The
6.x line carries a hundred and sixty-five fonts, a working writeHTML and real
header and footer hooks, and is what everybody means by TCPDF.

The body is a Blade template handed to writeHTML rather than a hundred Cell()
calls with millimetre coordinates, because this table has to be adjusted the
first time somebody sees it on paper, and nobody adjusts the second kind.

InvoiceMath is integer cents end to end, and rounds once. A percentage applied
line by line and rounded each time drifts from the same percentage applied to
the sum — three lines at 3,33 € less 10 % is 99 cents that way and 100 cents
the honest way, and the customer's calculator is what finds it. Gross is always
derived from net and never the reverse: taking VAT back out of a gross figure
returns a different number often enough to matter. A discount and a surcharge
are one operation with opposite signs, so nothing has to remember which of two
flags means which.

Quantities are thousandths, because hours, gigabytes and part-months are all
real, and are printed with the trailing zeroes stripped — "12,000 Std." reads
as a defect.

Every line shows quantity, unit price net, total net AND total gross: this goes
to businesses and to consumers, and each reads a different column first.

Verifying it took two goes and both failures were mine. `strings` over a PDF set
in a Unicode font finds nothing, because the text is stored as subset glyph ids
— the check has to render in a core font to be a check at all. And two of the
strings I then declared missing were simply expectations I had got wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 01:41:46 +02:00