# Phase 1 Process Spec ## Dokumentstatus 1. Typ: `operational` 2. Detaillierungsgrad: Event- und Ablaufebene 3. Zugehoerige Komponenten-Spezifikation: `/Users/mathias/Documents/Dokumente Chouchou/Codebases/erp_naurua/docs/SPEC_PHASE1_COMPONENTS.md` 4. Normative Quelle: `/Users/mathias/Documents/Dokumente Chouchou/Codebases/erp_naurua/docs/CONCEPT.md` ## Zweck Diese Datei ist die operative Umsetzungsvorlage fuer Schritt 1. Sie uebersetzt die fachlichen Entscheidungen in konkrete Ablaufregeln auf Event-Ebene. ## Geltungsbereich 1. Bestellimport aus Wix via n8n Webhook 2. Kontakt- und Adressanlage 3. Chargenzuordnung und Lagerbewegung 4. Teil-/Vollstorno inklusive Gegenbuchung 5. Outbound-Webhook ERP -> n8n 6. Direktverkauf als manuelle Sammelerfassung (ohne Kundenregistrierung) ## Begriffe 1. `allocated`: Charge ist zugeordnet und Lagerabgang ist bereits gebucht. 2. `reserved`: Optionaler Zwischenstatus, in Phase 1 standardmaessig nicht verwendet. 3. `open`-Charge: vorbereitete naechste Charge je Produkt. 4. `current`-Charge: aktive Abgangscharge je Produkt. 5. `closed`-Charge: abgeschlossene Charge. 6. `order_source`: Herkunft der Bestellung (`wix` oder `direct`). ## Harte Invarianten 1. Pro Produkt genau eine `current`-Charge. 2. Pro Produkt genau eine `open`-Charge. 3. `open` darf nie fehlen. 4. Verkaufsabgang nie ohne Charge. 5. Negativbestand auf Charge ist nicht erlaubt. 6. Datensaetze werden nicht geloescht; Statusaenderung statt Delete. 7. Direktverkaeufe haben `external_ref` mit Praefix `DIR-`. ## Prozess A: `order.imported` (Inbound Webhook, Quelle `wix`) ### Input Webhook-JSON aus dem Shop (Bestellung + lineItems). ### Schritte 1. Idempotenz pruefen ueber `BestellungNr` (`sales_order.external_ref`). 2. `sales_order` per Upsert speichern (`order_source = wix`, `payment_status = paid`). 3. `party`, `contact`, `address` speichern/aktualisieren. 4. Rechnungsadresse darf `NULL` sein; Lieferadresse normal speichern. 5. Pro `lineItem` internen `sellable_item` aufloesen: 1. zuerst ueber `external_article_number` 2. fallback ueber normalisierten Titel 6. Falls kein Mapping existiert: 1. Position trotzdem speichern (`raw_external_*` gesetzt) 2. in Audit als Mapping-Luecke markieren 7. Fuer gemappte Position jede `sellable_item_component` aufloesen. 8. Fuer jedes benoetigte Lagerprodukt Abgang auf `current`-Charge buchen: 1. `stock_move` mit `move_type = out` 2. `sales_order_line_lot_allocation` mit `allocation_status = allocated` 9. Chargensaldo gegen `v_stock_lot_balance` pruefen (kein negativer Bestand). 10. Wenn `current` nach Abgang `qty_net <= 0` hat: 1. alte `current` auf `closed` 2. vorhandene `open` auf `current` 3. neue `open` automatisch erzeugen (ohne `lot_number`) 11. Outbound-Event in Queue schreiben: `order.imported`. 12. Audit-Log schreiben (import + allocation + auto-switch falls erfolgt). ### Ergebnis Bestellung ist vollstaendig erfasst, Chargen sind zugeordnet, Lager ist aktualisiert. ## Prozess E: `direct.sale.captured` (manuelle ERP-Erfassung) ### Trigger Tages-/Sammelverkauf wird im ERP erfasst (z. B. Marktverkauf). ### Schritte 1. ERP erzeugt interne Bestellnummer mit Praefix `DIR-` (z. B. `DIR-20260329-00017`). 2. `sales_order` speichern mit: 1. `order_source = direct` 2. `party_id = NULL` (Laufkundschaft ohne Kontaktanlage) 3. `payment_status = paid` 3. Mengen je Produkt als `sales_order_line` speichern. 4. Gesamtpreis brutto wird auf Gesamtmenge verteilt und als `unit_price` je Zeile gespeichert. 5. Zahlungsart wird aus den Direktverkauf-Methoden gespeichert (`twint`, `cash`, `paypal`, `bank_transfer`). 6. Lagerabgang und Chargenzuordnung laufen identisch zu Prozess A. 7. Outbound-Event wird wie bei Import in die Queue gestellt (`order.imported` mit `orderSource = direct`). 8. Audit-Log schreibt `action = direct_sale_captured`. ## Prozess B: `order.cancelled.partial` ### Trigger Teil-Storno fuer einzelne Position oder Teilmenge. ### Schritte 1. Ziel-`sales_order_line` laden, `cancel_qty` validieren. 2. `qty_cancelled` erhoehen, `line_status = partially_cancelled` setzen. 3. Bereits `allocated` Menge in gleicher Hoehe rueckbuchen: 1. `stock_move` mit `move_type = adjustment` und positivem Eingang 2. gleiche `lot_id` wie Ursprungsallokation 4. Zugehoerige `sales_order_line_lot_allocation` auf `cancelled` setzen oder anteilig splitten. 5. Wenn Gesamtstorno der Position erreicht ist, `line_status = cancelled`. 6. Wenn alle Positionen storniert sind, `sales_order.order_status = cancelled`. 7. Outbound-Event in Queue schreiben: `order.cancelled.partial`. 8. Audit-Log schreiben. ## Prozess C: `order.cancelled.full` ### Trigger Vollstorno der Bestellung. ### Schritte 1. Alle offenen Positionen iterieren. 2. Je Position Restmenge stornieren wie in Prozess B. 3. `sales_order.order_status = cancelled`, `cancelled_at`, `cancelled_reason` setzen. 4. Outbound-Event in Queue schreiben: `order.cancelled.full`. 5. Audit-Log schreiben. ## Prozess D: `lot.auto_switched` ### Trigger Nach Abgang ist `current`-Charge leer (`qty_net <= 0`). ### Schritte 1. Alte `current` auf `closed` setzen. 2. Existierende `open` auf `current` setzen. 3. Neue `open` fuer dasselbe Produkt anlegen: 1. `lot_number = NULL` 2. `status = open` 4. Outbound-Event in Queue schreiben: `lot.auto_switched`. 5. Audit-Log schreiben. ## Outbound Webhook ERP -> n8n ### Ziel n8n erhaelt statusrelevante Bestell- und Chargenupdates aus dem ERP. ### Delivery Model 1. Outbox/Queue in DB: `outbound_webhook_event`. 2. Worker liest `pending`/`failed` nach `next_attempt_at`. 3. HTTP POST an n8n Incoming Webhook. 4. Bei 2xx: `sent`. 5. Bei Fehler: Retry mit Backoff, danach `dead_letter`. ### Security 1. Header `X-ERP-Signature`: HMAC-SHA256 ueber Raw Body. 2. Header `X-ERP-Event`: Eventtyp. 3. Header `X-ERP-Event-Key`: Idempotenzschluessel. ### Event Payload (Basis) ```json { "eventType": "order.imported", "eventKey": "order.imported:10466:2026-03-29T17:00:00Z", "occurredAt": "2026-03-29T17:00:00Z", "order": { "externalRef": "10466", "orderSource": "wix", "orderStatus": "imported", "paymentStatus": "paid", "amounts": { "net": 49.95, "shipping": 4.95, "tax": 0, "discount": 0, "total": 54.90, "currency": "CHF" } }, "lines": [ { "lineNo": 1, "qty": 1, "qtyCancelled": 0, "status": "allocated", "allocations": [ { "productSku": "REISHI_FLASCHE", "lotNumber": "2412.003", "qty": 1 } ] } ] } ``` ## Retry Policy (Standardannahme) 1. Maximal 10 Versuche. 2. Exponential Backoff: 1m, 2m, 4m, 8m, ... bis 12h. 3. Danach `dead_letter` + operativer Alert. ## Abverkauf-Warnung (Phase 1) 1. Die Prognose wird im ERP intern berechnet (`fn_refresh_sellout_forecast`). 2. Ausgabe erfolgt als Felder/Status fuer die UI (`sellout_date`, `warning_state`), nicht per E-Mail. 3. Warnlogik: `due_60d` bei Abverkaufdatum in <= 60 Tagen, `due_now` bei heute/ueberfaellig. ## Offene Details (werden spaeter mit realem n8n-Setup finalisiert) 1. Finale n8n Ziel-URL pro Umgebung (dev/staging/prod). 2. Secret-Rotation fuer Signatur. 3. Dead-letter-Verarbeitung (manuell oder Requeue-Button). 4. Exakte Liste weiterer Events fuer spaetere Module.