---
name: project-optimistic-locking
description: "Server-seitige optimistische Sperre für Customer-Writes (VERSION_CONFLICT) — Fix gegen Last-Write-Wins bei zwei Sales am selben Kunden, eingebaut 2026-06-29."
metadata: 
  node_type: memory
  type: project
  originSessionId: 31351046-6fed-46e2-b117-bcc9d8064774
---

Vor Go-live (Donnerstag 2026-07-02) das Concurrency-Verhalten der Customer-Writes gefixt.

**Ausgangslage (charakterisiert):** `@VersionColumn` war im Schreibpfad wirkungslos — `persistCustomer` machte `repo.merge(existing, scalars)`, das die `version` aus dem Client-Payload übernahm. Folge: Last-Write-Wins, `version` lief sogar rückwärts (2→1), stiller Datenverlust bei parallelen Editoren. Jeder Sales hat prinzipiell alle Kunden des Landes → der Fall ist real.

**Fix (rein BE, kein FE-Eingriff):**
- `libs/shared/src/lib/errors/error-catalog.ts`: neuer Code `VERSION_CONFLICT` (HTTP 409).
- `libs/customer/customer-be/src/lib/customer-core.service.ts`: `enforceVersion()` in `persistCustomer` vor dem Merge — `incoming.version !== existing.version` → `DomainException('VERSION_CONFLICT', …, forcedState: existing)`. Danach zählt `@VersionColumn` wieder hoch.
- **Kernregel:** Sperre greift nur, wenn `incoming.version != null`. Ein nie synchronisierter Neuanlage-Stand (version undefined), der nach Crash erneut gesendet wird, ist ein idempotenter Re-Send — sonst wäre at-least-once / Crash-Recovery (S8b) kaputtgegangen. Ein bereits synchronisierter Datensatz trägt clientseitig immer eine version (reconcile schreibt den Server-Stand lokal zurück).

**Verhalten:** stale Edit → 409 → FE rollt über den vorhandenen `forcedState`-Mechanismus (wie SAP-Readonly) auf den Server-Stand zurück, Fehler sichtbar. Verliererseite verliert ihre Änderung (mit Meldung); ein Merge-Dialog wäre eine spätere fachliche Erweiterung.

**Wichtig für künftige Fixes:** `version` ist als „wer ist neuer"-Vergleichsbasis untauglich (client-steuerbar). `lastUpdateTs` ist server-autoritativ (`@BeforeUpdate` → `Date.now()`) — das ist die verlässliche Basis. Belegt in den Tests.

**Tests:** `customer-core.concurrency.spec.ts` (5, BE-direkt gegen In-Memory-SQLite) + `sync-loop.integration.spec.ts` S10 (End-to-End Multi-Sales). Gesamt 8 Projekte grün. Siehe [[project-sync-testing]] für den Test-Harness.

**Lokaler Testlauf:** node 22 via fnm nötig (`eval "$(/opt/homebrew/bin/fnm env)" && fnm use 22`), better-sqlite3 baut nicht gegen node 26. node_modules waren nicht installiert → `npm ci`. Repo: `~/projects/fahrzeugeinrichtung-plato-backend`.
