222 lines
7.2 KiB
Markdown
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.
|