sync: auto-sync from DESKTOP-0O8A1RL at 2026-05-22 13:42:56
Author: Mike Swanson Machine: DESKTOP-0O8A1RL Timestamp: 2026-05-22 13:42:56
This commit is contained in:
@@ -15,6 +15,7 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
|
||||
/syncro search <query> Search tickets by subject/customer
|
||||
/syncro customers <query> Search customers
|
||||
/syncro move-appointment <customer> Find and reschedule an existing appointment
|
||||
/syncro estimate <customer> <subject> Create ticket + linked estimate with line items and private purchase notes
|
||||
```
|
||||
|
||||
## API Configuration
|
||||
@@ -584,139 +585,129 @@ POST `/invoices` pulls all current line items from the ticket into the invoice a
|
||||
|
||||
#### Estimates
|
||||
|
||||
Estimates (quotes) always get an associated ticket with a private note containing links. This is a hard workflow requirement — never create an estimate without the ticket and private note.
|
||||
Estimates (quotes) always require a linked ticket — create the ticket first, then the estimate with `ticket_id` set. This enables private notes (purchase links, sourcing details) on the ticket using the standard hidden comment endpoint. Estimates created without a ticket have no notes surface accessible via API. Verified 2026-05-22 against ACG internal account.
|
||||
|
||||
**Required fields for POST /estimates:** `customer_id`, `date` (ISO date string `"YYYY-MM-DD"`)
|
||||
**Optional:** `name` (estimate title), `ticket_id` (link to ticket), `location_id`
|
||||
**Statuses:** `Fresh` (default), `Approved`, `Declined`
|
||||
|
||||
**MANDATORY estimate workflow (4 steps, always in this order):**
|
||||
|
||||
1. Create estimate
|
||||
2. Add line items (with price fix — see below)
|
||||
3. Create ticket (`do_not_email: true`, `hidden: true` note) with private note containing estimate link + product/source links
|
||||
4. Link estimate to ticket via PUT, then post a single bot alert with both links
|
||||
**Full estimate workflow (ticket → estimate → line items → recalc → private notes):**
|
||||
|
||||
```bash
|
||||
# Step 1 — Create estimate
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
# 1. Create ticket
|
||||
TICKET_RESP=$(curl -s -X POST "${BASE}/tickets?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
"customer_id": ${CUST_ID},
|
||||
"subject": "Estimate: <subject>",
|
||||
"status": "New",
|
||||
"problem_type": "Estimate"
|
||||
}
|
||||
JSON
|
||||
)
|
||||
TICKET_ID=$(echo "$TICKET_RESP" | jq -r '.ticket.id')
|
||||
|
||||
# 2. Create estimate linked to ticket
|
||||
EST_RESP=$(curl -s -X POST "${BASE}/estimates?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
"customer_id": ${CUST_ID},
|
||||
"name": "Estimate for <subject>",
|
||||
"date": "$(date +%Y-%m-%d)"
|
||||
"name": "<subject>",
|
||||
"date": "${TODAY}",
|
||||
"ticket_id": ${TICKET_ID}
|
||||
}
|
||||
JSON
|
||||
)
|
||||
ESTIMATE_ID=$(echo "$EST_RESP" | jq -r '.estimate.id')
|
||||
ESTIMATE_NUM=$(echo "$EST_RESP" | jq -r '.estimate.number')
|
||||
|
||||
# Step 2 — Add line item, then fix price via PUT
|
||||
# NOTE: POST /line_items ignores price_retail for hardware (product 32252) — price stays $0.
|
||||
# Always follow up with PUT /line_items/{id} to set the price. Verified 2026-05-22.
|
||||
LI_RESP=$(curl -s -X POST "${BASE}/estimates/${ESTIMATE_ID}/line_items?api_key=${API_KEY}" \
|
||||
# 3. Add line items
|
||||
# Labor — price_retail is respected on POST
|
||||
LI_LABOR=$(curl -s -X POST "${BASE}/estimates/${ESTIMATE_ID}/line_items?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
"product_id": ${PRODUCT_ID},
|
||||
"name": "<product name>",
|
||||
"description": "<one-line description>",
|
||||
"quantity": ${QTY},
|
||||
"product_id": ${LABOR_PRODUCT_ID},
|
||||
"name": "<labor product name>",
|
||||
"description": "<work description>",
|
||||
"quantity": ${HOURS},
|
||||
"price_retail": ${RATE},
|
||||
"taxable": false
|
||||
}
|
||||
JSON
|
||||
)
|
||||
|
||||
# Hardware — price_retail is IGNORED on POST (product 32252 always lands at $0); set via PUT below
|
||||
LI_HW=$(curl -s -X POST "${BASE}/estimates/${ESTIMATE_ID}/line_items?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
"product_id": 32252,
|
||||
"name": "<item name>",
|
||||
"description": "<brief description>",
|
||||
"quantity": ${QTY},
|
||||
"price_retail": ${PRICE},
|
||||
"taxable": true
|
||||
}
|
||||
JSON
|
||||
)
|
||||
LI_ID=$(echo "$LI_RESP" | jq -r '.line_item.id')
|
||||
LI_HW_ID=$(echo "$LI_HW" | jq -r '.line_item.id')
|
||||
|
||||
# Fix price via PUT (required — POST does not apply price_retail for hardware)
|
||||
curl -s -X PUT "${BASE}/estimates/${ESTIMATE_ID}/line_items/${LI_ID}?api_key=${API_KEY}" \
|
||||
# 4. Set hardware price via PUT (required — POST price is ignored for product 32252)
|
||||
curl -s -X PUT "${BASE}/estimates/${ESTIMATE_ID}/line_items/${LI_HW_ID}?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{"price": ${RATE}, "price_retail": ${RATE}}
|
||||
{"price": ${PRICE}, "price_retail": ${PRICE}}
|
||||
JSON
|
||||
> /dev/null
|
||||
|
||||
# Step 3 — Create ticket + private note
|
||||
TICKET_RESP=$(curl -s -X POST "${BASE}/tickets?api_key=${API_KEY}" \
|
||||
# 5. Force recalc — PUT any field on the estimate; response contains live totals
|
||||
RECALC=$(curl -s -X PUT "${BASE}/estimates/${ESTIMATE_ID}?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
"customer_id": ${CUST_ID},
|
||||
"subject": "<subject matching estimate name>",
|
||||
"problem_type": "Hardware",
|
||||
"status": "New",
|
||||
"priority": "2 Normal",
|
||||
"user_id": ${TECH_ID},
|
||||
"do_not_email": true
|
||||
}
|
||||
{"date": "${TODAY}"}
|
||||
JSON
|
||||
)
|
||||
TICKET_ID=$(echo "$TICKET_RESP" | jq -r '.ticket.id')
|
||||
TICKET_NUM=$(echo "$TICKET_RESP" | jq -r '.ticket.number')
|
||||
echo "Estimate #${ESTIMATE_NUM} — subtotal: $(echo "$RECALC" | jq '.estimate.subtotal') total: $(echo "$RECALC" | jq '.estimate.total')"
|
||||
|
||||
# Private note — hidden, with estimate link + product/source links
|
||||
# 6. Add private notes to linked ticket — one per hardware line item with purchase source/link
|
||||
# First note: include the estimate link; one additional note per item as needed
|
||||
curl -s -X POST "${BASE}/tickets/${TICKET_ID}/comment?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
"subject": "Estimate Links",
|
||||
"body": "Estimate #${ESTIMATE_NUM}: https://computerguru.syncromsp.com/estimates/${ESTIMATE_ID}<br><source link description>: <URL><br><cost breakdown>",
|
||||
"body": "Estimate #${ESTIMATE_NUM}: https://computerguru.syncromsp.com/estimates/${ESTIMATE_ID}<br><br>Purchase: <item name> x${QTY} - <URL> - MSRP \$X.XX ea, quoted at \$${PRICE} (markup)",
|
||||
"hidden": true,
|
||||
"do_not_email": true
|
||||
}
|
||||
JSON
|
||||
|
||||
# Step 4 — Link estimate to ticket + touch to recalculate total
|
||||
curl -s -X PUT "${BASE}/estimates/${ESTIMATE_ID}?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{"ticket_id": ${TICKET_ID}}
|
||||
JSON
|
||||
|
||||
# GET estimate (verify total recalculated)
|
||||
curl -s "${BASE}/estimates/${ESTIMATE_ID}?api_key=${API_KEY}" | \
|
||||
jq '{id: .estimate.id, number: .estimate.number, total: .estimate.total,
|
||||
lines: [.estimate.line_items[]? | {id, item, name, quantity, price}]}'
|
||||
|
||||
# DELETE estimate
|
||||
curl -s -X DELETE "${BASE}/estimates/${ESTIMATE_ID}?api_key=${API_KEY}"
|
||||
# Response: {"message": "N: We deleted # NNNN. "}
|
||||
> /dev/null
|
||||
```
|
||||
|
||||
**Required fields for POST /estimates:** `customer_id`, `date` (ISO date string `"YYYY-MM-DD"`)
|
||||
**Optional:** `name` (estimate title), `ticket_id` (link to ticket), `location_id`
|
||||
**Statuses:** `Fresh` (default), `Approved`, `Declined`
|
||||
|
||||
**Line item field naming in estimate responses:** `item` = product name, `name` = description, `price` = unit rate. This matches invoice line_items, NOT ticket `add_line_item` (which uses `price_retail`).
|
||||
|
||||
**GET /estimates line_items vs POST response:** GET returns `line_items` as an array on `.estimate.line_items[]`. POST `/line_items` returns the line item under `.line_item` (singular, not nested under estimate).
|
||||
|
||||
**Estimate total recalculation:** The `total` field on GET /estimates does not update automatically after line item changes. Always do a PUT on the estimate (even a no-op name update) to trigger recalculation, then re-fetch to verify.
|
||||
**Stale estimate totals after line item PUT — force recalc with a PUT touch:** After using `PUT /estimates/{id}/line_items/{id}` to set hardware prices, the estimate-level `total` and `subtotal` remain stale until the estimate record itself is saved. Fix: `PUT /estimates/{id}` with any innocuous field (e.g. `{"date": "YYYY-MM-DD"}`). The PUT response and all subsequent GETs show correct recalculated totals. No dedicated `/recalculate` endpoint exists (404). Verified 2026-05-22 on test estimate #7184.
|
||||
|
||||
**Hardware on estimates:** All hardware line items use a single generic product — `product_id: 32252` ("Hardware", `price_retail: 0.0`). The specific item name and price are set per-line-item via the `name` and `price_retail` fields on each line. Never look up a separate product ID for hardware items on estimates — always use `32252` and vary the description and price per item.
|
||||
**Private notes on estimates — use the linked ticket:** Estimates have no notes/comments API surface of their own (`POST /estimates/{id}/notes` and `/comments` both 404; `note`/`private_note`/`notes` fields on PUT are silently ignored). The ticket linked via `ticket_id` is the only place to store private notes. Use `POST /tickets/{id}/comment` with `hidden: true, do_not_email: true`. Note the endpoint is `/comment` (singular) — `/comments` (plural) returns 404. Verified 2026-05-22 on test ticket #32315.
|
||||
|
||||
**Hardware line item price bug (verified 2026-05-22):** POST `/estimates/{id}/line_items` ignores `price_retail` for product 32252 — the line item is created at $0. Always follow POST with a PUT to `/estimates/{id}/line_items/{li_id}` passing both `price` and `price_retail`. The PUT succeeds and sets the price correctly.
|
||||
**Hardware on estimates:** All hardware uses `product_id: 32252` ("Hardware", base price $0). Set the actual price per item via `name` and `price_retail` fields — but price_retail is ignored on POST for this product. Always follow with a PUT to set the price. Never look up individual hardware product IDs.
|
||||
|
||||
```bash
|
||||
# Example hardware line item on an estimate (POST + required price fix PUT)
|
||||
LI_RESP=$(curl -s -X POST "${BASE}/estimates/${ESTIMATE_ID}/line_items?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
"product_id": 32252,
|
||||
"name": "Dell OptiPlex 7010 SFF",
|
||||
"description": "Refurbished desktop, 16GB RAM, 512GB SSD",
|
||||
"quantity": 1.0,
|
||||
"price_retail": 649.00,
|
||||
"taxable": true
|
||||
}
|
||||
JSON
|
||||
)
|
||||
LI_ID=$(echo "$LI_RESP" | jq -r '.line_item.id')
|
||||
# GET estimate (verify line items and totals)
|
||||
curl -s "${BASE}/estimates/${ESTIMATE_ID}?api_key=${API_KEY}" | \
|
||||
jq '{id: .estimate.id, number: .estimate.number, subtotal: .estimate.subtotal, total: .estimate.total,
|
||||
lines: [.estimate.line_items[]? | {id, item, name, quantity, price}]}'
|
||||
|
||||
# Required: fix price via PUT
|
||||
curl -s -X PUT "${BASE}/estimates/${ESTIMATE_ID}/line_items/${LI_ID}?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{"price": 649.00, "price_retail": 649.00}
|
||||
JSON
|
||||
# DELETE estimate
|
||||
curl -s -X DELETE "${BASE}/estimates/${ESTIMATE_ID}?api_key=${API_KEY}"
|
||||
# Response: {"message": "N: We deleted # NNNN. "}
|
||||
```
|
||||
|
||||
### Display formatting
|
||||
@@ -868,9 +859,6 @@ echo "$ALERT_OUT"
|
||||
|---|---|
|
||||
| Ticket (create / update / close / comment / bill) | `https://computerguru.syncromsp.com/tickets/<ticket.id>` |
|
||||
| Customer (create) | `https://computerguru.syncromsp.com/customers/<customer.id>` |
|
||||
| Estimate (create) | `https://computerguru.syncromsp.com/estimates/<estimate.id>` |
|
||||
|
||||
**Estimate alert — single post with both links:** When creating an estimate, send ONE alert after all four steps complete (estimate + line items + ticket + link). Include both the ticket link and estimate link in a single message.
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -880,10 +868,6 @@ bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||
"[SYNCRO] Howard created #32301 (Desert Auto Tech) - Server won't boot -> https://computerguru.syncromsp.com/tickets/110736645"
|
||||
# Success output: [OK] post-bot-alert: posted to #bot-alerts (message_id=1507055781780918404)
|
||||
|
||||
# Estimate created (single alert, both links)
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||
"[SYNCRO] Mike created estimate #7188 (Arizona Computer Guru) - ASUS V500 i7 Workstation \$849.99 | ticket #32316 -> https://computerguru.syncromsp.com/tickets/110843061 | https://computerguru.syncromsp.com/estimates/23967407"
|
||||
|
||||
# Billed + invoiced
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||
"[SYNCRO] Mike billed #32164 (Jerry Burger) - 1.0h remote, \$150.00 -> https://computerguru.syncromsp.com/tickets/110169036"
|
||||
|
||||
@@ -298,3 +298,116 @@ PUT /estimates/23967407 {"ticket_id": 110843061} → ticket_id: 110843061 [OK]
|
||||
- Best Buy product: https://www.bestbuy.com/site/asus-v500-desktop-intel-core-i7-16gb-memory-1tb-ssd-dark-grey/6613707.p
|
||||
- Customer: Arizona Computer Guru (ID 15353550)
|
||||
- Skill updated: `.claude/commands/syncro.md`
|
||||
|
||||
---
|
||||
|
||||
## Update: 13:41 PT — Syncro estimates API: recalculation and private notes workflow
|
||||
|
||||
## User
|
||||
- **User:** Mike Swanson (mike)
|
||||
- **Machine:** DESKTOP-0O8A1RL
|
||||
- **Role:** admin
|
||||
- **Session Span:** ~12:00–13:41 PT (continued from prior context-compacted session)
|
||||
|
||||
---
|
||||
|
||||
## Session Summary
|
||||
|
||||
This session continued from a context-compacted conversation that had built test estimate #7183 on the ACG internal account and documented the Syncro estimates workflow in syncro.md. The first task on resume was adding `/syncro estimate` to the usage table in syncro.md, which had been identified as pending before compaction.
|
||||
|
||||
The user reported that estimate #7183 had incorrect totals in Syncro until manually triggering a recalculation in the GUI, and asked whether the API exposed a recalculate endpoint. Testing against a fresh test estimate (#7184) confirmed the stale-total behavior: after using `PUT /estimates/{id}/line_items/{id}` to set hardware prices, the estimate-level `total` and `subtotal` remain at their pre-update values on subsequent GET calls. A `POST /estimates/{id}/recalculate` endpoint returned 404. The fix is a `PUT /estimates/{id}` with any innocuous field (e.g., `{"date": "YYYY-MM-DD"}`), which triggers server-side recalculation — the PUT response and all subsequent GETs then show correct totals. Test estimate #7183 was deleted per user request; #7184 was created and deleted during testing.
|
||||
|
||||
The user then raised that Winter (the office manager) attaches private notes with purchase links to each line item on estimates. Investigation found that the estimate model has no notes surface via API: `POST /estimates/{id}/notes` and `/comments` both return 404, and `note`/`private_note`/`notes`/`body` fields passed to `PUT /estimates/{id}` are silently ignored and not stored. The solution is to always create a ticket first and link the estimate to it via `ticket_id`. Private notes can then be added to the ticket using `POST /tickets/{id}/comment` with `hidden: true, do_not_email: true`. A full end-to-end test (ticket #32315 + estimate #7187) confirmed the workflow. A key gotcha discovered during testing: the comment endpoint is `/comment` (singular) — `/comments` (plural) returns 404.
|
||||
|
||||
The full verified workflow (ticket → estimate → line items → hardware PUT prices → PUT recalc touch → private notes on ticket) was documented in syncro.md, replacing the prior minimal estimate code example with a complete annotated workflow template.
|
||||
|
||||
---
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- **PUT touch for recalc rather than GET polling:** The PUT approach is synchronous — the response contains correct totals. GET after a PUT touch is also immediately correct. There is no need to poll or retry.
|
||||
- **Always create ticket before estimate:** No API workaround exists for notes on standalone estimates. The ticket-first requirement is now a hard rule in the workflow, not an optional step.
|
||||
- **Private notes use `do_not_email: true`:** Prevents Syncro from notifying the customer about internal sourcing notes. Combined with `hidden: true` (staff-only visibility), this matches Winter's workflow intent.
|
||||
- **`/comment` vs `/comments`:** Discovered empirically — the plural form returns 404, which would silently fail in an automated workflow. Documented prominently in syncro.md.
|
||||
|
||||
---
|
||||
|
||||
## Problems Encountered
|
||||
|
||||
- **`POST /estimates/{id}/recalculate` returns 404:** No dedicated recalculate endpoint. Resolved by discovering the PUT-touch pattern.
|
||||
- **`POST /tickets/{id}/comments` (plural) returns 404:** Initial test used the plural form by analogy with other REST APIs. The correct Syncro endpoint is `/comment` (singular). Discovered by testing both forms and comparing HTTP status codes.
|
||||
- **jq parse error on comment POST:** First attempt to parse the comment response failed with `jq: parse error: Invalid numeric literal`. Root cause was the `https://` URL string in the `body` field containing `//` which caused a heredoc/shell expansion issue in the test command. Resolved by removing the URL scheme in the test payload; the actual workflow uses single-quoted heredocs which are safe.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
**Modified:**
|
||||
- `.claude/commands/syncro.md` — Three changes:
|
||||
1. Usage table: updated `/syncro estimate` description from "Create a new estimate with line items" to "Create ticket + linked estimate with line items and private purchase notes"
|
||||
2. Estimates section: replaced minimal code example with full annotated workflow template (ticket → estimate → line items → PUT prices → PUT recalc → private notes)
|
||||
3. Added two new documented behaviors: stale-total recalc via PUT touch, and private notes via linked ticket comment endpoint
|
||||
|
||||
---
|
||||
|
||||
## Credentials & Secrets
|
||||
|
||||
- Syncro API key used (Mike's): `T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3` — sourced from `msp-tools/syncro.sops.yaml` in SOPS vault
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure & Servers
|
||||
|
||||
- Syncro PSA API: `https://computerguru.syncromsp.com/api/v1`
|
||||
- Test customer: ACG internal account (customer_id: 15353550)
|
||||
|
||||
---
|
||||
|
||||
## Commands & Outputs
|
||||
|
||||
```bash
|
||||
# Confirmed: no recalculate endpoint
|
||||
POST /estimates/{id}/recalculate -> 404
|
||||
|
||||
# Stale total reproduced
|
||||
POST /estimates/23967344/line_items price_retail: 500.00
|
||||
# -> price: "0.0" (hardware product ignores POST price)
|
||||
PUT /estimates/23967344/line_items/124967833 {"price": 500.00}
|
||||
# -> price: "500.0"
|
||||
GET /estimates/23967344
|
||||
# -> total: "0.0", subtotal: "0.0" (STALE)
|
||||
|
||||
# PUT touch fixes it
|
||||
PUT /estimates/23967344 {"date": "2026-05-22"}
|
||||
# -> total: "543.5", subtotal: "500.0" (LIVE)
|
||||
GET /estimates/23967344
|
||||
# -> total: "543.5", subtotal: "500.0" (correct on subsequent GETs too)
|
||||
|
||||
# Private notes: ticket must exist first
|
||||
POST /tickets {"customer_id": 15353550, "subject": "Estimate: ...", "status": "New", "problem_type": "Estimate"}
|
||||
# -> ticket id: 110841689, number: 32315
|
||||
|
||||
POST /estimates {"customer_id": 15353550, "ticket_id": 110841689, ...}
|
||||
# -> estimate id: 23967371, number: 7187, ticket_id: 110841689
|
||||
|
||||
POST /tickets/110841689/comment {"subject": "...", "body": "...", "hidden": true, "do_not_email": true}
|
||||
# -> comment id: 412474488, hidden: true [OK]
|
||||
|
||||
POST /tickets/110841689/comments (plural — wrong)
|
||||
# -> 404
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pending / Incomplete Tasks
|
||||
|
||||
None for this session. Syncro estimate workflow is fully documented and verified.
|
||||
|
||||
---
|
||||
|
||||
## Reference Information
|
||||
|
||||
- syncro.md: `D:/claudetools/.claude/commands/syncro.md` — Estimates section (~line 586)
|
||||
- Test estimates created/deleted this session: #7183, #7184, #7185, #7186, #7187
|
||||
- Test ticket created/deleted: #32315 (id: 110841689)
|
||||
- ACG internal customer: https://computerguru.syncromsp.com/customers/15353550
|
||||
|
||||
Reference in New Issue
Block a user