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>
3.9 KiB
status, date, applyTo, supersededBy
| status | date | applyTo | supersededBy | |||||
|---|---|---|---|---|---|---|---|---|
| Accepted | 2026-09-16 |
|
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 (
IsExternalfalse — 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__setInvoiceFilewritesWHERE [file] IS NULLandfds__setInvoiceFinalnever 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!IsDraftgate 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__setInvoiceFiletimeout that caused the miss. The client preview timeout ($inv.pdfPreviewTimeout) must stay generous (120 s). inv/rdocstill serves external (MFR) documents purely fromfds__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/idocalready had. - Regenerate drafts too, to mirror
req/idoc's render path.req/idocrenders a draft on the fly without storing it; persisting a draft throughfds__setInvoiceFileis what causes the permanent-draft defect above. - Return 404 and rely on the
_BackupAndClearInvoiceFilerecovery links. That proc is a manual-only maintenance tool, and theinv/rdoc?create=1link it prints was never implemented on that endpoint.