dojo: neue Einträge — Plenty-Catalog-API + Stock-Movements processRowType-Filter
This commit is contained in:
@@ -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).
|
**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**
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -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.
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user