21. Facility invoice generation
Date: 2026-06-06
Status
Accepted
Context
Step 11 generates the weekly USD invoice a facility pays for a contractor's services. The two prior invoices (INV-0990, INV-0991) were produced manually in Wise; the owner provided them as the layout
- data reference. They are Wise-style: header, "Billed to"/"Issued by", a big "
USD due by ", a line table (Quantity / Unit price / Tax / Total), totals, and a "Ways to pay" block with fixed Wise local + international bank details. The schema already had invoices(uniqueinvoice_number, period,total_amount_cents,amount_paid_cents, status incl.partial/void,due_date,pdf_storage_path,variance_flagged) andinvoice_line_items(withhours= contracted/billed,actual_hours,hourly_rate_cents,amount_cents,line_type).
Decisions confirmed with the owner: weekly (Mon–Sun) cadence; @react-pdf/renderer for the
PDF (over the plan's Puppeteer — no headless-Chromium binary, deterministic, testable, trivial to
deploy); replicate the INV-0990/0991 layout exactly.
Decision
- Bill contracted hours; flag variance. Each line bills the contractor's weekly contracted
hours × the USD
hourly_rate_cents(integer cents,lib/money). The hours actually worked that week are recorded per line and compared to contracted; a gap strictly greater than the facility'svariance_flag_threshold_pct(default 10) flags the line and setsinvoices.variance_flagged, surfaced to the owner before sending. The flag never changes the billed amount (the spec's locked basis). One service line per active assignment at the facility; the latest overlapping assignment version per contractor is chosen deterministically (id tiebreak). - Sequential numbering from INV-0992, allocated atomically. A
next_invoice_numbersetting (seeded 992) is read-and-incremented under a row lock inside thecreate_facility_invoiceRPC, so concurrent generation can't collide or skip. - Atomic creation + double-invoice guard.
create_facility_invoice(SECURITY DEFINER, service-role only) allocates the number, inserts the invoice (draft) + line items, and is guarded by an in-transaction check and a partial unique index(company, facility, period_start, period_end) where status <> 'void'— at most one active invoice per facility+week; voiding frees the period to re-issue. AnexpectedTotalCentsfield binds the figure the owner confirmed. - PDF rendered then stored. After the RPC, the service renders the
@react-pdf/rendererdocument (fixed issuer + Wise bank details from a seededinvoice_configsetting; reference = invoice number) to a buffer and uploads it to a privateinvoicesStorage bucket (<facility_id>/<number>.pdf). Downloads go through/admin/invoices/[id]/download, which re-checks the admin and 302s to a 60s signed URL. A render/upload failure leaves the invoice without a PDF (regenerable), never an unpaid gap. - No money movement, no email. Generation only drafts the invoice + PDF; the owner downloads and sends it. (Gmail delivery is Step 12.) Owner-only for preview/generate/void; staff can view/download.
- Audit attribution via the transaction-local
app.actor_idGUC (ADR-0020), so create/void are attributed to the owner under the service role.
Consequences
- The pure engine (
lib/invoice) reproduces INV-0990/0991 to the cent and is unit-tested; the real PDF template is render-tested to a valid%PDF. Money stays integer USD cents throughout. - Voiding is the correction path (a paid invoice cannot be voided); the partial unique index makes re-issue after void safe and a duplicate active invoice impossible.
- Deferred / pending:
- Pay-online QR. The samples carry a Wise hosted-payment QR. We have no hosted-invoice URL yet
(Wise is Step 14, draft-only), so
payOnlineUrlis anullsetting and the QR/"Pay online" column is omitted until configured; the bank-detail columns are the substantive payment method. - Logo.
logoDataUrlis an optional setting; the header renders the "Invoice" wordmark until a logo is supplied. - Cadence. Weekly only for now (matches the contracted-hours model); bi-monthly/monthly would bill weekly × weeks-in-period.
- Interest / overage / adjustment line types exist in the schema but are out of scope here (interest is Step 15; approved overage lines fold in with Step 9's data when needed).
- Pay-online QR. The samples carry a Wise hosted-payment QR. We have no hosted-invoice URL yet
(Wise is Step 14, draft-only), so