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

@@ -92,6 +92,21 @@ $PYTHON -c "import datetime; d = datetime.date(YYYY, M, D); print(d.strftime('%A
If any check fails, complete the missing step before reporting done. This rule fires on initial estimate creation AND on every subsequent "add X to the estimate" request. Incident: 2026-05-22, UPS added to estimate #7189 without a ticket note — caught by Winter.
**Emailing an invoice/estimate returns 200 "Email sent" even when it reached NOBODY — never
report delivery from the 200 alone.** `POST /invoices/{id}/email` and `/estimates/{id}/email`
take no recipient param; Syncro picks it from the record. If `customer.email` is null (and no
recipient contact is flagged), the send is a silent no-op that still returns HTTP 200. Before
emailing, confirm the customer has a valid `email`. Note `customer.email` is **UNIQUE
tenant-wide** — reusing one returns `{"success":false,"message":["Email has already been
taken"]}`; a `PUT /customers` that fails returns that error shape (not `{"customer":{...}}`),
so check `.success` on customer writes instead of assuming a silent no-op.
**Living test-findings log:** `.claude/standards/syncro/test-findings.md` records verified
behaviors/gotchas discovered while exercising the skill on the Howard Test sandbox
(customer 36118743). Consult + append to it as new API quirks surface — it is how the skill
avoids re-learning the same trap. Critical entries are folded into these Hard Rules and the
Verified Response Shapes table.
## Implementation
When invoked, use the Syncro REST API via `curl`. All requests include `?api_key=<key>` as query parameter (NOT in header — Syncro uses query param auth).
@@ -568,6 +583,13 @@ Every endpoint's response shape, verified against the live API. Parse exactly as
| Add schedule line | POST `/schedules/{id}/line_items` | `{"schedule_line_item": {...}}` | `.schedule_line_item.id` |
| Update schedule line | PUT `/schedules/{id}/line_items/{li_id}` | `{"schedule_line_item": {...}}` | `.schedule_line_item.quantity`, `.schedule_line_item.price_retail` |
| Delete schedule line | DELETE `/schedules/{id}/line_items/{li_id}` | `{"success": true}` | — |
| Create customer | POST `/customers` | `{"customer": {...}}` | `.customer.id` |
| Update customer | PUT `/customers/{id}` | **success** `{"customer": {...}}` / **error** `{"success": false, "message": [...]}` | check `.success` — email is unique tenant-wide |
| Create contact | POST `/contacts` | **FLAT** `{"id": N, "email": ...}` (NOT wrapped) | `.id` — GET-verify; null-parse ≠ failure |
| Update contact | PUT `/contacts/{id}` | **FLAT** `{"id": N, ...}` | `.id` — `primary`/`receives_invoices` flags are ignored via API |
| Create contract | POST `/contracts` | write OK but body does NOT echo the object | GET `/contracts` + match; never retry on null |
| Email invoice | POST `/invoices/{id}/email` | `{"message": "Email sent."}` | **200 even with NO recipient** — verify `customer.email` first |
| Email estimate | POST `/estimates/{id}/email` | `{"message": "Email sent"}` | **200 even with NO recipient** — verify `customer.email` first |
**Invoice GET line_items field names differ from ticket line_items:** `item` = product name, `name` = description, `price` = unit rate. Do not use `price_retail` when reading invoice line items.