- 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.
154 lines
10 KiB
Markdown
154 lines
10 KiB
Markdown
---
|
||
status: Active
|
||
lastUpdated: 2026-07-10
|
||
applyTo:
|
||
- "Fuchs/Services/InvoiceDraft*"
|
||
- "Fuchs/Services/IInvoiceDraft*"
|
||
- "Fuchs/Services/ReminderDraft*"
|
||
- "Fuchs/Services/IReminderDraft*"
|
||
- "Fuchs/code/InvoiceDraftSession.cs"
|
||
- "Fuchs/code/InvoiceDraftCalculator.cs"
|
||
- "Fuchs/code/ReminderDraftSession.cs"
|
||
- "Fuchs/code/ReminderDraftCalculator.cs"
|
||
- "Fuchs/Notifications/DraftPreviewHub.cs"
|
||
- "Fuchs/Notifications/*DraftNotifier*"
|
||
- "Fuchs/Controllers/IntranetController.InvoiceDraft.cs"
|
||
- "Fuchs/Controllers/IntranetController.ReminderDraft.cs"
|
||
- "Fuchs/js/intranet/**"
|
||
relatedDecisions:
|
||
- "0006-backend-authoritative-draft-editing.md"
|
||
- "0007-targeted-draft-signalr-groups.md"
|
||
---
|
||
|
||
# Live draft editing (backend-authoritative invoice previews)
|
||
|
||
## Summary
|
||
While a back-office user edits an invoice draft, the authoritative state is held in
|
||
server memory, not in the browser. The browser posts single edits, the server mutates
|
||
the cached record, recomputes totals/VAT and re-validates, then pushes a "state changed"
|
||
signal so the browser re-fetches and re-renders. This makes the backend the single source
|
||
of truth (server-computed sums, consistency checks, in-place PDF preview, change history,
|
||
explicit discard), reversing the earlier stateless editor. Invoices were the pilot;
|
||
reminders now mirror the same design (see "Reminders" below).
|
||
|
||
## How it works
|
||
|
||
```
|
||
Open: Browser --POST inv/dopen {id | payload}--> server builds InvoiceDraftSession, caches it
|
||
Browser --SignalR JoinDraft(token)--> joins the draft's group; spinner while loading
|
||
Browser --POST inv/dstate {token}--> renders admin/new/req + server sums + validation
|
||
|
||
Edit: Browser --POST inv/dpatch {token, delta}--> mutate + recompute + validate + version++
|
||
Server --SignalR draftReady{token,version}--> Browser re-fetches inv/dstate, re-renders
|
||
|
||
Preview: Browser --POST inv/dpreview {token}--> PDF rendered straight from the cache (no upload)
|
||
Save: Browser --POST inv/dsave {token}--> flush cache->DB (RegisterInvoiceAsync) + EventService toast
|
||
History: Browser --POST inv/dhistory {token}--> change list -> "Änderungshistorie" dialog
|
||
Discard: Browser --POST inv/ddiscard {token}--> reload session from DB draft -> draftReady
|
||
Close: Browser --POST inv/dclose {token}--> session removed (+ LeaveDraft)
|
||
|
||
Expiry: Server (timer) --SignalR draftExpiring{token,secondsLeft}--> warn "bitte zwischenspeichern"
|
||
Server (evict) --SignalR draftClosed{token,reason}--> close the editor with a reason
|
||
```
|
||
|
||
- **Session** (`InvoiceDraftSession`) is a pure data holder: the editable payload as the
|
||
exact editor JSON (`admin` / `new` / `req` blocks with `items`), plus server-computed
|
||
`Sums`, `ValidationMessages`, `History`, `Version`, `Token`, `InvId`, `LastAccessUtc`.
|
||
- **Calculation** (`InvoiceDraftCalculator`, static/pure) ports the former client math:
|
||
`RecomputeLineValues` (net/VAT/service-net/service-VAT per line from raw quantity ×
|
||
price × VAT rate, the `quantChange`/`setVat` port), `RecomputeTotals`
|
||
(the `invSumUpdate`/`csms` aggregation + §13b reverse-charge), `RecomputePositions`
|
||
(numbers every line except heading/free-text lines continuously across the whole invoice —
|
||
mirroring the editor's `invSumUpdate`, so the editor and the PDF show identical `Pos.` numbers,
|
||
including after a reorder),
|
||
and `Validate` (email/address/items/VAT-rate/negative-total checks). Being pure, it is
|
||
exhaustively unit-tested. **The online editor performs no arithmetic of any kind** — not
|
||
header, footer, sums, totals, taxes, nor a single line's own net/VAT value: `$inv.quantChange`
|
||
and `$inv.setVat` only post the raw field the user changed (qty/price/vat rate), and
|
||
`$inv.invSumUpdate` only reassembles the row-contract array needed to post `req` — it computes
|
||
no totals, no VAT breakdown, and no service-refund note figures. All of those are rendered
|
||
exclusively from `dstate.sums` via `$inv.d.footer`.
|
||
- **Sanitisation & reorder.** Scalar text deltas (`title`/`email`/`address`/`provisionperiod`/
|
||
`provisionlocation`) and the section heading (`block.replace`) are stripped of the editor's
|
||
TinyMCE HTML (`<p>…</p>`, `<br>`) to plain text in `ApplyDelta` (`HtmlToPlain`) — the backend
|
||
is the single source of truth, so no HTML reaches the DB, the PDF or a reloaded draft. Section
|
||
drags post a `block.order` delta (`["id",…]`) that reorders `Req`; positions are then
|
||
renumbered and pushed back via the view state (`applyState`/`applyPositions`). The change
|
||
history records the **changed field** (e.g. the new heading text), never the whole block JSON.
|
||
The PDF (`FuchsPdf`) renders a heading row per block (`FdsInvoiceData.InvoiceBlocks`) and shows
|
||
every position's price (set members are priced like standalone lines; only `setonly` collapses
|
||
them), so the PDF preview mirrors the online editor.
|
||
- **Orchestration** (`InvoiceDraftEditService`, scoped) opens sessions (from a fresh
|
||
payload or by reloading a DB draft via `fds__getInvoice`, reshaped like
|
||
`BuildInvoiceRequestList`), applies deltas (`ApplyDelta`), builds the view-state DTO,
|
||
flushes to the DB by reusing `IInvoiceService.RegisterInvoiceAsync` (no new persistence
|
||
path), renders previews from a synthesised registration, and discards by reloading.
|
||
- **Cache** (`InvoiceDraftCache`, singleton) stores sessions by token with an idle sliding
|
||
TTL; `InvoiceDraftExpiryService` (a `BackgroundService`) warns before, and evicts after,
|
||
the TTL. TTL/warn-lead are configurable under `Fuchs:DraftEditing`.
|
||
- **Signals** (`DraftPreviewHub` at `/draftpreview` + `IDraftNotifier`) are targeted at the
|
||
editing browser via a group named after the session token: `draftReady`, `draftExpiring`,
|
||
`draftClosed`. Business success/failure still flows through `IEventService`/`NotificationHub`.
|
||
- **Frontend** (`$fis.draft` in `fis_main.js`, editor in `fis.inv_shared.js`) opens/joins,
|
||
posts one delta per change, shows a loading state whenever awaiting a signal, and offers
|
||
"Änderungen verwerfen" and "Änderungshistorie" menu actions. It computes nothing — no
|
||
header, footer, sums, totals, taxes, or per-line values.
|
||
|
||
## Key files
|
||
- `Fuchs/code/InvoiceDraftSession.cs` — session + `ChangeHistoryEntry` + `InvoiceDraftSums`.
|
||
- `Fuchs/code/InvoiceDraftCalculator.cs` — pure recompute + validate.
|
||
- `Fuchs/Services/InvoiceDraftCache.cs` / `IInvoiceDraftCache.cs` — in-memory store + TTL.
|
||
- `Fuchs/Services/InvoiceDraftEditService.cs` / `IInvoiceDraftService.cs` — orchestration + delta contract.
|
||
- `Fuchs/Services/InvoiceDraftExpiryService.cs` — idle warn/evict monitor.
|
||
- `Fuchs/Notifications/DraftPreviewHub.cs`, `DraftNotifier.cs`, `IDraftNotifier.cs` — targeted signals.
|
||
- `Fuchs/Controllers/IntranetController.InvoiceDraft.cs` — `inv/d*` endpoints.
|
||
- `Fuchs/js/intranet/fis_main.js`, `Fuchs/js/intranet/modules/fis.inv_shared.js` — client.
|
||
|
||
## Reminders (Zahlungserinnerung)
|
||
|
||
Reminders mirror the same backend-authoritative model with a reminder-shaped session. A
|
||
reminder chases a single invoiced amount, so the machinery is simpler than an invoice's:
|
||
there are no line-item blocks, VAT grouping or reordering — just recipient fields and the
|
||
amount pair.
|
||
|
||
- **Endpoints** are `rem/d*` (`dopen`/`dstate`/`dpatch`/`dpreview`/`dsave`/`dhistory`/`dclose`),
|
||
dispatched from `Do_Process_Reminder`. Finalise + email still runs through the existing
|
||
`rem/conf` (`HandleReminderConf`), exactly as invoices finalise through `req/sconf`.
|
||
- **Session** (`ReminderDraftSession`) holds the editor's `new` (subject / invoiceaddress /
|
||
invoiceemail / text / amount / amount_payed / CustomValues) and `rem` (invid / type /
|
||
invoiceid / invoicedate) blocks, plus server-computed `Sums` (`AmountTotal`, `AmountPayed`,
|
||
`AmountOpen`). It reuses the shared `ChangeHistoryEntry`; validation uses
|
||
`ReminderDraftValidationMessage`.
|
||
- **Calculation** (`ReminderDraftCalculator`, static/pure): `AmountOpen = AmountTotal − AmountPayed`,
|
||
plus email/address/subject/open-amount plausibility checks. Exhaustively unit-tested.
|
||
- **Deltas** (`ReminderDraftDelta`): scalar `email`/`address`/`subject`/`text` (HTML-sanitised via
|
||
the shared `InvoiceDraftEditService.HtmlToPlain`), the numeric `amount`/`amount_payed`
|
||
(normalised to an invariant decimal string), and `contact` (→ `CustomValues`).
|
||
- **Orchestration** (`ReminderDraftEditService`, scoped) flushes to the DB by reusing
|
||
`IReminderService.RegisterReminderAsync`, and renders previews from a synthesised
|
||
`ReminderRegistration` (including the single-invoice `invoices` row the reminder PDF table
|
||
renders) so a preview needs no DB round-trip. **Note:** `RegisterReminderAsync` is create-only
|
||
(there is no `fds__setReminder` update proc), so a re-saved reminder draft does not update the
|
||
prior DB row — the primary flow (preview → confirm) flushes once immediately before finalising.
|
||
- **Cache/expiry** (`ReminderDraftCache` singleton + `ReminderDraftExpiryService`) mirror the
|
||
invoice ones and share the same `Fuchs:DraftEditing` TTL config.
|
||
- **Signals** reuse the shared `DraftPreviewHub` + `IDraftNotifier` unchanged — the token-keyed
|
||
groups serve invoice and reminder drafts alike.
|
||
- **Frontend** (`$inv.rd` in `fis.inv_shared.js`) opens/joins on `rem/dopen`, posts one delta per
|
||
inline edit and per item-row amount change, renders the open-amount footer + validation from the
|
||
server state, and previews/finalises through `rem/dpreview` → `rem/dsave` → `rem/conf`. It shares
|
||
the invoice editor DOM; `$inv.d` and `$inv.rd` each key off their own token, so the shared inline
|
||
editor safely no-ops for whichever mode is inactive.
|
||
|
||
### Reminder key files
|
||
- `Fuchs/code/ReminderDraftSession.cs` — session + `ReminderDraftSums` + `ReminderDraftValidationMessage`.
|
||
- `Fuchs/code/ReminderDraftCalculator.cs` — pure open-amount recompute + validate.
|
||
- `Fuchs/Services/ReminderDraftCache.cs` / `IReminderDraftCache.cs` — in-memory store + TTL.
|
||
- `Fuchs/Services/ReminderDraftEditService.cs` / `IReminderDraftService.cs` — orchestration + delta contract.
|
||
- `Fuchs/Services/ReminderDraftExpiryService.cs` — idle warn/evict monitor.
|
||
- `Fuchs/Controllers/IntranetController.ReminderDraft.cs` — `rem/d*` endpoints.
|
||
|
||
## Related decisions
|
||
- [0006 — Backend-authoritative draft editing](../Decisions/0006-backend-authoritative-draft-editing.md)
|
||
- [0007 — Targeted draft SignalR groups](../Decisions/0007-targeted-draft-signalr-groups.md)
|