feat: Enhance invoice file backup procedure to include JSON snapshots
Playwright Tests / test (push) Canceled after 0s

- Updated the stored procedure `fds__admin_backupAndClearInvoiceFile.sql` to back up not only the invoice file but also JSON snapshots of related data (invoices, items, details, service requests) to ensure consistency and traceability.
- Modified the table structure of `fds__invoice_file_backups` to accommodate new JSON columns for storing these snapshots.
- Added an ADR (0030) document to outline the decision and rationale behind these changes, emphasizing the importance of maintaining a consistent set of data alongside the backed-up file.
This commit is contained in:
Stefan
2026-09-17 22:39:01 +02:00
parent 249a945f5e
commit 9569c1b65e
14 changed files with 187 additions and 23 deletions
+8
View File
@@ -22,6 +22,14 @@ rest of the Administration menu (`fds_sys > 4`), and reachable both from the
menu (Administration → Spezialfunktionen) and by opening the URL
`/specialties` directly.
The menu entry is deliberately hidden: it only appears when the Administration
dropdown is opened with **Shift** held (Shift+click, or Shift+Enter/Space on
the focused toggle); a plain open hides it again
(`$fis.bindShiftOnlyMenuItems` in `fis_main_menu.js`, driven by the item's
`lclass: 'shift-only'`). This keeps the maintenance actions out of casual
reach without adding another authorization level — the item is still part of
the menu array, so the `/specialties` deep link is unaffected.
## How it works
- **Module load** follows the standard contract (see
+4 -1
View File
@@ -32,7 +32,10 @@ Access is restricted to users whose `fds_sys` module authorization is greater th
does it push a single "Administration" dropdown entry into `$ocms.ocmsmenu` — containing this
module's "Systemüberwachung" (`init:admin`) item alongside `usr`'s "Benutzerverwaltung"
(`init:usr`) item — and re-render `#mainmenu`. Users below the threshold never see the
dropdown and never fetch either module's script.
dropdown and never fetch either module's script. Within the dropdown, "Systemüberwachung"
(like "Spezialfunktionen") is a `shift-only` item: it is listed only when the dropdown is
opened with **Shift** held (`$fis.bindShiftOnlyMenuItems`); the `/admin` deep link is
unaffected. This is a convenience/accident guard, not an authorization level.
2. **Server-side gate** — every `/do/admin/*` endpoint resolves
`fis_getModuleAuth('fds_sys', authuser)` and returns 401 unless it is > 4. The only
exception is `admin/auth`, which returns `{ manage: 0 }` for unauthorized users so the
@@ -0,0 +1,69 @@
---
status: Accepted
date: 2026-09-17
applyTo:
- "Fuchs_Database/dbo/Stored Procedures/fds__admin_backupAndClearInvoiceFile.sql"
- "Fuchs_Database/dbo/Tables/fds__invoice_file_backups.sql"
supersededBy: ""
---
# 0030 — Invoice-file backup carries a JSON snapshot of the invoice data
## Context
ADR 0027 introduced `[dbo].[fds__invoice_file_backups]`: one row per
admin backup-and-clear action holding the invoice's `[file]` bytes before
they are cleared for regeneration (ADR 0025). The backup held only the
rendered PDF. The data it was rendered from — the `fds__invoices` row, its
`fds__invoice_items` rows, its `fds__invoice_details` row and its
`fds__invoice_servicerequests` link rows — lives on in
the live tables and keeps changing (payment status, modification stamps,
later edits). A backed-up file could therefore no longer be matched against
the data state it was produced from, which is exactly the question the
backup exists to answer when a regenerated document differs from the
original.
## Decision
- Each backup row additionally stores four JSON snapshots, written in the
same statement and transaction as the file copy:
- `InvoiceJson` — the `fds__invoices` row (`FOR JSON PATH,
WITHOUT_ARRAY_WRAPPER`), **without** `[file]`, which is already stored as
bytes in the row and would otherwise be base64-duplicated;
- `InvoiceItemsJson` — all `fds__invoice_items` rows for the invoice as a
JSON array, ordered by `SortOrder`, `Position`, `Id`;
- `InvoiceDetailsJson` — the `fds__invoice_details` row, or `NULL` if the
invoice has none;
- `InvoiceServiceRequestsJson` — all `fds__invoice_servicerequests` rows
for the invoice as a JSON array, ordered by `SortOrder`, `Id`. Not part
of the rendered document, but part of the invoice's data set.
- The snapshots are JSON columns on the **same** backup row rather than
four mirror tables: one row is one consistent set (file + data), the
atomicity guarantee of ADR 0027 covers it without extra keys, and adding a
column to an invoice table does not require touching the backup schema.
The item/detail/service-request snapshots use `SELECT *`, so new columns appear
automatically; the invoice snapshot lists its columns explicitly only to
exclude `[file]` — a new `fds__invoices` column must be added to that
list.
- Restore is manual (`OPENJSON` in SSMS); no app-side reader is added. The
snapshots are forensic evidence, not a rollback mechanism.
## Consequences
- Backup rows grow by the size of the invoice's data (small next to the
PDF). No retention policy is added — same standing as ADR 0027.
- `INCLUDE_NULL_VALUES` is used so a `NULL` in the snapshot is
distinguishable from a column that did not exist at backup time.
- Adding a column to `fds__invoices` requires updating the explicit column
list in the proc; `fds__invoice_items` / `fds__invoice_details` /
`fds__invoice_servicerequests` need no change.
## Alternatives considered
- **Mirror tables** (`fds__invoice_backups`, `fds__invoice_items_backups`,
…) keyed by backup `uid`: typed and queryable, but four schemas that
drift with every invoice-table change and four inserts to keep atomic.
Rejected for maintenance cost; the snapshots are read rarely and by hand.
- **Snapshot only the invoice header**: the items are what the PDF body is
rendered from; leaving them out would not answer "what did this file
contain and why".