Files
erp_naurua/docs/PROCESS_PHASE1.md

222 lines
7.2 KiB
Markdown

# 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.