dojo: Kontaktnotizen (REST messages), Contact Properties, PUT-silent-noop (#31, #32)

This commit is contained in:
Liborius
2026-04-10 14:43:33 +00:00
parent d5e3504f38
commit b4604877f3
2 changed files with 143 additions and 17 deletions

145
DOJO.md
View File

@@ -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, '<br>');
// 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: `<p>${htmlText}</p>`,
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.
---