diff --git a/.claude/commands/syncro.md b/.claude/commands/syncro.md index 8ec5133..5fe2e81 100644 --- a/.claude/commands/syncro.md +++ b/.claude/commands/syncro.md @@ -33,6 +33,10 @@ Create, update, close, comment on, and bill tickets in Syncro PSA. **Billing:** Always ask for minutes and labor type before adding any line item. Never assume a default. +**Emergency/after-hours billing — check prepaid first:** Before adding a `26184` (Emergency) line item, `GET /customers/` and read `prepay_hours`. If `prepay_hours > 0`, the customer has a prepaid block — bill `26118` (Onsite) at `quantity × 1.5` instead (prepaid debits by quantity, not by dollars). Never stack `26118` + `26184` for the same hours — the Emergency product rate already has the 1.5× multiplier baked in. Verified 2026-04-23 on ticket #32203 (Desert Auto Tech) after Winter caught the bug. + +**Line-item `price_retail` MUST be set explicitly:** Earlier guidance to "omit `price_retail` and let Syncro auto-calc from the product rate" was wrong — the rate does NOT populate automatically. Fetch it with `GET /products/` → `.product.price_retail` and pass it on `add_line_item`. Omitting it leaves the line at $0.00 and the invoice posts at $0.00 (verified 2026-04-23 on #32203). + ## Implementation When invoked, use the Syncro REST API via `curl`. All requests include `?api_key=` as query parameter (NOT in header — Syncro uses query param auth). @@ -220,19 +224,39 @@ curl -s -X POST "${BASE}/tickets/${ID}/delete_timer_entry?api_key=${API_KEY}" \ - `description` — required (422 if missing) - `product_id` — labor product ID (see list below) - `quantity` — decimal hours (0.5 = 30 min, 1.0 = 1 hour) -- `price_retail` — **only price field that saves**; `price`, `retail_price`, `rate`, `price_cents` all silently ignored and leave line at $0.00. **Do not hardcode rates** — omit `price_retail` and Syncro auto-calculates from the product's configured rate. +- `price_retail` — **only price field that saves**; `price`, `retail_price`, `rate`, `price_cents` all silently ignored and leave line at $0.00. **Always set `price_retail` explicitly.** Omitting it leaves the line at $0.00 and the invoice generates at $0 (verified 2026-04-23 on #32203). Fetch the current rate with `GET /products/` → `.product.price_retail`, then pass that value on `add_line_item`. - `taxable: false` — **always set explicitly**; labor products default to no-tax in GUI but the API applies tax if this is omitted **Do NOT remove ticket line items after invoicing.** Leave them on the ticket — the "Add/View Charges" button and billing verification by techs depends on seeing line items there. -**Labor product IDs:** -- `1190473` — Labor - Remote Business (standard remote work) -- `26118` — Labor - Onsite Business -- `26184` — Labor - Emergency or After Hours Business -- `9269129` — Labor - Prepaid Project Labor -- `9269124` — Labor - Internal Labor -- `26117` — Fee - Travel Time -- `68055` — Labor - Website Labor +**Labor product IDs** (rates verified 2026-04-23; always fetch live with `GET /products/` before billing): + +| Product ID | Name | Rate | Notes | +|---|---|---|---| +| `1190473` | Labor - Remote Business | — | standard remote work | +| `26118` | Labor - Onsite Business | $175.00/hr | base onsite rate | +| `26184` | Labor - Emergency or After Hours Business | $262.50/hr | **1.5× onsite; time-and-a-half baked into the rate.** Non-prepaid customers only. Do NOT stack with `26118` for the same hours. | +| `9269129` | Labor - Prepaid Project Labor | — | debits from customer `prepay_hours` bank | +| `9269124` | Labor - Internal Labor | — | | +| `26117` | Fee - Travel Time | — | | +| `68055` | Labor - Website Labor | — | | + +**Emergency / after-hours billing branches by whether customer has prepaid labor:** + +Check: `GET /customers/` → `.customer.prepay_hours` (string; `"0.0"` means no prepaid, any non-zero means prepaid block exists). + +| `prepay_hours` | Regular hours | Emergency / after-hours | +|---|---|---| +| `0` / null (no prepaid) | `26118`, qty = actual_hours | `26184`, qty = actual_hours (rate already 1.5×) | +| `> 0` (has prepaid block) | `26118`, qty = actual_hours | `26118`, qty = actual_hours × **1.5** | + +**Rationale (Winter, 2026-04-23):** Prepaid blocks debit by QUANTITY, not dollars. To charge time-and-a-half against a prepaid block we bump the quantity to 1.5× on the Onsite product rather than switching to the Emergency product — switching would double-count because the Emergency product has the 1.5× already built into its dollar rate. + +**Example — 2 hour emergency onsite job:** +- Non-prepaid customer: one line of 2.0 hrs × `26184` → $525.00 billed +- Prepaid customer: one line of 3.0 hrs × `26118` → debits 3 hrs from the prepaid block ($525.00 equivalent, drawn from prepay) + +Winter caught this on #32203 (Desert Auto Tech) 2026-04-23 after I stacked 1hr `26118` + 1hr `26184` for a single hour of emergency work — the $ doubled because the 1.5× was applied twice. #### Timer Entries (time tracking reference) @@ -278,17 +302,25 @@ When showing ticket detail, include: ### Billing workflow **ALWAYS ask the user for minutes and labor type before logging any time. Never assume a default.** -**ALWAYS show a preview of the comment to the user before posting. Wait for confirmation.** +**ALWAYS show a preview of the comment + line items to the user before posting. Wait for confirmation.** +**ALWAYS read `customer.prepay_hours` before choosing the labor product for emergency work.** When `/syncro bill ` is called: -1. Get ticket details +1. `GET /tickets/{id}` for ticket detail, then `GET /customers/{customer_id}` to read `prepay_hours` 2. Ask: "How many minutes should I bill, and what labor type? (remote / onsite / emergency / project / internal)" -3. Draft the comment body and show it to the user for review before posting -4. Post comment: `POST /tickets/{id}/comment` -5. Add billable line item: `POST /tickets/{id}/add_line_item` with quantity in decimal hours, `price_retail`, `name`, `description` -6. Create invoice: `POST /invoices` with `{"ticket_id": N, "customer_id": N, "category": "Standard"}` -7. Verify invoice: `GET /invoices/{id}` to confirm line items transferred -8. Update ticket status to `Invoiced` +3. Decide product + quantity using the emergency-branching table above: + - Non-prepaid + emergency → product `26184`, qty = actual hours + - Prepaid + emergency → product `26118`, qty = actual hours × 1.5 + - Otherwise → per `--labor` mapping below, qty = actual hours +4. `GET /products/{product_id}` → `.product.price_retail` to fetch the current rate +5. Draft the comment body and show it to the user — with the product, quantity, rate, and computed total — for review before any POST +6. Post comment: `POST /tickets/{id}/comment` +7. Add billable line item: `POST /tickets/{id}/add_line_item` with `product_id`, `quantity`, `price_retail` (from step 4), `name`, `description`, `taxable: false` +8. Create invoice: `POST /invoices` with `{"ticket_id": N, "customer_id": N, "category": "Standard"}` +9. Verify invoice: `GET /invoices/{id}` → confirm `.invoice.total` matches `qty × price_retail` +10. Update ticket status to `Invoiced` + +**If `.invoice.total` comes back $0.00** (line items went in with null price): `PUT /tickets/{id}/update_line_item` with `price_retail` on each item, then `DELETE /invoices/{bad_id}` and re-POST `/invoices`. Recovery verified on #32203 (2026-04-23). **Correct pattern:** ```bash @@ -319,6 +351,8 @@ curl -s -X PUT "${BASE}/tickets/${ID}?api_key=${API_KEY}" \ `--labor` maps to product IDs: `remote` → 1190473, `onsite` → 26118, `emergency` → 26184, `project` → 9269129, `internal` → 9269124, `travel` → 26117, `website` → 68055 +**Override:** `emergency` becomes `26118` with `quantity × 1.5` when the customer has `prepay_hours > 0`. See the Emergency billing branching table above. + ### Error handling - 401: API key invalid or expired diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 8431648..baf0d92 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -24,6 +24,7 @@ - [Bypass Permissions Setting](feedback_bypass_permissions_setting.md) - Set permissions.defaultMode to bypassPermissions in settings.json on all machines - [365 Remediation Tool](feedback_365_remediation_tool.md) - Always means Graph API app fabb3421, not CIPP - [Ollama Tier-0 Routing](feedback_ollama_tier0_routing.md) - Route drafts/summaries/classifications through Ollama (qwen3:14b). Mike designed ClaudeTools this way — not optional. +- [Syncro Emergency Billing](feedback_syncro_emergency_billing.md) — Emergency = 1.5× multiplier, not additive. Branch by `customer.prepay_hours`: no-prepaid → `26184` at actual hrs; prepaid → `26118` at hrs×1.5. Never stack. Always set `price_retail`. ## Machine - [ACG-5070 Workstation Setup](reference_workstation_setup.md) - Windows 11 Pro clean install 2026-03-30, replaced CachyOS. All tools installed. diff --git a/.claude/memory/feedback_syncro_emergency_billing.md b/.claude/memory/feedback_syncro_emergency_billing.md new file mode 100644 index 0000000..fe2680e --- /dev/null +++ b/.claude/memory/feedback_syncro_emergency_billing.md @@ -0,0 +1,23 @@ +--- +name: Syncro emergency/after-hours billing — check prepay_hours first +description: Emergency labor is 1.5× multiplier, not additive. Branch by customer.prepay_hours — wrong branch doubles or undercharges. Applies to every /syncro bill for emergency work. +type: feedback +--- + +**Rule:** Before adding any Emergency/after-hours labor line item on a Syncro ticket, `GET /customers/` and read `prepay_hours`. + +- If `prepay_hours == 0` (no prepaid block): use product `26184` (Labor - Emergency/After Hours) at quantity = actual hours. The $262.50/hr rate already has the 1.5× multiplier baked in. +- If `prepay_hours > 0` (customer has a prepaid block): use product `26118` (Labor - Onsite) at quantity = actual hours × 1.5. Prepaid blocks debit by QUANTITY, not dollars, so we bump qty instead of swapping to the Emergency product. + +Never stack `26118` + `26184` for the same hour of work. Pick one path based on the prepaid state. + +**Why:** Learned on ticket #32203 (Desert Auto Tech) 2026-04-23. Howard asked to bill "1 hour onsite + 1 hour emergency onsite." I posted both as separate additive line items and the invoice came out at $437.50 when the correct bill for 1 actual hour of emergency work was $262.50. Winter caught it and explained the rule: "the goal is to have it bill at time and a half." The Emergency product = time-and-a-half by rate; prepaid accounts = time-and-a-half by quantity. Swapping products AND multiplying quantity double-counts. + +**How to apply:** +- Every `/syncro bill` for emergency/after-hours work: check `prepay_hours` BEFORE choosing the product. Do not shortcut this. +- For a 2-hour emergency job: + - Non-prepaid customer → one line, 2.0 hrs × `26184` → $525.00 + - Prepaid customer → one line, 3.0 hrs × `26118` → 3 hours debit from block +- Always set `price_retail` explicitly on `add_line_item`. The old "omit and let Syncro auto-calc" guidance was wrong — the rate does not populate from the product config, and the invoice will post at $0 if `price_retail` is missing. Fetch the current rate with `GET /products/`. +- Never let a customer-facing invoice post without verifying `.invoice.total` matches the expected `qty × price_retail`. +- Full rules and examples live in `.claude/commands/syncro.md` under the "Labor product IDs" section.