`booked_asset` stores an asset record ID, and `npm run seed` deletes and
re-creates every asset — so the id a line was booked against stops existing after
a seed. The line then points at nothing and silently drops out of monthly_income,
which is exactly the reported symptom: booked yesterday, unbooked today, with the
line still carrying the correct code.
The booking step now re-derives the pointer from the register code the line
already carries (`asset_code`, falling back to the machine code) whenever the
stored target is empty or no longer exists. It only ever points a line at the
asset its own code names, so it never invents a match and never clears a
site booking. A line with no matching code stays unbooked exactly as before.
This makes the documented run order self-healing:
`npm run seed && node scripts/book-invoice.mjs` now restores the links, so a
reset cannot silently unbook the register.
Verified by running a full seed and re-booking: 29 pointers were invalidated by the
seed and all 29 were re-linked, giving 45 posted rows and 3 unbooked lines — the
two MPLANT05 containers and one WT02 trailer, which have no matching plant item.
Adds an Invoices sub-menu under /income with a summary table (one row per
invoice, click to expand the full detail) and the data model behind it.
Data:
- invoices + invoice_lines PocketBase collections, created idempotently by
scripts/setup-invoice-collections.mjs
- line items transcribed from PHI 166/167/171 with per-invoice arithmetic
validation in the seed; a bad invoice is refused without blocking the rest
- invoice-*.json in data/source are auto-imported, so adding an invoice is
one file
Booking:
- manual book-in per line (Booked to), grouped by category
- POST /api/invoice-lines/:id/book moves the income row rather than adding a
second one, so a booking can be changed freely
- scripts/book-invoice.mjs is idempotent: rows are stamped with
monthly_income.invoice_line and replaced on re-run
- booked amount follows lib/invoice-booking-policy.json (bookInclusiveVat),
read by both the app and the CLI so they cannot disagree
Money:
- the R24,205.13 column on these invoices is PLUS VAT, not escalation. The
seed now rejects VAT that disagrees with the stated rate.
- booked amounts are VAT-inclusive per the client ruling of 2026-10-01; the
VAT stays visible rather than hidden.
Uploads:
- drag-and-drop PDF attached to invoices.source_pdf
- GET /api/invoices/:id/document proxies it — the collection is rule-locked,
so a direct PocketBase /api/files link 404s in a browser
Fixes a month-aggregation bug this introduced: buildIncomeRows assigned into
months[i], so a machine with two income rows in one month (one per invoice,
which is normal multi-site use) showed only the last. SS01 September read
R20,700 instead of R35,305 while its own YTD included both. Months now
accumulate, per-month profit is computed once after accumulation, and
lib/rows.test.ts pins it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>