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"
|
||||
|
||||
Reference in New Issue
Block a user