---
name: Syncro billing rules — products, rates, taxes, attribution, emergency, warranty
description: How to bill a Syncro ticket correctly — fetch live rates, use real product names, pick the right labor type for the delivery channel, set taxable=false on labor (AZ), warranty product 1049360, emergency ×1.5 (branch by prepay_hours), preserve original tech's user_id on corrections, estimate hardware uses product 32252.
metadata:
  type: feedback
---

Rules only. Incident detail, verbatim Mike quotes, ticket numbers, dates, the tech user_id table, and the labor-product table all live in [[feedback_syncro_history]] — read on-demand when judging an edge case. API mechanics: [[feedback_syncro_api]]. Workflow: [[feedback_syncro_workflow]].

**Run billing THROUGH the `/syncro` skill — do not hand-roll these calls.** The rules below describe what the skill does when it bills; they are the spec, not a license to free-hand `curl` from memory (that produces malformed tickets Winter has to fix). Invoke `/syncro` (after-hours -> `/syncro-emergency-billing`). See [[feedback_skill_first_routing]]. "`add_line_item` directly" (§1) means "not the timer workflow" — NOT "bypass the skill."

`.claude/commands/syncro.md` is the authoritative live product table.

---

## 1. Bill with `add_line_item` directly — never the timer workflow

`POST /tickets/{id}/add_line_item` is the billing path for ALL work (labor, warranty, internal, hardware). The timer workflow (`timer_entry → charge_timer_entry`) is **not used**.

**Payload:** `product_id`, `quantity` (decimal hours), `price_retail` (fetched live), `name` (the product's REAL name — see §3), `description` (free-text work narrative), `taxable: false` for labor.

---

## 2. ALWAYS fetch the rate live

Fetch `price_retail` from `GET /products/<id>` → `.product.price_retail` before billing. The product-ID table in `.claude/commands/syncro.md` is valid for IDs but **not** dollar amounts — rates vary by contract and change.

```bash
RATE=$(curl -s "$BASE/products/$PRODUCT_ID?api_key=$API_KEY" | jq -r '.product.price_retail')
```

Use `$RATE` for drafts, the user preview, and the `price_retail` field.

---

## 3. NEVER invent or rename labor line items

Every labor line MUST be an existing Syncro product, billed under its **REAL name** (from `GET /products/<id>` → `.product.name`, verbatim). Work-specific narrative goes in `description`, never the `name`.

**Why:** invented names break the Syncro → QuickBooks sync. QB maps each labor line to an existing item; a fabricated name has no QB match and messes up the accounting. If no existing product fits, **STOP and ask Mike** — never invent one.

Product table lives in [[feedback_syncro_history]] (and `.claude/commands/syncro.md`).

---

## 4. Labor type must match delivery channel — never "Prepaid project labor"

Pick the labor product matching how work was delivered: **remote** (most common), **onsite**, **in-shop**, or **web**. Resolve `product_id` via `GET /products?search=remote+labor` etc.

**Never default to "Prepaid project labor"** — it is **exempt** and does NOT consume hours from a customer's prepaid block. Block accounting silently drifts.

**Verify:** after billing a prepay-block customer, confirm the block balance dropped by the expected hours. If it didn't, the labor type was wrong.

---

## 5. Labor is NEVER taxable in Arizona

Pass `"taxable": false` explicitly on every labor line. The product config has `taxable: false`, but `add_line_item` does **not** inherit it — posts as `taxable: true` regardless. Applies to remote, onsite, in-shop, emergency, warranty, prepaid.

---

## 6. Warranty / no-charge → product `1049360`, never patch the price

Warranty work uses `product_id: 1049360` ("Labor- Warranty work", $0/hr, non-taxable). The line generates at $0 from `price_retail × quantity` — no need to flag or patch anything.

**Do NOT** pick a regular labor product and try to neutralize it with `billable: false` or by patching `price_retail` to `0`. **Prices are set by selecting the correct product.** If you reach for `update_line_item` to drop a price, that's the signal to back up and pick a different `product_id`.

The only legitimate `update_line_item price_retail` use is the Syncro auto-gen-zero recovery case (auto-line came in at $0 instead of the product's rate).

---

## 7. Emergency / after-hours — ×1.5 applied ONCE; branch by `prepay_hours`

`GET /customers/<id>` and read `prepay_hours` BEFORE adding any emergency line. Emergency = **time-and-a-half, applied ONCE**. Never bill a separate regular line + emergency line for the same hours.

**No prepaid block (`prepay_hours == 0`):**
- product `26184`, quantity = **actual hours** (do NOT also ×1.5 the quantity)
- `price_retail` by delivery channel (the 1.5× lives in the dollars):
  - Onsite emergency = `$262.50` (175 × 1.5; 26184's default).
  - Remote / In-Shop emergency = `$225` (150 × 1.5) → override `price_retail` to `225`.

**Prepaid block (`prepay_hours > 0`):**
- product `26184`, quantity = **actual hours × 1.5** (premium goes in the QUANTITY)
- Delivery channel / dollar rate is irrelevant; prepaid blocks debit by quantity. Invoice nets to $0; block debits hrs×1.5.
- e.g. 1.5 emergency hrs → `26184` @ `2.25`.

Always set `price_retail` explicitly — the rate doesn't auto-populate and the line posts $0 if omitted. Verify after: `.invoice.total` (non-prepaid) or block decrement (prepaid).

---

## 8. API key follows the BILLING TECH — always

**Attribution is determined by which API key you use.** Every `add_line_item` / `remove_line_item` call is logged as the owner of that key. `user_id` in the payload does NOT override this.

**Common-sense defaults (confirmed by Howard 2026-06-23):**
- Howard asks for billing → use Howard's key (he's billing himself)
- Mike asks for billing → use Mike's key
- Told "put X hours in for [tech]" → use that tech's key, regardless of who is asking
- Split ticket ("2 hrs for Mike, 1 hr for Howard") → two separate `add_line_item` calls, each with the correct tech's key

**Vault paths:**
- Howard → `msp-tools/syncro-howard.sops.yaml` → `credentials.credential`
- Mike → `msp-tools/syncro.sops.yaml` → `credentials.credential`

```bash
HOWARD_KEY=$(bash .claude/scripts/vault.sh get-field msp-tools/syncro-howard credentials.credential)
MIKE_KEY=$(bash .claude/scripts/vault.sh get-field msp-tools/syncro credentials.credential)

# Each line item call uses the BILLING TECH's key as a query param:
curl -s -X POST "https://computerguru.syncromsp.com/api/v1/tickets/{id}/add_line_item?api_key=${HOWARD_KEY}" ...
curl -s -X POST "https://computerguru.syncromsp.com/api/v1/tickets/{id}/add_line_item?api_key=${MIKE_KEY}" ...
```

**Auth note:** `?api_key=` is the attribution mechanism. The `Authorization: <key>` header works for reads but does NOT control line-item attribution — always use `?api_key=` for billing writes.

**Corrections:** wrong key used → `remove_line_item` with any key (doesn't matter), then re-`add_line_item` with the correct tech's key. `update_line_item` does NOT fix `user_id`.

**Ticket ownership:** adding notes or labor does NOT change `.ticket.user_id`. Multiple techs routinely work the same ticket. Only change ticket ownership when explicitly asked.

Tech user_id table → [[feedback_syncro_history]].

---

## 9. Estimate hardware → product `32252`

All hardware on estimates uses one generic product: `product_id: 32252` ("Hardware", `price_retail: 0.0`). Differentiate via `name` ("Dell OptiPlex 7010") and `price_retail` (actual cost). Hardware is typically `taxable: true`. Never look up individual hardware product IDs — there's only one.
