---
phase: 12-orders-invoices-company-matching
plan: 05
type: execute
wave: 4
depends_on: ["12-04"]
files_modified:
  - scripts/verify-pax8-orders-matching.ts
autonomous: false
requirements: [PAX8-06, PAX8-10, PAX8-11]

must_haves:
  truths:
    - "A real full sync populates pax8_order_items with per-company id and billing period rows (SC#1)"
    - "At least one pax8_companies row carries a confident auto-match (autotask_company_id + match_confidence >= 0.90) (SC#2)"
    - "No-match and ambiguous PAX8 companies have pax8_company_match_review rows (empty array for no-match) (SC#3)"
    - "Running the sync a second time does not change already-resolved matches (SC#4 idempotency)"
  artifacts:
    - path: "scripts/verify-pax8-orders-matching.ts"
      provides: "live full-sync + DB assertion harness for the phase's 4 success criteria"
  key_links:
    - from: "scripts/verify-pax8-orders-matching.ts"
      to: "pax8-sync-service fullSync"
      via: "invokes a real sync then queries the resulting rows"
      pattern: "fullSync|/api/pax8/sync"
---

<objective>
Prove the phase's four ROADMAP success criteria against real PAX8 data and the
real dev Postgres — the truest verification, mirroring Phase 11's live-run
verification plan (11-03). Unit tests (Plans 02-04) prove the logic with mocks;
this plan proves the integration end-to-end and lets the developer eyeball the
match distribution, since conservative billing-reconciliation matching (D-01) is
a judgment the user explicitly cares about.

Purpose: End-to-end confidence that orders populate, auto-matches persist,
flags are created (never silently guessed), and a human decision survives a
re-sync.
Output: a reusable verification script + a human confirmation of the results.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/12-orders-invoices-company-matching/12-RESEARCH.md
@.planning/phases/12-orders-invoices-company-matching/12-CONTEXT.md

<interfaces>
- POST /api/pax8/sync (existing route) fires a fire-and-forget fullSync;
  GET /api/pax8/sync returns inProgress + counts + history. App runs on port 3100.
- Alternatively invoke getPax8SyncService().fullSync('verify') directly from a
  tsx script (dotenv from ../.env.local), like scripts/verify-pax8-auth.ts.
- DB access for assertions: docker exec pulse-postgres psql -U pulse_user -d pulse_autotask.
- Migration 093 (Plan 01) must already be applied to the dev DB.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Write scripts/verify-pax8-orders-matching.ts (live full-sync + assertions)</name>
  <files>scripts/verify-pax8-orders-matching.ts</files>
  <read_first>
    - scripts/verify-pax8-auth.ts (dotenv + getPax8Client + secret-safe logging pattern; run via npx tsx)
    - lib/services/pax8-sync-service.ts (getPax8SyncService / fullSync from Plan 04)
    - .planning/phases/12-orders-invoices-company-matching/12-RESEARCH.md (Live-verified distribution — expected ~80 exact matches at 1.00, so auto-links should be substantial; ~56k order items total)
  </read_first>
  <action>
    Create scripts/verify-pax8-orders-matching.ts (npx tsx), dotenv from
    ../.env.local, secret-safe logging only. It runs getPax8SyncService().fullSync
    ('verify') once, awaiting completion, then queries the dev DB (via a direct
    postgresClient.query, since the script runs in-process) and prints a report:

    - SC#1: count of pax8_order_items WHERE is_deleted=false AND pax8_company_id
      IS NOT NULL, and count with start_period IS NOT NULL. Both must be > 0.
    - SC#2: count of pax8_companies WHERE autotask_company_id IS NOT NULL AND
      match_confidence >= 0.90 AND match_method = 'pg_trgm'. Print a sample of 5
      (pax8 name, matched autotask company_name, score). Count must be > 0.
    - SC#3: count of pax8_company_match_review rows WHERE resolved_at IS NULL; of
      those, count with candidate_company_ids = '{}' (no-match, D-03) and count
      with array_length >= 1 (ambiguous/below-threshold). Report both.
    - SC#4: capture the (pax8_company_id, autotask_company_id, match_confidence)
      for auto-matched rows into a snapshot; then run fullSync('verify') a SECOND
      time; re-read the same rows and assert none of the previously-auto-matched
      rows changed AND that no row with a resolved review (simulate none exist yet
      — Phase 14 owns manual resolution) was mutated. Print PASS/FAIL for
      idempotency (auto-match set stable across two runs).

    Print a final verdict block: SC#1..SC#4 each PASS/FAIL with the underlying
    numbers. Exit non-zero if any SC fails so the verify command surfaces it.
    Never log the client secret or token.
  </action>
  <acceptance_criteria>
    - `npx tsx scripts/verify-pax8-orders-matching.ts` runs a real sync and prints a verdict block with SC#1, SC#2, SC#3, SC#4 each marked PASS or FAIL and their counts
    - SC#1 count of pax8_order_items with non-null pax8_company_id is > 0
    - SC#2 count of pax8_companies with autotask_company_id set and match_confidence >= 0.90 is > 0
    - SC#3 reports both the empty-candidate (no-match) review count and the ambiguous review count
    - SC#4 shows the auto-match set is identical across two consecutive full syncs (idempotency PASS)
    - Script exits 0 only when all four criteria pass; no secret/token appears in output
  </acceptance_criteria>
  <verify>
    <automated>npx tsx scripts/verify-pax8-orders-matching.ts; echo "exit=$?"</automated>
  </verify>
  <done>A live full sync populates orders/items and match state; all four success criteria print PASS with real numbers; the auto-match set is stable across two runs.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 2: Human review of the auto-match sample + success-criteria verdict</name>
  <action>PAUSE for the developer. This is a human-verify checkpoint: the developer reviews the SC#1..SC#4 verdict block and the sample of auto-matches produced by Task 1's script, confirming the conservative matches are correct (D-01) and the flagged queue is genuinely ambiguous/no-match. Do not auto-approve — resume only on the developer signal below.</action>
  <what-built>
    Historical PAX8 invoice line items are synced into pax8_order_items (with
    per-company id + billing period + dual cost), PAX8 companies are auto-linked to
    Autotask companies at similarity >= 0.90 (conservative, no near-ties), and
    no-match/ambiguous companies are flagged in pax8_company_match_review rather
    than silently guessed. The verification script ran two full syncs and confirmed
    idempotency.
  </what-built>
  <how-to-verify>
    1. Review the verdict block from `npx tsx scripts/verify-pax8-orders-matching.ts`
       (SC#1..SC#4 all PASS).
    2. Eyeball the SC#2 sample of 5 auto-matches: each PAX8 company name should
       clearly be the same real company as its matched Autotask company_name.
       Conservative intent (D-01): if any sampled auto-match looks wrong, that is a
       problem — report it (the threshold may need raising).
    3. Optionally spot-check the review queue:
       `docker exec pulse-postgres psql -U pulse_user -d pulse_autotask -c "SELECT p.name, r.candidate_company_ids, r.match_confidences FROM pax8_company_match_review r JOIN pax8_companies p ON p.id=r.pax8_company_id WHERE r.resolved_at IS NULL LIMIT 10"`
       — confirm flagged companies are genuinely ambiguous or genuinely have no
       clear Autotask match.
    4. Confirm a per-company cost query returns rows:
       `docker exec pulse-postgres psql -U pulse_user -d pulse_autotask -c "SELECT pax8_company_id, count(*), sum(line_total) FROM pax8_order_items WHERE is_deleted=false GROUP BY 1 ORDER BY 2 DESC LIMIT 5"`.
  </how-to-verify>
  <resume-signal>Type "approved" if the auto-matches look correct and all SC pass, or describe any wrong match / bad flag so the threshold or mapping can be adjusted.</resume-signal>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| live PAX8 API → dev Postgres | Real historical billing data written during verification |
| verification output → developer | Match results reviewed by a human before the phase is accepted |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-12-02 | Tampering / Repudiation | auto-match correctness | mitigate | Human-verify checkpoint reviews a sample of auto-matches; conservative D-01 threshold means wrong auto-links should be near-zero and any spotted one blocks approval |
| T-12-04 | Tampering | idempotency | mitigate | SC#4 double-run asserts the auto-match set is stable and (by design) resolved rows are never overwritten |
| T-12-03 | Information Disclosure | verification script logging | mitigate | Secret-safe logging copied from verify-pax8-auth.ts; summary/counts only |
| T-12-SC | Tampering | package installs | accept | No new packages |
</threat_model>

<verification>
- Verification script exits 0 with SC#1..SC#4 all PASS.
- Human confirms the auto-match sample is correct.
- Full test suite (`npm test`) still green.
</verification>

<success_criteria>
All four ROADMAP Phase 12 success criteria proven against live data; auto-match sample human-approved; idempotency confirmed across two syncs.
</success_criteria>

<output>
Create `.planning/phases/12-orders-invoices-company-matching/12-05-SUMMARY.md` when done. Record the SC counts and the human verdict.
</output>
