User Stories — Epic 3: Produkte & Preise

Methodik: Connextra (Als/möchte/damit) + Gherkin-Akzeptanzkriterien (Gegeben/Wenn/Dann) + INVEST-Qualitätsgate. Sprache der Stories DE; Fachbegriffe/Codes wie im Schema. Bezug: Modul 3 (12-produkte-preise.md), Schema Dok 30 §3.3 (box_products) + §3.4 (zones/price_lists/price_list_items/product_depot_rates) + §3.2 (vat_rates), RLS Dok 30 §13. Rollen-Codes kanonisch (UPPERCASE): ADMIN, OPERATIONS, BUCHHALTUNG, READONLY (sowie FAHRER/AFFILIATE = lesend). IDs: US-PRC-NN (stabil). Status: Entwurf (zur Freigabe).


Personas in diesem Epic

RollePersonBezug zu Produkten & Preisen
OPERATIONSMarkusKatalog- & Preislisten-Pflege (Produkte, Zonen, Items, Depot-Sätze) — Schreibrecht 🔲 offen (Default: OPS schreibt, s. Dok 30 §13 Fn 3)
BUCHHALTUNGMarielaMWST-Satz-Stammdaten (vat_rates), liest Preise; kein Katalog-Schreibrecht
ADMINMarcelalles + Löschen + PRICE_OVERRIDE (Default-Inhaber)
alle internen RollenOPS/BUC/FAHPreise/Zonen/Depot lesen (notwendig für Offerte, Logistik, Tracking)
READONLYTreuhänder/Gastnur lesen

INVEST-Gate (für alle Stories dieses Epics geprüft): jede Story ist unabhängig schneidbar, verhandelbar, liefert Geschäftswert, schätzbar, klein genug für einen Schritt und über die Gherkin-Szenarien testbar (inkl. Negativfälle/RLS).

Querschnitts-Invariante (BR-50, gilt für alle Resolver-/Preis-Stories): Der Preis ist serverseitig autoritativ. Kein Client/Konsument (Offerte, Auftrag) übernimmt einen mitgelieferten Preis ungeprüft — die Server-Action ruft immer den Resolver auf und vergleicht. Abweichung ohne Recht PRICE_OVERRIDE ⇒ Ablehnung. (Abkehr vom IST-Bug, bei dem der Preis-Override nur im Frontend galt — BR-46.)


US-PRC-01 — Box-Produkt-Katalog pflegen

Als OPERATIONS möchte ich Box-Produkte (Box/Fass/Zafacón/Service) mit stabilem Code, Kategorie, Maßen und mehrsprachigen Labels anlegen und pflegen, damit Offerten und Logistik aus einem konsistenten Katalog schöpfen.

Akzeptanzkriterien

  • Gegeben ich lege ein Produkt an, wenn ich code, category (box/barrel/bin/service), label_de/es/en setze, dann wird ein box_products-Datensatz mit active = true gespeichert; code ist UNIQUE und danach nicht mehr änderbar (stabiler Fachcode).
  • Gegeben ein Produkt mit laufenden Referenzen (Offerten/Boxen), wenn ich es „entfernen" will, dann wird nicht gelöscht, sondern active = false gesetzt (historische Referenzen bleiben les-/auflösbar).
  • Gegeben Kategorie service, dann sind Maße (length/width/height_cm, volume_liters, max_weight_kg) optional/NULL erlaubt.
  • Gegeben is_returnable und default_deposit_chf, dann sind sie am Produkt pflegbar — is_returnable ist die einzige Quelle für „rückgabefähig" (Box materialisiert davon, F-22).

Traceability: box_products (§3.3), K-07, F-22.


US-PRC-02 — Produkt OTRO (Freimaß) als Katalogeintrag führen

Als OPERATIONS möchte ich das Sonderprodukt OTRO (Übergröße/Freimaß) ohne feste Produktmaße führen, damit Übergrößen erfasst und später per Volumen-Tarif bepreist werden können.

Akzeptanzkriterien

  • Gegeben das Produkt OTRO, wenn ich es pflege, dann trägt es keine festen Produktmaße — die Maße B/H/L kommen pro Box (boxes.length_cm/width_cm/height_cm), nicht aus dem Produkt.
  • Gegeben OTRO, dann wird es im Preisraster nicht über einen Festpreis je Stück, sondern über den Volumen-Tarif aufgelöst (US-PRC-09) — der Katalog kennzeichnet es entsprechend.
  • Gegeben Standard-Box-Typen (BOX_MEDIANA/JUMBO/MAXI/MEGA …), dann bleiben sie festpreisbasiert (US-PRC-05).

Traceability: box_products Seed OTRO (§3.3), Entscheid E7 (Hybrid-Tarif). (E6 ✅ entschieden 2026-06-28: Katalog korrekt + Bild je Typ image_path; E7 Hybrid-Tarif, Rundung ceil; Zonen 🔲.)


US-PRC-03 — Zielzonen (DR) pflegen

Als OPERATIONS möchte ich Zielzonen der Dominikanischen Republik (z. B. SDQ, STI, PROV) mit Code und Labels pflegen, damit Preise je Zone differenziert werden können.

Akzeptanzkriterien

  • Gegeben ich lege eine Zone an, wenn ich code (UNIQUE), label_de/es/en und country_code (Default DO) setze, dann wird ein zones-Datensatz mit active = true gespeichert.
  • Gegeben eine Zone mit referenzierenden price_list_items, wenn ich sie löschen will, dann wird das Löschen verhindert (ON DELETE RESTRICT); stattdessen active = false.
  • Gegeben die DR-Zonen-Taxonomie ist noch nicht final, dann ist die Liste erweiterbar, ohne Code-Änderung (TEXT-Codes, kein DB-Enum).

Traceability: zones (§3.4), K-08. (Entscheid-abhängig: vollständige DR-Zonen-Taxonomie 🔲, E7.)


US-PRC-04 — Effektiv-datierte Preisliste anlegen

Als OPERATIONS möchte ich eine neue Preisliste als Zeitscheibe (valid_from, optional valid_until) anlegen, damit saisonale/monatliche Preisrunden ohne Datenverlust abgebildet werden.

Akzeptanzkriterien

  • Gegeben ich erstelle eine Preisliste, wenn ich name und valid_from setze, dann wird ein price_lists-Kopf gespeichert; valid_until ≥ valid_from ist erzwungen (pl_valid_range).
  • Gegeben ich setze is_default = true, dann ist genau eine Default-Liste pro Zeitpunkt zulässig (partieller Unique-Index idx_price_lists_one_default); eine bisherige Default-Liste wird vorher abgeschlossen (valid_until = valid_from − 1).
  • Gegeben Überschneidungen mit bestehenden Listen (gleiches Produkt+Zone bereits abgedeckt), wenn ich aktiviere, dann erscheint eine Warnung (kein Blocker).
  • Gegeben die Aktivierung, dann wird ein audit_log-Eintrag (table_name='price_lists', action='insert'/'update') geschrieben.

Traceability: price_lists (§3.4), Workflow §4.2, audit_log (§6.1).


US-PRC-05 — Preislisten-Items pflegen (Produkt × Zone)

Als OPERATIONS möchte ich je Preisliste den Preis pro Produkt × Zone erfassen (manuell oder per CSV-Import), damit der Resolver für jede Kombination einen Treffer findet.

Akzeptanzkriterien

  • Gegeben eine Preisliste, wenn ich ein Item mit product_id, zone_id und unit_price_chf anlege, dann wird es gespeichert; pro Liste ist die Kombination (product_id, zone_id) eindeutig (pli_unique_product_zone).
  • Gegeben ein CSV-Import (product_code, zone_code, unit_price_chf, depot_chf), wenn ich importiere, dann zeigt eine Vorschau zeilengenaue Validierungsfehler (z. B. unbekannter product_code) und importiert nur valide Zeilen.
  • Gegeben ein negativer Preis, dann wird er abgelehnt (CHECK unit_price_chf >= 0 + UI vor Submit).
  • Gegeben ein aktives Produkt ohne Item in dieser Liste, dann erscheint eine Warnung „kein Preis — Resolver schlägt fehl" (kein Blocker beim Speichern).

Traceability: price_list_items (§3.4), Workflow §4.2, K-12.


US-PRC-06 — Aktive Preisliste ist unveränderlich (Korrektur = neue Liste)

Als ADMIN/OPERATIONS möchte ich, dass Items einer bereits aktiven Preisliste nicht editierbar sind, damit eingefrorene Offerten-/Auftrags-Snapshots revisionssicher bleiben (OR 957 / GeBüV).

Akzeptanzkriterien

  • Gegeben ein Item einer aktiven (gültigen) Preisliste, wenn ich unit_price_chf ändern will, dann wird die Änderung abgelehnt (UI verbietet; serverseitig blockiert).
  • Gegeben ein Korrekturbedarf, wenn ich korrigiere, dann lege ich eine neue Preisliste mit valid_from = heute an; bestehende Offerten behalten ihren price_list_item_id-Snapshot.
  • Gegeben eine Item-Anlage/Listen-Aktivierung, dann entsteht je Vorgang ein audit_log-Eintrag (entity = price_list/price_list_item).

Traceability: Revisionssicherheit §8.1, Workflow §4.3, audit_log (§6.1).


US-PRC-07 — Preis-Resolver: Preis zu Datum X / Produkt Y / Zone Z

Als Konsument (Offerte/Auftrag via Server-Action) möchte ich für (Produkt × Zone × Datum) deterministisch den gültigen Preis + price_list_item_id erhalten, damit der Snapshot eindeutig und reproduzierbar eingefroren werden kann.

Akzeptanzkriterien

  • Gegeben ein Datum, ein Produkt und eine Zone, wenn der Resolver läuft, dann wählt er die Liste mit valid_from <= Datum UND (valid_until IS NULL ODER valid_until >= Datum); bei mehreren gewinnt das jüngste valid_from.
  • Gegeben kein zonenspezifisches Item, wenn der Resolver sucht, dann fällt er auf zone_id IS NULL (zonenunabhängiger Grundpreis) zurück; immer noch kein Treffer ⇒ Fehler PRICE_NOT_FOUND (Offerte blockiert).
  • Gegeben ein expliziter priceListId-Override, wenn dort kein Item für Produkt+Zone existiert, dann Fehler (Override muss vollständig sein) — kein stiller Fallback auf eine andere Liste.
  • Gegeben ein Treffer, dann liefert der Resolver price_list_item_id, unit_price_chf und den depot_chf (US-PRC-10) als ResolvedPrice zurück.

Traceability: Resolver §4.1, price_list_items (§3.4). (Deckt BR-50 ab: serverseitige Preisautorität.)


US-PRC-08 — Resolver-Ambiguität erkennen (Negativfall)

Als ADMIN möchte ich, dass der Resolver bei zwei gleichrangigen Preislisten klar fehlschlägt statt zu raten, damit Fehlkonfigurationen sichtbar werden, bevor ein falscher Preis eingefroren wird.

Akzeptanzkriterien

  • Gegeben zwei gültige Listen mit gleichem valid_from (beide nicht eindeutig priorisierbar), wenn der Resolver läuft, dann liefert er Fehler AMBIGUOUS_PRICE statt eines beliebigen Preises.
  • Gegeben AMBIGUOUS_PRICE, dann wird der Vorgang (Offerte) blockiert und der Admin zur Bereinigung der überlappenden Listen aufgefordert.
  • Gegeben ein Admin-Testscreen (Datum/Produkt/Zone → „Preis berechnen"), dann zeigt er die gewinnende Liste samt Gültigkeitszeitraum oder den jeweiligen Fehlercode.

Traceability: Resolver-Edge-Cases §4.1, Validierungen §7. (Deckt Roadmap-Testfälle AMBIGUOUS_PRICE/PRICE_NOT_FOUND ab.)


US-PRC-09 — Volumen-Tarif für OTRO (Freimaß, E7-Hybrid)

Als OPERATIONS möchte ich, dass Boxen des Produkts OTRO über B/H/L (pro Box) und eine konfigurierte Tarifeinheit bepreist werden, damit Übergrößen ohne Festpreis fair abgerechnet werden.

Akzeptanzkriterien

  • Gegeben eine OTRO-Box mit length_cm/width_cm/height_cm, wenn der Resolver den Preis ermittelt, dann rechnet er über das Volumen mit der konfigurierten Tarifeinheit app_settings.volume_tariff_unit (IST-Divisor 100) — fortgeführte IST-Logik ⌈ (B×H×L) / Tarifeinheit × Tarif ⌉.
  • Gegeben das Zwischenergebnis ist nicht ganzzahlig, wenn der Tarif angewandt wird, dann greift die definierte Rundungsregel (E7) deterministisch und nachvollziehbar — gleiche Eingabe ⇒ gleiches Ergebnis.
  • Gegeben eine Standard-Box (nicht OTRO), dann kommt der Volumen-Tarif nicht zur Anwendung (Festpreis via US-PRC-05/07).
  • Gegeben fehlende B/H/L an einer OTRO-Box, dann kann kein Volumenpreis ermittelt werden (Fehler/Hinweis statt Null-Preis).

Traceability: Entscheid E7/G7 (§3.3 Hinweis), boxes.*_cm (§7), app_settings.volume_tariff_unit (§6.1 / Modul 17). (Entscheid-abhängig: Tarifeinheit, Rundung, DR-Zonen-Differenzierung des Volumen-Tarifs 🔲.)


US-PRC-10 — Depot-Sätze pflegen & anzeigen

Als OPERATIONS möchte ich je Preisliste einen Depot-Satz (Pfand) pro Produkt pflegen, damit Logistik/Finanzen den korrekten, eingefrorenen Pfandbetrag verwenden.

Akzeptanzkriterien

  • Gegeben eine Preisliste, wenn ich depot_chf für ein Produkt setze, dann wird ein product_depot_rates-Datensatz gespeichert; pro Liste ist (price_list_id, product_id) eindeutig (pdr_unique_product_per_list); depot_chf >= 0 (0 erlaubt).
  • Gegeben der Resolver liefert einen Preis, dann liefert er den Depot-Satz derselben price_list_id + product_id mit; existiert keiner, ist depot_chf = null (kein Depot für dieses Produkt).
  • Gegeben eine aktive Preisliste, dann sind Depot-Sätze analog zu Items unveränderlich (Korrektur = neue Liste); historische Sätze bleiben über ältere Listen abrufbar.

Traceability: product_depot_rates (§3.4), default_deposit_chf (§3.3), Konsument deposit_orders.depot_rate_id (§6.2 / Dok 30 §5).


US-PRC-11 — Preis serverseitig autoritativ + PRICE_OVERRIDE als eigene Permission

Als ADMIN möchte ich, dass ein manuelles Übersteuern des aufgelösten Preises ein eigenes Recht (PRICE_OVERRIDE) erfordert und serverseitig erzwungen wird, damit niemand ohne Berechtigung einen abweichenden Preis durchsetzen kann.

Akzeptanzkriterien

  • Gegeben ein Konsument liefert einen Preis mit, wenn die Server-Action speichert, dann vergleicht sie „gelieferter Preis == Resolver-Preis"; bei Gleichheit wird gespeichert (BR-50).
  • Gegeben eine Abweichung ohne Recht PRICE_OVERRIDE, wenn gespeichert werden soll, dann wird mit 403 PRICE_OVERRIDE_DENIED abgelehnt.
  • Gegeben ein Nutzer mit PRICE_OVERRIDE (Default ADMIN; OPERATIONS optional 🔲), wenn er übersteuert, dann wird der abweichende Preis akzeptiert und mit Begründung im audit_log protokolliert.
  • Gegeben Rollen ohne das Recht, dann können sie Positionen anlegen, aber den Preis nicht übersteuern.

Traceability: Permission PRICE_OVERRIDE (13-offerten.md §8.x, BR-46→BR-50), RLS-Fußnote zu quote_lines-UPDATE (Dok 30 §13). (Deckt Audit-Härtung data/sec ab: Preisautorität serverseitig.)


US-PRC-12 — Geld-Präzision: CHF als numeric, kein Float

Als BUCHHALTUNG möchte ich, dass alle Geldbeträge in numeric (CHF) geführt und ohne Gleitkomma-Rundungsfehler berechnet werden, damit Preise, Depot- und MWST-Beträge revisionssicher und exakt sind.

Akzeptanzkriterien

  • Gegeben Preis-/Depot-Felder, dann sind sie als numeric(10,2) (CHF) bzw. numeric(12,2) definiert — nie Float; UI-Eingaben werden vor dem Speichern entsprechend validiert/normalisiert.
  • Gegeben eine Resolver- oder Volumen-Tarif-Berechnung, dann erfolgt sie in Dezimal-/numeric-Arithmetik; das Endergebnis wird nach der definierten Regel (E7) auf 2 Nachkommastellen CHF gerundet.
  • Gegeben Mehrwährung ist out of scope, dann ist currency = 'CHF' der Default; ein späterer DOP/USD-Ausbau bräuchte currency + Kurs (🔲, dokumentiert).

Traceability: Geld-Konvention numeric/CHF (Dok 30 Präambel; Compliance §1.7, OR 957a). (Deckt Audit-Härtung data-01 ab: Geld-Präzision.)


US-PRC-13 — MWST-Satz-Stammdaten pflegen

Als BUCHHALTUNG möchte ich MWST-Sätze (vat_rates) als effektiv-datierte Stammdaten pflegen, damit bei MWST-Aktivierung der korrekte Satz verfügbar ist (Schema-ready, initial inaktiv).

Akzeptanzkriterien

  • Gegeben ich pflege einen Satz, wenn ich code (z. B. STD/RED/EXEMPT), rate_pct, valid_from (optional valid_to) setze, dann wird ein vat_rates-Datensatz gespeichert; active ist Default false (Aktivierung via Settings).
  • Gegeben price_list_items.unit_price_chf, dann ist es als Nettopreis exkl. MWST definiert; die MWST-Berechnung lebt auf Offerten-/Rechnungsebene (Modul 4/6), nicht in der Preisliste.
  • Gegeben Schreibrechte, dann pflegt BUCHHALTUNG/ADMIN die vat_rates; andere interne Rollen lesen nur.

Traceability: vat_rates (§3.2), MWST-Hinweis §8.4, Offerten §8.3. (Entscheid-abhängig: MWST-Pflicht/-Aktivierung 🔲.)


US-PRC-14 — MWST-Satz beim Einfrieren mitspeichern

Als BUCHHALTUNG möchte ich, dass bei aktivierter MWST der Satz (vat_code + vat_rate_percent) zusammen mit dem Betrag persistiert wird, damit ein Beleg auch nach späteren Satzänderungen reproduzierbar bleibt.

Akzeptanzkriterien

  • Gegeben MWST ist aktiv, wenn ein Konsument (Offerte/Rechnung/Buchung) den Preis einfriert, dann werden vat_code, vat_rate_percent und vat_amount_chf auf der Quell-Zeile mitgespeichert (persistiert, nicht nur aus einer View abgeleitet).
  • Gegeben ein späterer Satzwechsel in vat_rates, dann bleibt der eingefrorene Beleg unverändert (historischer Satz erhalten).
  • Gegeben MWST ist inaktiv, dann bleiben die MWST-Felder NULL (kein MWST-Ausweis) — ohne Fehler.

Traceability: vat_code/vat_rate_percent/vat_amount_chf (Dok 30 §2.5/§2.7/§4.1, F-18), vat_rates (§3.2). (Deckt Audit-Härtung data-02 ab: MWST-Satz mitspeichern.)


US-PRC-15 — Preise lesen, Schreiben rollenbasiert beschränkt (RLS)

Als interne Rolle möchte ich Preise/Zonen/Depot-Sätze lesen, aber nur berechtigt schreiben können, damit Datenintegrität und Mandanten-/Rollentrennung gewahrt sind.

Akzeptanzkriterien

  • Gegeben eine beliebige authentifizierte interne Rolle (ADM/BUC/OPS/FAH) oder AFF/RO, wenn sie Preisdaten liest, dann ist SELECT auf box_products/zones/price_lists/price_list_items/product_depot_rates erlaubt (notwendig für Offerte/Logistik).
  • Gegeben OPERATIONS, wenn Schreiben auf Katalog/Preislisten erfolgt, dann ist CRU erlaubt (Default-Annahme; finaler Schreibumfang 🔲, Dok 30 §13 Fn 3); D (Löschen) bleibt ADMIN.
  • Gegeben BUCHHALTUNG/READONLY, wenn sie Katalog/Preislisten schreiben wollen, dann wird abgelehnt (nur R); BUCHHALTUNG schreibt ausschließlich vat_rates (US-PRC-13).
  • Gegeben kein authentifizierter Zugriff (anon), dann kein Preis-Lesezugriff (Session-Token erforderlich).

Traceability: RLS-Matrix Dok 30 §13 (Zeilen box_products/zones/price_lists/price_list_items/product_depot_rates, Fn 3), §8.3. (Entscheid-abhängig: Schreibrechte OPERATIONS 🔲, §14.)


Schätzung & Priorität (Vorschlag — vom Team final zu bestätigen)

StoryGrößePriorität (MoSCoW)
US-PRC-01 Box-Produkt-Katalog pflegenMMust
US-PRC-02 Produkt OTRO (Freimaß) führenSMust
US-PRC-03 Zielzonen (DR) pflegenSMust
US-PRC-04 Effektiv-datierte Preisliste anlegenMMust
US-PRC-05 Preislisten-Items pflegen (Produkt×Zone)LMust
US-PRC-06 Aktive Liste unveränderlichMMust
US-PRC-07 Preis-Resolver (Produkt×Zone×Datum)LMust
US-PRC-08 Resolver-Ambiguität (AMBIGUOUS_PRICE)SShould
US-PRC-09 Volumen-Tarif OTRO (E7-Hybrid)LShould
US-PRC-10 Depot-Sätze pflegen & anzeigenMMust
US-PRC-11 Preisautorität + PRICE_OVERRIDEMMust
US-PRC-12 Geld-Präzision (numeric/CHF)SMust
US-PRC-13 MWST-Satz-StammdatenSShould
US-PRC-14 MWST-Satz mitspeichernMShould
US-PRC-15 Preise lesen / Schreiben rollenbasiert (RLS)MMust

Größe = grobe Aufwands-Indikation (S/M/L), nicht Story Points. Priorität nach MoSCoW (Must/Should/Could). Beides ist ein Vorschlag zur Release-Planung — die verbindliche Schätzung/Priorisierung macht das Team im Backlog.


Abdeckung & offene Punkte (dieses Epic)

  • 15 Stories decken Modul 3 end-to-end ab: Katalog (Box-Produkte inkl. OTRO/Freimaß) → Zonen (DR) → effektiv-datierte Preislisten + Items → Preis-Resolver (Produkt×Zone×Datum, PRICE_NOT_FOUND/AMBIGUOUS_PRICE) → Volumen-Tarif für OTRO (E7) → Depot-Sätze → MWST-Stammdaten, plus Querschnitt (Preisautorität, Geld-Präzision, RLS).
  • Audit-Verzahnung: US-PRC-11 (Preis serverseitig autoritativ, BR-50, + PRICE_OVERRIDE als eigene Permission — data/sec), US-PRC-12 (Geld-Präzision, data-01), US-PRC-14 (MWST-Satz mitspeichern, data-02), US-PRC-06 (Unveränderlichkeit aktiver Listen, OR 957) — die Akzeptanzkriterien encodieren die Härtungen als testbare Szenarien.
  • Entscheid-abhängig: US-PRC-02 (E6 ✅ entschieden: Katalog korrekt + Bild je Typ), US-PRC-09 (Rundung ✅ ceil; Tarifeinheit/Zonen 🔲), US-PRC-03 (DR-Zonen-Taxonomie 🔲), US-PRC-13 (MWST ✅ aktiv/inkl.), US-PRC-15 (Schreibrechte OPERATIONS vs. nur ADMIN/BUCHHALTUNG 🔲, Dok 30 §13 Fn 3 / §14).
  • Nächster Schritt: Freigabe → INVEST-Red-Team-Pass → Fan-out auf die übrigen Epics im selben Muster.