---
name: Syncro lessons — incidents, quotes, and the events behind the rules
description: Detail and incident archive backing the Syncro feedback rules. Read this when you need to judge an edge case, verify a rule is still right, or understand WHY a rule exists. Tickets, dates, verbatim quotes, and tech user_id table live here so the main rule files can stay terse. Companion to feedback_syncro_api / billing / workflow.
metadata:
  type: feedback
---

This file is the **incident archive** behind [[feedback_syncro_api]], [[feedback_syncro_billing]], and [[feedback_syncro_workflow]]. Those three files state the rules; this one preserves the events, quotes, and specifics so you can judge edge cases and verify a rule still applies. Read on-demand, not at session start.

---

## Tech user_id table

Used by [[feedback_syncro_billing]] Section "Corrections preserve attribution".

| Tech    | user_id |
|---------|---------|
| Mike    | `1735`  |
| Howard  | `1750`  |
| Winter  | `1737`  |
| Rob     | `1760`  |

---

## Labor / hardware product IDs (verify rates live)

Used by [[feedback_syncro_billing]]. Rates change — always `GET /products/<id>` for `.product.price_retail`. The product IDs are stable; the table in `.claude/commands/syncro.md` is authoritative for the live list.

| product_id | name                                          | role                     | typical rate |
|------------|-----------------------------------------------|--------------------------|--------------|
| `1190473`  | Labor - Remote Business                       | remote (most common)     | $150         |
| `26118`    | Labor - Onsite Business                       | onsite                   | $175         |
| `573881`   | Labor - In-Shop Business                      | in-shop                  | (check live) |
| `26184`    | Labor - Emergency or After Hours Business     | emergency/after-hours    | $262.50      |
| `1049360`  | Labor- Warranty work                          | warranty / no-charge     | $0           |
| `32252`    | Hardware                                      | generic estimate hardware| $0 base      |

---

## Incidents — by ticket / date

### #32333 (2026-05-28) — Content-Type missing
- **Posted a comment** without `-H "Content-Type: application/json"`. Syncro returned a **400 HTML error page** twice before the fix. The HTML response looks like a hard failure but it's just a missing header.
- **Lesson** → [[feedback_syncro_api]] Section "Content-Type required".

### #32332 (Cascades — Chris Knight new-user setup, 2026-05-27) — multi-lesson
This one ticket informed several rules at once:

1. **Fabricated labor names.** Product `26118` ("Labor - Onsite Business") was billed on two lines as `"Emergency Call Setup"` and `"Onsite Computer Setup"` — both invented. Breaks the Syncro → QuickBooks sync because QB maps each labor line to an existing item.
   - Mike's exact words: *"You CANNOT make up labor items. You MUST use existing items only for all labor items… the labor item must use the ones that already exist in syncro (otherwise it messes things up in Quickbooks)."*
   - **Lesson** → [[feedback_syncro_billing]] "Never invent labor names".

2. **Prepaid emergency billed as a split made-up onsite/emergency.** 1.5 hours of emergency on a prepaid customer (Cascades has 27h block). Howard had split it into fabricated onsite + emergency lines. Correct shape: ONE line on `26184` at quantity `2.25` (hrs×1.5).
   - This also confirmed the **2026-05-27 update**: prepaid emergency now uses `26184` at ×1.5 quantity, REPLACING the older "prepaid → onsite `26118` at ×1.5" rule. 26184 labels the line correctly as emergency and maps right in QuickBooks.
   - **Lesson** → [[feedback_syncro_billing]] "Emergency labor".

3. **Correction via Mike's API key would have stolen Howard's commission.** The original line was Howard's (`user_id=1750`). Correcting via `update_line_item` preserved `user_id=1750`. A remove + re-add would have defaulted the new line to Mike's `1735` (the API-key owner).
   - **Lesson** → [[feedback_syncro_billing]] "Corrections preserve attribution".

4. **Ticket ownership is sticky.** Mike confirmed (also 2026-05-27): adding notes or labor to a ticket does NOT change the ticket owner. Multiple techs routinely work the same ticket. Only change ticket ownership when explicitly asked.
   - **Lesson** → [[feedback_syncro_billing]] same section.

### #32312 (Winter, 2026-05-21) — wrong day of week
- "Saturday" was computed as **May 24**, which is actually a **Sunday**. The appointment landed on the wrong day and didn't appear where Winter expected it on the calendar.
- **Lesson** → [[feedback_syncro_workflow]] "Verify appointment day-of-week". Always display `Day YYYY-MM-DD` (e.g., "Saturday 2026-05-23") in the preview, never the numeric date alone.

### 2026-05-21 (Mike) — timer workflow superseded
- Mike confirmed the timer workflow (`timer_entry → charge_timer_entry`) is not used. The previous rule requiring timers was wrong and caused repeated billing failures (wrong product on the timer, `product_id` silently ignored by `charge_timer_entry`).
- **Lesson** → [[feedback_syncro_billing]] "Bill with add_line_item directly". The timer-response-shape rule in [[feedback_syncro_api]] is now historical / reference-only.

### #32304 (Cascades, 2026-05-20) — hardcoded rates wrong
- Hardcoded rate table in the skill had Labor - Remote Business at $150/hr. The correct rate was $175/hr. Rates vary by contract and change over time.
- **Lesson** → [[feedback_syncro_billing]] "Fetch rates live".

### Kittle Design #32263 (Howard, 2026-05-08) — appointment owner
- Howard created an 11:30 AM onsite to set up Joshua. I auto-added Howard's `user_id` to the appointment's `user_ids` array without confirming whether Howard was the owner or just an attendee.
- Howard's direction: *"when setting up an appointment confirm the appointment owner — don't just add additional attendees."*
- **Lesson** → [[feedback_syncro_workflow]] "Ask the appointment owner explicitly".

### #32225 (Sombra Residential, 2026-05-06) — warranty hack
- Picked product `1190473` (Labor - Remote Business, $150/hr) for a warranty cleanup, set `billable: false` on the (then-used) timer, and assumed the timer flag would zero the line. Syncro silently overrode `billable: false` and the line came in at **$75**. I patched `price_retail` to `$0` to "fix" it.
- Howard caught it: warranty has its own product in the dropdown, and **patching dollar amounts is never how this is solved**. Use `1049360`.
- The earlier guidance in `.claude/commands/syncro.md` ("Warranty → use closest labor product with `billable=false`") was wrong; warranty has its own product like Onsite, Remote, Emergency.
- **Lesson** → [[feedback_syncro_billing]] "Warranty → product 1049360, never patch the price".

### #32253 (Cascades, 2026-05-05) — duplicate timers from wrong jq path (HISTORICAL)
- The skill doc used `TIMER_ID=$(jq -r '.timer.id // .timer_entry.id')` on a flat response → resolved to `null`. A `null` TIMER_ID broke `charge_timer_entry` ("Not found"). The script retried and created a duplicate timer.
- Created two 0.5hr duplicate timers; deleted one via `delete_timer_entry`.
- **Lesson** → [[feedback_syncro_api]] timer section. Timers are no longer the workflow (see 2026-05-21), so this is reference-only.

### 2026-05-04 (Winter) — wrong labor type default, blank-contact rule generalized
Two adjacent lessons:
1. Tickets I created used "Prepaid project labor" as the auto-selected labor type. That product is **exempt** and does NOT consume hours from a customer's prepaid block. Block-hour accounting silently drifted. Winter is fixing them retroactively.
   - **Lesson** → [[feedback_syncro_billing]] "Labor type matches delivery channel".
2. Winter generalized the prior Cascades-only blank-contact rule to "most customers" — blank `contact_id` lets Syncro apply company-level email defaults, which route to the right people. Setting a specific contact overrides that.
   - **Lesson** → [[feedback_syncro_workflow]] "Leave contact blank by default".

### #32203 (Desert Auto Tech, 2026-04-23) — emergency was additive
- Billed "1h onsite + 1h emergency" as two additive lines = **$437.50**. The correct shape is ONE emergency line at time-and-a-half — 1 actual hour of emergency should bill at $262.50 (or $225 remote).
- **Lesson** → [[feedback_syncro_billing]] "Emergency = ×1.5 applied once".

### #32142 (2026-04-23) — comment dup from wrapper-key error
- POST to `/comment` returned `{"comment": {...}}`. The script parsed `.id` (returning `null`), saw an error, and retried — creating a duplicate.
- **Lesson** → [[feedback_syncro_api]] "Response wrappers". POST `/comment` response is `{"comment": {...}}` — use `.comment.id`. Also: when GETting to verify, check ALL comments not just `[-3:]` — the new comment may not be the most recent if other activity occurred.

### 2026-04-22 — ticket dup from retry
- A `POST /tickets` response looked wrong (null fields, jq error). The response wrapper is `{"ticket": {...}}` — `.ticket.id` not `.id`. Retried the POST, creating a duplicate ticket.
- **Lesson** → [[feedback_syncro_api]] "No idempotency — GET before retry" applies to ticket creation, not just comments.

### #32185 — comment dup from shell-quoting error
- Subject contained an em-dash (`—`) → shell interpolation issue → the POST appeared to fail but actually succeeded. Retry created a duplicate comment. Comments cannot be deleted via API.
- **Lesson** → [[feedback_syncro_api]] "No idempotency". Hardening: write comment payloads to a temp file (e.g. `tmp/syncro_comment.json`) before posting — avoids shell quoting/encoding failures that produce misleading errors on requests that actually succeeded.

### HTML formatting incident — `<ul>/<li>` rendered as one line
- Posted a comment with `<ul><li>` items; Syncro's renderer collapsed them into a single line with no spacing. Had to post a corrected duplicate.
- **Lesson** → [[feedback_syncro_api]] "HTML formatting". Use `<br>` inside a `<p>` wrapper for bulleted lists.

### Cascades — Meredith Kuhn keeps being the wrong default
- At Cascades of Tucson (`customer_id 20149445`), Syncro's contact picker repeatedly pre-selects **Meredith Kuhn** (Assistant Manager, ASSISTMAN-PC). She's the wrong contact — assigning her overrides distribution emails and routes notifications only to her.
- Howard surfaced the pattern; Mike confirmed the global blank-contact rule on 2026-05-24.
- **Lesson** → [[feedback_syncro_workflow]] "Cascades-specific guard".

---

## Superseded rules — kept for context

| Rule (old)                                                  | Replaced by                                                              | Date         |
|-------------------------------------------------------------|--------------------------------------------------------------------------|--------------|
| All time billing through `timer_entry → charge_timer_entry` | Bill with `add_line_item` directly                                       | 2026-05-21   |
| Prepaid emergency → onsite `26118` at ×1.5                  | Prepaid emergency → emergency `26184` at qty ×1.5                        | 2026-05-27   |
| Blank contact only at Cascades                              | Blank contact for ALL customers by default                               | 2026-05-04   |
| Warranty → closest labor product with `billable=false`      | Warranty → product `1049360` (its own product)                           | 2026-05-06   |

---

## Related ACG-internal references

- **Skill doc:** `.claude/commands/syncro.md` — current labor product table, billing workflow, examples.
- **API docs:** `api-docs.syncromsp.com` (Swagger spec).
- **Tenant attribution rule (cross-product):** per-user-key attribution; see `/syncro` Attribution rule and `[[feedback_psa_default_syncro]]`.
