Files
claudetools/.claude/memory/feedback_syncro_billing.md
Howard Enos 350c251513 sync: auto-sync from GURU-BEAST-ROG at 2026-06-23 15:54:03
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-06-23 15:54:03
2026-06-23 15:54:23 -07:00

6.8 KiB
Raw Blame History

name, description, metadata
name description metadata
Syncro billing rules — products, rates, taxes, attribution, emergency, warranty How to bill a Syncro ticket correctly — fetch live rates, use real product names, pick the right labor type for the delivery channel, set taxable=false on labor (AZ), warranty product 1049360, emergency ×1.5 (branch by prepay_hours), preserve original tech's user_id on corrections, estimate hardware uses product 32252.
type
feedback

Rules only. Incident detail, verbatim Mike quotes, ticket numbers, dates, the tech user_id table, and the labor-product table all live in feedback_syncro_history — read on-demand when judging an edge case. API mechanics: feedback_syncro_api. Workflow: feedback_syncro_workflow.

.claude/commands/syncro.md is the authoritative live product table.


1. Bill with add_line_item directly — never the timer workflow

POST /tickets/{id}/add_line_item is the billing path for ALL work (labor, warranty, internal, hardware). The timer workflow (timer_entry → charge_timer_entry) is not used.

Payload: product_id, quantity (decimal hours), price_retail (fetched live), name (the product's REAL name — see §3), description (free-text work narrative), taxable: false for labor.


2. ALWAYS fetch the rate live

Fetch price_retail from GET /products/<id>.product.price_retail before billing. The product-ID table in .claude/commands/syncro.md is valid for IDs but not dollar amounts — rates vary by contract and change.

RATE=$(curl -s "$BASE/products/$PRODUCT_ID?api_key=$API_KEY" | jq -r '.product.price_retail')

Use $RATE for drafts, the user preview, and the price_retail field.


3. NEVER invent or rename labor line items

Every labor line MUST be an existing Syncro product, billed under its REAL name (from GET /products/<id>.product.name, verbatim). Work-specific narrative goes in description, never the name.

Why: invented names break the Syncro → QuickBooks sync. QB maps each labor line to an existing item; a fabricated name has no QB match and messes up the accounting. If no existing product fits, STOP and ask Mike — never invent one.

Product table lives in feedback_syncro_history (and .claude/commands/syncro.md).


4. Labor type must match delivery channel — never "Prepaid project labor"

Pick the labor product matching how work was delivered: remote (most common), onsite, in-shop, or web. Resolve product_id via GET /products?search=remote+labor etc.

Never default to "Prepaid project labor" — it is exempt and does NOT consume hours from a customer's prepaid block. Block accounting silently drifts.

Verify: after billing a prepay-block customer, confirm the block balance dropped by the expected hours. If it didn't, the labor type was wrong.


5. Labor is NEVER taxable in Arizona

Pass "taxable": false explicitly on every labor line. The product config has taxable: false, but add_line_item does not inherit it — posts as taxable: true regardless. Applies to remote, onsite, in-shop, emergency, warranty, prepaid.


6. Warranty / no-charge → product 1049360, never patch the price

Warranty work uses product_id: 1049360 ("Labor- Warranty work", $0/hr, non-taxable). The line generates at $0 from price_retail × quantity — no need to flag or patch anything.

Do NOT pick a regular labor product and try to neutralize it with billable: false or by patching price_retail to 0. Prices are set by selecting the correct product. If you reach for update_line_item to drop a price, that's the signal to back up and pick a different product_id.

The only legitimate update_line_item price_retail use is the Syncro auto-gen-zero recovery case (auto-line came in at $0 instead of the product's rate).


7. Emergency / after-hours — ×1.5 applied ONCE; branch by prepay_hours

GET /customers/<id> and read prepay_hours BEFORE adding any emergency line. Emergency = time-and-a-half, applied ONCE. Never bill a separate regular line + emergency line for the same hours.

No prepaid block (prepay_hours == 0):

  • product 26184, quantity = actual hours (do NOT also ×1.5 the quantity)
  • price_retail by delivery channel (the 1.5× lives in the dollars):
    • Onsite emergency = $262.50 (175 × 1.5; 26184's default).
    • Remote / In-Shop emergency = $225 (150 × 1.5) → override price_retail to 225.

Prepaid block (prepay_hours > 0):

  • product 26184, quantity = actual hours × 1.5 (premium goes in the QUANTITY)
  • Delivery channel / dollar rate is irrelevant; prepaid blocks debit by quantity. Invoice nets to $0; block debits hrs×1.5.
  • e.g. 1.5 emergency hrs → 26184 @ 2.25.

Always set price_retail explicitly — the rate doesn't auto-populate and the line posts $0 if omitted. Verify after: .invoice.total (non-prepaid) or block decrement (prepaid).


8. API key follows the BILLING TECH — always

Attribution is determined by which API key you use, not the requesting user. Every call (add_line_item, remove_line_item, comments) is logged as the owner of that key.

Rule (confirmed by Howard 2026-06-23): use the tech's own API key whenever billing is attributed to a specific person:

  • Bill for Howard → msp-tools/syncro-howard.sops.yamlcredentials.credential
  • Bill for Mike → msp-tools/syncro.sops.yamlcredentials.credential
# Howard's key
HOWARD_KEY=$(bash .claude/scripts/vault.sh get-field msp-tools/syncro-howard credentials.credential)
curl -s -X POST "...?api_key=${HOWARD_KEY}" ...

# Mike's key (default / fallback)
MIKE_KEY=$(bash .claude/scripts/vault.sh get-field msp-tools/syncro credentials.credential)
curl -s -X PUT "...?api_key=${MIKE_KEY}" ...

Note: add_line_item and remove_line_item use ?api_key= query param auth. The Authorization: <key> header also works for most endpoints but does NOT control attribution — ?api_key= is the attribution mechanism.

Corrections: if a line was added under the wrong key (wrong user_id), use remove_line_item then re-add with the correct tech's key. update_line_item does NOT change user_id even when you pass it explicitly.

Ticket ownership: adding notes or labor does NOT change .ticket.user_id. Multiple techs routinely work the same ticket. Only change ticket ownership when explicitly asked.

Tech user_id table → feedback_syncro_history.


9. Estimate hardware → product 32252

All hardware on estimates uses one generic product: product_id: 32252 ("Hardware", price_retail: 0.0). Differentiate via name ("Dell OptiPlex 7010") and price_retail (actual cost). Hardware is typically taxable: true. Never look up individual hardware product IDs — there's only one.