diff --git a/ANTI-PATTERNS.md b/ANTI-PATTERNS.md
index 26c5e8f..e5c5d74 100644
--- a/ANTI-PATTERNS.md
+++ b/ANTI-PATTERNS.md
@@ -245,6 +245,21 @@ await fetch('/rest/batch', { body: JSON.stringify({ payloads }) });
---
+## 18. Property-Relation aktualisieren ohne valueId
+
+```javascript
+// FALSCH: PUT ohne id in relationValues → HTTP 200, aber kein Update!
+await client.put(`/rest/properties/relations/${existing.id}`, {
+ relationValues: [{ value: 'Ja', lang: 'de' }], // id fehlt → silent noop
+});
+```
+
+**Problem:** Plenty akzeptiert den PUT mit HTTP 200, aber der Wert wird nicht gespeichert, wenn `relationValues[0].id` fehlt. Kein Fehler, kein Hinweis — es passiert einfach nichts.
+
+**Fix:** Prüfen ob `valueId` vorhanden. Wenn nicht: Relation löschen und neu erstellen (DELETE + POST). **Siehe DOJO.md #32**
+
+---
+
## 17. Fester Delay für lang laufende Bulk-Operationen
```javascript
diff --git a/DOJO.md b/DOJO.md
index 732fdf8..3500fee 100644
--- a/DOJO.md
+++ b/DOJO.md
@@ -573,30 +573,46 @@ db.function('business_hours', (startIso, endIso) => {
---
-## 25. Property-Relation erstellen (Selection-Eigenschaft)
+## 25. Property-Relation erstellen (Item und Kontakt)
-**Lektion:** Um eine Auswahl-Eigenschaft an einer Variation zu setzen, einen POST auf `/rest/properties/relations` senden.
+**Lektion:** `/rest/properties/relations` funktioniert für Items **und** Kontakte — nur `relationTypeIdentifier` ändert sich.
-**Pattern:**
+**Pattern (Item-Eigenschaft, Selection):**
```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
- })
+await client.post('/rest/properties/relations', {
+ propertyId: 5,
+ relationTypeIdentifier: 'item', // für Variationen
+ relationTargetId: variationId, // mainVariationId
+ selectionRelationId: 48 // Auswahlwert-ID
});
```
-**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)`
+**Pattern (Kontakt-Eigenschaft, Textwert):**
+```javascript
+await client.post('/rest/properties/relations', {
+ propertyId: 52,
+ relationTypeIdentifier: 'contact', // für Kontakte
+ relationTargetId: contactId,
+ relationValues: [{ value: 'Ja', lang: 'de' }], // 'lang' ist Pflichtfeld!
+});
+```
-**Entdeckt:** 2026-04-08. Artikeltyp-Eigenschaft (Property 5) auf Variationen setzen.
+**Lesen (Kontakt):**
+```javascript
+const resp = await client.get('/rest/properties/relations', {
+ params: { propertyId, relationTypeIdentifier: 'contact', relationTargetId: contactId },
+});
+const entries = Array.isArray(resp.data) ? resp.data : (resp.data.entries || []);
+const existing = entries.find(r => r.relationTargetId === contactId);
+```
+
+**Besonderheiten:**
+- `selectionRelationId` bei Items wird als `relationValues[0].value` gespeichert, nicht im gleichnamigen Feld.
+- **Unique Constraint** auf `propertyId + targetId + type`: Duplikat-POST → HTTP 500.
+- `lang` ist bei `relationValues` ein Pflichtfeld — ohne `lang` wird der Wert nicht gespeichert.
+- Response-Format inkonsistent: mal Array, mal `{ entries: [] }` — immer beide Fälle behandeln.
+
+**Entdeckt:** 2026-04-08 (Items), 2026-04-10 (Kontakte).
---
@@ -772,6 +788,11 @@ await sleep(delay * 2); // Extra-Pause vor Retry
| `/rest/properties/relations/{relId}` | PUT | Eigenschafts-Verknüpfung aktualisieren |
| `/rest/properties/relations/{relId}` | DELETE | Eigenschafts-Verknüpfung löschen |
| `/rest/batch` | POST | Bis zu 20 API-Calls in einem Request bündeln |
+| `/rest/user` | GET | Eigene User-ID und Name des API-Users abrufen |
+| `/rest/messages` | POST | Kontaktnotiz erstellen (categoryId: 3 = Kontaktnotizen-Tab) |
+| `/rest/accounts/contacts` | GET | Kontakte laden (filter: classId, with: options) |
+| `/rest/accounts/contacts/{id}` | PUT | Kontakt aktualisieren (z.B. classId ändern) |
+| `/rest/accounts/contacts/classes` | GET | Alle Kundenklassen laden (flaches Objekt, kein Array!) |
---
@@ -791,4 +812,94 @@ await sleep(delay * 2); // Extra-Pause vor Retry
7. **Fraktionale Status-IDs:** 5.1, 5.12, 5.32, 6.01 etc. — `parseFloat()` verwenden, nie `parseInt()`.
+8. **Property-Relation PUT ohne valueId wird silent ignoriert:** Wenn `relationValues[0].id` fehlt, akzeptiert Plenty das PUT mit HTTP 200, speichert aber nichts. Immer DELETE + POST als Fallback. **Siehe #32**
+
+---
+
+# XII. Kontakt-API
+
+## 31. Kontaktnotizen via Messenger-API erstellen
+
+**Lektion:** Kontaktnotizen im "Kontaktnotizen"-Tab erscheinen **nicht** über einen dedizierten Notiz-Endpoint, sondern über `POST /rest/messages` mit `categoryId: 3`.
+
+**Warum:** Der alte Notiz-Endpoint (`/rest/contacts/{id}/notes` o.ä.) erzeugt Einträge, die im falschen Tab landen oder nicht sichtbar sind. Die Messenger-API mit `categoryId: 3` ist der korrekte Weg.
+
+**Pattern:**
+```javascript
+// 1. Erst API-User-ID ermitteln (Pflichtfeld für 'from'!)
+const userResp = await client.get('/rest/user');
+const userId = userResp.data?.id;
+const userName = userResp.data?.real_name || userResp.data?.user || 'System';
+
+// 2. Text HTML-escapen und Zeilenumbrüche konvertieren
+const htmlText = escapeHtml(text).replace(/\n/g, '
');
+
+// 3. Notiz erstellen
+await client.post('/rest/messages', {
+ from: { type: 'user', value: userId, name: userName },
+ to: { user: [], role: [], allUsers: false },
+ whispered: true,
+ title: 'Customer notes',
+ message: `
${htmlText}
`, + referrer: { type: 'backend', value: userId, name: userName }, + linkedTo: [{ type: 'contact', value: contactId }], + categoryId: 3, // 3 = Kontaktnotizen-Tab +}); +``` + +**Wichtig:** +- `from.value` muss eine gültige User-ID sein — nicht weglassen oder erfinden +- `message` muss HTML sein (kein Plaintext), Text vorher escapen +- `linkedTo` verknüpft die Notiz mit dem Kontakt +- `whispered: true` macht die Notiz intern (nicht an Kunden sichtbar) + +**Entdeckt:** 2026-04-10. Atradius-LimitMgmt-Projekt, Kontaktnotizen nach Limit-Schließung. + +--- + +## 32. Property-Relation aktualisieren: PUT ohne valueId ist ein silent Noop + +**Lektion:** Wenn eine bestehende Property-Relation in `relationValues[0]` keine `id` hat, wird ein `PUT` auf die Relation mit HTTP 200 beantwortet, aber der Wert **nicht** gespeichert. + +**Warum:** Plenty erwartet beim PUT die interne Value-ID aus `relationValues[0].id`. Fehlt sie, akzeptiert die API den Request, tut aber nichts. + +**Pattern (sicher):** +```javascript +const existing = entries.find(r => r.relationTargetId === contactId); + +if (existing) { + const valueId = existing.relationValues?.[0]?.id; + + if (valueId) { + // Value-ID bekannt → normaler PUT + await client.put(`/rest/properties/relations/${existing.id}`, { + relationValues: [{ id: valueId, value: newValue, lang: 'de' }], + }); + } else { + // Keine Value-ID → DELETE + POST (PUT würde silent ignoriert) + await client.delete(`/rest/properties/relations/${existing.id}`); + await client.post('/rest/properties/relations', { + propertyId, relationTypeIdentifier: 'contact', relationTargetId: contactId, + relationValues: [{ value: newValue, lang: 'de' }], + }); + } +} else { + // Noch keine Relation → POST + await client.post('/rest/properties/relations', { + propertyId, relationTypeIdentifier: 'contact', relationTargetId: contactId, + relationValues: [{ value: newValue, lang: 'de' }], + }); +} +``` + +**Anti-Pattern:** +```javascript +// FALSCH: PUT ohne id in relationValues → HTTP 200, aber kein Update! +await client.put(`/rest/properties/relations/${existing.id}`, { + relationValues: [{ value: newValue, lang: 'de' }], // id fehlt → noop +}); +``` + +**Entdeckt:** 2026-04-10. Atradius-LimitMgmt. Wert nach PUT immer noch der alte. + ---