fix(drafts): serialize draft session flushes and register reminders via fds__setReminder
Playwright Tests / test (push) Canceled after 0s

Make invoice/reminder live-draft edits concurrency-safe with an explicit
DraftFlushResult, add the fds__setReminder stored procedure, and harden the
date helper functions. Includes concurrency and SQL contract tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Stefan
2026-10-01 13:57:40 +02:00
co-authored by Claude Fable 5.1
parent d0002ccdad
commit 546648533b
21 changed files with 813 additions and 140 deletions
@@ -107,16 +107,15 @@ public partial class IntranetController
{
if (!HasForm("token")) return BadRequest400();
string token = Form("token");
var before = _invoiceDrafts.Get(token);
if (before == null) return DraftGone();
bool existed = !string.IsNullOrEmpty(before.InvId);
var fdInv = await _invoiceDrafts.FlushToDbAsync(token, UserAccountID, DbSec);
if (fdInv == null) return DraftGone();
// Create vs update is decided inside the flush (flushes of one session are serialised), not
// from the session state seen here: a save overlapping the first registration is an update.
var flush = await _invoiceDrafts.FlushToDbAsync(token, UserAccountID, DbSec);
if (flush == null) return DraftGone();
var fdInv = flush.Data;
if (string.IsNullOrEmpty(fdInv.Id))
return await InvoiceIssueResult("Der Zwischenstand konnte aufgrund eines Fehlers nicht gespeichert werden.");
await _events.InvoiceDraftRegisteredAsync(fdInv, existed, UserAccountID);
await _events.InvoiceDraftRegisteredAsync(fdInv, flush.Updated, UserAccountID);
var after = _invoiceDrafts.Get(token);
if (after != null) await _draftNotifier.SignalDraftReadyAsync(after.Token, after.Version);
return await JSONAsync(new { ok = true, invid = fdInv.Id });
@@ -82,16 +82,14 @@ public partial class IntranetController
{
if (!HasForm("token")) return BadRequest400();
string token = Form("token");
var before = _reminderDrafts.Get(token);
if (before == null) return DraftGone();
bool existed = !string.IsNullOrEmpty(before.RemId);
var fdRem = await _reminderDrafts.FlushToDbAsync(token, UserAccountID, DbSec);
if (fdRem == null) return DraftGone();
// Create vs update is decided inside the flush (serialised per session) — see HandleDraftSave.
var flush = await _reminderDrafts.FlushToDbAsync(token, UserAccountID, DbSec);
if (flush == null) return DraftGone();
var fdRem = flush.Data;
if (string.IsNullOrEmpty(fdRem.Id))
return await ReminderIssueResult("Der Zwischenstand konnte aufgrund eines Fehlers nicht gespeichert werden.");
await _events.ReminderDraftRegisteredAsync(fdRem, existed, UserAccountID);
await _events.ReminderDraftRegisteredAsync(fdRem, flush.Updated, UserAccountID);
var after = _reminderDrafts.Get(token);
if (after != null) await _draftNotifier.SignalDraftReadyAsync(after.Token, after.Version);
return await JSONAsync(new { ok = true, remid = fdRem.Id });