---
phase: 11-company-catalog-subscription-sync
plan: 03
type: execute
wave: 3
depends_on: [11-02]
files_modified: []
autonomous: false
requirements: [PAX8-03, PAX8-04, PAX8-05, PAX8-08]

user_setup:
  - service: pax8
    why: "Live sync run requires real PAX8 partner credentials to populate Postgres"
    env_vars:
      - name: PAX8_CLIENT_ID
        source: "PAX8 developer portal (devx.pax8.com) — provisioned client ID (per SEED-002)"
      - name: PAX8_CLIENT_SECRET
        source: "PAX8 developer portal (devx.pax8.com) — provisioned client secret"

must_haves:
  truths:
    - "A real sync run populates pax8_companies with rows (SC#1)"
    - "A real sync run populates pax8_products and pax8_subscriptions with rows, and subscriptions carry price + partner_cost (SC#2)"
    - "A subscription joined to pax8_products shows a readable product name and category, not a bare SKU/product id (SC#3)"
    - "The client and sync service issue no PAX8 write calls (SC#4 / PAX8-08)"
  artifacts: []
  key_links:
    - from: "POST /api/pax8/sync"
      to: "pax8_companies / pax8_subscriptions / pax8_products"
      via: "live sync run populates rows"
      pattern: "pax8_"
---

<objective>
End-to-end verification of the Phase 11 sync against the live PAX8 API and the dev
database — the four ROADMAP success criteria can only be proven with real
credentials and a real run (matching the project convention that no integration
client has an automated live-API test tier).

Purpose: Confirm the sync actually populates readable, cost-bearing data and holds
the read-only invariant before the phase is marked complete.

Output: developer confirmation (no code changes in this plan).
</objective>

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

<context>
@.planning/ROADMAP.md
@.planning/phases/11-company-catalog-subscription-sync/11-01-SUMMARY.md
@.planning/phases/11-company-catalog-subscription-sync/11-02-SUMMARY.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Static read-only invariant proof (SC#4 / PAX8-08)</name>
  <files>lib/services/pax8-client.ts, lib/services/pax8-sync-service.ts</files>
  <read_first>
    - lib/services/pax8-client.ts
    - lib/services/pax8-sync-service.ts
  </read_first>
  <action>
    Prove by inspection that no data-write call to the PAX8 API exists. Grep the
    client and the sync service for mutating HTTP verbs and confirm the only POST
    anywhere in the PAX8 client is the `/v1/token` OAuth handshake (an auth
    exchange, not a data-resource write), and that the sync service issues no HTTP
    at all (it only calls the client's read methods). Record the grep output in the
    summary as the PAX8-08 evidence.
  </action>
  <acceptance_criteria>
    - `grep -nE "method:[[:space:]]*['\"](PUT|PATCH|DELETE)" lib/services/pax8-client.ts` returns nothing
    - The only POST in pax8-client.ts targets `https://api.pax8.com/v1/token`
    - `grep -nE "method:[[:space:]]*['\"](POST|PUT|PATCH|DELETE)" lib/services/pax8-sync-service.ts` returns nothing
  </acceptance_criteria>
  <verify>
    <automated>! grep -nE "method:[[:space:]]*['\"](PUT|PATCH|DELETE)" lib/services/pax8-client.ts && ! grep -nE "method:[[:space:]]*['\"](POST|PUT|PATCH|DELETE)" lib/services/pax8-sync-service.ts && echo OK</automated>
  </verify>
  <done>Grep evidence confirms no PAX8 data-write path exists (SC#4 upheld).</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 2: Live sync run + database verification (SC#1-3)</name>
  <files></files>
  <read_first>
    - .planning/ROADMAP.md Phase 11 Success Criteria #1-3
    - .planning/phases/11-company-catalog-subscription-sync/11-02-SUMMARY.md
  </read_first>
  <action>
    Human-verification checkpoint (no code changes). Pause and have the developer
    run the live PAX8 sync against real credentials and confirm the database
    outcomes described in how-to-verify below. Do not auto-approve — this gate
    confirms ROADMAP Success Criteria #1-3, which cannot be proven without live
    credentials and a real run.
  </action>
  <what-built>
    Pax8SyncService.fullSync (read-only) and POST /api/pax8/sync, which sync PAX8
    companies, subscriptions (with customer price + partner cost), and a
    referenced-only readable product catalog into Postgres with soft-delete
    reconciliation.
  </what-built>
  <how-to-verify>
    1. Ensure `PAX8_CLIENT_ID` and `PAX8_CLIENT_SECRET` are set in the environment
       the dev server reads (`.env` / `.env.local`) and restart the app if needed.
    2. Trigger a sync: `curl -X POST http://localhost:3100/api/pax8/sync` — expect
       `{ ok: true, message: "PAX8 sync started" }`. An immediate second POST while
       it runs should return HTTP 409.
    3. Wait for completion, then check status: `curl http://localhost:3100/api/pax8/sync`
       — expect `inProgress: false` and non-zero `counts` for companies,
       subscriptions, and products, plus a recent `sync_history` entry with
       status `completed`.
    4. In psql against the dev DB confirm SC#1/#2: `SELECT count(*) FROM pax8_companies WHERE is_deleted=false;`
       is > 0; `SELECT count(*) FROM pax8_subscriptions WHERE is_deleted=false;` is > 0;
       `SELECT count(*) FROM pax8_subscriptions WHERE price IS NOT NULL OR partner_cost IS NOT NULL;`
       is > 0 (dual cost columns populated).
    5. Confirm SC#3 (readable via join, not bare SKU):
       `SELECT s.id, p.name AS product_name, p.category, s.quantity, s.price, s.partner_cost
        FROM pax8_subscriptions s JOIN pax8_products p ON p.id = s.product_id
        WHERE s.is_deleted=false LIMIT 5;` — rows show a human-readable product name
       and a category value (not a UUID/SKU).
    6. Confirm D-01 referenced-only catalog: every non-deleted product is referenced
       by at least one subscription — `SELECT count(*) FROM pax8_products p WHERE p.is_deleted=false
        AND NOT EXISTS (SELECT 1 FROM pax8_subscriptions s WHERE s.product_id = p.id);`
       should be 0 (or explain any expected exceptions).
    7. (Optional soft-delete spot check, D-05/D-06) Re-run the sync; confirm counts
       stay stable and no rows were hard-deleted (is_deleted toggling only).
  </how-to-verify>
  <acceptance_criteria>
    - GET /api/pax8/sync reports non-zero counts for companies, subscriptions, and products and a completed sync_history entry
    - A subscription joined to pax8_products returns a readable product name + category (SC#3)
    - At least one subscription row has price and/or partner_cost populated (SC#2)
    - No non-deleted product is unreferenced by any subscription (D-01), barring explained exceptions
  </acceptance_criteria>
  <verify>
    <human-check>Developer confirms the DB queries in how-to-verify return the expected non-zero, readable, cost-bearing results.</human-check>
  </verify>
  <done>All four ROADMAP Phase 11 success criteria confirmed against live data; developer typed "approved".</done>
  <resume-signal>Type "approved" once the DB checks pass, or describe what failed.</resume-signal>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Pulse server → PAX8 API | Live outbound read-only calls with real credentials |
| Operator → .env secrets | Real PAX8 client secret placed in a committed .env is a disclosure risk (per CLAUDE.md warning) |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-11-09 | Information Disclosure | PAX8_CLIENT_SECRET in committed .env | accept | CLAUDE.md documents that .env is committed; treated like every other integration secret in this repo — flag to operator, prefer .env.local if available; no code change in scope |
| T-11-04 | Elevation of Privilege | live sync run (SC#4 write-invariant) | mitigate | Task 1 grep proof plus the run itself only exercises read endpoints; upholds PAX8-08 end-to-end |
</threat_model>

<verification>
- Static grep proof: no PAX8 write path (Task 1)
- Live run populates all three tables; join yields readable name + category; costs populated (Task 2)
</verification>

<success_criteria>
All four ROADMAP Phase 11 success criteria verified against the live PAX8 API and
the dev database, and the read-only invariant (PAX8-08 / SC#4) confirmed by
inspection and by the run exercising only read endpoints.
</success_criteria>

<output>
Create `.planning/phases/11-company-catalog-subscription-sync/11-03-SUMMARY.md` when done
</output>
