# Bestandssync JTL → WEBSALE

Der Shop bekommt seinen Bestand ausschließlich von hier. Diese Seite beschreibt,
**was gesendet wird**, **warum**, und **welche Sicherungen greifen**.

## Rechenweg je Artikel

Identisch in allen Push-Pfaden und in `websale:stock:compare` — Abweichungen im
Vergleich sind damit echte Drift, keine Rechenunterschiede.

```
Basis    = manueller Override (websale_stock_overrides)  ODER  JTL-Verfügbar
JTL-Verfügbar = QuantityTotal − Versandsperre − Verfügbarkeitssperre − Picklisten
gesendet = max(0, Basis − Reservierungs-Overlay)
```

Das **Reservierungs-Overlay** zieht die Mengen eigener importierter, noch nicht
versendeter Bestellungen ab — die REST-API weist Auftragsreservierungen nicht
aus. Das Push-Lager gehört exklusiv dem Onlineshop, der Connector kennt also
alle reservierenden Aufträge selbst.

## Mengengerüst: welche Artikel überhaupt gesendet werden

Hier lag die Überverkaufs-Lücke. `GET /stocks?warehouseId=X` liefert **nur
Artikel mit Bestand > 0** (verifiziert 03.08.2026: 27.614 Bestandszeilen für
Lager 94, davon 3.894 mit Bestand > 0 — und exakt die kamen zurück).

Ein Artikel, der auf 0 fällt, verschwindet damit aus dem Feed. Weil der Push
absolute Mengen für genau das sendet, was er bekommen hat, **erfährt der Shop
die 0 nie** und verkauft den letzten gemeldeten Wert weiter.

Deshalb kommt das Mengengerüst nicht aus dem Feed, sondern aus der Wawi-DB
(`StockScopeService`, rein lesend): **alle Artikel, die im Push-Lager je einen
Wareneingang hatten.** Wer davon keine `/stocks`-Zeile mehr hat, wird explizit
mit 0 gesendet.

Warum nicht schlicht alle Artikel: an ProductIDs, die der Shop nicht kennt, legt
WEBSALE beim `SetStocks` neue Einträge an (Antwort-Status `Created`). Von 27.614
Bestandszeilen hatten nur 4.526 Artikel je eine Bewegung — die übrigen ~23.000
existieren nur formal und würden Karteileichen erzeugen.

## Nicht-Shop-Artikel

Produktionsartikel (Nummernkreis `700…`) liegen im selben Lager, gehören aber
nicht in den Shop. Bis 03.08.2026 hat der Push sie mitgeschickt: 83 ProductIDs
mit zusammen 141.640 Einheiten in der Bestandstabelle von WEBSALE, ohne dass es
dort ein Produkt dazu gibt. Beleg für die Abgrenzung: von 78.427 je bestellten
Positionen trägt **jede** den Präfix `100`.

`STOCK_SKU_EXCLUDE_PREFIXES` (Default `700`) hält sie aus allen Push-Pfaden und
aus dem Vergleich heraus. Bewusst eine Ausschluss- und keine Positivliste: ein
neuer Shop-Nummernkreis wird dadurch weiter gepusht (harmlos), während eine
Positivliste ihn stillschweigend aussperren würde — und ein Artikel ohne
Bestandspflege ist genau die Überverkaufs-Falle, die dieser Umbau schließt.

## Sicherungen

| Schalter | Default | Wirkung |
|---|---|---|
| `STOCK_PUSH_ENABLED` | `true` | Aus = alles läuft nur als Dry-Run |
| `STOCK_SCOPE_ENABLED` | `true` | Aus = kein Mengengerüst, ausverkaufte Artikel bleiben im Shop stehen |
| `STOCK_SCOPE_MIN_FEED_RATIO` | `0.8` | Wächter, siehe unten |
| `STOCK_SKU_EXCLUDE_PREFIXES` | `700` | Nummernkreise, die nicht in den Shop gehören |
| `STOCK_OVERRIDES_ENABLED` | `true` | Kill-Switch für alle manuellen Overrides |

**Der Wächter** ist die wichtigste Sicherung. Liefert die REST-API weniger als
`MIN_FEED_RATIO` der laut Wawi-DB bestandsführenden Artikel (Timeout, halbe
Seite, Rechteproblem), werden **keine** Nullen ergänzt — sonst würde ein
API-Aussetzer den halben Katalog auf 0 setzen und den Shop flächendeckend
abverkaufsunfähig machen. Im sync-Log erscheint dann `StockPush: Feed
unplausibel klein`. Fällt der DB-Weg ganz aus, verhält sich der Push wie vor dem
Umbau: nur Feed-Mengen, nie falsche Nullen.

## Diagnose

```bash
# Eine SKU über alle Ebenen: /stocks roh, Verfügbar, Overlay, Netto, Live-Shop
php artisan websale:stock:debug 10004471-3304

# Drift-Wächter (Exit 1 bei Abweichung, cron-tauglich)
php artisan websale:stock:compare --all --only-diff

# Voll-Abgleich: Dry-Run bzw. produktiv über die Queue
php artisan websale:stock:push
php artisan websale:stock:push --queue --chunk=200

# Manuelle Overrides
php artisan websale:stock:override list
```

**Nach jeder Code-Änderung `php artisan queue:restart`** — die Worker halten den
Code im Speicher, sonst läuft die alte Fassung weiter. Der Voll-Abgleich läuft
alle 5 Minuten (`routes/console.php`).

## Der Vorfall vom 03.08.2026

Artikel `10004471-3304` wurde mit Auftrag `AU-202608-541473` fünffach
überverkauft. Ablauf:

1. **13.07. 10:52** — WMS bucht den Bestand in Lager 94 als „Missing Inventory"
   von 174 auf 0 aus. Kein Shop-Verkauf, eine externe Korrektur.
2. Ab dieser Sekunde fehlt der Artikel im `/stocks`-Feed. Der Voll-Abgleich
   sendet für ihn nichts mehr, der Shop behält seinen letzten Wert.
3. **26.07.** — Auftrag `178509516408828` enthält denselben Artikel. Sein Import
   scheitert am Log-Rechte-Fehler (behoben 31.07.). Der Bestands-Push hängt am
   **Ende** des Import-Jobs und fällt damit aus — die Selbstheilung bleibt aus.
4. **03.08. 10:43** — 5 Stück verkauft. JTL bucht ohne Widerspruch, weil der
   Artikel „Bestand kleiner Null erlaubt" trägt.
5. **03.08. 08:45 UTC** — der Push nach dem Auftragsimport meldet 0.0. Drei
   Wochen zu spät.

`websale:stock:compare` hatte dieselbe blinde Stelle (gleiche Quelle) und
meldete durchgehend „alle synchron". Beide Wege lesen jetzt dasselbe
Mengengerüst.
