Files
erp_naurua/docs/PROCESS_PHASE1.md

7.2 KiB

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)

{
  "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.