fix: invoice/reminder PDF preview 404s and their unlogged root causes
An invoice's finalisation-time PDF render/store (fds__setInvoiceFile) can fail silently; the row stays finalised with [file] = NULL and inv/rdoc (the invoice list's preview icon) 404'd forever with no trace in any log. - inv/rdoc and rem/idoc now regenerate and persist a missing document on read, mirroring the legacy VB "idoc" fallback (Fuchs_Database ADR 0025), gated to finalised + non-external invoices only — a draft render must never be persisted, since fds__setInvoiceFile/fds__setReminderFile only write WHERE [file] IS NULL and finalisation would then silently no-op. - StoreInvoiceDocumentFileAsync/StoreReminderDocumentFileAsync now log every distinct failure (render threw, render produced 0 bytes, DB write threw, DB write matched 0 rows) instead of collapsing them into a single silent empty-array return. The 0-rows case needed switching from setSQLValue_async (reports success once the command completes, whatever the row count) to setSQLValue_async_result, whose AffectedRows makes a no-op UPDATE distinguishable from an actual write for the first time. - HandleRequestIdoc/HandleReminderIdoc no longer treat StoreInvoiceDocumentFileAsync's Array.Empty<byte>() failure signal as success (a bare `ct == null` check never caught it), which would have served a 0-byte "PDF". - rdoc/HandleRequestIdoc reject a missing 'id' form field instead of doing wasted work with an empty id. - _BackupAndClearInvoiceFile.sql is annotated as a manual-only maintenance utility, never called from app code, so it's clear why running it leaves no application log trail. - Client-side: $inv.pdfPreviewTimeout was 120 (ms) instead of 120000 — the preview dialog was timing out client-side after 120ms on every call. - Fixed a genuine process-wide test flake: PdfPipelineTests and ERechnungMapperTests both install PdfSharp's global font resolver, and whichever ran first would win, making the other's tests fail deterministically once eRechnungLib's SystemFontResolver got there first. FuchsPdf.EnsureFontResolver is now also race-safe under concurrent first use (a check-then-set without a lock). Also removed the InvoiceService.PublishAsync failure logging added mid-session after code review found it duplicated every call site's own, more detailed logging against ADR 0024's single-write-path rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b5613bee0d
commit
99bf5337f8
@@ -0,0 +1,78 @@
|
||||
---
|
||||
status: Accepted
|
||||
date: 2026-09-16
|
||||
applyTo:
|
||||
- "Fuchs/Controllers/IntranetController.Invoices*.cs"
|
||||
- "Fuchs/Controllers/IntranetController.Requests.cs"
|
||||
- "Fuchs/Services/InvoiceService.cs"
|
||||
- "Fuchs_Database/dbo/Stored Procedures/fds__getReportDocument.sql"
|
||||
- "Fuchs_Database/dbo/Stored Procedures/fds__setInvoiceFile.sql"
|
||||
supersededBy: ""
|
||||
---
|
||||
|
||||
# 0025 — Invoice document endpoints regenerate a missing final PDF on read
|
||||
|
||||
## Context
|
||||
|
||||
A finalised invoice's PDF is rendered exactly once, at finalisation
|
||||
(`req/sconf` → `StoreInvoiceDocumentFileAsync` → `fds__setInvoiceFile`), and
|
||||
stored in `fds__invoices.[file]` (primary) plus blob storage (archive). If that
|
||||
single write fails — observed on 2026-09-16 for `W7VAL`/`R2026-0258`, where the
|
||||
`fds__setInvoiceFile` call timed out under DB load — the invoice is left
|
||||
finalised with `[file] = NULL` and nothing ever retries.
|
||||
|
||||
The read endpoints then disagreed: `req/idoc` regenerated a missing file
|
||||
(a fallback carried over from the legacy VB `fuchs_ocms_intranet_requests.vb`
|
||||
"idoc" case), while `inv/rdoc` — the endpoint the invoice list's preview icon
|
||||
actually calls — read `fds__getReportDocument` and returned a permanent 404.
|
||||
|
||||
## Decision
|
||||
|
||||
Both invoice document read endpoints (`req/idoc`, `inv/rdoc`) regenerate and
|
||||
persist the PDF via `StoreInvoiceDocumentFileAsync` when no stored bytes exist,
|
||||
**only** when the invoice is
|
||||
|
||||
- **finalised** (`IsFinal`, i.e. `!FdsInvoiceData.IsDraft`), and
|
||||
- **not external** (`IsExternal` false — an MFR-sourced document has no
|
||||
template to regenerate from; a missing MFR file stays a 404).
|
||||
|
||||
A read is therefore allowed to write `[file]`, upload the blob and emit
|
||||
`InvoiceFileCreated` — the same side effects finalisation would have had.
|
||||
|
||||
Every failure inside `StoreInvoiceDocumentFileAsync`/`StoreReminderDocumentFileAsync`
|
||||
(render threw, render produced no bytes, DB write threw, DB write matched 0
|
||||
rows) is logged at Error level individually, because the method signals all
|
||||
four to callers with the same empty array. The 0-rows case needed switching
|
||||
from `setSQLValue_async` (reports success once `ExecuteNonQueryAsync`
|
||||
completes without throwing, regardless of rows affected) to
|
||||
`setSQLValue_async_result`, whose `AffectedRows` makes a no-op `UPDATE ...
|
||||
WHERE [file] IS NULL` distinguishable from an actual write — this is what
|
||||
made the original incident's failure invisible to every log.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Drafts are never persisted by a read.** `fds__setInvoiceFile` writes
|
||||
`WHERE [file] IS NULL` and `fds__setInvoiceFinal` never clears `[file]`; a
|
||||
draft render stored early would make the later final write a no-op and the
|
||||
invoice would carry the watermarked draft PDF forever. The `!IsDraft` gate is
|
||||
load-bearing — do not relax it.
|
||||
- The regenerated PDF is rendered from the invoice's *current* data. That is
|
||||
acceptable because a finalised invoice is immutable in the editor; it is not
|
||||
acceptable for anything mutable.
|
||||
- A read on a cold miss now costs a render plus a DB write, so it is exposed
|
||||
to the same `fds__setInvoiceFile` timeout that caused the miss. The client
|
||||
preview timeout (`$inv.pdfPreviewTimeout`) must stay generous (120 s).
|
||||
- `inv/rdoc` still serves external (MFR) documents purely from
|
||||
`fds__getReportDocument`; regeneration never applies to them.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Retry at finalisation instead of on read.** Would cover the store failure
|
||||
but not an already-damaged row; regeneration on read heals both and matches
|
||||
the behaviour `req/idoc` already had.
|
||||
- **Regenerate drafts too, to mirror `req/idoc`'s render path.** `req/idoc`
|
||||
*renders* a draft on the fly without storing it; persisting a draft through
|
||||
`fds__setInvoiceFile` is what causes the permanent-draft defect above.
|
||||
- **Return 404 and rely on the `_BackupAndClearInvoiceFile` recovery links.**
|
||||
That proc is a manual-only maintenance tool, and the `inv/rdoc?create=1`
|
||||
link it prints was never implemented on that endpoint.
|
||||
Reference in New Issue
Block a user