diff --git a/DECISIONS.md b/DECISIONS.md index a804541c..6694f821 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -588,3 +588,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-27] hashApiKey stays SHA-256 against CodeQL js/insufficient-password-hash: the input is 32 CSPRNG bytes, not a user-chosen password, so no KDF work factor is meaningful against 256 bits; the hash is also the primary-key lookup on every MCP request, and changing it would invalidate every live gnubok_sk_ key since the hash IS the stored credential. [2026-07-27] mcp-oauth consent form action is HTML-escaped even though the CodeQL js/reflected-xss finding is not exploitable (WHATWG URL parsing already percent-encodes " < > in the query component): & is not in that encode set so the attribute was emitting invalid raw ampersands, and resting the page on an unstated parser-normalisation invariant is one refactor away from being wrong. [2026-07-27] Compliance-review artifact unpacks to runner.temp instead of the workspace root: extracting fork-influenced content over the trusted checkout, with AWS secrets in scope, was safe only because stage 1 happens to write fixed filenames; moving it makes overwrite unreachable by construction. +[2026-07-27] Supplier-invoice 'overdue' stays a stored status, made symmetric instead of derived (#1206): added approved_at as the durable attest marker and an un-flip branch in update_overdue_supplier_invoices(), rather than computing overdue at read time. Computing it would have touched every list/filter/report query that reads status plus the v1 API contract; the symmetric-cron fix is the same user-visible outcome at a fraction of the blast radius. diff --git a/app/(dashboard)/supplier-invoices/[id]/page.tsx b/app/(dashboard)/supplier-invoices/[id]/page.tsx index 5de975f7..b685de19 100644 --- a/app/(dashboard)/supplier-invoices/[id]/page.tsx +++ b/app/(dashboard)/supplier-invoices/[id]/page.tsx @@ -26,6 +26,7 @@ import { DocumentViewButton } from '@/components/bookkeeping/DocumentViewButton' import { useCompanySettings } from '@/components/settings/useSettings' import { formatAmount, formatCurrency } from '@/lib/utils' import { getDisplayTotal } from '@/lib/invoices/rounding' +import { canApproveSupplierInvoice } from '@/lib/supplier-invoices/lifecycle' import type { SupplierInvoice, SupplierInvoiceItem, SupplierInvoicePayment, BASAccount } from '@/types' interface EditableLine { @@ -490,7 +491,10 @@ export default function SupplierInvoiceDetailPage() { contextRef={`supplier_invoice:${invoice.id}`} size="default" /> - {invoice.status === 'registered' && !invoice.is_credit_note && ( + {/* Attest keys off approved_at, not the status: the overdue cron + flips unbooked invoices to 'overdue' just by aging, and gating on + 'registered' alone left them with no way through attest (#1206). */} + {canApproveSupplierInvoice(invoice) && !invoice.is_credit_note && (