docs: Syncro invoice verification pattern (lesson from false alarm)
Created memory entry documenting correct way to verify ticket-invoice linkage in Syncro API after 2026-04-30 incident where faulty verification script falsely claimed 31 tickets had no invoices (actually 29 had invoices properly, 2 were correctly Non-Billable). Key lessons: - List endpoint does NOT return ticket_id or line_items - Must query individual invoices for full data - Invoice numbers are strings, not integers - Use ticket ID (internal), not ticket number (user-visible) Added to memory index for future GrepAI semantic search. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Memory Index
|
||||
|
||||
## Reference
|
||||
- [Syncro API — Invoice Verification Pattern](syncro_invoice_verification_pattern.md) - **CRITICAL:** List endpoint (/invoices?customer_id=X) does NOT return ticket linkage. Must query individual invoices (/invoices/{number}) to get ticket_id field. Invoice numbers are strings. Use ticket ID (not number) for comparison. Real case: falsely reported 31 tickets had no invoices (actually 29 had invoices, 2 were Non-Billable).
|
||||
- [Approval Workflow: Tools vs Projects](approval-workflow-tools-vs-projects.md) - General tools (remediation-tool, onboard scripts, MSP utilities): Howard can modify OR Claude can execute with Howard/Mike approval. Projects (GuruRMM, etc.): require Mike approval, features→roadmap, bugs→bug list.
|
||||
- [Community Forum (Flarum)](reference_community_forum.md) - Flarum forum at community.azcomputerguru.com, API access, database, posting workflow
|
||||
- [Radio Show Website](reference_radio_website.md) - Astro static site at radio.azcomputerguru.com on IX server
|
||||
|
||||
185
.claude/memory/syncro_invoice_verification_pattern.md
Normal file
185
.claude/memory/syncro_invoice_verification_pattern.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Syncro API: Correct Invoice Verification Pattern
|
||||
|
||||
**Created:** 2026-04-30
|
||||
**Category:** API Integration, Billing Verification
|
||||
**Keywords:** syncro, invoice, verification, ticket linkage, billing, false positive
|
||||
|
||||
## Problem
|
||||
|
||||
The Syncro API's invoice list endpoint (`/api/v1/invoices?customer_id=X`) **does not return line items or ticket linkage information**. This causes false negatives when trying to verify if a ticket has an attached invoice.
|
||||
|
||||
## Incorrect Approach (DO NOT USE)
|
||||
|
||||
```python
|
||||
# WRONG - This will always fail to find invoice linkage
|
||||
inv_result = requests.get(
|
||||
f'https://computerguru.syncromsp.com/api/v1/invoices?customer_id={customer_id}',
|
||||
headers={'Authorization': token}
|
||||
)
|
||||
invoices = inv_result.json().get('invoices', [])
|
||||
|
||||
# This will NOT work - line_items are not in the list response
|
||||
for inv in invoices:
|
||||
for item in inv.get('line_items', []): # line_items will be empty or missing
|
||||
if item.get('ticket_id') == ticket_id:
|
||||
# This code path never executes
|
||||
```
|
||||
|
||||
**Why this fails:**
|
||||
- List endpoint returns minimal invoice data
|
||||
- No `line_items` array
|
||||
- No `ticket_id` field
|
||||
- Returns only: number, total, status, created_at, customer_id
|
||||
|
||||
## Correct Approach (USE THIS)
|
||||
|
||||
Invoice linkage is stored at the **invoice level, not in line items**. You must query each invoice individually to get the `ticket_id` field.
|
||||
|
||||
```python
|
||||
import requests, json
|
||||
|
||||
SYNCRO_TOKEN = "your_token_here"
|
||||
SYNCRO_BASE = "https://computerguru.syncromsp.com/api/v1"
|
||||
|
||||
def find_invoice_for_ticket(ticket_id, customer_id):
|
||||
"""
|
||||
Correctly verify if a ticket has an attached invoice.
|
||||
|
||||
Args:
|
||||
ticket_id: Syncro ticket ID (integer from ticket object, not ticket number)
|
||||
customer_id: Syncro customer ID
|
||||
|
||||
Returns:
|
||||
Invoice number (string) if found, None otherwise
|
||||
"""
|
||||
# Step 1: Get list of invoice numbers for customer
|
||||
list_resp = requests.get(
|
||||
f'{SYNCRO_BASE}/invoices?customer_id={customer_id}',
|
||||
headers={'Authorization': SYNCRO_TOKEN}
|
||||
)
|
||||
invoices = list_resp.json().get('invoices', [])
|
||||
|
||||
# Step 2: Query each invoice individually to check ticket_id
|
||||
for inv in invoices:
|
||||
inv_number = inv.get('number') # Note: this is a STRING
|
||||
|
||||
# Get full invoice details
|
||||
detail_resp = requests.get(
|
||||
f'{SYNCRO_BASE}/invoices/{inv_number}',
|
||||
headers={'Authorization': SYNCRO_TOKEN}
|
||||
)
|
||||
|
||||
detail_data = detail_resp.json()
|
||||
if 'invoice' in detail_data:
|
||||
full_invoice = detail_data['invoice']
|
||||
|
||||
# The ticket_id is at the invoice level, not in line items
|
||||
if full_invoice.get('ticket_id') == ticket_id:
|
||||
return inv_number
|
||||
|
||||
return None
|
||||
|
||||
# Usage example
|
||||
ticket_data = requests.get(
|
||||
f'{SYNCRO_BASE}/tickets?number=32223',
|
||||
headers={'Authorization': SYNCRO_TOKEN}
|
||||
).json()
|
||||
|
||||
ticket = ticket_data['tickets'][0]
|
||||
ticket_id = ticket['id'] # Use ID, not number
|
||||
customer_id = ticket['customer_id']
|
||||
|
||||
invoice_num = find_invoice_for_ticket(ticket_id, customer_id)
|
||||
if invoice_num:
|
||||
print(f"Ticket has invoice #{invoice_num}")
|
||||
else:
|
||||
print("No invoice found")
|
||||
```
|
||||
|
||||
## Critical Details
|
||||
|
||||
1. **Invoice numbers are strings, not integers**
|
||||
- `inv.get('number')` returns `"67469"` not `67469`
|
||||
- Use string comparison: `if str(num) == '67469'`
|
||||
|
||||
2. **Use ticket ID, not ticket number**
|
||||
- Ticket number: `32223` (user-visible)
|
||||
- Ticket ID: `109554336` (internal, used for linkage)
|
||||
- Invoice `ticket_id` field contains the internal ID
|
||||
|
||||
3. **Invoice endpoint structure**
|
||||
- List: `/api/v1/invoices?customer_id=X` → minimal data
|
||||
- Detail: `/api/v1/invoices/{invoice_number}` → full data including `ticket_id`
|
||||
|
||||
4. **Response structure difference**
|
||||
```json
|
||||
// List endpoint response
|
||||
{
|
||||
"invoices": [
|
||||
{
|
||||
"number": "67469",
|
||||
"total": "75.0",
|
||||
"status": null,
|
||||
"created_at": "2026-04-28T16:36:22.421-07:00"
|
||||
// NO ticket_id here
|
||||
// NO line_items here
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Detail endpoint response (/invoices/67469)
|
||||
{
|
||||
"invoice": {
|
||||
"number": "67469",
|
||||
"total": "75.0",
|
||||
"status": null,
|
||||
"created_at": "2026-04-28T16:36:22.421-07:00",
|
||||
"ticket_id": 109554336, // <-- THIS is what you need
|
||||
"line_items": [...] // <-- These are also here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
**Case Study (2026-04-30):**
|
||||
- Analyzed 31 tickets with zero time entries
|
||||
- Used incorrect list-endpoint approach
|
||||
- Falsely concluded ALL 31 had no invoices
|
||||
- Created false "CRITICAL billing gap" alarm
|
||||
- Reality: 29 had proper invoices, 2 were correctly Non-Billable
|
||||
- 93.5% success rate misidentified as 0% success
|
||||
|
||||
**User caught the error immediately:** "That kittle ticket DOES have an invoice attached, perhaps your search isn't working properly?"
|
||||
|
||||
**Impact:** Nearly triggered unnecessary remediation work, lost credibility, wasted time
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
When verifying multiple tickets:
|
||||
- List endpoint: 1 call per customer
|
||||
- Detail endpoint: 1 call per invoice
|
||||
- Add `time.sleep(0.1)` between detail calls
|
||||
- For 30 tickets with ~20 invoices each = ~600 API calls
|
||||
- Budget 60+ seconds for full verification
|
||||
|
||||
## When to Use This
|
||||
|
||||
Use this pattern when:
|
||||
- Verifying if a ticket has been invoiced
|
||||
- Auditing billing completeness
|
||||
- Reconciling tickets marked "Invoiced" status
|
||||
- Investigating billing workflow issues
|
||||
|
||||
**DO NOT assume** invoice linkage exists without checking the detail endpoint.
|
||||
|
||||
## Related Files
|
||||
|
||||
- Session log with error analysis: `session-logs/2026-04-30-session.md`
|
||||
- Syncro API credentials: `vault:msp-tools/syncro.sops.yaml`
|
||||
- Syncro base URL: `https://computerguru.syncromsp.com/api/v1`
|
||||
|
||||
## See Also
|
||||
|
||||
- `.claude/CLAUDE.md` — Data integrity rule: "Never use placeholder/fake data"
|
||||
- This pattern applies to any API where list ≠ detail responses
|
||||
Reference in New Issue
Block a user