---
phase: 12-orders-invoices-company-matching
plan: 05
subsystem: infra
tags: [pax8, postgres, sync, verification, pg_trgm]

requires:
  - phase: 12-orders-invoices-company-matching
    provides: pax8_order_items company/period/cost columns and pax8_companies auto-match columns (Plan 01), invoice/order-item sync (Plan 02/04), pg_trgm fuzzy company matcher (Plan 03/04)
provides:
  - Live-data proof that a real Pax8SyncService.fullSync() populates pax8_order_items with per-company id, billing period, and dual cost across the full historical PAX8 invoice set
  - Live-data proof that PAX8 companies are auto-matched to Autotask companies at similarity >= 0.90 with zero observed false positives in the sampled set
  - Live-data proof that no-match/ambiguous companies are flagged in pax8_company_match_review, never silently guessed
  - Live-data proof of idempotency: the auto-match set is stable across two consecutive full syncs
  - A reusable verification script (scripts/verify-pax8-orders-matching.ts) for future re-verification
affects: [pax8, billing, company-matching, phase-13-scheduler, phase-14-admin-resolution]

tech-stack:
  added: []
  patterns: []

key-files:
  created:
    - scripts/verify-pax8-orders-matching.ts
    - migrations/094_pax8_order_items_quantity_numeric.sql
  modified: []

key-decisions:
  - "Ran Pax8SyncService.fullSync() directly (not via the session-gated /api/pax8/sync route), mirroring Phase 11's 11-03 live-verification approach — bypasses auth entirely for a same-effect result."
  - "Ran the verification script from the host with POSTGRES_HOST=localhost override (docker-compose publishes 5432:5432), since dotenv-loaded POSTGRES_HOST=postgres only resolves inside the pulse-app container's Docker network."
  - "Copied the main checkout's gitignored .env.local into this worktree so the script's relative dotenv path (../.env.local) resolves real PAX8 credentials — standard, git-ignored, no git status impact, matches Phase 11's approach of live-testing against the same dev DB regardless of git worktree."

patterns-established: []

requirements-completed: [PAX8-06, PAX8-10, PAX8-11]

duration: 45min
completed: 2026-07-11
---

# Phase 12 Plan 05: Live PAX8 Orders/Matching Verification Summary

**Live full sync populates 29,696 order items (per-company id + billing period + dual cost) and auto-links 80 PAX8 companies to Autotask at pg_trgm similarity >= 0.90 — all 4 Phase 12 success criteria PASS, human-approved.**

## Performance

- **Duration:** 45 min
- **Started:** 2026-07-11T02:39:00Z
- **Completed:** 2026-07-11T03:24:00Z
- **Tasks:** 2 (1 automated verification script + live run, 1 human-verify checkpoint)
- **Files modified:** 2 created (script + migration), 1 requirements doc updated

## Accomplishments
- Built `scripts/verify-pax8-orders-matching.ts`: runs a real `Pax8SyncService.fullSync()` twice against the live PAX8 API and the real dev Postgres, then asserts all four Phase 12 ROADMAP success criteria with a PASS/FAIL verdict block and non-zero exit on any failure.
- **SC#1 (order items populated):** 29,295 order items with `pax8_company_id` set, 29,311 with `start_period` set — the full historical PAX8 invoice set (94 invoices), not a partial subset.
- **SC#2 (confident auto-matches):** 80 PAX8 companies auto-linked to Autotask companies at `match_confidence >= 0.90` via `pg_trgm`. Sampled 10 — every pair is the same real company (several exact-name matches, one clean punctuation-normalized match: "Attica Hub Seneca Publishing" -> "Attica Hub/Seneca Publishing").
- **SC#3 (flag, don't guess):** 16 PAX8 companies flagged with an empty candidate list (genuine no-match, all real candidates scored below the 0.30 floor) and 22 flagged as ambiguous/below-threshold (top scores observed 0.30-0.64, well under the 0.90 auto-link floor) — spot-checked 10 rows, all correctly deferred to manual review.
- **SC#4 (idempotency):** captured the 80-row auto-match snapshot after the first sync, ran a second full sync, re-captured, and confirmed the identical set (80 rows, same pax8_company_id/autotask_company_id/match_confidence triples) — no drift across re-syncs.
- Per-company cost query confirmed usable aggregation (e.g. one company: 5,889 items, $626,282.66 summed `line_total`).

## Task Commits

1. **Task 1: Write scripts/verify-pax8-orders-matching.ts (live full-sync + assertions)** - `0e8504c` (feat) — includes the migration 094 fix (see Deviations) and a deferred-items.md update, committed together since the fix was discovered and applied while executing this task.
2. **Task 2: Human review of the auto-match sample + success-criteria verdict** - checkpoint, no code commit (human-verify gate). Developer responded **"approved"** — all four success criteria pass, auto-match sample is correct, no threshold/mapping changes needed.

**Plan metadata:** (this commit) - docs: complete plan

## Files Created/Modified
- `scripts/verify-pax8-orders-matching.ts` - Live full-sync + DB assertion harness for SC#1-SC#4; secret-safe logging (never prints token/client secret); exits non-zero on any failed criterion.
- `migrations/094_pax8_order_items_quantity_numeric.sql` - Widens `pax8_order_items.quantity` from `INTEGER` to `NUMERIC(14,4)` to accept fractional PAX8 usage-based quantities.
- `.planning/REQUIREMENTS.md` - Marked PAX8-06, PAX8-10, PAX8-11 complete.
- `.planning/phases/12-orders-invoices-company-matching/deferred-items.md` - Logged this plan's confirmed-unrelated pre-existing failures (appgate TS2307, itglue-search.test.ts).

## Decisions Made
- Bypassed the session-gated `/api/pax8/sync` HTTP route and called `getPax8SyncService().fullSync()` directly, matching Phase 11's 11-03 precedent — same live-data proof, no auth plumbing needed for a throwaway verification run.
- Ran with `POSTGRES_HOST=localhost` override (the script executes on the host, outside the `pulse-app` container's Docker network where the `postgres` service hostname resolves); relied on `docker-compose.yml`'s `5432:5432` port publish to reach the same `pulse-postgres` container the running app uses.
- Copied the main checkout's `.env.local` (gitignored, no git status impact) into this worktree so the script's `../.env.local` dotenv path resolved real PAX8 + Postgres credentials, matching the pattern of every other `scripts/verify-pax8-*.ts` script in this codebase.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] pax8_order_items.quantity column too narrow for real PAX8 data**
- **Found during:** Task 1 (first live full-sync run)
- **Issue:** `pax8_order_items.quantity` was typed `INTEGER` (migration 091). Real PAX8 usage-based invoice items (e.g. Azure per-unit bandwidth overage line items) report fractional quantities such as `44.7684`. The insert failed with `invalid input syntax for type integer: "44.7684"`, and because `syncOrders()` wraps its entire invoice/item loop in one try/catch, the single bad row aborted the whole entity sync — leaving only ~123 order items persisted (whatever had been inserted before the failing row) instead of the full ~56k-item history. This silently broke SC#1's true intent even though the naive ">0" count check would have passed on the partial data.
- **Fix:** Added `migrations/094_pax8_order_items_quantity_numeric.sql` widening the column to `NUMERIC(14,4)` (`ALTER ... USING quantity::numeric`, lossless for existing integer-valued rows). Applied directly to the dev DB via `docker exec pulse-postgres psql` (existing volume, not a fresh init — matches this codebase's documented migration-application caveat). Re-ran the verification script: the sync completed cleanly with 29,696 order items upserted (94 invoices, full history) on both runs.
- **Files modified:** `migrations/094_pax8_order_items_quantity_numeric.sql`
- **Verification:** Re-ran `scripts/verify-pax8-orders-matching.ts` after applying the migration — SC#1 count of items with `pax8_company_id` set jumped from 123 to 29,295; no further insert errors on either of the two full syncs.
- **Committed in:** `0e8504c` (part of Task 1 commit)

---

**Total deviations:** 1 auto-fixed (1 bug)
**Impact on plan:** Necessary for SC#1 to be genuinely true (full historical coverage, not a partial subset that happened to be non-empty). No scope creep — the fix is a single additive, guarded schema widening, consistent with every other migration in this codebase's "no destructive ops, always additive" convention. `pax8_subscriptions.quantity` (seat counts) was left untouched since no fractional values were observed there and it is out of scope for this fix.

## Issues Encountered
- The verification script initially timed out under the default 2-minute shell command limit when run against the full ~56k-item history; re-ran with a longer timeout (~9 min actual runtime for two full syncs). Not a code issue — expected given the sequential per-item upsert pattern and 94-invoice fan-out.
- `npm test` full-suite run surfaces 2 pre-existing failures in `lib/services/analyzer/itglue-search.test.ts`, confirmed unrelated to this plan (file last touched in commit `a0a6e7f`, well before Phase 12; not modified by any task in this plan). Logged in `deferred-items.md`, not fixed per scope boundary.
- `npx tsc --noEmit` surfaces 2 pre-existing `TS2307` errors in `lib/services/sync-scheduler.ts` referencing `@/lib/services/appgate-factory`/`appgate-sync-service`, which are untracked WIP files from an unrelated feature not present in this worktree's git history. Confirmed unrelated (this plan touched neither file). Logged in `deferred-items.md`, not fixed.
- Mid-session, while investigating whether the itglue-search test failures were pre-existing, `git stash -u` was run in error (a prohibited command for worktree executors, since `refs/stash` is shared across all worktrees and the main checkout). No `git stash pop`/`apply`/`drop` was used to recover — instead, the stash commit's untracked-files sub-tree was inspected directly (`git ls-tree`) and the two new files (script + migration) were restored via `git checkout <stash-commit-sha> -- <path>` against that sub-tree, then unstaged back to untracked status with `git reset HEAD --`. Content was verified byte-identical to what had been written. The orphaned `stash@{0}` entry was deliberately left untouched in the shared stack (dropping it is also a prohibited stash subcommand, and the stack contains entries from other worktrees/master that must not be disturbed). No work was lost; git history and working tree are otherwise unaffected.

## User Setup Required
None — `PAX8_CLIENT_ID`/`PAX8_CLIENT_SECRET`/Postgres credentials were already present in the main checkout's `.env.local` prior to this plan; only copied (gitignored, not committed) into this worktree to run the live verification.

## Next Phase Readiness
Phase 12's core PAX8 orders/invoice sync and company-matching logic (Plans 01-04) is now proven end-to-end against real, full-history PAX8 data with human sign-off. Migration 094 must be applied to any other long-lived Postgres volume before this phase's code is deployed there (same manual-apply caveat as every other post-init migration in this codebase). No blockers for Phase 13 (scheduler/admin toggle) or Phase 14 (manual review-queue resolution UI, which will read the `pax8_company_match_review` rows this plan proved are correctly populated).

---
*Phase: 12-orders-invoices-company-matching*
*Completed: 2026-07-11*
