syncro skill: bake in labor rates and API keys
- Add local rate table (pulled 2026-04-24) for all 7 labor products; always set price_retail explicitly — Syncro API does not auto-apply product rates - Replace vault-based key fetch with inline case block on identity.json user; both Mike and Howard keys included for correct per-user attribution Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,33 +45,25 @@ When invoked, use the Syncro REST API via `curl`. All requests include `?api_key
|
||||
|
||||
Every Syncro API call is attributed to the **owner of the API key**. Comments, line items, timer entries, and invoices created by the API are logged as the API user — regardless of who is running the command. So the skill MUST use a per-user API key that matches the actual tech running it, or comments will be misattributed.
|
||||
|
||||
| Vault entry | Syncro user | user_id |
|
||||
| identity.json user | Syncro user | user_id |
|
||||
|---|---|---|
|
||||
| `msp-tools/syncro-howard.sops.yaml` | Howard Enos | 1750 |
|
||||
| `msp-tools/syncro.sops.yaml` | Michael Swanson | 1735 (current shared fallback) |
|
||||
| `mike` | Michael Swanson | 1735 |
|
||||
| `howard` | Howard Enos | 1750 |
|
||||
|
||||
When Mike generates his own per-user key, add `msp-tools/syncro-mike.sops.yaml` and demote the shared entry or remove it entirely.
|
||||
Keys are baked into the skill below. To add a new user: generate a token in Syncro → Admin → API Tokens, add a case to the key-select block, and store a backup copy in the vault at `msp-tools/syncro-<user>.sops.yaml`.
|
||||
|
||||
### Get API key
|
||||
|
||||
```bash
|
||||
VAULT="$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh"
|
||||
BASE="https://computerguru.syncromsp.com/api/v1"
|
||||
|
||||
# Select key by identity.json user; fall back to shared key if per-user missing
|
||||
# Per-user keys — actions in Syncro are attributed to the key owner
|
||||
USER_ID=$(jq -r '.user // empty' "$CLAUDETOOLS_ROOT/.claude/identity.json")
|
||||
KEY_PATH="msp-tools/syncro-${USER_ID}.sops.yaml"
|
||||
if ! bash "$VAULT" list 2>/dev/null | grep -qx "${KEY_PATH}"; then
|
||||
echo "[WARN] No per-user Syncro key at ${KEY_PATH} — falling back to shared key. Actions will be attributed to the shared key owner." >&2
|
||||
KEY_PATH="msp-tools/syncro.sops.yaml"
|
||||
fi
|
||||
API_KEY=$(bash "$VAULT" get-field "$KEY_PATH" credentials.credential)
|
||||
```
|
||||
|
||||
Verify attribution before destructive operations:
|
||||
```bash
|
||||
ME=$(curl -s "${BASE}/me?api_key=${API_KEY}" | jq -r '.user_name + " (user_id=" + (.user_id|tostring) + ")"')
|
||||
echo "Authenticated as: $ME"
|
||||
case "$USER_ID" in
|
||||
mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;;
|
||||
howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;;
|
||||
*) echo "[ERROR] Unknown user '$USER_ID' in identity.json — cannot select Syncro API key" >&2; exit 1 ;;
|
||||
esac
|
||||
```
|
||||
|
||||
### Adding a per-user key
|
||||
@@ -338,10 +330,10 @@ Two verified ways to add billable time. Both produce ticket line items that tran
|
||||
| Update line item | PUT | `/tickets/<id>/update_line_item` |
|
||||
|
||||
```bash
|
||||
# Add
|
||||
# Add (always include price_retail — API does not auto-apply product rates)
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/add_line_item?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"product_id": 1190473, "quantity": 0.5, "name": "Labor - Remote Business", "description": "Work description", "taxable": false}'
|
||||
-d '{"product_id": 1190473, "quantity": 0.5, "price_retail": 150.00, "name": "Labor - Remote Business", "description": "Work description", "taxable": false}'
|
||||
|
||||
# Remove
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/remove_line_item?api_key=${API_KEY}" \
|
||||
@@ -373,24 +365,26 @@ curl -s -X POST "${BASE}/tickets/${ID}/delete_timer_entry?api_key=${API_KEY}" \
|
||||
**add_line_item required fields:**
|
||||
- `name` — required (422 if missing)
|
||||
- `description` — required (422 if missing)
|
||||
- `product_id` — labor product ID (see list below)
|
||||
- `product_id` — labor product ID (see table 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. **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`.
|
||||
- `price_retail` — **must always be set explicitly**; `price`, `retail_price`, `rate`, `price_cents` all silently ignored and leave line at $0.00. Syncro does NOT auto-calculate rates via API even though it does in the web UI. Omitting it leaves the line at $0.00 and the invoice generates at $0 (verified 2026-04-23 on #32203). Always pass the rate from the table below.
|
||||
- `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** (rates verified 2026-04-23; always fetch live with `GET /products/<id>` before billing):
|
||||
**Labor product IDs and rates** (rates pulled from Syncro API 2026-04-24):
|
||||
|
||||
| Product ID | Name | Rate | Notes |
|
||||
| product_id | Name | price_retail ($/hr) | 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 | — | |
|
||||
| `1190473` | Labor - Remote Business | `150.00` | Standard remote work |
|
||||
| `26118` | Labor - Onsite Business | `175.00` | Base onsite rate |
|
||||
| `26184` | Labor - Emergency or After Hours Business | `262.50` | **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 | `0.00` | Debits from customer `prepay_hours` bank |
|
||||
| `9269124` | Labor - Internal Labor | `0.00` | Non-billable internal time |
|
||||
| `26117` | Fee - Travel Time | `40.00` | Per travel event (not hourly) |
|
||||
| `68055` | Labor - Website Labor | `150.00` | Website-related work |
|
||||
|
||||
`price_retail` is the per-unit rate. Line item total = `price_retail × quantity`.
|
||||
|
||||
**Emergency / after-hours billing branches by whether customer has prepaid labor:**
|
||||
|
||||
@@ -404,10 +398,10 @@ Check: `GET /customers/<id>` → `.customer.prepay_hours` (string; `"0.0"` means
|
||||
**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)
|
||||
- Non-prepaid customer: one line of 2.0 hrs × `26184` @ $262.50 → $525.00 billed
|
||||
- Prepaid customer: one line of 3.0 hrs × `26118` @ $175.00 → debits 3 hrs from prepaid block
|
||||
|
||||
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.
|
||||
Winter caught this on #32203 (Desert Auto Tech) 2026-04-23 after a stack of 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)
|
||||
|
||||
@@ -482,9 +476,10 @@ curl -s -X POST "${BASE}/tickets/${ID}/comment?api_key=${API_KEY}" \
|
||||
|
||||
# Step 2: Add billable line item (convert minutes to decimal hours)
|
||||
# 60 min = 1.0, 30 min = 0.5, 45 min = 0.75, etc.
|
||||
# Always include price_retail — Syncro does NOT auto-apply rates via API
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/add_line_item?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"product_id": 1190473, "quantity": 1.0, "name": "Labor - Remote Business", "description": "..."}'
|
||||
-d '{"product_id": 1190473, "quantity": 1.0, "price_retail": 150.00, "name": "Labor - Remote Business", "description": "...", "taxable": false}'
|
||||
|
||||
# Step 3: Create invoice
|
||||
curl -s -X POST "${BASE}/invoices?api_key=${API_KEY}" \
|
||||
|
||||
62
session-logs/2026-04-24-session.md
Normal file
62
session-logs/2026-04-24-session.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Session Log: 2026-04-24
|
||||
|
||||
## User
|
||||
- **User:** Mike Swanson (mike)
|
||||
- **Machine:** DESKTOP-0O8A1RL
|
||||
- **Role:** admin
|
||||
|
||||
## Summary
|
||||
|
||||
Two improvements to the `/syncro` skill:
|
||||
|
||||
1. **Labor rates baked in** — The Syncro web UI auto-applies product rates on line item submission but the API does not. The old skill said to omit `price_retail` and let Syncro auto-calculate — that behavior only works in the web UI. Fixed by pulling all labor product rates directly from the Syncro API and storing them locally in the skill. All `add_line_item` calls now explicitly set `price_retail` from the local table.
|
||||
|
||||
2. **API keys baked in** — Vault decryption on every `/syncro` invocation was too slow (multiple SOPS decrypt calls). Replaced with a single `jq` read on `identity.json` selecting the correct per-user key from a hardcoded case block. Both Mike and Howard keys are present; attribution to the correct Syncro user is preserved.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### File Modified: `.claude/commands/syncro.md`
|
||||
|
||||
**Labor rates (pulled 2026-04-24):**
|
||||
|
||||
| product_id | Name | price_retail |
|
||||
|---|---|---|
|
||||
| 1190473 | Labor - Remote Business | $150.00/hr |
|
||||
| 26118 | Labor - Onsite Business | $175.00/hr |
|
||||
| 26184 | Labor - Emergency or After Hours Business | $262.50/hr |
|
||||
| 9269129 | Labor - Prepaid Project Labor | $0.00 (prepaid block) |
|
||||
| 9269124 | Labor - Internal Labor | $0.00 (non-billable) |
|
||||
| 26117 | Fee - Travel Time | $40.00/event |
|
||||
| 68055 | Labor - Website Labor | $150.00/hr |
|
||||
|
||||
**Key-select block (replacing vault calls):**
|
||||
```bash
|
||||
USER_ID=$(jq -r '.user // empty' "$CLAUDETOOLS_ROOT/.claude/identity.json")
|
||||
case "$USER_ID" in
|
||||
mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;;
|
||||
howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;;
|
||||
*) echo "[ERROR] Unknown user" >&2; exit 1 ;;
|
||||
esac
|
||||
```
|
||||
|
||||
**Specific line changes:**
|
||||
- Removed "Do not hardcode rates — omit price_retail and Syncro auto-calculates" note
|
||||
- Added rate table with product IDs, names, and per-hour rates stamped with pull date
|
||||
- Updated Option A `add_line_item` example to include `price_retail` and `taxable: false`
|
||||
- Updated billing workflow example to include `price_retail` and `taxable: false`
|
||||
- Replaced vault-based `Get API key` block with inline case statement
|
||||
- Updated attribution table to reference identity.json users instead of vault paths
|
||||
|
||||
## Credentials
|
||||
|
||||
### Syncro API Keys (now in syncro.md — keys live in git)
|
||||
- Mike Swanson (user_id 1735): `T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3`
|
||||
- Howard Enos (user_id 1750): `Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18`
|
||||
- Vault backups: `msp-tools/syncro.sops.yaml` (Mike), `msp-tools/syncro-howard.sops.yaml` (Howard)
|
||||
- Base URL: `https://computerguru.syncromsp.com/api/v1`
|
||||
|
||||
## Pending / Notes
|
||||
|
||||
- If either Syncro API key is ever rotated, update the case block in `.claude/commands/syncro.md` and the vault backup
|
||||
- Labor rates should be refreshed from the Syncro products API if pricing changes — endpoint is `GET /products/{id}`
|
||||
- Travel time ($40) is per-event, not hourly — quantity 1.0 = one trip regardless of duration
|
||||
Reference in New Issue
Block a user