diff --git a/DECISIONS.md b/DECISIONS.md index 1aa5e83c..5b1ffa6e 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1171,6 +1171,7 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-22] Per-company invoice sending domains are gated by a manually granted capability (custom_sender_domain), deliberately NOT in PAID_CAPABILITIES: the opt-in must not be trial-seeded or written by the Stripe subscription sync, and non-grantees must see an unchanged invoicing settings page (the section hides on the 403 capability_blocked envelope). The sending-domain module has no Resend orphan-adoption path (a name that already exists is a 409), because the same Resend account holds the platform's own outbound domain. The delivery log was left untouched (no from_address column): adding it would re-open the hardened invoice_deliveries evidence triggers/redaction paths for a nice-to-have, and the log already measures delivered/bounced per send. [2026-08-22] company_sending_domains verification state (domain, status, resend_domain_id, dns_records, verified_at, last_checked_at) is service-role only via a BEFORE trigger keyed on the JWT role claim; tenant JWTs may only open a pending claim and edit sender_local_part/sender_name/enabled. Skeptic refutation: RLS alone let a granted admin insert {domain: platform sender domain, status: verified} through PostgREST and send invoice mail as the platform. The claim/verify helpers therefore take a separate service-role writer for those columns. Second refutation: a domain Resend later flips to failed made every invoice send for that company fail; the Resend adapter now retries once as the platform sender when an explicit company From is rejected (nothing was sent on the rejected attempt, so the retry cannot double-send). [2026-08-22] Sending-domain verification writes bind by (id, company_id, domain, resend_domain_id IS NULL) and verify/webhook compare Resend's domain name with the row before writing verified; resolveInvoiceSender additionally refuses reserved platform domains and non-hostnames at send time. Skeptic re-check: a tenant could delete and re-insert its pending row under the same id with a reserved domain during the claim's Resend round-trip (TOCTOU), and the service-role writer updated by id alone. Defense in depth over a single gate. +[2026-08-24] #1717 öre settlement in match_batch_allocate replicates the ORE_ROUNDING_SETTLEMENT_MAX = 1.00 kr policy band in SQL (reject overshoot only at >= 1 kr; absorb sub-krona diffs to 3740 with full 1510/2440 clear) instead of routing the batch through planInvoicePayment: the RPC is one atomic transaction and must stay self-contained; the band constants are documented as mirrors of lib/money.ts. The mark-paid route and invoice-detail button now accept partially_paid (settleInvoicePayment's CAS guard always did) so a stuck invoice can be closed; the remaining-aware dialog proposal is deliberately scoped to SEK + accrual + no ROT/RUT deduction: cash partial completion is server-blocked (cashPartialBlockReason), FX needs a payment-day conversion the pure proposal lacks, and a ROT/RUT remainder is Skatteverket's 1513 share settled by the payout flow. [2026-08-24] Assistant empty answer is a typed failure (EmptyModelAnswerError, 502 'Assistenten gav inget svar. Försök igen.'), one manual retry, NO auto-retry: the silent 200 with an empty answer was the 'Tänker then nothing' bug after the RIP-3 cutover (1500-token cap on /api/agent/ask), and auto-retry would double model spend while hiding the regression. Related deliberate request-shape change: anthropic-family's step-exhausted fallback now keeps tools with tool_choice none, because replaying a transcript containing tool_use/tool_result without declaring tools is rejected by the Messages API (the old shape 400ed every step-exhausted answer). general.help stays on the single-call runtime (founder decision); the fix raises headroom to 5400 tokens and surfaces the failure instead. [2026-08-24] Issue #1820 self-billed credit fix: creditConfirmNumber()/originalRef fall back invoice_number -> external_invoice_number (typed 400 INVOICE_CREDIT_NO_NUMBER if both null) instead of relaxing the DB numbering constraint or dropping the type-the-number confirm step; the confirm step stays (dropping it is a founder call). The invoice-date Forval chip surfaces in ALL editor modes, not only self-billed: the silent today-default exists in every mode and the chip line already carries the due date. In self-billed mode fakturadatum + mottagningsdatum render uncollapsed next to the external number (transcription fields, not defaults); the panel rows are hidden there because registering the same RHF field twice desyncs the inputs. The v1 credit route's existing id-slice fallback was left unchanged (public API behavior). [2026-08-24] No-IBAN reconnect pairing (issue #1709) uses only per-currency exactly-one-each-side elimination, deliberately WITHOUT name equality: ASPSPs reformat product names between consents, so requiring it would silently disable the fix for the banks that need it, while the one-per-currency guard already bounds a mis-pair to skipping rows whose account+date+amount+occurrence all collide. upsertFromPsd2 needed no change: its explicit reuse_cash_account_id promote path already covers a same-connection holder, so the fix only names the paired row from the callback. diff --git a/app/(dashboard)/invoices/[id]/page.tsx b/app/(dashboard)/invoices/[id]/page.tsx index 67cfe6e5..fafd80be 100644 --- a/app/(dashboard)/invoices/[id]/page.tsx +++ b/app/(dashboard)/invoices/[id]/page.tsx @@ -1540,7 +1540,9 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st {t('mark_as_sent')} )} - {(invoice.status === 'sent' || invoice.status === 'overdue') && isRealInvoice && !isCreditNote && ( + {/* partially_paid included (#1717): completes a stuck partial, e.g. + a sub-krona öresavrundning remaining, via the same dialog. */} + {(invoice.status === 'sent' || invoice.status === 'overdue' || invoice.status === 'partially_paid') && isRealInvoice && !isCreditNote && (