commit 200d52f9172cbf16f68cb8d2eecabce045f75b5a Author: Sebastian Poll Date: Thu Apr 9 21:23:56 2026 +0000 Initial release: Plenty Dojo Wissensbasis Geteilte Plentymarkets API Learnings (DOJO.md, ANTI-PATTERNS.md), Claude Code Skill (/plenty-dojo), und Install-Script für neue User. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6a9760 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# PID-spezifische Dateien (shop-interne Lektionen) +# Für Kollegen-Sharing: diesen Block entfernen oder eigenes Repo nutzen +instances/ diff --git a/ANTI-PATTERNS.md b/ANTI-PATTERNS.md new file mode 100644 index 0000000..c808d8c --- /dev/null +++ b/ANTI-PATTERNS.md @@ -0,0 +1,231 @@ +# Plenty Dojo — Anti-Patterns + +> Dinge, die man mit der Plentymarkets API **nicht** tun sollte. +> Diese Datei ist allgemeingültig und kann an jeden Plenty-Entwickler weitergegeben werden. +> +> Shop-spezifische Anti-Patterns stehen in `instances/.md`. +> Ausführliche Erklärungen stehen in `DOJO.md`. + +--- + +## 1. Delta-Sync mit exaktem oder zu knappem Timestamp + +```javascript +// FALSCH: Exakter Timestamp +const syncSince = lastSyncTimestamp; + +// AUCH FALSCH: 3h Overlap reicht nicht +const OVERLAP = 3 * 60 * 60 * 1000; +``` + +**Problem:** Race Condition — wenn ein Sync exakt zur gleichen Sekunde läuft, in der eine Order geändert wird, fällt die Order permanent durch alle folgenden Syncs. 3h Overlap half nicht, weil zwischen Container-Restarts (Deploys) und der Order-Änderung mehr als 3h vergehen können. + +**Fix:** 24h Overlap. Kostet ~200 extra Orders pro Sync, eliminiert aber die Race Condition. **Siehe DOJO.md #4** + +--- + +## 2. Statushistorie nach Timestamp sortieren + +```sql +ORDER BY createdAt DESC -- oder ORDER BY status_at DESC +``` + +**Problem:** Mehrere Statuswechsel in derselben Sekunde → zufällige Reihenfolge → falscher "letzter" Status. + +**Fix:** `ORDER BY plenty_id ASC/DESC` — eindeutig, aufsteigend, chronologisch. **Siehe DOJO.md #13** + +--- + +## 3. Aufträge im Schnellfeuer-Modus verschieben + +```javascript +for (const order of orders) { + await api.updateStatus(order.id, 6); // kein Warten, keine Prüfung +} +``` + +**Problem:** Event-Procedures reagieren sofort auf Statuswechsel. Race Conditions zwischen eigenem Write und Plenty-Automationen sind häufig. + +**Fix:** Pause + Verifizieren nach jedem Write. **Siehe DOJO.md #15** + +--- + +## 4. Timestamps ohne Timezone-Offset senden + +```javascript +"2026-03-30T02:30:00" // kein Offset — Interpretation unklar +``` + +**Problem:** Plenty's Verhalten bei fehlenden Offsets ist undokumentiert und endpoint-abhängig. Bei DST-Wechseln kritisch. + +**Fix:** Immer `+00:00` für UTC. **Siehe DOJO.md #19** + +--- + +## 5. Nur Zielstatus prüfen, nicht die Transition + +```javascript +if (order.statusId === 6) { /* OK */ } +``` + +**Problem:** Auftrag kann im Zielstatus sein, aber zwischendurch durch andere Status gelaufen sein. Oder er war kurz dort und wurde weitergeschoben. + +**Fix:** Statushistorie abrufen und Transition als Sequenz prüfen. **Siehe DOJO.md #15** + +--- + +## 6. contactId ohne Relations-Fallback lesen + +```javascript +const contactId = order.contactId; // kann undefined sein! +``` + +**Problem:** `contactId` ist nicht immer direkt am Order-Objekt vorhanden. Ohne `relations` in den `with`-Parametern fehlt er komplett. + +**Fix:** +```javascript +const contactId = order.contactId + || order.relations?.find(r => r.referenceType === 'contact')?.referenceId; +``` +**Siehe DOJO.md #6, Quirk #1** + +--- + +## 7. Status-IDs als Integer behandeln + +```javascript +if (parseInt(order.statusId) === 5) { /* FALSCH */ } +if (order.statusId === 5) { /* FALSCH */ } +``` + +**Problem:** Plenty verwendet fraktionale Status-IDs: 5.1, 5.12, 5.32, 6.01 etc. `parseInt(5.12)` ergibt `5`, `5.12 === 5` ist `false`. + +**Fix:** `parseFloat()` und Range-Vergleiche: +```javascript +const s = parseFloat(order.statusId); +if (s >= 5 && s < 6) { /* Pool */ } +``` +**Siehe DOJO.md #16** + +--- + +## 8. Status-History-Response als Array erwarten + +```javascript +const entries = history; // Manchmal ist es aber { entries: [] }! +``` + +**Problem:** Der Endpoint gibt manchmal ein Array, manchmal ein Objekt mit `entries`-Feld zurück. + +**Fix:** +```javascript +const entries = Array.isArray(history) ? history : (history.entries || []); +``` +**Siehe DOJO.md #14** + +--- + +## 9. Dokument sofort nach Generierung fetchen + +```javascript +await api.post(`/rest/orders/${id}/documents/invoice/generate`); +const docs = await api.get(`/rest/orders/${id}/documents`); +// → Dokument hat status: 'pending', nicht 'done'! +``` + +**Problem:** Dokument-Generierung ist asynchron. Das Dokument ist nicht sofort verfügbar. + +**Fix:** Retry-Loop mit steigenden Delays (3s, 5s, 8s, 10s, 15s), immer auf `status === 'done'` filtern. **Siehe DOJO.md #17** + +--- + +## 10. Dokument-Download über Order-Endpoint + +```javascript +// FALSCH: +await api.get(`/rest/orders/${id}/documents/${docId}`); +``` + +**Problem:** Der Download-Endpoint ist `/rest/documents/{docId}` — OHNE Order-Prefix! + +**Fix:** +```javascript +const res = await api.getRaw(`/rest/documents/${docId}`); +const buffer = Buffer.from(await res.arrayBuffer()); +``` +**Siehe DOJO.md #18** + +--- + +## 11. amounts-Array ohne Null-Check lesen + +```javascript +const total = order.amounts[0].invoiceTotal; // Can crash! +``` + +**Problem:** `amounts` ist ein Array das leer sein kann. Kein Array-Zugriff ohne Check. + +**Fix:** +```javascript +const total = order.amounts?.[0]?.invoiceTotal ?? null; +``` +**Siehe DOJO.md #11, Quirk #7** + +--- + +## 12. Bundle-Komponenten als eigene Artikel zählen + +```javascript +const serverCount = items.filter(i => isServer(i)).reduce((sum, i) => sum + i.quantity, 0); +// → Zählt Bundle-Komponenten (typeId 3) doppelt! +``` + +**Problem:** Bundle-Header (`typeId 2`) enthält die Gesamtmenge. Bundle-Komponenten (`typeId 3`) sind die Einzelteile des Bundles. Beides zählen = Doppelzählung. + +**Fix:** Nur `typeId 1` (Variation), `typeId 2` (Bundle-Header), `typeId 11` (Variations-Bundle) zählen. `typeId 3` (Komponenten) ignorieren. **Siehe DOJO.md #12** + +--- + +## 13. Tag-Operationen als Fehler behandeln wenn idempotent + +```javascript +// FALSCH: 409 beim Hinzufügen und 404 beim Löschen als Fehler werfen +const res = await api.post(`.../variation_tags`, { tagId: 123 }); +if (res.status !== 200) throw new Error('Tag konnte nicht gesetzt werden'); +``` + +**Problem:** `409 Conflict` beim Hinzufügen = Tag existiert bereits (gewünschter Zustand!). `404 Not Found` beim Löschen = Tag existiert nicht (auch gewünscht!). + +**Fix:** Diese HTTP-Codes als Erfolg behandeln, nicht als Fehler. + +--- + +## 14. Neue Eigenschaften über den Merkmale-Endpoint setzen + +```javascript +// FALSCH: variation_properties ist für das ALTE Merkmal-System +await api.post(`/rest/items/${itemId}/variations/${varId}/variation_properties`, { + propertyId: 5, selectionRelationId: 48 +}); +// → 500 Foreign Key Constraint (plenty_character_item) +``` + +**Problem:** `variation_properties` arbeitet mit dem Legacy-Merkmal-System (`plenty_character_*`-Tabellen). Neue Eigenschaften (Property-System) werden über `/rest/properties/relations` verwaltet — komplett andere API. + +**Fix:** `/rest/properties/relations` mit `relationTypeIdentifier: 'item'` verwenden. **Siehe DOJO.md #25** + +--- + +## 15. Mehrere Bulk-Scripts parallel gegen die API laufen lassen + +```javascript +// Script A: Care Packs setzen (600ms delay) +// Script B: Lizenzen setzen (600ms delay) +// → Effektiv ~3,3 Requests/s → 429 Rate Limit! +``` + +**Problem:** Jedes Script hält seinen eigenen Delay ein, aber die API sieht die Summe aller Requests. Andere Crons/Services teilen sich dasselbe Rate-Limit-Budget. + +**Fix:** Bulk-Operationen immer sequenziell (eins nach dem anderen). Delay ≥ 1.500ms wenn andere Services parallel laufen. **Siehe DOJO.md #2** + +--- diff --git a/DOJO.md b/DOJO.md new file mode 100644 index 0000000..fea3b8d --- /dev/null +++ b/DOJO.md @@ -0,0 +1,725 @@ +# Plenty Dojo — Allgemeine Plentymarkets-Wissensbasis + +> Gesammelte Lektionen aus der Arbeit mit der Plentymarkets REST API. +> Diese Datei enthält **allgemeingültige Patterns und Erkenntnisse**, die für jeden Plentymarkets-Entwickler relevant sind — unabhängig vom konkreten Shop oder Projekt. +> +> Shop-spezifische Lektionen (eigene Stati, Workflows, Konfigurationen) stehen in `instances/.md`. +> Kurzform-Anti-Patterns stehen in `ANTI-PATTERNS.md`. + +--- + +# I. Authentifizierung & API-Client + +## 1. Token-Refresh: Konservativ vor Ablauf erneuern + +**Lektion:** Der Access-Token läuft nach ca. 60 Minuten ab. Refresh nach 50 Minuten statt bei Ablauf einplanen. + +**Warum:** Wenn ein Token während eines laufenden Requests abläuft, schlägt der Request fehl. Ein 10-Min-Puffer gibt genug Spielraum, auch bei langsamen Paginierungs-Schleifen. + +**Pattern:** +```javascript +const TOKEN_LIFETIME_MS = 50 * 60 * 1000; // 50 Min (konservativer Puffer) +if (Date.now() >= this.tokenExpiresAt) await this.authenticate(); +``` + +**Entdeckt:** Produktiv seit 2025. Mehrere Projekte nutzen 50min-Threshold ohne Probleme. + +--- + +## 2. Rate Limiting: Fester Delay zwischen Requests + +**Lektion:** Die Plenty API hat kein dokumentiertes Rate-Limit-Header-System wie z.B. GitHub. Stattdessen muss man einen eigenen festen Delay zwischen Requests einbauen. + +**Warum:** Ohne Delay kommen ab ~20 Requests/Sekunde `429 Too Many Requests`-Fehler. Die API gibt kein `Retry-After`-Header zurück. + +**Pattern:** +```javascript +const REQUEST_DELAY = 500; // 500–600ms zwischen Requests +async _rateLimit() { + const elapsed = Date.now() - this.lastRequestTime; + if (elapsed < REQUEST_DELAY) await sleep(REQUEST_DELAY - elapsed); + this.lastRequestTime = Date.now(); +} +``` + +**Best Practice:** 500ms für Read-Heavy-Workloads, 600ms wenn auch Writes dabei sind. Bei 429-Fehlern exponentiell backoff: 2s → 4s → 8s. + +**ACHTUNG — Long Period Write Limit:** Bei zu vielen Writes über einen längeren Zeitraum (z.B. tausende POSTs in Folge) gibt es ein **zweites, härteres Rate Limit**: `"long period write limit reached"`. Dieses blockiert **ALLE Requests** inklusive `/rest/login` — nicht nur 429, sondern die gesamte API ist für den Account gesperrt. Dauer: **~2 Stunden** (getestet: nach 60min noch gesperrt, nach 2h wieder frei). Nicht dokumentiert. Kein `Retry-After` Header. Lösung: Bulk-Endpoints nutzen (z.B. `POST /rest/items/variations/variation_properties` für bis zu 50 pro Request), damit die Gesamtzahl der Writes drastisch sinkt. + +**Entdeckt:** Produktiv seit 2025. 500ms hat sich als zuverlässig erwiesen. Long Period Limit entdeckt 2026-04-08 nach ~15.000 Einzel-POSTs auf `/rest/properties/relations`. + +--- + +## 3. Retry-Strategie: 401 und 429 unterschiedlich behandeln + +**Lektion:** `401 Unauthorized` und `429 Rate Limit` erfordern verschiedene Retry-Strategien. + +**Pattern:** +```javascript +const MAX_RETRIES = 3; + +// 401: Token abgelaufen → neu authentifizieren, einmal retrien +if (res.status === 401 && attempt === 0) { + await this.authenticate(); + continue; // retry +} + +// 429: Rate-Limited → exponentieller Backoff +if (res.status === 429) { + const delay = 2000 * Math.pow(2, attempt); + await sleep(delay); + continue; // retry +} +``` + +**Wichtig:** Bei 401 nur **einmal** retrien (nach Re-Auth). Wenn der zweite Versuch auch 401 gibt, sind die Credentials falsch — nicht endlos loopen. + +--- + +# II. Order-Suche & Sync + +## 4. Delta-Sync: Großzügige Zeitüberlappung (24h) verwenden + +**Lektion:** Beim Delta-Sync (Orders seit `updatedAt >= X` laden) niemals exakt den letzten Sync-Timestamp verwenden. **Mindestens 24 Stunden** Overlap einbauen. + +**Warum:** +- **Race Condition:** Wenn ein Sync exakt zur gleichen Sekunde läuft, in der eine Order geändert wird, kann die Order durchfallen. Der Sync speichert seinen Timestamp NACH dem API-Call, aber die Order wurde WÄHREND des Calls geändert. Jeder folgende Sync startet nach der Order und übersieht sie permanent. +- **Zeitumstellung (DST):** Bei der Umstellung CET → CEST (oder umgekehrt) verschiebt sich die lokale Zeit um 1 Stunde. +- **NTP-Drift:** Server-Uhren können um Sekunden bis Minuten voneinander abweichen. +- **Plenty-interne Verzögerungen:** Das `updatedAt`-Feld wird nicht immer sofort aktualisiert. +- **Fehlgeschlagene Syncs / Container-Restarts:** Wenn ein Sync fehlschlägt oder der Container neu startet, kann die Lücke Stunden betragen. + +**Pattern:** +```javascript +const SYNC_OVERLAP_MS = 24 * 60 * 60 * 1000; // 24 Stunden +const syncSince = lastSyncTimestamp + ? new Date(new Date(lastSyncTimestamp).getTime() - SYNC_OVERLAP_MS) + .toISOString().replace(/\.\d+Z$/, '+00:00') + : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) + .toISOString().replace(/\.\d+Z$/, '+00:00'); +``` + +**Voraussetzung:** Die Upsert-Logik muss idempotent sein (`ON CONFLICT DO UPDATE`), damit doppelt geladene Orders keinen Schaden anrichten. + +**Aufwand:** Etwas mehr Orders pro Sync (idempotent, also kein Problem). Bei typischen Sync-Intervallen absolut akzeptabel. + +**Warum nicht 3h?** 3h wurde initial implementiert, erwies sich aber als zu knapp: Eine Order wurde sekundengenau zum Sync-Zeitpunkt geändert. Durch Container-Restarts (Deploys) kann sich der nächste Sync so weit verschieben, dass ein 3h-Overlap die Order nicht mehr erfasst. + +**Entdeckt:** 2026-04-02. Race Condition bei Sync und gleichzeitigem Statuswechsel. 3h-Overlap half nicht, 24h löste das Problem sofort. + +--- + +## 5. Order-Search-Endpoint: Korrekte Struktur + +**Lektion:** Die Auftragssuche in Plenty läuft über `POST /rest/orders/search`, nicht über einen GET-Endpoint. + +**Pattern:** +```javascript +const data = await client.search('/rest/orders/search', { + page: 1, + itemsPerPage: 250, // Maximum pro Seite + with: [ + 'orderItems', // Positionen + 'orderItems.variation', // Variations-Details (Kategorien etc.) + 'comments', // Auftragskommentare + 'dates', // Datumsfelder (Warenausgang etc.) + 'properties', // Versandart, Zahlart, Zahlstatus + 'relations', // Kontakt-ID (Fallback) + 'addresses', // Rechnungs-/Lieferadresse + 'amounts' // Rechnungsbetrag, bezahlter Betrag + ], + conditionType: 'and', // Oder 'or' für OR-Verknüpfung + fields: [ + { field: 'updatedAt', operator: 'gte', value: '2026-01-15T12:00:00+00:00' } + ] +}); +``` + +**Response-Struktur:** +```javascript +{ + entries: [...], // Aufträge dieser Seite + isLastPage: true/false, // Letzte Seite? + lastPageNumber: 5 // Gesamtzahl Seiten +} +``` + +**Pagination:** Seiten durchlaufen bis `isLastPage === true` ODER `page >= lastPageNumber`. + +**Operatoren:** `gte` (>=), `gt` (>), `lte` (<=), `lt` (<), `eq` (=), `in` (in Array). + +--- + +## 6. With-Parameter: Was man wirklich braucht + +**Lektion:** Die `with`-Parameter bestimmen, welche Related Data mitgeladen wird. Ohne sie fehlen wichtige Felder. + +| Parameter | Was es liefert | Wann nötig | +|-----------|---------------|------------| +| `orderItems` | Positionen mit Name, Menge, VariationID | Fast immer | +| `orderItems.variation` | Variations-Kategorien | Für Klassifizierung nach Warengruppe | +| `comments` | Auftragskommentare | Wenn Kommentare angezeigt/ausgewertet werden | +| `dates` | Warenausgang, Enddatum etc. | Für Versanderkennung | +| `properties` | Versandprofil, Zahlart, Zahlstatus | Für Versandart-/Zahlungslogik | +| `relations` | Kontakt-Zuordnung | Für Contact-ID (Fallback!) | +| `addresses` | Rechnungs-/Lieferadresse | Für Kundennamen, Firma | +| `amounts` | Rechnungsbetrag, bezahlt | Für Zahlstatus-Anzeige | + +**Wichtig:** `relations` muss immer dabei sein, weil `contactId` manchmal nur über `relations.find(r => r.referenceType === 'contact').referenceId` erreichbar ist — nicht direkt am Order-Objekt. + +--- + +## 7. Verschwundene Aufträge erkennen (Orphan Detection) + +**Lektion:** Aufträge können in der lokalen DB existieren, aber in Plenty nicht mehr im erwarteten Status-Bereich sein. Das passiert z.B. bei manuellen Stornierungen oder Status-Korrekturen. + +**Pattern:** Regelmäßig (z.B. täglich per Active Refresh) alle lokalen Aufträge mit Plenty abgleichen: +1. Alle Orders im erwarteten Status-Bereich aus Plenty laden (Full Sync) +2. Vergleichen mit lokaler DB +3. Für fehlende Orders: Statushistorie einzeln abfragen +4. Status in lokaler DB korrigieren oder als "verschwunden" markieren + +**Tipp:** Wenn die Plenty API einen 404 für einen Auftrag zurückgibt, mit einem internen Marker (z.B. `status_id = -1`) versehen, damit er nicht bei jedem Sync erneut abgefragt wird. + +**Entdeckt:** Plenty-KPI-Projekt. Orders, die manuell in Status 8+ verschoben wurden, blieben in der lokalen DB als "aktiv" stehen. + +--- + +# III. Auftrags-Datenmodell + +## 8. Order-Properties: Versandart, Zahlart, Zahlstatus + +**Lektion:** Versandart, Zahlungsmethode und Zahlungsstatus sind **nicht** direkte Felder am Auftrag, sondern stecken im `properties`-Array mit verschiedenen `typeId`-Werten. + +**Mapping:** +| `typeId` | Inhalt | Datentyp | +|----------|--------|----------| +| 2 | Shipping Profile ID | Integer als String | +| 3 | Payment Method ID | Integer als String | +| 4 | Payment Status | String-Enum | + +**Payment Status Werte:** `'unpaid'`, `'partlyPaid'`, `'fullyPaid'`, `'overpaid'` + +**Pattern:** +```javascript +const shippingProfileId = parseInt( + order.properties.find(p => p.typeId === 2)?.value, 10 +); +const paymentMethodId = parseInt( + order.properties.find(p => p.typeId === 3)?.value, 10 +); +const paymentStatus = order.properties.find(p => p.typeId === 4)?.value; +// → 'unpaid', 'partlyPaid', 'fullyPaid', 'overpaid' +``` + +--- + +## 9. Order-Dates: Warenausgang und andere Datumsfelder + +**Lektion:** Datumsfelder wie der Warenausgang stecken im `dates`-Array mit verschiedenen `typeId`-Werten. + +**Bekannte Datums-TypeIDs:** +| `typeId` | Bedeutung | +|----------|-----------| +| 5 | Warenausgang (Goods Issue) | +| 10 | Enddatum | + +**Pattern:** +```javascript +const goodsIssueDate = order.dates?.find(d => d.typeId === 5)?.date; +if (goodsIssueDate) { + // Auftrag hat Warenausgang — wurde versendet +} +``` + +--- + +## 10. Adressen: Billing vs. Shipping über pivot.typeId + +**Lektion:** Adressen haben keinen eigenen `type`-String, sondern ein `pivot.typeId` das bestimmt, ob es die Rechnungs- oder Lieferadresse ist. + +| `pivot.typeId` | Adresstyp | +|----------------|-----------| +| 1 | Rechnungsadresse | +| 2 | Lieferadresse | + +**Namensfelder:** +| Feld | Inhalt | +|------|--------| +| `name1` | Firmenname | +| `name2` | Vorname | +| `name3` | Nachname | + +**Pattern:** +```javascript +const billing = order.addresses?.find(a => a.pivot?.typeId === 1); +const contactName = billing + ? [billing.name2, billing.name3].filter(Boolean).join(' ') + : null; +const company = billing?.name1 || null; +``` + +--- + +## 11. Amounts: Rechnungsbetrag und Zahlungsbetrag + +**Lektion:** Das `amounts`-Array enthält Beträge, aber das Array kann leer sein und hat meistens nur einen Eintrag. + +**Pattern:** +```javascript +const amount = order.amounts?.[0]; +const invoiceTotal = amount?.invoiceTotal ?? null; +const paidAmount = amount?.paidAmount ?? null; +``` + +**Wichtig:** Immer mit Null-Checks arbeiten — `amounts` kann `[]` oder `undefined` sein. + +--- + +## 12. OrderItem TypeIDs: Variationen, Bundles, Komponenten + +**Lektion:** Nicht jede Position in einem Auftrag ist ein "echtes" Produkt. Das `typeId`-Feld der OrderItems unterscheidet: + +| `typeId` | Bedeutung | Zählen? | +|----------|-----------|---------| +| 1 | Einzelne Variation | Ja | +| 2 | Bundle-Header | Ja (als 1 Stück) | +| 3 | Bundle-Komponente | Nein (gehört zum Bundle) | +| 11 | Variations-Bundle | Ja | + +**Wichtig:** Beim Zählen von Artikeln (z.B. "wie viele Server im Auftrag?") darf man Bundle-Komponenten (`typeId 3`) nicht mitzählen — sonst wird jeder Server doppelt gezählt. + +--- + +# IV. Status-System + +## 13. Statushistorie: Nach ID sortieren, nicht nach Timestamp + +**Lektion:** Die Statushistorie eines Auftrags darf **nicht** nach `createdAt`/Timestamp sortiert werden. Es gibt regelmäßig mehrere Statusbewegungen in derselben Sekunde. + +**Warum:** Plenty erzeugt bei Event-Procedures, Batch-Statuswechseln oder automatisierten Prozessen mehrere Einträge mit exakt demselben Timestamp. `ORDER BY createdAt` liefert bei Gleichstand eine zufällige Reihenfolge. + +**Pattern:** +```sql +-- Richtig: nach ID sortieren (aufsteigend, eindeutig, chronologisch) +ORDER BY plenty_id ASC; + +-- Neuesten Status ermitteln: +ORDER BY plenty_id DESC LIMIT 1; +``` + +**Entdeckt:** 2026-04-01. Statushistorie-Ansicht zeigte Einträge in falscher Reihenfolge. + +--- + +## 14. Status-History-Endpoint: Response-Format ist inkonsistent + +**Lektion:** `GET /rest/orders/{id}/status-history` gibt manchmal ein Array, manchmal ein Objekt mit `entries`-Feld zurück. + +**Pattern:** +```javascript +const history = await client.get(`/rest/orders/${orderId}/status-history`); +const entries = Array.isArray(history) ? history : (history.entries || []); +``` + +**Immer** beide Formate behandeln — sich auf eines zu verlassen führt zu sporadischen Fehlern. + +--- + +## 15. Schreib-Operationen: Immer verifizieren, nie blind vertrauen + +**Lektion:** Nach jedem Statuswechsel (`PUT /rest/orders/{id}` mit `{ statusId: X }`) den tatsächlichen Zustand prüfen. + +**Warum:** +- Plenty hat interne Event-Procedures, die sofort auf Statuswechsel reagieren und den Auftrag weiter verschieben können. +- HTTP 200 bedeutet "Request angenommen", nicht "Status dauerhaft gesetzt". +- Race Conditions zwischen eigenen Writes und Plenty-Automatisierungen sind häufig. + +**Pattern:** +1. Aktuellen Status prüfen (Pre-Check, damit wir nicht rückwärts schieben) +2. `PUT /rest/orders/{id}` mit `{ statusId: targetStatus }` +3. 1–2 Sekunden warten +4. Status erneut abfragen +5. Bei Abweichung: Statushistorie holen und analysieren +6. Aussagekräftige Meldung ausgeben + +--- + +## 16. Status-Werte sind Floats, nicht Integers + +**Lektion:** Plentymarkets verwendet dezimale Statuswerte wie `5.1`, `5.12`, `5.32`, `6.01`. Diese müssen als Floats behandelt werden. + +**Pattern:** +```javascript +const statusId = parseFloat(order.statusId || 0); +if (statusId >= 5 && statusId < 6) { /* im Pool */ } +``` + +**Anti-Pattern:** +```javascript +if (order.statusId === 5) { /* FALSCH: 5.1 !== 5 */ } +if (parseInt(order.statusId) === 5) { /* FALSCH: verliert Dezimalstellen */ } +``` + +--- + +# V. Dokumente + +## 17. Dokument-Generierung ist asynchron + +**Lektion:** `POST /rest/orders/{id}/documents/{type}/generate` generiert das Dokument asynchron. Die Response bedeutet nicht, dass das Dokument sofort verfügbar ist. + +**Wichtige Dokumenttypen:** +| Typ-String | Dokument | +|------------|----------| +| `order_custom` | Kommissionierschein (Picking List) | +| `invoice` | Rechnung | + +**Pattern (Invoice mit Retry):** +```javascript +// 1. Generierung anstoßen +await client.post(`/rest/orders/${orderId}/documents/invoice/generate`); + +// 2. Mit Retry warten und abholen +const delays = [3000, 5000, 8000, 10000, 15000]; +for (const delay of delays) { + await sleep(delay); + const docs = await client.get(`/rest/orders/${orderId}/documents`); + const invoice = docs.entries + ?.filter(d => d.type === 'invoice' && d.status === 'done') + ?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0]; + if (invoice) { + const res = await client.getRaw(`/rest/documents/${invoice.id}`); + const buffer = Buffer.from(await res.arrayBuffer()); + return buffer; // PDF-Binary + } +} +``` + +**Wichtig:** +- Dokument-Download geht über `/rest/documents/{docId}` (nicht über den Order-Endpoint!) +- Immer `status === 'done'` prüfen — pending Dokumente sind noch nicht fertig +- Nach `createdAt` absteigend sortieren, um das neueste zu bekommen + +--- + +## 18. Dokument-Listing: Endpoint und Filterung + +**Lektion:** Alle Dokumente eines Auftrags auflisten über `GET /rest/orders/{id}/documents`. + +**Response:** +```javascript +{ + entries: [ + { + id: 12345, // Dokument-ID (für Download) + type: 'order_custom', // Dokumenttyp + status: 'done', // 'done' oder 'pending' + createdAt: '2026-...' // Erstellungszeitpunkt + } + ] +} +``` + +**Download:** Binärdaten über `GET /rest/documents/{docId}` — liefert den PDF-Inhalt direkt. Methode `getRaw()` verwenden, dann `res.arrayBuffer()` → `Buffer.from()`. + +--- + +# VI. Timestamps & Zeitzonen + +## 19. Timestamps: Immer explizit UTC mit Offset + +**Lektion:** Alle Timestamps an die Plenty API immer mit explizitem Timezone-Offset senden (`+00:00`), nie ohne. + +**Warum:** Plenty's Interpretation von Timestamps ohne Offset ist nicht dokumentiert und kann sich je nach Endpoint unterscheiden. Bei DST-Wechseln besonders kritisch. + +**Pattern:** +```javascript +new Date().toISOString().replace(/\.\d+Z$/, '+00:00') +// → "2026-04-02T10:15:00+00:00" +``` + +**Anti-Pattern:** +```javascript +new Date().toISOString().replace('Z', '') +// → "2026-04-02T10:15:00.000" — Interpretation unklar +``` + +--- + +# VII. Kategorie-System + +## 20. Kategorie-Baum: Flache Liste mit Parent-Referenzen + +**Lektion:** Kategorien in Plenty sind eine flache Liste mit `parentCategoryId`-Referenzen. Es gibt keinen Tree-Endpoint — den Baum muss man selbst aufbauen. + +**Pattern:** +```javascript +// 1. Alle Kategorien laden (paginiert) +const allCategories = []; +let page = 1, hasMore = true; +while (hasMore) { + const data = await client.get(`/rest/categories?itemsPerPage=250&page=${page}`); + allCategories.push(...data.entries); + hasMore = !data.isLastPage; + page++; +} + +// 2. Nachfahren einer Root-Kategorie per BFS finden +function findDescendants(rootId, categories) { + const ids = new Set([rootId]); + const queue = [rootId]; + while (queue.length > 0) { + const current = queue.shift(); + for (const cat of categories) { + if (cat.parentCategoryId === current && !ids.has(cat.id)) { + ids.add(cat.id); + queue.push(cat.id); + } + } + } + return ids; +} +``` + +--- + +## 21. Item-Kategorien: Über die Variation abfragen + +**Lektion:** Die Kategorien eines Items bekommt man nicht direkt am Item, sondern über die Variation. + +**Endpoint:** `GET /rest/items/{itemId}/variations/{variationId}?with=variationCategories` + +**Response:** +```javascript +{ + variationCategories: [ + { categoryId: 93 }, // Kategorie-ID + { categoryId: 142 } + ] +} +``` + +**Tipp:** Ergebnisse lokal cachen (z.B. SQLite-Tabelle `item_category_cache`), um die API nicht bei jedem Sync zu belasten. Kategorien ändern sich selten. + +--- + +# VIII. Tags & Item-Management + +## 22. Tags sind Variations-bezogen, nicht Item-bezogen + +**Lektion:** Tags werden pro Variation gesetzt, nicht pro Item. + +**Endpoints:** +``` +POST /rest/items/{itemId}/variations/{variationId}/variation_tags + Body: { tagId: 123 } + +DELETE /rest/items/{itemId}/variations/{variationId}/variation_tags/{tagId} +``` + +**Besonderheiten:** +- `409 Conflict` beim Hinzufügen = Tag existiert bereits → ignorieren +- `404 Not Found` beim Löschen = Tag existiert nicht → ignorieren +- Beides sind erwartete Zustände, keine Fehler + +--- + +# IX. Cycle Times & KPIs + +## 23. Durchlaufzeiten berechnen: Statushistorie statt Auftragsdaten + +**Lektion:** Um Durchlaufzeiten (z.B. "wie lange dauert es von Auftragseingang bis Versand?") zu berechnen, die Statushistorie verwenden — nicht die Auftrags-Timestamps. + +**Pattern:** +```sql +-- Erste Ankunft in Status-Range ermitteln +SELECT MIN(status_at) as first_5 +FROM status_history +WHERE order_id = ? AND status_id >= 5 AND status_id < 6; + +-- Cycle Times berechnen +SELECT + MIN(CASE WHEN status_id >= 5 AND status_id < 6 THEN status_at END) as first_5, + MIN(CASE WHEN status_id >= 6 AND status_id < 7 THEN status_at END) as first_6, + MIN(CASE WHEN status_id >= 7 THEN status_at END) as first_7 +FROM status_history +WHERE order_id = ?; + +-- Dann: 5→6 = first_6 - first_5, 6→7 = first_7 - first_6, 5→7 = first_7 - first_5 +``` + +**Tipp:** Für Business-Hours-Berechnung Wochenenden ausschließen. SQLite braucht dafür eine Custom Function: +```javascript +db.function('business_hours', (startIso, endIso) => { + // Montag-Freitag zählen, Samstag/Sonntag ignorieren +}); +``` + +--- + +# X. Properties (neue Eigenschaften, nicht Merkmale!) + +## 24. Properties vs. Merkmale: Zwei verschiedene Systeme + +**Lektion:** Plentymarkets hat **zwei** Eigenschaftssysteme, die nicht kompatibel sind: + +| System | API-Felder | Endpunkte | +|--------|-----------|-----------| +| **Merkmale** (alt) | `itemProperties`, `variationProperties` | `/rest/items/{id}/variations/{varId}/variation_properties` | +| **Eigenschaften** (neu) | `properties` (via `with=properties`) | `/rest/properties/relations` | + +**Wichtig:** Der `variation_properties`-Endpoint ist für das alte Merkmal-System. Neue Eigenschaften (wie Auswahl-Properties) laufen über `/rest/properties/relations`. + +--- + +## 25. Property-Relation erstellen (Selection-Eigenschaft) + +**Lektion:** Um eine Auswahl-Eigenschaft an einer Variation zu setzen, einen POST auf `/rest/properties/relations` senden. + +**Pattern:** +```javascript +const res = await fetch(`${BASE}/rest/properties/relations`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + propertyId: 5, // ID der Eigenschaft + relationTypeIdentifier: 'item', // Verknüpfungstyp + relationTargetId: variationId, // mainVariationId des Artikels + selectionRelationId: 48 // ID des Auswahlwerts + }) +}); +``` + +**Besonderheiten:** +- `selectionRelationId` wird NICHT im gleichnamigen Response-Feld gespeichert (bleibt `null`), sondern als `relationValues[0].value` mit `lang: "0"`. Das Plenty-UI interpretiert es trotzdem korrekt. +- **Unique Constraint** auf `propertyId + targetId + type`: Duplikat-POST → HTTP 500 (Integrity Constraint). Sicher als "bereits gesetzt" behandeln. +- Auslesen: `GET /rest/items/{itemId}/variations/{varId}?with=properties` → `properties.find(p => p.propertyId === X)` + +**Entdeckt:** 2026-04-08. Artikeltyp-Eigenschaft (Property 5) auf Variationen setzen. + +--- + +## 26. Property-Selections abrufen + +**Lektion:** Die Auswahlwerte einer Eigenschaft bekommt man **nicht** über einen dedizierten Selection-Endpoint, sondern über die Property selbst mit `with=selections`. + +**Pattern:** +```javascript +const prop = await get(`/rest/properties/${propertyId}?with=selections`); +for (const sel of prop.selections) { + const name = sel.relation?.relationValues?.find(v => v.lang === 'de')?.value; + console.log(`ID: ${sel.id} → ${name}`); +} +``` + +**Nicht funktionierend:** +- `GET /rest/properties/{id}/selections` → 404 +- `GET /rest/properties/selections?propertyId={id}` → 403 (fehlende Berechtigung) + +**Entdeckt:** 2026-04-08. + +--- + +## 27. Kategorie-Suche: Kein rekursiver Filter + +**Lektion:** Die Plenty-API hat **keinen** rekursiven Kategorie-Filter. `categoryId=X` liefert nur Artikel, die direkt in Kategorie X liegen — nicht in Unterkategorien. + +**Pattern:** Kategorie-Baum selbst aufbauen per BFS: +```javascript +// 1. Alle Kategorien laden +const allCats = []; let page = 1; +while (true) { + const data = await get(`/rest/categories?itemsPerPage=250&page=${page}&type=item&with=details`); + if (!data.entries?.length) break; + allCats.push(...data.entries); + if (data.isLastPage) break; + page++; +} + +// 2. BFS: Alle Nachfahren einer Root-Kategorie finden +function getDescendants(rootId) { + const ids = new Set([rootId]); + const queue = [rootId]; + while (queue.length) { + const current = queue.shift(); + for (const cat of allCats) { + if (cat.parentCategoryId === current && !ids.has(cat.id)) { + ids.add(cat.id); queue.push(cat.id); + } + } + } + return ids; +} + +// 3. Artikel pro Kategorie laden und deduplizieren +const allItems = new Map(); +for (const catId of getDescendants(rootId)) { + const data = await get(`/rest/items/variations?categoryId=${catId}&isMain=true&itemsPerPage=250`); + for (const v of data.entries) allItems.set(v.itemId, v); +} +``` + +**Entdeckt:** 2026-04-08. `includeChildren=true` und `recursive=true` → 422 Invalid Filter. + +--- + +## 28. Item-Texte: Nur am Item, nicht an der Variation + +**Lektion:** Artikelnamen (`name1`, `name2`, `name3`) stehen im `texts`-Array des Items, nicht an der Variation. + +**Pattern:** +```javascript +// Richtig: +const item = await get(`/rest/items/${itemId}`); +const name = item.texts?.[0]?.name1; + +// Falsch — variation.name ist oft leer: +const variation = await get(`/rest/items/${itemId}/variations/${varId}`); +variation.name; // → '' oder undefined +``` + +**Entdeckt:** 2026-04-08. Alle 1612 Care Pack Artikel zeigten "(kein Name)" weil variation.name abgefragt wurde statt item.texts. + +--- + +# XI. API-Endpunkt-Referenz + +| Endpoint | Methode | Zweck | +|----------|---------|-------| +| `/rest/login` | POST | Authentifizierung (Username + Password) | +| `/rest/orders/search` | POST | Aufträge suchen (mit Filtern & Pagination) | +| `/rest/orders/{id}` | GET | Einzelauftrag laden | +| `/rest/orders/{id}` | PUT | Auftrag aktualisieren (z.B. Status ändern) | +| `/rest/orders/{id}/status-history` | GET | Statushistorie abrufen | +| `/rest/orders/{id}/documents` | GET | Dokumente eines Auftrags auflisten | +| `/rest/orders/{id}/documents/{type}/generate` | POST | Dokument generieren (async) | +| `/rest/documents/{docId}` | GET | Dokument-Binary herunterladen | +| `/rest/categories` | GET | Alle Kategorien (paginiert) | +| `/rest/items/{itemId}/variations/{varId}` | GET | Variation mit Kategorien laden | +| `/rest/items/{itemId}/variations/{varId}/variation_tags` | POST | Tag hinzufügen | +| `/rest/items/{itemId}/variations/{varId}/variation_tags/{tagId}` | DELETE | Tag entfernen | +| `/rest/properties/{id}?with=selections` | GET | Eigenschaft mit Auswahlwerten laden | +| `/rest/properties/relations` | POST | Eigenschafts-Verknüpfung erstellen (neue Properties) | +| `/rest/properties/relations/{relId}` | PUT | Eigenschafts-Verknüpfung aktualisieren | +| `/rest/properties/relations/{relId}` | DELETE | Eigenschafts-Verknüpfung löschen | + +--- + +# XI. Bekannte API-Eigenheiten (Quirks) + +1. **contactId fehlt manchmal:** Nicht direkt am Auftrag, nur über `relations.find(r => r.referenceType === 'contact')` erreichbar. Deshalb `relations` immer mit-laden. + +2. **PUT-Response kann leer sein:** `PUT /rest/orders/{id}` gibt manchmal einen leeren Body zurück. Immer prüfen ob Text vorhanden bevor JSON-Parse. + +3. **Amounts-Array kann leer sein:** `order.amounts` ist ein Array, meistens 1 Eintrag, kann aber `[]` sein. Immer `amounts?.[0]?.invoiceTotal` mit Null-Check. + +4. **Status-History-Format variiert:** Manchmal Array, manchmal `{ entries: [] }`. Immer `Array.isArray(x) ? x : (x.entries || [])`. + +5. **Dokument-Download-URL ist anders:** Nicht `/rest/orders/{id}/documents/{docId}`, sondern `/rest/documents/{docId}` — ohne Order-Prefix! + +6. **Generierte Dokumente haben erstmal status=pending:** Nach `generate` sofort fetchen liefert ein unfertiges Dokument. Immer auf `status === 'done'` filtern. + +7. **Fraktionale Status-IDs:** 5.1, 5.12, 5.32, 6.01 etc. — `parseFloat()` verwenden, nie `parseInt()`. + +--- diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9b6682 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Plenty Dojo + +Gesammelte Lektionen aus der Arbeit mit der **Plentymarkets REST API** — von AI-Agenten für AI-Agenten. + +Dieses Repo ist eine geteilte Wissensbasis, die Claude Code (oder andere AI-Coding-Agenten) beim Arbeiten mit Plentymarkets nutzen und erweitern. + +## Was ist drin? + +| Datei | Inhalt | Geteilt? | +|-------|--------|----------| +| `DOJO.md` | Allgemeine API-Patterns & Lektionen | Ja | +| `ANTI-PATTERNS.md` | Was man NICHT tun soll | Ja | +| `instances/.md` | Shop-spezifische Lektionen (Status, IDs, Workflows) | Nein (gitignored) | + +## Installation + +### 1. Repo klonen + +```bash +git clone ~/.claude/plenty-dojo +``` + +### 2. Install-Script ausführen + +```bash +bash ~/.claude/plenty-dojo/install.sh +``` + +Das Script: +- Symlinkt den Skill nach `~/.claude/commands/plenty-dojo.md` +- Erstellt `instances/` mit einer Vorlage für deine Plenty-ID + +### 3. Plenty-ID eintragen + +Öffne `~/.claude/plenty-dojo/instances/.md` und trage deine shop-spezifischen Learnings ein. Diese Datei wird nicht ins geteilte Repo gepusht. + +### 4. CLAUDE.md ergänzen + +Füge in die `CLAUDE.md` deines Projekts (oder global in `~/.claude/CLAUDE.md`) ein: + +```markdown +## Plenty Dojo +- Vor jeder Plenty-Integration die Dojo-Dateien lesen: + - `~/.claude/plenty-dojo/DOJO.md` + - `~/.claude/plenty-dojo/ANTI-PATTERNS.md` + - `~/.claude/plenty-dojo/instances/.md` (falls vorhanden) +- Nach jedem Bugfix prüfen, ob ein neues Learning ins Dojo gehört (`/plenty-dojo lernen`). +``` + +## Nutzung + +Der Skill wird über Claude Code aufgerufen: + +| Befehl | Was passiert | +|--------|-------------| +| `/plenty-dojo` oder `/plenty-dojo lernen` | Analysiert die aktuelle Konversation und schlägt neue Einträge vor | +| `/plenty-dojo zeigen` | Zeigt alle Einträge mit Statistik | +| `/plenty-dojo suchen ` | Durchsucht alle Dojo-Dateien | +| `/plenty-dojo sync` | Holt neue Einträge von anderen und pusht eigene | + +## Beitragen + +Direkt in `DOJO.md` oder `ANTI-PATTERNS.md` schreiben und pushen. Jeder Eintrag hat: + +- **Überschrift** mit fortlaufender Nummer +- **Lektion** — ein Satz +- **Warum** — was genau passiert ist +- **Pattern** — Codebeispiel (richtig) +- **Anti-Pattern** — Codebeispiel (falsch) +- **Entdeckt** — Datum + Kontext + +Der `/plenty-dojo lernen` Befehl hilft beim Formulieren und Einordnen. diff --git a/commands/plenty-dojo.md b/commands/plenty-dojo.md new file mode 100644 index 0000000..3308e62 --- /dev/null +++ b/commands/plenty-dojo.md @@ -0,0 +1,70 @@ +Plenty Dojo — Lerne aus dem aktuellen Kontext und pflege die Plentymarkets-Wissensbasis. + +Das Dojo liegt im Verzeichnis, in dem diese Skill-Datei verlinkt ist: `~/.claude/plenty-dojo/`. +Ermittle den tatsächlichen Pfad dynamisch über den Symlink dieser Datei: + +``` +DOJO_DIR = Verzeichnis in dem DOJO.md liegt (Eltern-Verzeichnis von commands/) +``` + +Die Struktur: +- **DOJO.md** — Allgemeine Plentymarkets-Lektionen (geteilt via Git) +- **ANTI-PATTERNS.md** — Was man NICHT tun soll (geteilt via Git) +- **instances/*.md** — Shop-spezifische Lektionen pro Plenty-ID, z.B. `instances/7843.md` (lokal) + +## Wenn $ARGUMENTS "lernen" enthält oder leer ist: + +1. **Kontext analysieren:** Durchsuche die aktuelle Konversation nach Erkenntnissen über die Plentymarkets API. Suche nach: + - Bugs die durch API-Verhalten verursacht wurden + - Workarounds die nötig waren + - Timing-/Race-Condition-Probleme + - Datenformat-Überraschungen + - Statuswechsel-Eigenheiten + - Sync-Probleme + +2. **Bestehende Einträge lesen:** Lies DOJO.md, ANTI-PATTERNS.md und alle vorhandenen `instances/*.md` Dateien. Prüfe, ob das Learning schon dokumentiert ist. + +3. **Klassifizieren:** Entscheide für jedes neue Learning: + - **Allgemein** (DOJO.md + ANTI-PATTERNS.md) — betrifft jeden Plenty-Entwickler + - **Shop-spezifisch** (instances/.md) — betrifft nur die eigene Konfiguration, Stati, IDs + +4. **Eintrag formulieren:** Jeder Eintrag MUSS enthalten: + - **Überschrift** mit fortlaufender Nummer + - **Lektion** — ein Satz, der das Learning zusammenfasst + - **Warum** — was genau passiert ist und warum es ein Problem war + - **Pattern** — Codebeispiel wie man es richtig macht + - **Anti-Pattern** — Codebeispiel wie man es NICHT macht (wenn sinnvoll) + - **Entdeckt** — Datum und kurzer Kontext + + **WICHTIG:** In geteilten Dateien (DOJO.md, ANTI-PATTERNS.md) niemals konkrete Order-IDs, Kundendaten, Auftragszahlen, Projektnamen oder shop-spezifische Mengen nennen. Stattdessen generische Beschreibungen verwenden. + +5. **Dem User zeigen:** Zeige den formulierten Eintrag und frage: + - "Soll ich das ins Dojo aufnehmen?" + - "Ist das allgemein (DOJO.md) oder shop-spezifisch (instances/.md)?" + +6. **Eintragen:** Nach Bestätigung den Eintrag in die richtige Datei schreiben. Bei allgemeinen Lektionen auch einen Kurzeintrag in ANTI-PATTERNS.md anlegen. + +## Wenn $ARGUMENTS "zeigen" oder "status" enthält: + +Lies und zeige alle Dojo-Dateien mit einer Zusammenfassung: +- Anzahl Einträge pro Datei +- Letzte Ergänzung +- Kategorien der Einträge + +## Wenn $ARGUMENTS "suchen " enthält: + +Durchsuche alle Dojo-Dateien nach dem Begriff und zeige relevante Einträge. + +## Wenn $ARGUMENTS "sync" enthält: + +Synchronisiere das Dojo-Repo mit dem Remote: + +```bash +cd +git pull --rebase +git add DOJO.md ANTI-PATTERNS.md +git diff --cached --quiet || git commit -m "dojo: neue Einträge" +git push +``` + +Zeige danach eine kurze Zusammenfassung: neue Einträge von anderen, eigene gepushte Änderungen. diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..db7ae5b --- /dev/null +++ b/install.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Plenty Dojo — Install Script +# Symlinkt den Skill und richtet die lokale Instanz ein. + +DOJO_DIR="$(cd "$(dirname "$0")" && pwd)" +COMMANDS_DIR="${HOME}/.claude/commands" + +echo "Plenty Dojo installieren..." +echo " Dojo-Verzeichnis: ${DOJO_DIR}" + +# 1. Commands-Verzeichnis sicherstellen +mkdir -p "${COMMANDS_DIR}" + +# 2. Skill symlinken +if [ -L "${COMMANDS_DIR}/plenty-dojo.md" ]; then + echo " Symlink existiert bereits, wird aktualisiert..." + rm "${COMMANDS_DIR}/plenty-dojo.md" +elif [ -f "${COMMANDS_DIR}/plenty-dojo.md" ]; then + echo " Bestehende Skill-Datei wird gesichert nach plenty-dojo.md.bak" + mv "${COMMANDS_DIR}/plenty-dojo.md" "${COMMANDS_DIR}/plenty-dojo.md.bak" +fi + +ln -s "${DOJO_DIR}/commands/plenty-dojo.md" "${COMMANDS_DIR}/plenty-dojo.md" +echo " Skill verlinkt: ${COMMANDS_DIR}/plenty-dojo.md -> commands/plenty-dojo.md" + +# 3. Instances-Verzeichnis erstellen +mkdir -p "${DOJO_DIR}/instances" + +# 4. PID abfragen und Vorlage erstellen +if [ -z "$(ls -A "${DOJO_DIR}/instances/" 2>/dev/null)" ]; then + echo "" + read -rp " Deine Plentymarkets-ID (PID), z.B. 7843: " PID + if [ -n "${PID}" ]; then + cat > "${DOJO_DIR}/instances/${PID}.md" << TEMPLATE +# Plenty Dojo — PID ${PID} + +> Shop-spezifische Lektionen und Patterns für **Plenty-ID ${PID}**. +> Diese Datei enthält interne Workflows, eigene Statuswerte, Konfigurationen und Geschäftslogik. +> +> Allgemeingültige Plentymarkets-Lektionen stehen in \`DOJO.md\` (im Repo-Root). + +--- + + +TEMPLATE + echo " Instanz erstellt: instances/${PID}.md" + else + echo " Übersprungen — du kannst später manuell eine instances/.md anlegen." + fi +else + echo " Instanzen bereits vorhanden: $(ls "${DOJO_DIR}/instances/")" +fi + +echo "" +echo "Fertig! Nutze '/plenty-dojo' in Claude Code." +echo "" +echo "Vergiss nicht, in deiner CLAUDE.md auf das Dojo zu verweisen:" +echo " ~/.claude/plenty-dojo/DOJO.md" +echo " ~/.claude/plenty-dojo/ANTI-PATTERNS.md" +echo " ~/.claude/plenty-dojo/instances/.md"