fix(syncro): resolve billing SSOT — add_line_item is normal, timers outlier-only
Task 3/3a of the harness-optimization spec. Mike confirmed normal billing uses add_line_item; timers stay available only for explicit outlier requests, never the normal loop. Rewrote time-entry-protocol.md to defer to the /syncro command (SSOT for billing mechanics) and state timers are outlier-only; aligned the command's two absolute "no timers" lines. Contradiction removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
|
|||||||
|
|
||||||
## Hard Rules (violations have occurred — no exceptions)
|
## Hard Rules (violations have occurred — no exceptions)
|
||||||
|
|
||||||
**Billing uses `add_line_item` directly — do NOT use `timer_entry → charge_timer_entry`.** The timer workflow is not used. For all billable work (labor, warranty, internal), POST directly to `/tickets/<id>/add_line_item` with the correct `product_id`, `name`, `quantity` (decimal hours), `price_retail`, `description`, and `taxable: false`. The `name` field is required — Syncro returns `{"errors":"Name can't be blank"}` if omitted (verified 2026-05-21 on Cascades #32313).
|
**Normal billing uses `add_line_item` directly — do NOT use `timer_entry → charge_timer_entry` for routine billing.** Timers are an OUTLIER: use one ONLY if Mike explicitly requests a timer for a specific job, never for the normal billing loop. For all billable work (labor, warranty, internal), POST directly to `/tickets/<id>/add_line_item` with the correct `product_id`, `name`, `quantity` (decimal hours), `price_retail`, `description`, and `taxable: false`. The `name` field is required — Syncro returns `{"errors":"Name can't be blank"}` if omitted (verified 2026-05-21 on Cascades #32313).
|
||||||
|
|
||||||
**JSON payloads to curl: use heredoc with `--data-binary @-`, not `/tmp/*.json` files.** On Windows the Write tool resolves `/tmp/foo.json` to `C:\tmp\foo.json` while Git Bash resolves it to `%LOCALAPPDATA%\Temp\foo.json` — different real directories, so a payload written by Write may not be the file curl reads. Heredoc with `<<'JSON'` (single-quoted to suppress bash variable expansion inside the payload) avoids the file handoff entirely. See `.claude/memory/feedback_tmp_path_windows.md` — caused a wrong-comment incident on ticket #32225 on 2026-05-01 (rogue payload from a prior session).
|
**JSON payloads to curl: use heredoc with `--data-binary @-`, not `/tmp/*.json` files.** On Windows the Write tool resolves `/tmp/foo.json` to `C:\tmp\foo.json` while Git Bash resolves it to `%LOCALAPPDATA%\Temp\foo.json` — different real directories, so a payload written by Write may not be the file curl reads. Heredoc with `<<'JSON'` (single-quoted to suppress bash variable expansion inside the payload) avoids the file handoff entirely. See `.claude/memory/feedback_tmp_path_windows.md` — caused a wrong-comment incident on ticket #32225 on 2026-05-01 (rogue payload from a prior session).
|
||||||
|
|
||||||
@@ -618,7 +618,7 @@ curl -s "${BASE}/customers/${CUST_ID}?api_key=${API_KEY}" | jq '{id: .customer.i
|
|||||||
|
|
||||||
#### Line Items
|
#### Line Items
|
||||||
|
|
||||||
All billing uses `add_line_item` directly. Do not use `timer_entry → charge_timer_entry`. Do not use timers.
|
Normal billing uses `add_line_item` directly. Do not use `timer_entry → charge_timer_entry` for routine billing. Timers are an outlier — use one only when Mike explicitly requests a timer for a specific job (see `.claude/standards/syncro/time-entry-protocol.md`).
|
||||||
|
|
||||||
**Dead-end paths (all return 404 — do not probe):**
|
**Dead-end paths (all return 404 — do not probe):**
|
||||||
- `POST /ticket_line_items` — does not exist
|
- `POST /ticket_line_items` — does not exist
|
||||||
|
|||||||
@@ -1,62 +1,44 @@
|
|||||||
---
|
---
|
||||||
name: time-entry-protocol
|
name: time-entry-protocol
|
||||||
description: Always use timer_entry flow for billing; ask minutes and labor type before logging any time; never assume defaults
|
description: Normal Syncro billing uses add_line_item per the /syncro command; timers are an outlier path used only when Mike explicitly requests one; always confirm minutes + labor type before logging.
|
||||||
applies-to: syncro
|
applies-to: syncro
|
||||||
---
|
---
|
||||||
|
|
||||||
# Syncro Time Entry Protocol
|
# Syncro Time Entry Protocol
|
||||||
|
|
||||||
## Always ask before logging time
|
## Source of truth
|
||||||
|
|
||||||
Before logging any time entry, ask the user:
|
The `/syncro` command (`.claude/commands/syncro.md`) is the SINGLE source of truth for
|
||||||
1. How many minutes?
|
the billing mechanics — product IDs, rates, emergency and prepaid handling, the
|
||||||
2. What labor type? (onsite, remote, emergency, warranty, project, etc.)
|
line-item + invoice flow. Do not duplicate or contradict it here. This standard states
|
||||||
|
only the cross-cutting discipline.
|
||||||
|
|
||||||
Never assume a default. Never round up or fill in a number. Billing errors are client-facing, hard to reverse, and affect prepaid block balances. An incorrect time entry requires Winter (billing) to manually reverse it.
|
## Normal billing = add_line_item
|
||||||
|
|
||||||
## The required flow
|
Routine labor bills directly via `POST /tickets/{id}/add_line_item` (see the /syncro
|
||||||
|
command for the exact payload, product IDs, and `price_retail` rules). This is the
|
||||||
|
standard, expected path for all normal billing. (Confirmed by Mike, 2026-06-08.)
|
||||||
|
|
||||||
All time-bearing work must use `timer_entry → charge_timer_entry`, not bare `add_line_item`. This is a hard rule.
|
## Timers are an OUTLIER — not the billing loop
|
||||||
|
|
||||||
```
|
`timer_entry → charge_timer_entry` is NOT part of normal billing. Use it ONLY when Mike
|
||||||
1. POST /tickets/{id}/timer_entry — create the time record
|
explicitly asks for a timer on a specific job. The capability stays available, but it is
|
||||||
2. POST /tickets/{id}/charge_timer_entry — generate the line item from the timer
|
never the default and routine labor is never routed through it.
|
||||||
3. Verify line item: GET /tickets/{id} → check price_retail on the new line item
|
|
||||||
4. If price_retail is wrong: PUT /tickets/{id}/line_items/{item_id} to patch it
|
|
||||||
5. POST /invoices — roll line item onto invoice
|
|
||||||
6. PUT /tickets/{id} — set status to Invoiced
|
|
||||||
```
|
|
||||||
|
|
||||||
The `add_line_item` endpoint bypasses Syncro's time-tracking table entirely. Using it for labor means hours appear in the invoice but not in time-tracking reports (hours per client, technician productivity, average resolution time, prepay burn rate). After the 2026-04-30 audit, 31 closed tickets had 00:00:00 in time tracking because bare `add_line_item` was used for all of them.
|
When a timer IS explicitly requested:
|
||||||
|
1. `POST /tickets/{id}/timer_entry` → 2. `POST /tickets/{id}/charge_timer_entry` →
|
||||||
|
verify the generated line item's `price_retail` (patch via `update_line_item` if wrong).
|
||||||
|
- `billable: false` is silently ignored by the API on `timer_entry` — for warranty/free,
|
||||||
|
verify in the GUI that the charged line landed at $0 and patch if not.
|
||||||
|
|
||||||
## When bare add_line_item is acceptable
|
## Always confirm before logging (either path)
|
||||||
|
|
||||||
Only for non-time items:
|
Before logging any time, confirm: (1) how many minutes, (2) what labor type — onsite /
|
||||||
- Hardware/parts
|
remote / emergency / warranty / project. Never assume a default or round up. Billing
|
||||||
- Flat-fee services with no labor component
|
errors are client-facing, hard to reverse, and affect prepaid block balances (Winter has
|
||||||
- Software licenses
|
to reverse them manually).
|
||||||
|
|
||||||
Even warranty or free labor must use `timer_entry` with `billable: false`. The only exception is cancelled tickets where no work was performed.
|
## Prepaid
|
||||||
|
|
||||||
## Labor type reference
|
Check `prepay_hours` on the customer before billing — the /syncro command holds the
|
||||||
|
authoritative prepaid + emergency rules.
|
||||||
| Situation | Product | Note |
|
|
||||||
|-----------|---------|-------|
|
|
||||||
| Standard onsite | `26118` Onsite Business | At `hours × $175` |
|
|
||||||
| Emergency/after-hours | `26184` Emergency or After Hours | Full rate, no quantity multiplier |
|
|
||||||
| Prepaid project labor | `9269129` Prepaid Project Labor | At `$0/hr`; debits from prepay block |
|
|
||||||
| Warranty | Any labor product | `billable: false` on timer_entry |
|
|
||||||
|
|
||||||
## Prepaid customers
|
|
||||||
|
|
||||||
Before applying any rate, verify `prepay_hours` on the customer record:
|
|
||||||
```bash
|
|
||||||
curl -s "https://computerguru.syncromsp.com/api/v1/customers/${CUSTOMER_ID}?api_key=${API_KEY}" \
|
|
||||||
| jq '.customer.prepay_hours'
|
|
||||||
```
|
|
||||||
|
|
||||||
If `prepay_hours > 0`, use the prepaid product at `$0/hr` and verify the balance debits correctly after the invoice posts (Syncro may not debit until the invoice is paid in the GUI — flag for Winter if uncertain).
|
|
||||||
|
|
||||||
## Note on billable: false
|
|
||||||
|
|
||||||
The Syncro API ignores `billable: false` on `timer_entry` calls silently — the entry is created but the billing flag has no effect through the API. If a warranty/free entry is needed, create the timer entry, then verify through the GUI that the line item generated by `charge_timer_entry` is at $0. Patch with `update_line_item` if it came in at a non-zero rate.
|
|
||||||
|
|||||||
@@ -62,16 +62,19 @@ new staged runner.
|
|||||||
- Verify: two near-simultaneous `/save`s -> no wiki conflict markers; staged diff
|
- Verify: two near-simultaneous `/save`s -> no wiki conflict markers; staged diff
|
||||||
reviewed + applied cleanly.
|
reviewed + applied cleanly.
|
||||||
|
|
||||||
## Task 3a: Confirm billing SSOT truth [P0 prerequisite — Grok]
|
## Task 3a: Confirm billing SSOT truth [RESOLVED 2026-06-08]
|
||||||
- Hard prerequisite before Task 3 edits: confirm with Mike which is operational truth —
|
- DECISION (Mike): normal billing uses `add_line_item`; timers are NOT part of any
|
||||||
`/syncro` command's `add_line_item` (current practice) vs the `time-entry-protocol`
|
normal billing loop. The timer capability stays available ONLY for explicit outlier
|
||||||
standard's `timer_entry`. Picking wrong bakes a permanent contradictory money rule.
|
requests ("if I ever ask for a timer"). So the `/syncro` command is the operational
|
||||||
|
truth; the `time-entry-protocol` standard's "always timer_entry" was wrong.
|
||||||
|
|
||||||
## Task 3: Single source of truth — Syncro billing [P0]
|
## Task 3: Single source of truth — Syncro billing [DONE 2026-06-08]
|
||||||
Files: `.claude/commands/syncro.md`, `.claude/standards/syncro/time-entry-protocol.md`.
|
Files: `.claude/commands/syncro.md`, `.claude/standards/syncro/time-entry-protocol.md`.
|
||||||
- Per Task 3a's decision, make one the source and the other a reference; delete the
|
- Done: rewrote the standard to DEFER to the /syncro command (the SSOT for billing
|
||||||
contradictory/duplicated prose. Add a "command-restates-standard" lint to `/self-check`.
|
mechanics) and to state timers are outlier-only; aligned the command's two absolute
|
||||||
- Verify: one rule, two consistent references.
|
"no timers" lines to "outlier, only on explicit request." One rule, two consistent
|
||||||
|
references; the contradiction is gone.
|
||||||
|
- Still TODO (later): a "command-restates-standard" lint in `/self-check`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user