- Introduced comprehensive unit tests for the Fuchs_DataService library, covering DATEV header formatting, CSV/XML generation, and FdsMfrClient construction. - Implemented tests for FdsMfr.UpdateNeed parsing and FdsShared utility helpers, ensuring correct functionality and stability. - Added tests for FdsConfig and FdsMfrClient to validate configuration resolution and client construction. Document decisions on backend-authoritative invoice and reminder handling - Created ADR 0008 to clarify that all invoice types and reminder stages are backend-authoritative during drafting and previewing. - Established that all calculations and settings must be processed server-side, ensuring consistency between online editor and PDF outputs. Define irreversible mutations for set-price modes in invoices - Documented ADR 0009 to specify that the "Set mit Preis" and "Nur Set mit Preis" operations are irreversible mutations affecting service request blocks. - Clarified that these operations are not display toggles but actual data changes, ensuring clear expectations for invoice handling. Transition MFR ERP sync to in-process execution within the web app - Created ADR 0010 to outline the migration of Fuchs_DataService from a standalone service to an in-process library within the Fuchs web application. - Updated configuration and logging management to be handled by the host application, streamlining the sync process. Add publish profile and periodic hosted service for job scheduling - Introduced a publish profile for deployment to a specified folder. - Implemented PeriodicHostedService to manage multiple independent jobs, including the MFR ERP sync, with configurable execution intervals. Add dotnet-tools.json for EF Core CLI tools - Included dotnet-tools.json to manage the version of dotnet-ef for Entity Framework Core migrations and commands.
91 lines
5.1 KiB
Markdown
91 lines
5.1 KiB
Markdown
---
|
||
status: Accepted
|
||
date: 2026-07-10
|
||
applyTo:
|
||
- "Fuchs/Services/InvoiceDraft*"
|
||
- "Fuchs/Services/IInvoiceDraft*"
|
||
- "Fuchs/code/InvoiceDraftSession.cs"
|
||
- "Fuchs/code/InvoiceDraftCalculator.cs"
|
||
- "Fuchs/Notifications/DraftPreviewHub.cs"
|
||
- "Fuchs/Notifications/*DraftNotifier*"
|
||
- "Fuchs/Controllers/IntranetController.InvoiceDraft.cs"
|
||
- "Fuchs/js/intranet/**"
|
||
supersededBy: ""
|
||
---
|
||
|
||
# 0006 — Invoice draft editing is backend-authoritative over an in-memory cache
|
||
|
||
## Context
|
||
The invoice editor was deliberately **stateless**: the browser held the working
|
||
model, computed totals/VAT client-side (`invSumUpdate` in `fis.inv_shared.js`) and
|
||
re-posted the whole `invc` JSON on every preview/save. `EVAL_live_invoice_editing.md`
|
||
(2026) recommended keeping it that way and **against** a server-cached, SignalR-driven
|
||
model, because the real-time/co-editing benefits were weak for a single back-office
|
||
editor.
|
||
|
||
The product owner has since decided the trade-off differently and prioritised a
|
||
**single source of truth in the backend** with server-computed sums, server-side
|
||
plausibility/consistency checks, in-place PDF preview without re-upload, an automatic
|
||
change history, and an explicit discard. This decision records that reversal and the
|
||
architecture chosen to implement it.
|
||
|
||
## Decision
|
||
While a user edits an invoice draft, the authoritative state lives **server-side** in
|
||
an in-memory `InvoiceDraftSession` (`Fuchs/code/InvoiceDraftSession.cs`), held by the
|
||
singleton `IInvoiceDraftCache` and orchestrated by the scoped `IInvoiceDraftService`
|
||
(`InvoiceDraftEditService`). The browser is a pure view/input layer.
|
||
|
||
- **Truth & calculation on the server.** `InvoiceDraftCalculator` is the pure,
|
||
unit-tested port of the former client-side math (`quantChange` + `setVat` +
|
||
`invSumUpdate`), including per-line net/VAT/service-value multiplication
|
||
(`RecomputeLineValues`), the §13b reverse-charge rule and VAT-per-rate grouping. The
|
||
browser performs **no arithmetic whatsoever** — not even a single line's
|
||
`net = qty × price` — it only renders the server's `req`/`sums`.
|
||
- **Commands are ordinary POSTs; signals are SignalR.** The editor posts single edits
|
||
to `inv/dpatch` (and `dopen`/`dstate`/`dpreview`/`dsave`/`dhistory`/`ddiscard`/`dclose`).
|
||
The server mutates the session, recomputes, validates, bumps a version, and pings the
|
||
editing browser (`draftReady`) to re-fetch `inv/dstate`. See
|
||
[0007](0007-targeted-draft-signalr-groups.md) for the targeted-signal transport.
|
||
- **Cache-only until Zwischenspeichern/Finalise.** Opening builds the session (from a
|
||
brand-new payload or by reloading a DB draft); edits touch only the cache. `dsave`
|
||
flushes the session to the DB by reusing the existing
|
||
`IInvoiceService.RegisterInvoiceAsync` — **no new persistence path** — and reports
|
||
success/failure through the existing `IEventService` (ADR 0001). Finalise continues
|
||
through `req/sconf`.
|
||
- **Preview from cache.** `inv/dpreview` renders the draft PDF straight from the session
|
||
(synthesised registration), with no client upload.
|
||
- **Automatic change history.** Every applied patch appends a `ChangeHistoryEntry`
|
||
(cache-only, never persisted); `inv/dhistory` exposes it for the "Änderungshistorie"
|
||
dialog.
|
||
- **Idle lifecycle with user warning.** `InvoiceDraftExpiryService` warns the editing
|
||
browser before a session's idle TTL lapses (`draftExpiring`) and, on eviction, tells
|
||
it to close the editor with a reason (`draftClosed`). TTL and warning lead are under
|
||
`Fuchs:DraftEditing`.
|
||
|
||
## Consequences
|
||
- The server is now **stateful for in-progress drafts**. This is acceptable for a
|
||
single-instance deployment; **scale-out requires sticky sessions or a distributed
|
||
cache/SignalR backplane** — none exist today, so this is a documented limitation, not
|
||
a silent assumption.
|
||
- New editor interactions must be modelled as a **delta** applied server-side (add a
|
||
case in `InvoiceDraftEditService.ApplyDelta` + calculator handling), never as a new
|
||
client-side calculation. Do not reintroduce client-side totals.
|
||
- `FdsInvoiceData` stays a pure data holder; `InvoiceDraftSession` is likewise a data
|
||
holder, with all logic in the service/calculator (mirrors the existing service split).
|
||
- Reminders (Mahnungen) are intended to follow the identical pattern as a second phase;
|
||
this decision covers invoices first (the pilot) and applies to the reminder mirror
|
||
when built.
|
||
- `EVAL_live_invoice_editing.md` and `INVOICE_LIFECYCLE.md` §4/§10 (the "stateless
|
||
editor" invariant) are superseded by this decision for the draft-editing flow and have
|
||
been annotated accordingly.
|
||
|
||
## Alternatives considered
|
||
- **Keep the stateless editor** (the prior recommendation): rejected by the product
|
||
owner in favour of a backend single source of truth.
|
||
- **Full bidirectional SignalR hub for commands too**: rejected — edits as POSTs reuse
|
||
the existing controller/auth pattern and avoid a command reconnect/replay protocol; the
|
||
hub carries only coordination signals.
|
||
- **Write-through to the DB on every edit**: rejected — conflicts with the
|
||
"Zwischenspeichern = persist the cache" semantics and adds DB load; the cache is the
|
||
truth until an explicit save/finalise.
|