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(sowieFAHRER/AFFILIATE= lesend). IDs:US-PRC-NN(stabil). Status: Entwurf (zur Freigabe).
Personas in diesem Epic
| Rolle | Person | Bezug zu Produkten & Preisen |
|---|---|---|
OPERATIONS | Markus | Katalog- & Preislisten-Pflege (Produkte, Zonen, Items, Depot-Sätze) — Schreibrecht 🔲 offen (Default: OPS schreibt, s. Dok 30 §13 Fn 3) |
BUCHHALTUNG | Mariela | MWST-Satz-Stammdaten (vat_rates), liest Preise; kein Katalog-Schreibrecht |
ADMIN | Marcel | alles + Löschen + PRICE_OVERRIDE (Default-Inhaber) |
| alle internen Rollen | OPS/BUC/FAH | Preise/Zonen/Depot lesen (notwendig für Offerte, Logistik, Tracking) |
READONLY | Treuhänder/Gast | nur 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/ensetze, dann wird einbox_products-Datensatz mitactive = truegespeichert;codeist 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 = falsegesetzt (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_returnableunddefault_deposit_chf, dann sind sie am Produkt pflegbar —is_returnableist 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/enundcountry_code(DefaultDO) setze, dann wird einzones-Datensatz mitactive = truegespeichert. - Gegeben eine Zone mit referenzierenden
price_list_items, wenn ich sie löschen will, dann wird das Löschen verhindert (ON DELETE RESTRICT); stattdessenactive = 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
nameundvalid_fromsetze, dann wird einprice_lists-Kopf gespeichert;valid_until ≥ valid_fromist erzwungen (pl_valid_range). - Gegeben ich setze
is_default = true, dann ist genau eine Default-Liste pro Zeitpunkt zulässig (partieller Unique-Indexidx_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_idundunit_price_chfanlege, 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. unbekannterproduct_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 = heutean; bestehende Offerten behalten ihrenprice_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 <= DatumUND (valid_until IS NULLODERvalid_until >= Datum); bei mehreren gewinnt das jüngstevalid_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 ⇒ FehlerPRICE_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_chfund dendepot_chf(US-PRC-10) alsResolvedPricezurü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 FehlerAMBIGUOUS_PRICEstatt 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 mitlength_cm/width_cm/height_cm, wenn der Resolver den Preis ermittelt, dann rechnet er über das Volumen mit der konfigurierten Tarifeinheitapp_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_chffür ein Produkt setze, dann wird einproduct_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_idmit; existiert keiner, istdepot_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 mit403 PRICE_OVERRIDE_DENIEDabgelehnt. - Gegeben ein Nutzer mit
PRICE_OVERRIDE(DefaultADMIN;OPERATIONSoptional 🔲), wenn er übersteuert, dann wird der abweichende Preis akzeptiert und mit Begründung imaudit_logprotokolliert. - 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äuchtecurrency+ 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(optionalvalid_to) setze, dann wird einvat_rates-Datensatz gespeichert;activeist Defaultfalse(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/ADMINdievat_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_percentundvat_amount_chfauf 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
SELECTaufbox_products/zones/price_lists/price_list_items/product_depot_rateserlaubt (notwendig für Offerte/Logistik). - Gegeben
OPERATIONS, wenn Schreiben auf Katalog/Preislisten erfolgt, dann istCRUerlaubt (Default-Annahme; finaler Schreibumfang 🔲, Dok 30 §13 Fn 3);D(Löschen) bleibtADMIN. - Gegeben
BUCHHALTUNG/READONLY, wenn sie Katalog/Preislisten schreiben wollen, dann wird abgelehnt (nurR);BUCHHALTUNGschreibt ausschließlichvat_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)
| Story | Größe | Priorität (MoSCoW) |
|---|---|---|
| US-PRC-01 Box-Produkt-Katalog pflegen | M | Must |
US-PRC-02 Produkt OTRO (Freimaß) führen | S | Must |
| US-PRC-03 Zielzonen (DR) pflegen | S | Must |
| US-PRC-04 Effektiv-datierte Preisliste anlegen | M | Must |
| US-PRC-05 Preislisten-Items pflegen (Produkt×Zone) | L | Must |
| US-PRC-06 Aktive Liste unveränderlich | M | Must |
| US-PRC-07 Preis-Resolver (Produkt×Zone×Datum) | L | Must |
US-PRC-08 Resolver-Ambiguität (AMBIGUOUS_PRICE) | S | Should |
US-PRC-09 Volumen-Tarif OTRO (E7-Hybrid) | L | Should |
| US-PRC-10 Depot-Sätze pflegen & anzeigen | M | Must |
US-PRC-11 Preisautorität + PRICE_OVERRIDE | M | Must |
US-PRC-12 Geld-Präzision (numeric/CHF) | S | Must |
| US-PRC-13 MWST-Satz-Stammdaten | S | Should |
| US-PRC-14 MWST-Satz mitspeichern | M | Should |
| US-PRC-15 Preise lesen / Schreiben rollenbasiert (RLS) | M | Must |
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ürOTRO(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_OVERRIDEals 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
OPERATIONSvs. nurADMIN/BUCHHALTUNG🔲, Dok 30 §13 Fn 3 / §14). - Nächster Schritt: Freigabe → INVEST-Red-Team-Pass → Fan-out auf die übrigen Epics im selben Muster.