Add ERP order import endpoint and n8n order-ingest flow export
This commit is contained in:
221
docs/PROCESS_PHASE1.md
Normal file
221
docs/PROCESS_PHASE1.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user