125 lines
7.1 KiB
Markdown
125 lines
7.1 KiB
Markdown
---
|
||
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]].
|
||
|
||
`.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.
|