diff --git a/ANTI-PATTERNS.md b/ANTI-PATTERNS.md index e670ef9..153a58b 100644 --- a/ANTI-PATTERNS.md +++ b/ANTI-PATTERNS.md @@ -324,3 +324,53 @@ if (!ct.includes('application/json')) throw new Error('Silent-Deny'); **Stattdessen:** Den dedizierten Bulk-Pfad `/rest/pim/variations/clients` nutzen (DOJO #35). --- + +## 22. Catalog-`id`-Feld als Variation-ID interpretieren + +```javascript +// FALSCH: Plenty-Catalog liefert id = ITEM-ID, nicht Variation-ID +const variationId = catalogRow.id; +const movements = await plenty.get(`/rest/items/0/variations/${variationId}/stock/movements`); +// → 200 OK mit entries:[] → alle Artikel werden als Lager-Lieger fehlinterpretiert +``` + +**Problem:** Catalog-Output ist Item-zentriert, `id` ist die Item-ID. Stock-Movements brauchen die echte Variation-ID. Plenty gibt aber kein 404, sondern eine leere Liste zurück — silent failure. + +**Fix:** Im Plenty-UI explizit `variationId` (oder `mainVariationId`) als zusätzliches Feld zum Catalog hinzufügen, ODER vor jedem Movement-Call per `/rest/items/variations/{id}` (404=Item) zu Variation auflösen. **Siehe DOJO #38, #40** + +--- + +## 23. Stock-Movements ohne `processRowType`-Filter zählen + +```javascript +// FALSCH: alle qty<0 als Outflow → Lager-Umbuchungen + Korrekturen verfälschen +for (const m of entries) { + if (Number(m.quantity) < 0) totalOutflow += Math.abs(m.quantity); +} +``` + +**Problem:** Plenty mischt im Movements-Endpoint alle Bestandsänderungen — Verkäufe, interne Umbuchungen (`processRowType=1`, z.B. „Warenkorrektur" oder „Warenumbuchung über plentyWarehouse"), Lager-Eingänge etc. Ohne Filter wird ein langjährig im Lager liegender Artikel mit gelegentlichen Umbuchungen fälschlich als „aktiv verkauft" eingestuft. Reichweite ist zu kurz, Lager-Lieger nie erkannt. + +**Fix:** `if (Number(m.processRowType) !== 2) continue;` — nur `processRowType=2` (= order) ist ein echter Verkauf. **Siehe DOJO #39** + +--- + +## 24. `data?.entries ?? data` als Iterable annehmen + +```javascript +// FALSCH: data.entries ist die Array.prototype.entries-Method, wenn data Array ist +const rows = resp.data?.entries ?? resp.data ?? []; +for (const r of rows) { ... } // → "entries is not iterable" +``` + +**Problem:** `??` greift nur bei `null`/`undefined`, nicht bei truthy-aber-falscher-Type. Wenn `data` direkt ein Array ist (wie bei Plenty-Catalog-Downloads), gibt `data.entries` die Method-Referenz zurück (truthy aber keine Iterable). `for...of` crashed dann mit „entries is not iterable". + +**Fix:** Erst explizit auf Array prüfen, dann auf `entries`-Property: +```javascript +const rows = Array.isArray(data) ? data + : Array.isArray(data?.entries) ? data.entries + : []; +``` +Oder einen `findFirstArray()`-Helper. **Siehe DOJO #37, #38** + +--- diff --git a/DOJO.md b/DOJO.md index 24a8852..2867f8d 100644 --- a/DOJO.md +++ b/DOJO.md @@ -1058,3 +1058,185 @@ const paketBestand = Math.min(...components.map(componentLimit)); **Entdeckt:** 2026-05-01 — beim Aufbau einer Paket-Bestand-Berechnung über alle Bundles eines Bereichs. Initial mit `stockLimitation: 0`-Default getestet → Funktion lieferte immer `null`, weil ein Default 0 (=unbegrenzt) jeden Komponenten als "∞" einstuft. --- + +## 37. Catalog-API: Endpoint-Pfad, UUIDs, indirekter Zugriff via `/url/public` + +**Lektion:** Plenty's Catalog-Feature (für Bulk-Exports z.B. Google Shopping, OTTO) hat seinen eigenen Namespace `/rest/catalogs/catalogs/{id}/...` (doppeltes „catalogs" = Modul + Resource), die `{id}` ist eine **UUID** (nicht Integer), und die echten Daten kommen NICHT direkt vom Catalog-Endpoint, sondern über eine indirekte Public-Download-URL. + +**Warum:** Drei Endpoints unter dem Catalog-Pfad sehen ähnlich aus, liefern aber komplett verschiedene Dinge: +- `/content` → Filter-Definitionen (Catalog-Konfiguration), KEINE Daten +- `/export` → Catalog-Metadata (active-Flag, settings, name), KEINE Daten +- `/url/public` → eine externe Download-URL (z.B. `https://shop.example.com/rest/catalogs/export/{uuid}/download/public?hash=…`), die separat gefetcht werden muss → erst DAS sind die Daten + +Plus: ohne aktivierten Catalog + manuell getriggerten Export-Run liefert die Public-URL HTTP 500 (Datei existiert nicht). Bei Catalog-Änderungen erneut Export-Run nötig. + +**Pattern:** +```javascript +// 1. Public-URL holen +const urlResp = await client.get(`/rest/catalogs/catalogs/${encodeURIComponent(uuid)}/url/public`); +const downloadUrl = urlResp.data?.data ?? urlResp.data?.url; // Wrapper-Format variiert +// 2. Externe URL fetchen +const dataResp = await client.get(downloadUrl); +// 3. Response ist direkt Array oder Wrapper-Object — NICHT auf .entries verlassen +const rows = Array.isArray(dataResp.data) ? dataResp.data : findFirstArray(dataResp.data); +``` + +**Anti-Pattern:** +```javascript +// FALSCH: /content oder /export bringt keine Daten +const data = await client.get(`/rest/catalogs/${id}/content`); // Catalog-Filter-Konfiguration + +// FALSCH: Plenty-Spec-fern geratene Pfade +const data = await client.get(`/rest/plenty/export/${id}/export`); // 404 +const data = await client.get(`/rest/exports/${id}`); // 404 + +// FALSCH: UUID als Number behandeln (DB-Spalte INT, Frontend-Input type="number") +// → Browser frisst Buchstaben still, Save schickt null +``` + +**Verifikations-Quelle:** `https://github.com/plentymarkets/api-doc → plentymarkets/openApiV2/openApiV2.min.json` listet die echten Catalog-Pfade. + +**Entdeckt:** 2026-05-03 beim Bau eines Catalog-basierten Bulk-Imports. + +--- + +## 38. Catalog-Output: Field-Naming-Quirks + Setup-Anforderungen an den User + +**Lektion:** Plenty's Catalog-Output verwendet ein eigenes flaches Field-Naming, das in mehreren Punkten von der klassischen REST-API abweicht. Manche Felder (z.B. die Variation-ID) sind nur drin, wenn der User sie im Plenty-UI explizit zum Catalog hinzufügt. + +**Warum:** Catalog-Outputs sind „flach" und Item-zentriert (NICHT Variation-zentriert wie der Default-Bereich der REST-API): +- `id` → ITEM-ID (NICHT Variation-ID — überraschend) +- `variationId` (oder `mainVariationId`) → Variation-ID, MUSS vom User explizit ausgewählt werden, sonst nicht in der Response +- `stockPhysical` (NICHT `physicalStock` wie an `/rest/items/.../variations/{id}` — Plenty-Naming-Inkonsistenz hier wie an `/rest/stockmanagement/stock`) +- `categoryId` singular als Array von Number, NICHT `categories: [{id, ...}]` +- `purchasePrice` häufig als deutscher Komma-Decimal-String („1,01") — `parseFloat` schneidet am Komma ab → 1.0; vor parseFloat `replace(',', '.')` +- `name` direkt (NICHT `texts.name1` wie sonst) +- `isActive` direkt (NICHT `variation.isActive`) +- Response-Wrapper ist KEIN `{entries: [...]}`, sondern direkt Array oder anderer Wrapper + +**Setup-Wizard-Pflicht** für jeden neuen Catalog (User muss): +1. Catalog mit Format **JSON** anlegen (CSV macht Decimal-/Encoding-Probleme) +2. Pre-Filter `variation.isActive = true` setzen +3. Felder explizit auswählen (Liste s.o.) — fehlende Felder = `undefined` in Response +4. Catalog aktivieren + manuellen Export-Run triggern (sonst 500 auf Public-URL) +5. Public-URL-Generierung in Catalog-Settings einschalten +6. Catalog-UUID an die App übergeben + +**Pattern:** +```javascript +// Robuste Field-Extraktion mit Fallbacks für versch. Plenty-Versionen +const variationId = pickNumber(r, ['mainVariationId', 'variationId', 'variation.id']); +const itemId = pickNumber(r, ['itemId', 'item.id']) || pickNumber(r, ['id']); +const stock = pickFloat(r, ['stockPhysical', 'stock.stockPhysical']); +const price = pickFloat(r, ['purchasePrice']); // muss Komma-Decimal handlen +const cats = extractCategoryIds(r); // sucht categoryId/categories/parentCategoryId + +// pickFloat mit Komma-Handling: +const n = typeof v === 'string' ? parseFloat(v.replace(',', '.')) : Number(v); +``` + +**Anti-Pattern:** +```javascript +// FALSCH: id als Variation-ID behandeln → /stock/movements liefert 200 OK mit +// leerer Liste (nicht 404!), alle Artikel werden Lager-Lieger +const variationId = r.id; + +// FALSCH: physicalStock annehmen → undefined → Default 0 → alle Bestände 0 +const stock = r.physicalStock; + +// FALSCH: parseFloat ohne Komma-Replace bei deutschen Beträgen +const price = parseFloat(r.purchasePrice); // "1,01" → 1.0 + +// FALSCH: data.entries als Array annehmen wenn data direkt Array ist +const rows = data.entries ?? data ?? []; // .entries ist Array.prototype-Method! +for (const r of rows) { ... } // crashed mit "entries is not iterable" +``` + +**Entdeckt:** 2026-05-03 — ohne `variationId`-Feld waren 0 echte Movements auflösbar; alle Artikel landeten als Lager-Lieger in der Snapshot-Tabelle. + +--- + +## 39. Stock-Movements: `processRowType`-Filter für „echte Verkäufe" + +**Lektion:** Plenty's `/rest/items/0/variations/{id}/stock/movements`-Endpoint liefert ALLE Bestandsänderungen — nicht nur Verkäufe. Wer Verkaufs-Outflows messen will (Reichweite, Lagerumschlag, Lager-Lieger-Erkennung), muss zwingend auf `processRowType === 2` filtern. + +**Warum:** Plenty unterscheidet vier Typen von Stock-Movements: +- `1` = incoming item data set (= Lager-Buchung, Korrektur, Umbuchung) +- `2` = order (= **echter Verkauf**) +- `3` = deleted stock movement +- `4` = new stock intake + +Reason-Codes wie „Warenkorrektur (interne Verschiebung)" oder „Warenumbuchung über plentyWarehouse" haben alle `processRowType=1`. Wenn sie als Outflow gezählt werden: +- Lager-Lieger-Status fälschlich „aktiv" (intern verschoben = scheinbar verkauft) +- Reichweite künstlich kürzer (Σ-Outflow überschätzt) + +**Pattern:** +```javascript +for (const e of entries) { + const qty = Number(e.quantity ?? 0); + const at = e.createdAt ? new Date(e.createdAt) : null; + if (!at) continue; + + if (qty > 0) { + // Inflows ungefiltert: lastIncoming/firstIncoming sollen jede Lager-Aufnahme + // berücksichtigen (Wareneingang, Umbuchung, Korrektur sind alle „im Lager") + if (!lastIn || at > lastIn) lastIn = at; + } else if (qty < 0) { + // Outflows: NUR echte Aufträge (processRowType=2) zählen + if (Number(e.processRowType) !== 2) continue; + if (!lastOut || at > lastOut) lastOut = at; + totalOutflow += Math.abs(qty); + } +} +``` + +**Anti-Pattern:** +```javascript +// FALSCH: alle qty<0 als Outflow zählen → Lager-Umbuchungen verfälschen Werte +if (qty < 0) { + if (!lastOut || at > lastOut) lastOut = at; + totalOutflow += Math.abs(qty); +} +``` + +**Entdeckt:** 2026-05-03 — Item mit zwei „Outflows" (interne Korrektur + Umbuchung, beide processRowType=1) wurde fälschlich als aktiv-verkaufend statt Lager-Lieger erkannt. + +--- + +## 40. Stock-Movements: Item-ID vs. Variation-ID — silent failure + +**Lektion:** Der Pfad `/rest/items/0/variations/{id}/stock/movements` braucht eine echte Variation-ID. Wenn man fälschlich eine Item-ID übergibt, kommt **HTTP 200 OK mit leerem `entries`-Array** zurück — KEINE 404 oder Fehlermeldung. Bug ist silent. + +**Warum:** Plenty's Stock-Movements werden pro Variation indiziert. Eine Item-ID matcht nichts → leere Liste → der Code interpretiert das als „keine Bewegungen" → Artikel wird fälschlich als Lager-Lieger markiert. Tritt häufig auf, wenn man Catalog-Daten verarbeitet, deren `id`-Feld in Wirklichkeit die Item-ID ist (siehe DOJO #38). + +**Pattern:** +```javascript +// Vor Stock-Movement-Calls die ID-Type sicherstellen +async function resolveToVariationId(plenty, candidateId) { + try { + await plenty.get(`/rest/items/variations/${candidateId}`); + return candidateId; // ist bereits Variation-ID + } catch { + // Falls 404 → ist evtl. Item-ID → Hauptvariation auflösen + const vars = await plenty.get(`/rest/items/${candidateId}/variations`, { + params: { itemsPerPage: 50 } + }); + const main = vars.data?.entries?.find(v => v.isMain) ?? vars.data?.entries?.[0]; + if (!main) throw new Error(`Keine Variation für Item ${candidateId}`); + return main.id; + } +} +``` + +**Anti-Pattern:** +```javascript +// FALSCH: ID aus Catalog/Item-API direkt für Movements nutzen, ohne Type-Check +const movements = await plenty.get(`/rest/items/0/variations/${itemId}/stock/movements`); +// → 200 OK mit entries:[] → silent failure +``` + +**Diagnostik bei seltsamen Lager-Lieger-Werten:** zuerst per `/rest/items/variations/{id}` prüfen ob die ID eine Variation ist (404 = ist Item-ID, 200 = Variation). + +**Entdeckt:** 2026-05-03 — Catalog-Field `id` ist ItemID, falsche Verwendung an Stock-Movements führte zu 100% Lager-Lieger-False-Positives. + +---