Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0006-backend-authoritative-draft-editing.md
Stefan 49e3ed2673 Add unit tests for Fuchs_DataService and related components
- 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.
2026-07-16 13:34:23 +02:00

5.1 KiB
Raw Permalink Blame History

status, date, applyTo, supersededBy
status date applyTo supersededBy
Accepted 2026-07-10
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/**

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 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.RegisterInvoiceAsyncno 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.