docs(14-01): complete PAX8 companies API routes plan
- Add SUMMARY.md documenting the two routes, deviations, and next-phase readiness
This commit is contained in:
parent
2cc1abbf9a
commit
501c7e4106
1 changed files with 111 additions and 0 deletions
111
.planning/phases/14-pax8-ui-surface/14-01-SUMMARY.md
Normal file
111
.planning/phases/14-pax8-ui-surface/14-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
---
|
||||
phase: 14-pax8-ui-surface
|
||||
plan: 01
|
||||
subsystem: api
|
||||
tags: [postgres, next.js, pax8, api-route, requireAuth]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 12-orders-invoices-company-matching
|
||||
provides: pax8_order_items historical cost data, pax8_companies.autotask_company_id matching
|
||||
provides:
|
||||
- "GET /api/pax8/companies — paginated/sortable/searchable PAX8 company list with matched Autotask name and active subscription count"
|
||||
- "GET /api/pax8/companies/[id] — single company subscriptions + per-subscription latest-billed cost breakdown, windowed per subscription_id"
|
||||
affects: ["14-04 (the /pax8 page that consumes these routes)"]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Whitelisted ORDER BY column map for user-controllable sort params (never interpolate raw sort/order strings into SQL)"
|
||||
- "DISTINCT ON (subscription_id) windowed query for per-entity 'latest' lookups instead of a single global MAX cutoff"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- app/api/pax8/companies/route.ts
|
||||
- app/api/pax8/companies/[id]/route.ts
|
||||
modified: []
|
||||
|
||||
key-decisions:
|
||||
- "Both routes gate with requireAuth() only (D-07) — no admin/permission check, since this is read-only view data any authenticated manager may see"
|
||||
- "Cost breakdown uses order-item line_total as the billed amount, falling back to price*quantity only when no order-item row exists for a subscription (Pitfall 3)"
|
||||
- "Order-item rows whose subscription_id has no matching pax8_subscriptions row (tombstoned/unsynced) are still surfaced as historical breakdown rows with null billingTerm/status (Pitfall 4)"
|
||||
|
||||
patterns-established:
|
||||
- "Pattern: whitelist map for sort column resolution (SORT_COLUMNS) — reusable template for any future paginated/sortable list route in this codebase"
|
||||
|
||||
requirements-completed: [PAX8-13]
|
||||
|
||||
# Metrics
|
||||
duration: 12min
|
||||
completed: 2026-07-11
|
||||
---
|
||||
|
||||
# Phase 14 Plan 01: PAX8 Companies API Routes Summary
|
||||
|
||||
**Two read-only API routes backing the /pax8 Companies tab: an injection-safe paginated/sortable/searchable company list, and a per-company drill-down that correctly windows the "latest billed amount" per subscription instead of using a single global cutoff date.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 12 min
|
||||
- **Started:** 2026-07-11T18:16:00Z
|
||||
- **Completed:** 2026-07-11T18:28:23Z
|
||||
- **Tasks:** 2 completed
|
||||
- **Files modified:** 2 (both new)
|
||||
|
||||
## Accomplishments
|
||||
- `GET /api/pax8/companies` returns `{ items, total, limit, offset }` with camelCase fields, a hardcoded sort-column whitelist, and fully parameterized search/limit/offset — no SQL injection surface on any user-controllable input.
|
||||
- `GET /api/pax8/companies/[id]` returns `{ company, subscriptions, costTotal }`, correctly handling PAX8's NCE per-subscription anniversary billing (each subscription's own latest order-item row, not a single company-wide `MAX(start_period)`), uses `line_total` (never `unit_price * quantity`), and surfaces historical order-item rows for subscriptions that no longer exist in `pax8_subscriptions`.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: GET /api/pax8/companies — paginated company list** - `443b6ce` (feat)
|
||||
2. **Task 2: GET /api/pax8/companies/[id] — subscriptions + cost breakdown** - `2cc1abb` (feat)
|
||||
|
||||
**Plan metadata:** (this commit, following SUMMARY.md creation)
|
||||
|
||||
## Files Created/Modified
|
||||
- `app/api/pax8/companies/route.ts` — paginated/sortable/searchable PAX8 company list, requireAuth-gated
|
||||
- `app/api/pax8/companies/[id]/route.ts` — single-company subscriptions + per-subscription latest-billed cost breakdown
|
||||
|
||||
## Decisions Made
|
||||
- Both routes use `requireAuth()` only, never `requirePermission`, per CONTEXT.md D-07 — this is read-only view data for any authenticated manager, not an admin-gated action.
|
||||
- `match_confidence` and `autotask_company_id` are cast to `::text` in SQL and coerced with `Number()` in JS to avoid `pg`'s default numeric/bigint string-return behavior producing unexpected types downstream.
|
||||
- Tombstoned-subscription order-item rows (3 of 436 distinct subscription_ids referenced by `pax8_order_items` live-verified in RESEARCH.md to have no matching `pax8_subscriptions` row) are appended as extra breakdown rows with `billingTerm`/`status` set to `null`, per the plan's explicit instruction — these represent real historical spend that would otherwise silently vanish from `costTotal`.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written. One in-flight self-correction during Task 1 implementation (documented below) was caught and fixed before committing, not a deviation from the plan's design.
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Fixed parameter-index mismatch in the companies list total-count query**
|
||||
- **Found during:** Task 1 (GET /api/pax8/companies)
|
||||
- **Issue:** The total-count query reused the `searchFilter` SQL fragment built for the main list query (where `search` binds to `$3` because `$1`/`$2` are `limit`/`offset`), but passed only `[search]` as its params array — a mismatch that would bind the search term to `$1` while the SQL referenced `$3`, causing either a runtime bind error or (worse) a silently wrong/unfiltered count.
|
||||
- **Fix:** Built a separate `totalParams`/`totalSearchFilter` pair scoped to the total-count query's own parameter numbering (search binds to `$1` there, since that query has no limit/offset params).
|
||||
- **Files modified:** app/api/pax8/companies/route.ts
|
||||
- **Verification:** `npx tsc --noEmit --pretty` passes; manual trace of parameter binding for both the with-search and without-search cases confirms correct `$N` alignment.
|
||||
- **Committed in:** 443b6ce (Task 1 commit — fixed before first commit, not a follow-up)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (1 bug, caught pre-commit during implementation)
|
||||
**Impact on plan:** No scope creep — this was an implementation-time bug caught and fixed before the task was ever committed, not a change to the plan's design.
|
||||
|
||||
## Issues Encountered
|
||||
None.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Both routes are ready for Plan 04 (the `/pax8` page) to consume via `fetch()`.
|
||||
- Response shapes match the plan's `must_haves.artifacts` contract exactly: list route exports `GET` returning `{ items, total, limit, offset }`; drill-down route exports `GET` returning `{ company, subscriptions, costTotal }`.
|
||||
- No blockers for downstream plans in this wave (14-02, 14-03) — no file overlap, no shared state introduced.
|
||||
|
||||
---
|
||||
*Phase: 14-pax8-ui-surface*
|
||||
*Completed: 2026-07-11*
|
||||
Loading…
Add table
Add a link
Reference in a new issue