sync: auto-sync from HOWARD-HOME at 2026-04-23 13:34:46

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-04-23 13:34:46
This commit is contained in:
2026-04-23 13:34:47 -07:00
parent 1191123602
commit 34aad7639f
3 changed files with 75 additions and 17 deletions

View File

@@ -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/<id>` 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/<id>``.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=<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/<id>` → `.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/<id>` 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/<id>` → `.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 <number>` 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

View File

@@ -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.

View File

@@ -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/<id>` 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/<id>`.
- 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.