Playwright Tests / test (push) Canceled after 0s
- Added MailerAttachmentPolicy for validating email attachments against server rules. - Introduced fis.specialties.js for managing hidden maintenance actions via an admin interface. - Created fis.specialties.scss for styling the specialties management UI. - Added German localization for specialties management in fis.specialties_txt_de.js. - Developed CSS for specialties management in fis.specialties.css. - Implemented server-side stored procedure fds__admin_backupAndClearInvoiceFile for backing up and clearing invoice files. - Created fds__invoice_file_backups table to store backup records of invoice files.
115 lines
6.3 KiB
Markdown
115 lines
6.3 KiB
Markdown
---
|
|
status: Accepted
|
|
date: 2026-09-17
|
|
applyTo:
|
|
- "Fuchs/Controllers/IntranetController.Admin.cs"
|
|
- "Fuchs/Services/IInvoiceFileBackupService.cs"
|
|
- "Fuchs/Services/InvoiceFileBackupService.cs"
|
|
- "Fuchs/Services/IBlobStorageService.cs"
|
|
- "Fuchs/Services/AzureBlobStorageService.cs"
|
|
- "Fuchs_Database/dbo/Stored Procedures/fds__admin_backupAndClearInvoiceFile.sql"
|
|
- "Fuchs_Database/dbo/Tables/fds__invoice_file_backups.sql"
|
|
supersededBy: ""
|
|
---
|
|
|
|
# 0027 — Admin invoice-file backup-and-clear action
|
|
|
|
## Context
|
|
|
|
Support occasionally needs to force a finalised invoice's PDF to be regenerated
|
|
(e.g. it was rendered with wrong data before a fix, or is otherwise corrupt).
|
|
Since [0025](0025-invoice-document-regenerate-on-miss.md), clearing
|
|
`fds__invoices.[file]` on a finalised, non-external invoice is enough to make
|
|
the next `req/idoc`/`inv/rdoc` read regenerate and re-persist it.
|
|
|
|
A manual maintenance proc for this already exists,
|
|
`[dbo].[_BackupAndClearInvoiceFile]`, but its own header comment states it is
|
|
"Manual-only maintenance utility. Never called from application code", and
|
|
0025's "Alternatives considered" explicitly rejected relying on it from the
|
|
app, partly because it leaves no application log entry. We still want this
|
|
capability reachable from the app (a hidden, admin-gated action) rather than
|
|
requiring direct SSMS access, without contradicting that standing decision.
|
|
|
|
Additionally, `IBlobStorageService` archives invoice PDFs to Azure Blob
|
|
Storage under a fixed per-invoice blob name (`{invoiceId}/{fileName}`) and
|
|
always uploads with `overwrite: true`. If a cleared invoice's document is
|
|
later regenerated (0025), `StoreInvoiceDocumentFileAsync` re-uploads under
|
|
that same blob name — silently overwriting the very copy this action is
|
|
meant to preserve as a backup, unless it is moved first.
|
|
|
|
## Decision
|
|
|
|
- A **new** proc, `[dbo].[fds__admin_backupAndClearInvoiceFile]`, is added
|
|
rather than calling `_BackupAndClearInvoiceFile`. It copies the invoice's
|
|
current `[file]` bytes into a new table, `[dbo].[fds__invoice_file_backups]`
|
|
(one row per backup, not a timestamp-named ad-hoc table per call), then
|
|
clears `[file]` in the same transaction (`XACT_ABORT` + TRY/CATCH with
|
|
rollback and re-raise, so a clear can never commit without its backup row).
|
|
It logs through the normal
|
|
`fds__admin_activity` insert (matching `fds__setInvoiceFile`'s pattern) and
|
|
every call is also logged through the app's `ILogger`/DB-log-sink pipeline
|
|
(ADR 0024) — unlike the manual proc, this path is fully auditable.
|
|
`_BackupAndClearInvoiceFile` is left completely untouched as the manual
|
|
break-glass tool for cases the app can't reach (e.g. a corrupt row the
|
|
admin UI can't load).
|
|
- The action applies 0025's regeneration precondition **before** touching
|
|
anything: only a **finalised, non-external** invoice qualifies. Both the
|
|
service (`IsDraft`/`IsExternal`) and the proc (`[DateFinalized] IS NOT NULL
|
|
AND ISNULL([IsExternal],0) = 0`) enforce it. Clearing a draft's `[file]`
|
|
would let the next read persist a draft render through
|
|
`fds__setInvoiceFile` (`WHERE [file] IS NULL`), which then blocks the real
|
|
final PDF forever — the exact defect 0025 calls load-bearing; an external
|
|
(MFR) document has no template and would just become a permanent 404.
|
|
- `IBlobStorageService` gains `CopyInvoicePdfToBackupAsync`: a
|
|
**server-side copy** (`StartCopyFromUriAsync`, bytes never pass through
|
|
the app) of the existing blob to a timestamped backup name in the same
|
|
container — the original blob is **kept**, not renamed. A private source in
|
|
the same storage account is authorised by the request's own Shared Key
|
|
credential, so no SAS is generated. The copy runs *before* the DB clear,
|
|
so the archived original is safe before any regenerate-on-read (ADR 0025)
|
|
could overwrite the original name; because the original stays in place, a
|
|
DB error or no-op afterwards leaves the canonical archive exactly as it
|
|
was (with a harmless spare copy). Keeping the original also removes the
|
|
half-state a copy-then-delete would have ("copied but couldn't delete").
|
|
Best-effort: a copy failure is logged and counted
|
|
(`fuchs.blobstorage.backupcopies.failed`), the DB backup row stays the
|
|
authoritative copy, and the DB step still runs.
|
|
- The action is exposed as a `Do_Process_Admin` case,
|
|
`admin/backupclearinvoicefile/<invoiceId>`, gated by the existing
|
|
`fds_sys > 4` check and restricted to `POST` (a GET must never trigger it,
|
|
since GETs can be issued by crawlers/prefetchers without user intent). It
|
|
is not part of any regular workflow UI; its only frontend is a card on the
|
|
admin-only "Specialties" page (`/specialties`, see
|
|
`Docs/Concepts/admin-specialties.md`).
|
|
|
|
## Consequences
|
|
|
|
- Two invoice-file "backup and clear" code paths now exist for different
|
|
audiences: `_BackupAndClearInvoiceFile` (manual, SSMS, no app log) and
|
|
`fds__admin_backupAndClearInvoiceFile` (app-triggered, fully logged). Keep
|
|
them separate — do not merge them, and do not point the manual proc's
|
|
printed recovery links at the new admin action.
|
|
- `fds__invoice_file_backups` accumulates one row per backup with no
|
|
automatic retention/cleanup; if this ever becomes a storage concern, that
|
|
is a separate decision.
|
|
- Blob backup-copy is best-effort: if it fails (or storage is
|
|
disabled/unconfigured), the invoice's DB row is still backed up and
|
|
cleared — the archived blob is simply left as-is (and will be overwritten
|
|
by the next regeneration). The DB backup table is the only *guaranteed*
|
|
copy this action makes.
|
|
- The blob container accumulates one `backup_<timestamp>_…` copy per action
|
|
next to the current blob; like `fds__invoice_file_backups`, no retention
|
|
is applied.
|
|
|
|
## Alternatives considered
|
|
|
|
- **Call `_BackupAndClearInvoiceFile` directly from the app.** Rejected: its
|
|
own header comment and ADR 0025 both state it must never be
|
|
application-triggered, and it produces no application log entry — using it
|
|
here would need updating that comment and (since decisions are immutable)
|
|
superseding 0025's "Alternatives considered" note, for no real benefit over
|
|
a small dedicated proc.
|
|
- **Skip the blob rename and let the next regeneration overwrite the archived
|
|
blob.** Rejected: the user asked for the blob copy to also survive as a
|
|
backup, and a silent overwrite on next read would defeat that.
|