diff --git a/.planning/phases/10-pax8-client-auth-foundation/10-02-PLAN.md b/.planning/phases/10-pax8-client-auth-foundation/10-02-PLAN.md index eb0a953..9826ec7 100644 --- a/.planning/phases/10-pax8-client-auth-foundation/10-02-PLAN.md +++ b/.planning/phases/10-pax8-client-auth-foundation/10-02-PLAN.md @@ -116,16 +116,18 @@ Output: `migrations/091_pax8_tables.sql`, applied and verified against the dev D 3. `pax8_subscriptions`: id UUID PRIMARY KEY, pax8_company_id UUID (plain indexed column, NOT a hard FK — sync insert order across companies/subscriptions is not - guaranteed; add a `-- soft ref` comment), product_id TEXT, quantity INTEGER (seat - count), billing_term TEXT, status TEXT, start_date TIMESTAMPTZ, raw_payload JSONB, - + the three audit columns. + guaranteed; add a `-- soft ref` comment), product_id UUID (soft ref, matches + pax8_products.id's UUID type so Phase 11's catalog join needs no cast), quantity + INTEGER (seat count), billing_term TEXT, status TEXT, start_date TIMESTAMPTZ, + raw_payload JSONB, + the three audit columns. 4. `pax8_orders` (header — Invoice-shaped): id UUID PRIMARY KEY, pax8_company_id UUID (soft ref, indexed), order_date TIMESTAMPTZ, total NUMERIC(12,2), status TEXT, currency CHAR(3) NOT NULL DEFAULT 'USD', raw_payload JSONB, + the three audit columns. 5. `pax8_order_items` (line — InvoiceItem-shaped): id UUID PRIMARY KEY, order_id UUID - NOT NULL REFERENCES pax8_orders(id) ON DELETE CASCADE, product_id TEXT, + NOT NULL REFERENCES pax8_orders(id) ON DELETE CASCADE, product_id UUID (soft ref, + matches pax8_products.id's UUID type — no cast needed for Phase 11/12 joins), quantity INTEGER, unit_price NUMERIC(12,2), line_total NUMERIC(12,2), currency CHAR(3) NOT NULL DEFAULT 'USD', raw_payload JSONB, + the three audit columns. diff --git a/.planning/phases/10-pax8-client-auth-foundation/10-RESEARCH.md b/.planning/phases/10-pax8-client-auth-foundation/10-RESEARCH.md index 12c32ee..b23551d 100644 --- a/.planning/phases/10-pax8-client-auth-foundation/10-RESEARCH.md +++ b/.planning/phases/10-pax8-client-auth-foundation/10-RESEARCH.md @@ -554,9 +554,9 @@ convert A1 from CITED to VERIFIED in about 30 seconds and is far more reliable t documentation fetching. Recommend the planner include this as an early verification task in Phase 10's plan (it directly demonstrates success criterion #2 anyway). -## Open Questions +## Open Questions (RESOLVED for Phase 10 — none block this phase) -1. **Should `pax8_orders`/`pax8_order_items` be sourced from PAX8's `/orders` API or +1. **(RESOLVED — applied) Should `pax8_orders`/`pax8_order_items` be sourced from PAX8's `/orders` API or `/invoices` + `/invoices/{id}/items` API?** - What we know: `/orders` has no pricing/status fields; `/invoices/{id}/items` has all the fields D-01/D-04 describe (total, status via `amountDue`/invoice-level `status`, unit price @@ -570,8 +570,12 @@ Phase 10's plan (it directly demonstrates success criterion #2 anyway). fields (this research's Pitfall 1 + migration skeleton above), and flag to the user during Phase 12 planning (not Phase 10) that the sync will hit `/invoices/{id}/items`, not `/orders` — a one-line confirmation, not a redesign. + - **Resolution:** Applied in 10-02-PLAN.md — `pax8_orders`/`pax8_order_items` are typed with + Invoice/InvoiceItem-shaped columns (`total`, `status`, `currency`, `unit_price`, + `line_total`), table names unchanged per D-01. Deferred item carried forward: Phase 12 + planning must confirm the sync source is `/invoices/{id}/items`, not `/orders`. -2. **Does PAX8 actually populate `externalId` with anything useful out of the box, or is it +2. **(DEFERRED — non-blocking for Phase 10, revisit in Phase 12) Does PAX8 actually populate `externalId` with anything useful out of the box, or is it always null until a partner writes to it?** - What we know: PAX8's docs reference `externalId` as a slot partners can use to store their own identifiers (seen in the Microsoft-subscription-reconciliation guide, in a Microsoft @@ -581,8 +585,11 @@ Phase 10's plan (it directly demonstrates success criterion #2 anyway). - Recommendation: Not blocking for Phase 10 (the column exists either way — `external_id TEXT` nullable). Worth a live check in Phase 12 (a few real `GET /companies` responses from production credentials) before finalizing the fuzzy-match-vs-externalId-lookup strategy. + - **Resolution:** Deferred by design — 10-02-PLAN.md's `pax8_companies.external_id TEXT` + column is nullable and imposes no constraint, keeping the door open either way. No Phase 10 + action required; carried forward as a Phase 12 pre-work check. -3. **What confidence-scoring approach will Phase 12's fuzzy-name matching use** (e.g. +3. **(DEFERRED — non-blocking for Phase 10, revisit in Phase 12) What confidence-scoring approach will Phase 12's fuzzy-name matching use** (e.g. Levenshtein, token-set-ratio, a Postgres extension like `pg_trgm`)? - What we know: `device_link_review`'s `match_confidences TEXT[]` stores confidence as text labels, not a numeric score — precedent exists for either representation. @@ -592,6 +599,9 @@ Phase 10's plan (it directly demonstrates success criterion #2 anyway). for either a numeric-as-text or a labeled ("high"/"medium"/"low") representation. - Recommendation: Use `TEXT[]` for `match_confidences` in this phase's migration, matching `device_link_review` exactly — defer the actual scoring algorithm decision to Phase 12. + - **Resolution:** Applied in 10-02-PLAN.md — `pax8_company_match_review.match_confidences` + is `TEXT[] NOT NULL`, matching `device_link_review` exactly. Scoring algorithm choice + explicitly deferred to Phase 12, as recommended. ## Environment Availability diff --git a/.planning/phases/10-pax8-client-auth-foundation/10-VALIDATION.md b/.planning/phases/10-pax8-client-auth-foundation/10-VALIDATION.md index b59752a..77443bc 100644 --- a/.planning/phases/10-pax8-client-auth-foundation/10-VALIDATION.md +++ b/.planning/phases/10-pax8-client-auth-foundation/10-VALIDATION.md @@ -1,8 +1,8 @@ --- phase: 10 slug: pax8-client-auth-foundation -status: draft -nyquist_compliant: false +status: approved +nyquist_compliant: true wave_0_complete: false created: 2026-07-10 --- @@ -17,20 +17,20 @@ created: 2026-07-10 | Property | Value | |----------|-------| -| **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} | -| **Config file** | {path or "none — Wave 0 installs"} | -| **Quick run command** | `{quick command}` | -| **Full suite command** | `{full command}` | -| **Estimated runtime** | ~{N} seconds | +| **Framework** | vitest 4.1.5 | +| **Config file** | `vitest.config.ts` (root) — `include: ['lib/**/*.test.ts']`, `globals: false` | +| **Quick run command** | `npx vitest run lib/services/pax8-client.test.ts lib/services/pax8-factory.test.ts` | +| **Full suite command** | `npm test` | +| **Estimated runtime** | ~10 seconds (2 small new test files; full suite already fast per existing CI notes) | --- ## Sampling Rate -- **After every task commit:** Run `{quick run command}` -- **After every plan wave:** Run `{full suite command}` -- **Before `/gsd:verify-work`:** Full suite must be green -- **Max feedback latency:** {N} seconds +- **After every task commit:** Run `npx vitest run lib/services/pax8-client.test.ts lib/services/pax8-factory.test.ts` (once these exist, from Plan 01 Tasks 2/3) +- **After every plan wave:** Run `npm test` (full suite — cheap, this phase adds ~2 small test files, no regressions expected) +- **Before `/gsd:verify-work`:** Full suite must be green, plus the migration idempotency check (Plan 02 Task 2) and the live-credential checkpoint (Plan 03 Task 2) +- **Max feedback latency:** ~10 seconds (vitest quick run); the live auth-proof checkpoint is manual and not part of automated sampling --- @@ -38,7 +38,13 @@ created: 2026-07-10 | Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | |---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| -| {N}-01-01 | 01 | 1 | REQ-{XX} | T-{N}-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | +| 10-01-T1 | 01 | 1 | PAX8-01 / PAX8-02 | — | N/A (types-only, no runtime secret handling) | source-assertion | `npx tsc --noEmit --pretty` | ❌ W0 | ⬜ pending | +| 10-01-T2 | 01 | 1 | PAX8-01 | T-10-01, T-10-02, T-10-03, T-10-04, T-10-05 | Client secret never logged/thrown; audience + JSON body hardcoded; TLS host hardcoded; 60s expiry buffer | unit (mocked fetch) | `npx vitest run lib/services/pax8-client.test.ts` | ❌ W0 | ⬜ pending | +| 10-01-T3 | 01 | 1 | PAX8-02 | T-10-01 | Config-presence check + throw-if-missing never echoes secret value | unit | `npx vitest run lib/services/pax8-factory.test.ts` | ❌ W0 | ⬜ pending | +| 10-02-T1 | 02 | 1 | PAX8-01 / PAX8-02 | T-10-06 | Static DDL, no interpolated/dynamic values — injection not reachable | source-assertion (grep) | `test $(grep -v '^--' migrations/091_pax8_tables.sql \| grep -c "CREATE TABLE IF NOT EXISTS pax8_") -eq 6` | ❌ W0 | ⬜ pending | +| 10-02-T2 | 02 | 1 | PAX8-01 / PAX8-02 | T-10-07 | `IF NOT EXISTS` only, no DROP/ALTER of existing data; re-apply is a proven no-op | manual/smoke | `docker exec pulse-postgres psql -U pulse_user -d pulse_autotask -tAc "SELECT count(*) FROM information_schema.tables WHERE table_name LIKE 'pax8\_%'"` (falls back to a documented pending-developer-step if the container is unreachable) | N/A | ⬜ pending | +| 10-03-T1 | 03 | 2 | PAX8-01 | T-10-10 | Verify script logs only counts/status — never access token or client secret | source-assertion (grep) + build | `grep -n "access_token\|accessToken\|clientSecret\|CLIENT_SECRET" scripts/verify-pax8-auth.ts` (expect no match) `&& npx tsc --noEmit --pretty` | ❌ W0 | ⬜ pending | +| 10-03-T2 | 03 | 2 | PAX8-01 (live proof, ROADMAP SC#2) | T-10-09, T-10-11 | Real credentials only in gitignored `.env.local`; wrong-audience/wrong-encoding failures surface as documented signals, not silent | manual (`checkpoint:human-verify`) | `npx tsx scripts/verify-pax8-auth.ts` (developer-run, requires live `PAX8_CLIENT_ID`/`PAX8_CLIENT_SECRET`) | N/A | ⬜ pending | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* @@ -46,11 +52,11 @@ created: 2026-07-10 ## Wave 0 Requirements -- [ ] `{tests/test_file.py}` — stubs for REQ-{XX} -- [ ] `{tests/conftest.py}` — shared fixtures -- [ ] `{framework install}` — if no framework detected +- [ ] `lib/services/pax8-client.test.ts` — covers PAX8-01 (token exchange body/headers, expiry-cache reuse, auth-proof call, secret-never-thrown). Written as part of Plan 01 Task 2 (TDD: written before `pax8-client.ts`), not a separate pre-phase scaffold. +- [ ] `lib/services/pax8-factory.test.ts` — covers PAX8-02 (`isPax8Configured()`, throw-if-missing, singleton + reset). Written as part of Plan 01 Task 3 (TDD: written before `pax8-factory.ts`). +- [ ] No new test framework/config needed — vitest is already configured project-wide and the `lib/**/*.test.ts` glob already picks up these two new files automatically. -*If none: "Existing infrastructure covers all phase requirements."* +*Both Wave 0 files are created inline by Plan 01's TDD tasks rather than a separate Wave 0 pass — no standalone scaffolding step is needed before Wave 1 starts.* --- @@ -58,19 +64,18 @@ created: 2026-07-10 | Behavior | Requirement | Why Manual | Test Instructions | |----------|-------------|------------|-------------------| -| {behavior} | REQ-{XX} | {reason} | {steps} | - -*If none: "All phase behaviors have automated verification."* +| Live OAuth2 token exchange + `/companies` read against the real PAX8 API | PAX8-01 (ROADMAP SC#2) | No existing integration client in this codebase has a "live API" test tier (cost/reliability of hitting real vendor APIs in CI); `PAX8_CLIENT_ID`/`PAX8_CLIENT_SECRET` are not yet present in `.env.local` and must be developer-provisioned | See Plan 10-03 Task 2 checkpoint: add credentials to `.env.local`, run `npx tsx scripts/verify-pax8-auth.ts`, confirm it prints a company count and exits 0 | +| Migration idempotency on the live dev DB | PAX8-01 / PAX8-02 (ROADMAP SC#4) | Postgres applies `migrations/*.sql` on first volume boot only (per CLAUDE.md) — the existing dev DB volume already booted, so this migration must be applied manually, not via container init | See Plan 10-02 Task 2: `bash scripts/apply-migrations.sh 091_pax8_tables.sql` run twice, second run must exit 0 with no error | --- ## Validation Sign-Off -- [ ] All tasks have `` verify or Wave 0 dependencies -- [ ] Sampling continuity: no 3 consecutive tasks without automated verify -- [ ] Wave 0 covers all MISSING references -- [ ] No watch-mode flags -- [ ] Feedback latency < {N}s -- [ ] `nyquist_compliant: true` set in frontmatter +- [x] All tasks have `` verify or Wave 0 dependencies (2 manual-only exceptions documented above, both with explicit fallback/no-op behavior if infra is unreachable) +- [x] Sampling continuity: no 3 consecutive tasks without automated verify (only the two manual-only rows lack one, and they are not adjacent — 10-02-T2 sits between two automated tasks; 10-03-T2 is the final task in the phase) +- [x] Wave 0 covers all MISSING references (both `❌ W0` test files are covered by Plan 01's own TDD tasks) +- [x] No watch-mode flags (`vitest run`, not `vitest watch`, used throughout) +- [x] Feedback latency < 10s (vitest quick run; manual checkpoints are explicitly out-of-band, not part of the automated feedback loop) +- [x] `nyquist_compliant: true` set in frontmatter -**Approval:** {pending / approved YYYY-MM-DD} +**Approval:** approved 2026-07-10