---
phase: 12-orders-invoices-company-matching
plan: 04
subsystem: api
tags: [pax8, sync-service, invoices, company-matching, vitest]

# Dependency graph
requires:
  - phase: 12-02
    provides: Pax8Client.listAllInvoices/listAllInvoiceItems + confirmed live cost-field mapping
  - phase: 12-03
    provides: matchPax8Companies() fuzzy company matcher (Pax8CompanyMatchResult)
provides:
  - "Pax8SyncService.syncOrders() — nested invoice-header/item upsert + child-then-parent tombstone"
  - "Pax8SyncService.syncCompanyMatches() — Pax8EntitySyncResult wrapper around matchPax8Companies()"
  - "fullSync() wired with both new entity steps, rolling into the existing success/status/totals reducer"
  - "First unit tests for Pax8SyncService (lib/services/pax8-sync-service.test.ts)"
affects: [12-05]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "resolveCostColumns(item) helper — named seam for per-item-type cost mapping, currently a single default branch since 12-02-SUMMARY.md recorded CONFIRM for all observed types"
    - "Child-then-parent tombstone ordering (pax8_order_items before pax8_orders) to respect the FK"

key-files:
  created:
    - lib/services/pax8-sync-service.test.ts
  modified:
    - lib/services/pax8-sync-service.ts
    - .planning/phases/12-orders-invoices-company-matching/deferred-items.md

key-decisions:
  - "resolveCostColumns() collapses to a single default branch (unit_price<-price, line_total<-amountDue, partner_cost<-cost, partner_cost_total<-costTotal) because 12-02-SUMMARY.md recorded a CONFIRM verdict for every observed item type (subscription, prorate, one-time) — no divergence to encode. Kept as a named switch/helper rather than inline ternaries so a future divergence has an obvious place to branch."
  - "syncCompanyMatches() repurposes Pax8EntitySyncResult.tombstoned to carry the flagged-for-review count (flaggedAmbiguous + flaggedNoCandidate), documented inline, so the matcher fits the existing rollup shape without a new result field."

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

# Metrics
duration: 25min
completed: 2026-07-11
---

# Phase 12 Plan 04: Sync Service Wiring (Orders + Company Matching) Summary

**Wired both halves of Phase 12 into `Pax8SyncService.fullSync()` — a new `syncOrders()` step performing the nested invoice-header/item upsert with per-type cost-column resolution, and a new `syncCompanyMatches()` step delegating to Plan 03's matcher — plus the service's first unit tests.**

## Performance

- **Duration:** ~25 min
- **Started:** 2026-07-11T22:52:00Z
- **Completed:** 2026-07-11T22:55:30Z
- **Tasks:** 3
- **Files modified:** 3 (1 created, 1 modified, 1 tracking doc updated)

## Accomplishments

- `Pax8SyncService.syncOrders()` added: pages all invoice headers via `listAllInvoices()`, then for each header pages its items via `listAllInvoiceItems(invoiceId)` (12-RESEARCH.md Pattern 1 nested fetch). Upserts headers into `pax8_orders` (with `pax8_company_id` intentionally left `NULL` per Pitfall 1) and items into `pax8_order_items` (with `pax8_company_id`, `subscription_id`, `item_type`, `sku`, `description`, `start_period`, `end_period`, and the four dual-cost columns populated). Tombstones child (`pax8_order_items`) before parent (`pax8_orders`) using the existing `id <> ALL($1::uuid[])` pattern.
- `resolveCostColumns(item)` helper added: a named, reviewable seam that branches on `item.type`. Per 12-02-SUMMARY.md, all three observed types (`subscription`, `prorate`, `one-time`) were live-verified as CONFIRM against the default mapping, so the switch currently collapses to a single default branch — exactly as the plan's acceptance criteria anticipated for the "all CONFIRM" case.
- `Pax8SyncService.syncCompanyMatches()` added: delegates to Plan 03's `matchPax8Companies()`, shaping its `Pax8CompanyMatchResult` into a standard `Pax8EntitySyncResult` (`entity: 'company_matches'`).
- `fullSync()` now pushes `ordersResult` then `matchResult` after the products step, so `pax8_companies` is fully populated (via `syncCompanies()`) before matching runs. The existing rollup (`success = entities.every(...)`, `totalUpserted`/`totalTombstoned` reduce) picks both up unchanged — no changes needed to the rollup logic itself.
- `lib/services/pax8-sync-service.test.ts` created — the service's first unit tests. Mocks `postgresClient.query` and `pax8-company-matcher`'s `matchPax8Companies`, injects a mock `Pax8Client` via the constructor's optional `client` parameter, and exercises `fullSync()` end to end against a two-invoice/two-item fixture.

## Task Commits

Each task was committed atomically:

1. **Task 1: Add syncOrders() (nested invoice → item upsert + tombstone)** - `a06c1d3` (feat)
2. **Task 2: Add syncCompanyMatches() and wire both steps into fullSync()** - `5f96060` (feat)
3. **Task 3: Create lib/services/pax8-sync-service.test.ts** - `59294ce` (test)

_No TDD gate — plan is `autonomous: true` without a plan-level `type: tdd` frontmatter. Task 3 used `tdd="true"` but, matching 12-02-SUMMARY.md's precedent for its own Task 2, was executed as a single test-addition commit after the implementation (Tasks 1-2) already existed, following the existing test file's established pattern (`pax8-company-matcher.test.ts`'s mocking discipline)._

## Files Created/Modified

- `lib/services/pax8-sync-service.ts` - Added the `Pax8Invoice`/`Pax8InvoiceItem` type imports, the `resolveCostColumns()` module-level helper, the `syncOrders()` and `syncCompanyMatches()` private methods, the `matchPax8Companies` import, and the two new `entities.push(...)` calls in `fullSync()`
- `lib/services/pax8-sync-service.test.ts` - New file: 5 test cases covering header/item upserts with correct param binding, child-then-parent tombstoning, the default cost-column mapping across both fixture item types, matcher delegation, and the `fullSync()` entities array shape
- `.planning/phases/12-orders-invoices-company-matching/deferred-items.md` - Logged that the pre-existing `sync-scheduler.ts` TS2307 errors (unrelated `appgate-factory`/`appgate-sync-service` imports) still reproduce unchanged, plus 2 pre-existing, unrelated `itglue-search.test.ts` failures surfaced by the full `npm test` run

## Decisions Made

- **`resolveCostColumns()` collapses to a single default branch, exactly as anticipated.** 12-02-SUMMARY.md's live spot-check recorded a CONFIRM verdict for every observed item type (`subscription`, `prorate`, `one-time`) — no DIVERGENCE was found for any type. Per the plan's acceptance criteria (option 3: "If 12-02-SUMMARY.md marks CONFIRM for ALL types, the switch collapses to a single default branch"), the helper is still written as a named `switch (item.type)` with a documented default rather than inline ternaries, so a future divergence discovered for a new item type has an obvious, reviewable place to branch.
- **`syncCompanyMatches()`'s `tombstoned` field repurposing is documented inline**, per the plan's explicit instruction: it carries `flaggedAmbiguous + flaggedNoCandidate` (the count of `pax8_companies` rows flagged for manual review this run), not a soft-delete count, so the matcher step fits the existing `Pax8EntitySyncResult` shape without introducing a new result field.

## Deviations from Plan

None - plan executed exactly as written. Both cost columns and the matcher wrapper match the plan's action instructions field-for-field.

## Known Stubs

None. `resolveCostColumns()`'s single-branch switch is not a stub — it is the explicit, plan-anticipated outcome of 12-02-SUMMARY.md recording CONFIRM for every observed item type; it is a fully functional mapping, just not yet exercising a second branch because no divergence exists to encode.

## Issues Encountered

- Pre-existing, out-of-scope: `lib/services/sync-scheduler.ts:446`/`:450` TS2307 errors (missing `appgate-factory`/`appgate-sync-service` modules) reproduce unchanged before and after this plan's edits — confirmed unrelated (not touched by any Plan 04 task), consistent with Plans 01/02's prior findings.
- Pre-existing, out-of-scope: `npm test`'s full-suite run surfaces 2 failures in `lib/services/analyzer/itglue-search.test.ts` (unrelated to PAX8; that file was not modified by this plan). All PAX8-scoped suites pass green: `npx vitest run lib/services/pax8-client.test.ts lib/services/pax8-company-matcher.test.ts lib/services/pax8-sync-service.test.ts lib/services/pax8-factory.test.ts` → 31/31 passing.

## User Setup Required

None - no external service configuration required. This plan only extends existing, already-configured service code and adds tests.

## Next Phase Readiness

- `Pax8SyncService.fullSync()` now runs the complete Phase 12 pipeline: companies → subscriptions → products → orders (invoices + line items) → company matching, all rolling into one `Pax8SyncResult`.
- Plan 05 (or the next wave) can build on a fully wired sync service — no further sync-orchestration work is needed for PAX8-06/PAX8-10/PAX8-11.
- No blockers identified for subsequent plans.

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

## Self-Check: PASSED

All created files found on disk (`lib/services/pax8-sync-service.test.ts`,
`.planning/phases/12-orders-invoices-company-matching/12-04-SUMMARY.md`);
all task commits (`a06c1d3`, `5f96060`, `59294ce`) verified present in
`git log --oneline --all`.
