syncro: capture Phase-1 live-test findings (email-delivery trap + response shapes)

Test campaign on Howard Test sandbox (customer 36118743) surfaced traps the skill must not
re-learn:
- invoice/estimate email endpoints return 200 "Email sent" even with NO recipient
  (customer.email null) -> never report delivery from the 200; verify customer.email first.
- customer.email is UNIQUE tenant-wide; PUT /customers failure returns
  {success:false,message:[...]} not {customer:{...}} -> check .success.
- POST/PUT /contacts return a FLAT object (.id), not {contact:{...}}; contact recipient
  flags (primary/receives_invoices) are ignored via API.
- POST /contracts write succeeds but body doesn't echo the object; /contracts?customer_id
  doesn't filter server-side -> GET-verify + client-filter.

New living log .claude/standards/syncro/test-findings.md; critical items folded into
syncro.md Hard Rules + Verified Response Shapes. Correction logged to errorlog.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 15:59:35 -07:00
parent 996041406c
commit 12bb9f1544
3 changed files with 75 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
# Syncro Skill — Live Test Findings (running log)
Verified behaviors/gotchas discovered while testing `/syncro` against the live tenant on the
**Howard Test** sandbox customer (`customer_id 36118743`) + RMM-TEST-MACHINE. Newest on top.
Each entry is a rule the skill must not re-learn. Critical ones are also folded into
`syncro.md` (operating manual) and `api-reference.md` (breadth reference).
## 2026-07-10 — Phase 1 (PSA CRUD)
**Emailing an invoice/estimate returns 200 "Email sent" even when it delivered to NOBODY.**
`POST /invoices/{id}/email` and `POST /estimates/{id}/email` take **no recipient param**
Syncro picks the recipient from the record server-side. If `customer.email` is null (and no
contact is a flagged recipient), the API still returns `{"message":"Email sent"}` HTTP 200 but
nothing is sent. **NEVER report an email as delivered based on the 200 alone.** Ensure the
customer has a valid `email` (or a recipient contact) BEFORE emailing, and confirm real
delivery out-of-band when it matters.
**`customer.email` is UNIQUE across the tenant.** `PUT /customers/{id}` with an email already
used elsewhere returns `{"success":false,"message":["Email has already been taken"]}` and does
NOT set it. For a test customer that must reach a real person whose address is already in
Syncro, use plus-addressing (`user+tag@domain`) — unique in Syncro, still routes to the inbox
IF the mail tenant has plus-addressing enabled (M365: off by default in some tenants — verify).
**`PUT /customers/{id}` uses two different response shapes — always check for the error shape.**
Success → `{"customer":{...}}`. Validation failure → `{"success":false,"message":[...]}` with
the field unchanged. Parsing only `.customer.email` hides the failure (looks like a silent
no-op). Read the raw body / check `.success == false` on customer writes.
**Contacts return a FLAT object, not wrapped.** `POST /contacts` and `PUT /contacts/{id}`
return `{"id":N,"name":...,"email":...}` directly — NOT `{"contact":{...}}`. Parse `.id`, not
`.contact.id`. (The wrapped-parse returns null and looks like a failure when the write
succeeded — a blind retry then DUPLICATES the contact. GET-verify, never retry.)
**Contact recipient flags (`primary`, `receives_invoices`, `receives_reports`) are NOT settable
via the API** — `PUT /contacts/{id}` accepts them in the body but silently ignores them (stay
null). To designate an invoice-recipient contact, set it in the Syncro GUI.
**`POST /contracts` write succeeds but the response does not echo the created object** (fields
parse null). GET `/contracts` and match to confirm creation — never retry on the null.
**`GET /contracts?customer_id=N` does NOT filter server-side** — it returns ALL contracts.
Filter client-side by `.customer_id`.
**Reinforces the standing rule:** null/blank fields in a Syncro write response are NOT proof of
failure. Syncro has no idempotency, so a blind retry duplicates. Always GET the resource to
confirm true state before any follow-up action.
### Verified-working this phase (no gotcha, for coverage record)
POST/GET customer; POST contact; POST ticket (+ number); POST ticket comment (hidden);
POST add_line_item; POST invoice (from ticket); POST estimate + line_items + PUT recalc;
POST appointment; POST schedule (paused). All parse under the shapes documented in `syncro.md`.