Registers AppGate as a checkConfigOnly integration-health row and public
sync route, matching the existing factory + is<Name>Configured() pattern.
Committed now so Phase 13's worktree-isolated executors fork from a HEAD
that includes this integration-health.ts entry, since Plan 13-02 inserts
the PAX8 row immediately after it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
Developer reviewed the SC#1-SC#4 verdict block and the auto-match sample
and responded "approved" — all four success criteria pass, auto-matches
are correct, no threshold/mapping changes needed. Marks PAX8-06, PAX8-10,
PAX8-11 complete in REQUIREMENTS.md.
- scripts/verify-pax8-orders-matching.ts: runs a real Pax8SyncService.fullSync()
twice, then asserts all four Phase 12 success criteria (order items
populated with company id + billing period, confident auto-matches exist,
no-match/ambiguous companies flagged for review, auto-match set stable
across two syncs). Never logs secrets/tokens.
- migrations/094_pax8_order_items_quantity_numeric.sql (Rule 1 auto-fix):
pax8_order_items.quantity was INTEGER but real PAX8 usage-based invoice
items (e.g. Azure per-unit bandwidth overage) report fractional
quantities, which aborted the entire orders/order_items sync loop on the
first such row and silently truncated SC#1's item coverage to ~123 rows
instead of the full ~56k-row history. Widened to NUMERIC(14,4); applied
directly to the dev DB (existing volume, not a fresh init).
- deferred-items.md: logged pre-existing out-of-scope failures (appgate
TS2307 type errors, itglue-search.test.ts) confirmed unchanged by this
plan's files.
- First unit tests for Pax8SyncService, mocking postgresClient.query and
pax8-company-matcher's matchPax8Companies (following
pax8-company-matcher.test.ts's mocking discipline)
- Asserts INSERT INTO pax8_orders / pax8_order_items with companyId bound
into pax8_company_id and amountDue into line_total
- Asserts resolveCostColumns' default (CONFIRM) mapping for both observed
item types (subscription, one-time) per 12-02-SUMMARY.md
- Asserts child-then-parent tombstone ordering (pax8_order_items before
pax8_orders)
- Asserts fullSync's entities include 'orders' and 'company_matches'
No TDD gate — plan is autonomous without a plan-level `type: tdd`
frontmatter; the implementation already existed from Tasks 1-2, so this
follows the same single-commit test-addition pattern established by
12-02-SUMMARY.md's Task 2.
- syncCompanyMatches() delegates to matchPax8Companies() (Plan 03),
shaping its result into the standard Pax8EntitySyncResult
- fullSync() now pushes ordersResult then matchResult after products,
so pax8_companies is fully populated before matching runs
- Both steps roll up into the existing success/status/totals reducer
and sync_history record unchanged
- Adds Pax8SyncService.syncOrders(): pages all invoice headers, then
per-header pages its items (12-RESEARCH.md Pattern 1 nested fetch)
- resolveCostColumns() branches on item.type per 12-02-SUMMARY.md's
live spot-check verdicts (all types CONFIRM -> single default branch,
kept as a named seam for future divergence)
- pax8_orders.pax8_company_id stays NULL (Pitfall 1); per-company data
lives on pax8_order_items.pax8_company_id
- Tombstones child (pax8_order_items) before parent (pax8_orders) to
respect the FK, using the existing id <> ALL($1::uuid[]) pattern
- Ports device-link-reconciler.ts's findBy*/applyLink/recordConflict/
pickBestCandidate shape to a single pg_trgm similarity() score
- AUTO_LINK_THRESHOLD=0.90 (D-01), TIE_MARGIN=0.05 (D-02),
CANDIDATE_FLOOR=0.3, exported and tunable
- decide() implements D-01..D-04: auto-link only on unambiguous
high-confidence match, review with top-3 candidates (or empty array
when none clear the floor)
- applyLink()/recordConflict() guard resolved_at IS NOT NULL and
match_method IS DISTINCT FROM 'manual' (D-05/SC#4 idempotency)
- matchPax8Companies() scans the re-scoring-eligible subset of
pax8_companies and reports scanned/autoLinked/flaggedAmbiguous/
flaggedNoCandidate/durationMs
- Fetches the first invoice + its items, inspects one item of each
observed type (subscription, prorate, one-time), prints raw cost
fields, and emits a CONFIRM/DIVERGENCE verdict for the Plan 04 mapping:
unit_price<-price, line_total<-amountDue, partner_cost<-cost,
partner_cost_total<-costTotal
- Read-only (listAllInvoices/listAllInvoiceItems only); never prints the
client secret or access token
- Live run against the real PAX8 API confirms the mapping across all
three observed item types — resolves 12-RESEARCH.md Open Question 1
- listAllInvoices() concatenates pages in order, size=200 on each request
- listAllInvoiceItems(invoiceId) requests the nested /invoices/{id}/items
path and concatenates its pages
- Extends the existing GET-only / Authorization-header assertion to both
new methods (PAX8-08)
- listAllInvoices() pages the flat /invoices header list via paginateAll
- listAllInvoiceItems(invoiceId) pages the nested per-invoice
/invoices/{id}/items child resource
- Both GET-only, reusing the existing paginateAll helper (PAX8-08)
- Note: no /orders call added (12-RESEARCH.md Pitfall 3 — unreliable/504s)
- Log pre-existing unrelated sync-scheduler.ts TS2307 errors to
deferred-items.md (appgate-factory/appgate-sync-service not in this
worktree's git history)
- Replace stale unused Pax8Order/Pax8OrderItem stubs with live-verified
Pax8Invoice (header) and Pax8InvoiceItem (per-company line item) types
- Field shapes sourced from 12-RESEARCH.md live PAX8 API verification
- Enable pg_trgm extension for fuzzy company-name matching
- Add per-company id, billing period, and dual-cost columns to pax8_order_items
- Add auto-match columns (autotask_company_id, match_confidence, match_method, matched_at) to pax8_companies
- Applied to dev DB and verified idempotent