diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 9b4a8f7..e434345 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -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 diff --git a/.claude/memory/syncro_invoice_verification_pattern.md b/.claude/memory/syncro_invoice_verification_pattern.md new file mode 100644 index 0000000..74ee3e2 --- /dev/null +++ b/.claude/memory/syncro_invoice_verification_pattern.md @@ -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