Skip to content

feat(pax8): add safe ordering and billing-truth workflow#2501

Merged
ToddHebebrand merged 40 commits into
mainfrom
ToddHebebrand/pax8-ordering
Jul 16, 2026
Merged

feat(pax8): add safe ordering and billing-truth workflow#2501
ToddHebebrand merged 40 commits into
mainfrom
ToddHebebrand/pax8-ordering

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

Summary

  • add a partner-scoped Pax8 order ledger with RLS, direct and quote-staged authoring, preflight, submission, reconciliation, and no-blind-retry safeguards
  • add the organization ordering workspace, accepted-quote handoff, dependency-gated subscription actions, dynamic provisioning details, and accessible recovery flows
  • make Breeze's authorized order quantity the billing source of truth; Pax8-reported quantity is observation-only and surfaces drift instead of overwriting contract billing
  • require explicit Breeze quantities when creating linked manual contract lines, and harden product, subscription, contract-line, cancellation, and concurrency integrity
  • update API/product documentation and all five web locales for the final workflow

Why

Pax8's reported subscription quantity can be stale and may not match what Pax8 invoices. Automatically copying it into Breeze contract lines can bill customers incorrectly. This change routes money-bearing actions through an auditable order ledger, preserves approved quote membership, fails closed on ambiguous vendor outcomes, and keeps Pax8 quantity data as drift evidence only.

Validation

  • API Pax8/quote unit matrix: 359 tests
  • web matrix: 186 tests; final billing-truth delta: 141 tests
  • shared package: 1,202 tests
  • fresh PostgreSQL integrations: 34 tests
  • RLS coverage: 53 tests
  • fresh migration application, idempotence, and drift checks
  • API/web lint, TypeScript, and production builds
  • docs build: 146 pages; docs mapping: 57 tests
  • final whole-branch specification/security review: approved

E2E note

Authenticated live browser/API E2E was unavailable in this worktree because the required E2E credentials and matching web/API/Redis stack were not present. No real Pax8 vendor write was attempted. The scoped component/API and fresh-database evidence is recorded in docs/testing/FEATURE_TEST_LOG.md.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 14, 2026

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: 78bda43
Status: ✅  Deploy successful!
Preview URL: https://21647eb2.breeze-9te.pages.dev
Branch Preview URL: https://toddhebebrand-pax8-ordering.breeze-9te.pages.dev

View logs

@ToddHebebrand
ToddHebebrand marked this pull request as ready for review July 16, 2026 04:08
@ToddHebebrand
ToddHebebrand force-pushed the ToddHebebrand/pax8-ordering branch from d9a6036 to 0a93eb8 Compare July 16, 2026 04:17
Todd Hebebrand and others added 27 commits July 15, 2026 22:21
Adds the write path to the Pax8 integration: place, change, and cancel
subscriptions from a customer's org page or as the fulfillment step of an
approved quote.

Establishes Breeze as the source of truth for licensing and billing —
Pax8's Subscription.quantity is stale and does not match what Pax8
invoices, so the existing nightly quantity push into contract_lines is
demoted to drift detection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ten tasks, TDD throughout. Also corrects the spec: quote-acceptance stages
the order INSIDE the accept transaction (as Phase 5, alongside contract
creation), not in the post-commit tail — the tail exists only for Redis/BullMQ
side effects, and a staged order referencing rolled-back contract lines would
be unfixable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pax8 subscription quantity can differ from what Pax8 invoices. Keep the Breeze order ledger authoritative, persist quantity evidence, and expose read-only tenant-scoped drift instead of mutating contract billing during nightly sync.
Treat legacy snapshots as unknown, preserve the last genuine observation when Pax8 omits quantity, and expose the evidence bit to subscription consumers.
@ToddHebebrand
ToddHebebrand force-pushed the ToddHebebrand/pax8-ordering branch from 0a93eb8 to 78bda43 Compare July 16, 2026 04:21
@ToddHebebrand
ToddHebebrand merged commit 060e76b into main Jul 16, 2026
41 of 42 checks passed
@ToddHebebrand
ToddHebebrand deleted the ToddHebebrand/pax8-ordering branch July 16, 2026 04:30
ToddHebebrand added a commit that referenced this pull request Jul 16, 2026
…rtion (#2563)

## Summary

Fixes the red `Test API` CI job on `main`
(`quoteService.clone.test.ts`).

## Root cause

**Not a product bug** — a stale mock in `quoteService.clone.test.ts`
after two independently-merged PRs interacted:

- #2501 (pax8 ordering) added two DB selects inside `getQuote()`: a
`pax8OrderSummary` lookup and a conditional `pax8OrderLineSummary`
lookup, inserted between the existing `lines` select and `getQuote()`'s
return.
- #2535 (quote cloning) added `cloneQuote()`, which calls `getQuote()`
and then issues its own `quoteImages` select immediately after.

`quoteService.clone.test.ts` mocks `db.select()` with a shared FIFO
queue of canned results, and pushed exactly 4 entries (`quote`,
`blocks`, `lines`, `images`) — the correct count *before* #2501 landed.
With the extra `pax8OrderSummary` select now spliced into `getQuote()`,
the queue's "images" entry was consumed by the `pax8OrderSummary`
destructure instead, so the real `quoteImages` select fell through to
the queue's empty-array default. `imageIds` ended up an empty `Map`, so
remapping the image block's `imageId` legitimately found nothing and
`cloneQuote` correctly threw `QuoteServiceError('Quote image could not
be cloned', 409, 'IMAGE_NOT_FOUND')`.

Confirmed this can't happen in production: real Postgres queries are
scoped by real `WHERE` clauses per table and can't cross-contaminate the
way the mock's shared queue does.

## Fix

`apps/api/src/services/quoteService.clone.test.ts`: push one extra empty
result for the `pax8OrderSummary` select (i.e. "no staged Pax8 order for
this quote"), which also correctly short-circuits the conditional
`pax8OrderLineSummary` select exactly as the real code does. This makes
the mock's select order match `getQuote()`'s actual current select
order.

## Verification

- `pnpm vitest run src/services/quoteService.clone.test.ts
src/services/quoteService.test.ts src/services/quoteLifecycle.test.ts` —
31/31 passing (was 1 failing).
- `pnpm vitest run src/services/` (full suite, 5825 tests) — all green
except one unrelated pre-existing timing flake in
`encryptedColumnRegistry.test.ts` (5000ms scrypt timeout), confirmed to
pass in isolation and untouched by this change.
- Node 22.20.0, matching `.nvmrc`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Todd Hebebrand <todd@lanternops.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant