diff --git a/.claude/commands/syncro.md b/.claude/commands/syncro.md index c2eb646..bd51c0b 100644 --- a/.claude/commands/syncro.md +++ b/.claude/commands/syncro.md @@ -719,20 +719,48 @@ curl -s -X DELETE "${BASE}/invoices/${INV_ID}?api_key=${API_KEY}" POST `/invoices` pulls all current line items from the ticket into the invoice automatically. The POST response includes `.invoice.id` and `.invoice.total` — if either is null, GET `/invoices?customer_id=${CUST_ID}&per_page=5` and find the invoice by `ticket_id` match before taking any other action. -**Invoice Message (the on-screen "Invoice Message" text block) = the invoice `note` field.** It is per-invoice, prints on the invoice, and is set/edited with `PUT /invoices/{id}` body `{"note": "..."}` (response `{"invoice": {...}}`). Blank by default. Verified 2026-06-16 on the ACG internal test account (invoice #67741 — set/verified/restored). +**Invoice Message (the on-screen "Invoice Message" text block) = the invoice `note` field.** Per-invoice, prints on the invoice, set with `PUT /invoices/{id}` body `{"note":"..."}` (response `{"invoice":{...}}`). Blank by default. Verified 2026-06-16 on the ACG internal test account (#67741 set/verify/restore). -**Block-rate upsell hint — non-block customers ONLY.** When you invoice a customer with **no prepaid block** (`customer.prepay_hours == 0`), set the invoice note to the one-line hint so it prints on their invoice. Customers who already have a block (`prepay_hours > 0`) must **not** get it. Add this right after the invoice is created (`$PREPAY` = `customer.prepay_hours`, already fetched in billing Step 1): +**Auto invoice-note policy** — every invoice we generate gets a one-line note driven by the customer's prepaid block (`customer.prepay_hours`): + +| Customer | Note set on the invoice | +|---|---| +| **No block** (`prepay_hours == 0`) | `Interested in discounted labor? Ask us about block-rate pricing.` | +| **Block, ≥ 4 hrs left** | `Block hours remaining: N.` | +| **Block, < 4 hrs left** | `Block hours remaining: N. You're running low — reply to renew...` **+ tags Winter in #bot-alerts** | + +Reusable helper — call it once the invoice exists (needs `$BASE`, `$API_KEY`, `$REPO_ROOT`): ```bash -# Block-rate hint on the invoice — only for customers with NO prepaid block. -if [ "$(awk "BEGIN{print ((${PREPAY:-0})+0>0)?1:0}")" = "0" ]; then - curl -s -X PUT "${BASE}/invoices/${INVOICE_ID}?api_key=${API_KEY}" \ - -H "Content-Type: application/json" \ - --data-binary '{"note": "Interested in discounted labor? Ask us about block-rate pricing."}' >/dev/null -fi +# set_invoice_note [] +# Arg 3 matters for AD-HOC labor billing: pass the prepay fetched in billing Step 1 so a block +# JUST depleted to 0 still counts as a block customer (shows "remaining: 0 + renew", not the upsell). +# Omit arg 3 for recurring (recurring invoices don't debit the block → current balance is correct). +set_invoice_note() { + local INV_ID="$1" CID="$2" PRE="${3:-}" CUST REMAIN NAME CHECK NOTE CUR R + CUST=$(curl -s "${BASE}/customers/${CID}?api_key=${API_KEY}" | tr -d '\000-\037') + REMAIN=$(echo "$CUST" | jq -r '.customer.prepay_hours // "0"') + NAME=$(echo "$CUST" | jq -r '.customer.business_name // .customer.fullname // "customer"') + CHECK="${PRE:-$REMAIN}" # block status source (pre-billing if given) + R=$(awk "BEGIN{printf \"%g\",(${REMAIN:-0})+0}") # tidy: 20.5, 3, 0 + if [ "$(awk "BEGIN{print ((${CHECK:-0})+0>0)?1:0}")" = "0" ]; then + NOTE="Interested in discounted labor? Ask us about block-rate pricing." + elif [ "$(awk "BEGIN{print ((${REMAIN:-0})+0<4)?1:0}")" = "1" ]; then + NOTE="Block hours remaining: ${R}. You're running low — reply to renew your block and keep your discounted rate." + bash "$REPO_ROOT/.claude/scripts/post-bot-alert.sh" \ + "[SYNCRO] <@624666486362996755> LOW BLOCK — ${NAME} has ${R} prepaid hrs left (< 4) on invoice #${INV_ID} (cust ${CID}). Reach out about renewal." + else + NOTE="Block hours remaining: ${R}." + fi + CUR=$(curl -s "${BASE}/invoices/${INV_ID}?api_key=${API_KEY}" | tr -d '\000-\037' | jq -r '.invoice.note // ""') + if [ -n "$CUR" ] && [ "$CUR" != "null" ]; then echo "[note] skip ${INV_ID}: already has a note"; return 0; fi + curl -s -X PUT "${BASE}/invoices/${INV_ID}?api_key=${API_KEY}" -H "Content-Type: application/json" \ + --data-binary "$(jq -nc --arg n "$NOTE" '{note:$n}')" >/dev/null + echo "[note] ${INV_ID} (${NAME}): ${NOTE}" +} ``` -Keep it to that one line. If the invoice `note` is already non-empty (a real per-invoice message), do **not** clobber it — only set the hint when the note is blank. +Winter's Discord id `624666486362996755` in the alert content (`<@...>`) pings her in #bot-alerts (post-bot-alert sets no `allowed_mentions`, so content mentions ping). **Never clobber a non-empty note** — a tech may have typed a real per-invoice message. One alert per low-block invoice. #### Recurring Invoice Schedules @@ -834,6 +862,26 @@ curl -s -X DELETE "${BASE}/schedules/${SCHED_ID}/line_items/${LI_ID}?api_key=${A **Pax8 link field:** `recurring_type_id` on a schedule line item holds the Pax8 subscription UUID. Pax8 sets this when syncing subscriptions and uses it to identify which line to PUT when a subscription changes quantity or price. +#### Recurring invoice note sweep (block hours / low-block alerts on auto-generated invoices) + +Recurring invoices are generated by Syncro **automatically** — we don't run code at generation time — so to put the same invoice-note policy (block hours remaining / low-block renew + Winter tag / non-block upsell) on them, run a **sweep** after the recurring run that scans recently-generated recurring invoices and applies `set_invoice_note` (the helper in the Invoices section). Recurring invoices carry a non-null `schedule_id`; they do **not** debit the block, so the current `prepay_hours` is the right remaining figure (omit the pre-billing arg). + +```bash +# Sweep recurring invoices from the last N days and set their block-hours note. +SINCE=$(date -d '8 days ago' +%Y-%m-%d 2>/dev/null || date -v-8d +%Y-%m-%d) +PAGE=1 +while :; do + BATCH=$(curl -s "${BASE}/invoices?per_page=100&page=${PAGE}&api_key=${API_KEY}" | tr -d '\000-\037') + ROWS=$(echo "$BATCH" | jq -r --arg since "$SINCE" '.invoices[] | select(.schedule_id != null) | select(.date >= $since) | "\(.id) \(.customer_id)"') + [ -z "$ROWS" ] && break + echo "$ROWS" | while read -r INV CID; do set_invoice_note "$INV" "$CID"; done # no pre-billing arg + CNT=$(echo "$BATCH" | jq -r '.invoices | length'); [ "${CNT:-0}" -lt 100 ] && break + PAGE=$((PAGE+1)) +done +``` + +Idempotent (skips invoices that already have a note). Run it on demand after monthly billing, or schedule it (a cron/scheduled agent the day after each recurring run). Low-block customers still get the renewal line + a Winter ping, once per invoice. + #### Estimates 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. @@ -1068,13 +1116,9 @@ INVOICE_ID=$(echo "$INV_RESP" | jq -r '.invoice.id') INVOICE_TOTAL=$(echo "$INV_RESP" | jq -r '.invoice.total') # If INVOICE_ID is null: GET /invoices?customer_id=${CUST_ID}&per_page=5, find by ticket_id -# 3b. Block-rate hint on the invoice note — ONLY for customers with NO prepaid block. -# ($PREPAY = customer.prepay_hours, fetched in Step 1. Block customers don't get the hint.) -if [ "$(awk "BEGIN{print ((${PREPAY:-0})+0>0)?1:0}")" = "0" ]; then - curl -s -X PUT "${BASE}/invoices/${INVOICE_ID}?api_key=${API_KEY}" \ - -H "Content-Type: application/json" \ - --data-binary '{"note": "Interested in discounted labor? Ask us about block-rate pricing."}' >/dev/null -fi +# 3b. Invoice-note policy: non-block -> upsell hint; block -> hours remaining; block <4hr -> renew + tag Winter. +# Pass the PRE-billing prepay ($PREPAY from Step 1) so a just-depleted block still counts as block. +set_invoice_note "$INVOICE_ID" "$CUST_ID" "$PREPAY" # helper defined in the Invoices section # 4. Mark Invoiced curl -s -X PUT "${BASE}/tickets/${ID}?api_key=${API_KEY}" \