feat(worklist): the next Skatteverket payment with bankgiro, OCR and due date under Att göra on Hem (#2435)

Part (a) of #2187. A twelfth worklist category, skattekonto_payment_due:
the earliest upcoming skattekonto charge whose sum exceeds the last synced
saldo, computed once in lib/worklist (server-side twin of the /skattekonto
page's Nästa dragning math) and rendered as one Betala row on Hem with the
shortfall, bankgiro 5050-1055, the OCR reference and the due date.

The row appears only when money has to move: a saldo that covers the charge
yields nothing, and no upcoming charge yields nothing. Ignored rows take
part, since Skatteverket draws them regardless of our flag. Without a
balance snapshot the full charge is the amount. Without an org number the
row keeps its bankgiro and date and drops the OCR.

No table, route or migration: /api/worklist/counts picks the category up
through getWorklistCounts, and Hem passes the computed row into the same
options wave as the expense payouts.

Part (b), a betalfil for the skattekonto payment, stays a follow-up: the
existing payment-file route is AGI-scoped.


Claude-Session: https://claude.ai/code/session_0179bdetHyofL6ATfQxB5wP5

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-08 21:25:01 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent a7dcaac6ad
commit 20925f6c65
10 changed files with 353 additions and 9 deletions
+1
View File
@@ -1675,6 +1675,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-09-08] #2425 ROT/RUT: the picker's begäran rows keep single-click handoff and gain a checkbox per row for a bundled pick, instead of a new multi-select mode or an ambiguity-tolerant matcher. Why: the server (request_ids, settleRotRutPayoutRequestSet) and the confirm dialog already take N begäran since #2360; only the manual fallback was 1:1, and a firm whose jobs share a price hits the matcher's ambiguity refusal often. Guessing between two same-amount begäran would clear the wrong 1513 fordran, so the choice stays with the user.
[2026-09-08] Issue #2426 ROT/RUT begäran state in the invoice list: normal states (Att begära, Skapad, Uppladdad, Beviljad) render as muted text and only Delvis beviljad and Avslagen get a chip (design convention 5: a fönsterputsfirma where every invoice is ROT/RUT would otherwise carry the same chip on every row); the filter is a third ContextPicker (?rotrut=) gated on rot_rut_enabled or an invoiced deduction, not extra entries in the status view, so a ROT/RUT state can be combined with any status view and companies without ROT/RUT never see it. One predicate (lib/invoices/rot-rut-list-status.ts) feeds the column, the filter and its counts; the state is read from a rot_rut_payout_request_items embed on the existing list query, no new API and no migration.
[2026-09-08] Receipt purchase date: fixed the extraction prompt's description of invoice.invoiceDate (schema comment plus an explicit "invoiceDate on receipts" rule) instead of adding a receipt-specific purchaseDate field. Why: prod (last 30 days) showed receipts losing the date on 46% of items (75% via WhatsApp) while supplier invoices lost it on 0.4%, and purchaseTime was filled on nearly every affected receipt: the date was described as a bare ISO field under the invoice block beside a purchaseTime rule marked "receipts only", and the model read that as "invoice-only". A new field would need a schema change, a UI column and every consumer (WhatsApp M4 ack date, the Bokför entry-date default that falls back to today, the worklist) taught a second date; the prompt fix stops the class with no data-shape change. Existing undated items are not re-extracted by this; a backfill is a separate call.
[2026-09-08] #2187 (a) Att göra row "Betala in till Skatteverket" shows the shortfall (next charge minus the last synced saldo, the full charge only when no snapshot exists) and appears only when something has to be paid in, instead of a deadline-style row for every upcoming charge: a company whose saldo covers the dragning has nothing to do, and the /skattekonto page's Nästa dragning math already defines the number, now computed once in lib/worklist for Hem and /api/worklist/counts. Part (b), a betalfil for the skattekonto payment, stays a follow-up: the existing payment-file route (app/api/skatteverket/tax-payments/[period]/payment-file) is AGI-scoped (amount from agi_declarations, reachable only from a salary run) and needs an amount source before it can pay a general dragning.
[2026-09-08] Issue #2224 offert -> kundorder: the quote source was added to the existing proforma -> order service (lib/sales-orders/convert-to-sales-order.ts) and to gnubok_convert_invoice as a target parameter staged under the existing convert_invoice operation type, instead of a second service, a separate MCP tool and a new pending-operation type. Why: one conversion path and one guard pair (a quote with a live order cannot become an invoice, a quote with a live invoice cannot become an order) keep the proforma and quote flows identical, and reusing the operation type keeps the pending-operations type list untouched. The quote flips to accepted like quote -> invoice does; cancelling it like the proforma path would hide the customer's accepted agreement. Skeptic refutation: for an already-accepted quote that accepted -> accepted compare-and-set cannot detect a concurrent conversion, so the "never twice" guard moved into the database (migration 20260908165000: one live kundorder per source, and two triggers that lock the quote row and refuse a live order beside a live converted invoice and vice versa); the service pre-checks stay for the friendly error. Second refutation: order -> invoice booked a foreign-currency invoice 1:1 when Riksbanken had no rate (pre-existing, but the quote now depends on that path and the fail-closed quote -> invoice route is refused while an order lives), so createInvoiceFromSalesOrder now fails closed with SALES_ORDER_INVOICE_FX_RATE_UNAVAILABLE. Locking the quote decision (open/declined) while a live order exists was left out: the DB trigger only knows about converted invoices and changing it is a migration; a declined quote with a live order is harmless since the order carries the sale.
[2026-09-08] Issue #2224 follow-up from the correctness skeptic: the quote decision (open/declined) is now locked in the database while a live kundorder exists (migration 20260908165100 extends invoices_quote_decision_guard), reversing the earlier call to leave it open; a declined offert behind a confirmed, invoiced order was a contradictory agreement trail and the dashboard hid the re-accept button, so the quote was stuck. The three source and decision guards run as SECURITY DEFINER: a SELECT FOR UPDATE under RLS admits only the caller's active company, so a multi-company member writing for another company through raw PostgREST got no row, no lock and no guard. Both landed as a second migration rather than an edit of 20260908165000, which was already applied to staging under that version.
[2026-09-08] Draft invoice PDF marks a draft with one diagonal, faint word (UTKAST / DRAFT) across every page instead of a banner in the top margin (#2437): a banner reads as UI chrome on a document, a watermark reads as a stamp and leaves the preview pixel-identical to the final print. The long legal sentence (saknar löpnummer, ML 17 kap 24 §) is dropped on purpose: the word alone says the document is not a valid invoice, and the download dialog (#2399) already explains why before the file exists. Rotation and opacity sit on a padded wrapper View so the word turns about its own centre. Skeptic refutation accepted: the first cut (#6b7280 at 0.14, about 92% brightness) would drop out of a monochrome print or greyscale scan, and a numbered draft otherwise prints title, number and OCR like a real faktura; now #4b5563 at 0.3 (about 79% brightness), with a test pinning the composited grey between 70% and 85%. A 1-bit scan can still threshold the word away; a second explicit line on numbered drafts was left out because the request was the word alone, and that residual is Emil's call. Second refutation accepted: the overlay is emitted as the LAST child of the Page, because react-pdf paints in document order and `fixed` does not hoist, so an overlay emitted first was painted under the opaque payment and customer boxes and the word vanished on the page that carries totals and OCR; a test now inflates the PDF content streams and asserts the glyph run comes after the last rectangle fill on every page. BETALD and MAKULERAD banners are left as they are.