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
@@ -426,8 +426,20 @@ public class InvoiceService : IInvoiceService
|
||||
{
|
||||
byte[] ba;
|
||||
try { ba = await RenderInvoicePdfBytesAsync(invoice, draft); }
|
||||
catch (Exception ex) { _logger.LogError(ex, "StoreInvoiceDocumentFileAsync render failed for {Id}", invoice.Id); ba = Array.Empty<byte>(); }
|
||||
if (ba.Length == 0) return Array.Empty<byte>();
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "StoreInvoiceDocumentFileAsync render failed for {Id}", invoice.Id);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
if (ba.Length == 0)
|
||||
{
|
||||
// Distinct from the catch above: the renderer returned normally but with no bytes, and
|
||||
// callers only see the same empty array either way.
|
||||
_logger.LogError(
|
||||
"StoreInvoiceDocumentFileAsync: render for {Id} completed without an exception but produced 0 bytes, draft={Draft}, user={User}",
|
||||
invoice.Id, draft, userAccountId);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
var pl = new List<SqlParameter>
|
||||
{
|
||||
@@ -435,10 +447,22 @@ public class InvoiceService : IInvoiceService
|
||||
SQL_VarChar("@Id", invoice.Id),
|
||||
new("@file", SqlDbType.VarBinary) { Value = ba }
|
||||
};
|
||||
bool r = await setSQLValue_async(
|
||||
var setResult = await setSQLValue_async_result(
|
||||
"EXECUTE [dbo].[fds__setInvoiceFile] @Id, @file;",
|
||||
Conn, pl, Security: dbSec, options: new FIS_SQLOptions());
|
||||
if (!r) return Array.Empty<byte>();
|
||||
if (!setResult.success) return Array.Empty<byte>();
|
||||
if (setResult.AffectedRows == 0)
|
||||
{
|
||||
// fds__setInvoiceFile writes WHERE [file] IS NULL — 0 rows affected without an
|
||||
// exception means the UPDATE simply didn't match (already stored, wrong id, or the
|
||||
// invoice vanished under us), not a SQL failure setSQLValue_async_result would report.
|
||||
// ExecuteNonQueryAsync returning rows-affected=0 is not an error by itself, so this was
|
||||
// previously indistinguishable from success — see ADR 0025.
|
||||
_logger.LogError(
|
||||
"StoreInvoiceDocumentFileAsync: fds__setInvoiceFile updated 0 rows for {Id} (file already stored, or invoice missing), user={User}",
|
||||
invoice.Id, userAccountId);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
string fileName = invoice.InvoiceRegistration?.getString("DocumentName")
|
||||
.ne($"Rechnung_{invoice.Id}.pdf") ?? $"Rechnung_{invoice.Id}.pdf";
|
||||
|
||||
@@ -139,8 +139,18 @@ public class ReminderService : IReminderService
|
||||
{
|
||||
byte[] ba;
|
||||
try { ba = await RenderReminderPdfBytesAsync(reminder, draft); }
|
||||
catch (Exception ex) { _logger.LogError(ex, "StoreReminderDocumentFileAsync render failed for {Id}", reminder.Id); ba = Array.Empty<byte>(); }
|
||||
if (ba.Length == 0) return Array.Empty<byte>();
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "StoreReminderDocumentFileAsync render failed for {Id}", reminder.Id);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
if (ba.Length == 0)
|
||||
{
|
||||
_logger.LogError(
|
||||
"StoreReminderDocumentFileAsync: render for {Id} completed without an exception but produced 0 bytes, draft={Draft}, user={User}",
|
||||
reminder.Id, draft, userAccountId);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
var pl = new List<SqlParameter>
|
||||
{
|
||||
@@ -148,10 +158,19 @@ public class ReminderService : IReminderService
|
||||
SQL_VarChar("@authuser", userAccountId),
|
||||
new("@file", SqlDbType.VarBinary) { Value = ba }
|
||||
};
|
||||
bool r = await setSQLValue_async(
|
||||
var setResult = await setSQLValue_async_result(
|
||||
"EXECUTE [dbo].[fds__setReminderFile] @Id, @file;",
|
||||
Conn, pl, Security: dbSec, options: new FIS_SQLOptions());
|
||||
if (!r) return Array.Empty<byte>();
|
||||
if (!setResult.success) return Array.Empty<byte>();
|
||||
if (setResult.AffectedRows == 0)
|
||||
{
|
||||
// fds__setReminderFile writes WHERE [file] IS NULL — see the matching comment/ADR 0025
|
||||
// note in InvoiceService.StoreInvoiceDocumentFileAsync.
|
||||
_logger.LogError(
|
||||
"StoreReminderDocumentFileAsync: fds__setReminderFile updated 0 rows for {Id} (file already stored, or reminder missing), user={User}",
|
||||
reminder.Id, userAccountId);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
string fileName = reminder.ReminderRegistration?.getString("DocumentName")
|
||||
.ne($"Zahlungserinnerung_{reminder.Id}.pdf") ?? $"Zahlungserinnerung_{reminder.Id}.pdf";
|
||||
|
||||
Reference in New Issue
Block a user