test(billing): checkout lifecycle contract tests against local Postgres#47
Merged
Merged
Conversation
Adds a checkout-lifecycle contract lane that exercises the real checkout_lifecycle_* SQL RPCs in supabase/migrations/20260712193633_checkout_deletion_lifecycle.sql through the production processStripeEvent/getCreditBalanceCents billing policy, with only Stripe mocked. Covers concurrent completion+deletion races, missing vs fenced users, registered/ unregistered sessions, refund states (partial/pending/failed/ canceled/succeeded), crash-window recovery, and replayed events. The lane runs via `bun test` (needs bun:sql) against 127.0.0.1:54322, guarded to a disposable local Supabase database only, and skips cleanly with a clear message when unreachable so plain `bun test`/vitest and CI's default check job stay green. CI's database job runs it for real via `bun run test:supabase` after `supabase db start`. Removes the checkout-lifecycle-policy cases duplicated in the MemorySupabase billing.test.ts fake now that the contract lane characterizes that behavior against the durable SQL directly, keeping the fake and its remaining fast non-lifecycle unit tests (webhook verification, checkout-session creation/idempotency, validation, and database-error resilience). Refs #37 (CAND-1).
7 tasks
ElbertePlinio
added a commit
that referenced
this pull request
Jul 19, 2026
Replace the settings-sync last-writer-wins choreography (conditional `.update()` guarded by `updated_at <`, falling back to an `ignoreDuplicates` `.upsert()`, falling back to a third read to learn the outcome) with one durable `settings_sync_lww_write` SQL function that locks the (user_id, field_group) row, compares/writes/tombstones, and always returns the authoritative row in the same call. pushGroup/deleteGroup keep all validation/sanitization and now just call the durable RPC; `written`/`stale` result semantics are unchanged. The migration is additive only (#35): it adds the function, nothing else. Adds a local-Postgres contract lane (packages/sync/test/lww.contract.test.ts) following PR #47's pattern (skips cleanly without Postgres, wired into `test:supabase`), covering real concurrent push/push, first-insert/ first-insert, and push/delete races, equal-timestamp ties, and older data racing in after a newer tombstone. pgTAP coverage (supabase/tests/database/settings_sync_lww.test.sql) adds microsecond canonicalization, the existing future-timestamp check constraint, and per-user isolation. Issue #37 CAND-3.
ElbertePlinio
added a commit
that referenced
this pull request
Jul 19, 2026
…cation (#51) Issue #37 (CAND-4): operator-router's idempotency only became durable AFTER provider work. The handler checked `credit_ledger` for a completed route, invoked the OpenAI-compatible provider, and only then wrote a debit row keyed by the idempotency key — two concurrent requests for the same key could both find nothing completed, both call the provider, and the losing response was simply discarded after the winner's debit landed (wasted provider spend with no compensating record). New `router_attempts` table and three service-role-only RPCs (`router_attempt_claim` / `router_attempt_complete` / `router_attempt_fail`, supabase/migrations/20260722000000_router_attempt_claim.sql) move the durable decision in front of the provider call: at most one caller ever owns "go call the provider" for a given (user_id, idempotency_key) at a time. A claim that is neither completed nor explicitly failed is recoverable once its lease elapses. This is additive only: existing `credit_ledger` rows written before this migration remain authoritative for old completed routes (`findDebitedRouteResult`), independent of whether a `router_attempts` row exists for that key. `createOperatorRouterHandler` now claims before invoking the provider, completes the claim with the provider outcome, then debits — releasing the claim immediately on a definitive provider failure, and skipping straight to a debit retry (no re-invocation) when a claim is found already completed. - `packages/edge-shared/src/index.ts`: `claimRouteAttempt`, `completeRouteAttempt`, `failRouteAttempt`, `findDebitedRouteResult` own the RPC/route-metadata encode-decode boundary; `debitCredits` is unchanged. `createOperatorRouterHandler` takes `serviceSupabase` instead of separate `findCompletedRoute`/`debit` callbacks, and returns 409 `attempt_in_progress` while another claim is live. - `supabase/functions/operator-router/index.ts`: wires the caller's service-role client straight through; drops the adapter-owned `findCompletedRoute` query and `debit` callback. - `packages/edge-shared/test/edge-shared.test.ts`: replaces the `findCompletedRoute`/`debit` fakes with a `FakeRouterServiceSupabase` reimplementing the claim/complete/fail/debit RPC contract, covering the full claim -> provider -> complete -> debit choreography, live claims, lease recovery, and debit-retry-without-re-invoking-the- provider. - `supabase/tests/database/router_attempt_claim.test.sql`: pgTAP coverage for claim/in_progress/completed outcomes, idempotent completion replay, failure recovery, lease-based recovery, per-user isolation, privilege grants, and input validation. - `packages/edge-shared/test/router-attempt.contract.test.ts` (+ `router-fixtures.ts`, `postgres-router-client.ts`): a local-Postgres contract lane, following #47/#50's convention, proving the durable claim/complete/fail/debit functions against real Postgres, including two genuinely concurrent claims for the same key resolving to exactly one claim, and a full `createOperatorRouterHandler` run proving the provider is invoked and the ledger is debited exactly once across two concurrent same-key requests. Wired into `bun run test:supabase`. - Bumps `@pickforge/*` 0.11.0 -> 0.12.0 and re-pins every Deno function's `npm:` import, per the existing publish-then-pin convention. Validated: `bun run typecheck`; `bun run test` (366/366); `bun run build`; `supabase db reset` (all migrations apply cleanly); `bun run test:supabase` against real local Postgres — pgTAP (178 tests, including the new router_attempt_claim lane), welcome-credits concurrency, the CAND-1 checkout-lifecycle contract lane (20/20) and CAND-3 sync LWW contract lane (8/8) stay green, and the new router attempt contract lane (8/8) all pass; `supabase db advisors --local --type security --fail-on warn` clean. `deno check` against the real npm registry isn't possible pre-publish (same documented gap as #48: published `@pickforge/*` is still behind main) — confirmed the pre-existing gap is unchanged (this diff's remaining `deno check` errors are a strict subset of main's) and validated the adapter's typing directly against local `src` via `tsc` instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements CAND-1 of #37 (issue #37's PR 1 — checkout lifecycle local-Postgres interface): a contract-test lane that characterizes checkout/deletion lifecycle behavior against the real durable SQL rather than the in-memory billing test fake.
What changed
packages/billing/test/checkout-lifecycle.contract.test.ts— 20 cases exercising the productionprocessStripeEvent/getCreditBalanceCentsbilling policy against the realcheckout_lifecycle_*RPCs insupabase/migrations/20260712193633_checkout_deletion_lifecycle.sql, with only Stripe mocked. Covers:charge_already_refundedpackages/billing/test/postgres-lifecycle-client.ts— aSupabaseClientLikeadapter backed by a realbun:sqlPostgres connection. Reimplements no lifecycle policy; everycheckout_lifecycle_*decision is made by the durable SQL functions. Every query/RPC is wrapped in its ownSAVEPOINTso a caught error on one call (e.g. a duplicate-key retry) doesn't poison the outer per-test transaction, matching how independent PostgREST requests behave in production.packages/billing/test/lifecycle-fixtures.ts— connection/guard helpers restricted to the disposable local Supabase database (127.0.0.1:54322), rollback-per-test isolation, and non-transactional cleanup for the one true-concurrency test.package.json—test:supabasenow also runs the contract lane viabun test.vitest.config.ts— excludes*.contract.test.tsso plainvitest run/bun run test(CI'scheckjob) stays green without Postgres; CI'sdatabasejob runs it for real aftersupabase db start.packages/billing/test/billing.test.ts— removed the checkout-lifecycle-policy cases now duplicated by the contract lane (fenced/missing-user refund flows, refund-state transitions, deletion-race scenarios, event replay/dedupe). Kept theMemorySupabasefake and its remaining fast non-lifecycle unit tests (webhook verification, checkout-session creation/idempotency-key generation, input validation, database-error resilience, balance/ledger reads).Skip behavior
When local Postgres/Supabase isn't reachable, the lane logs a clear message and skips every test via
describe.skipIf, sobun test/vitest runand CI's defaultcheckjob stay green without Docker.Validation
bun run typecheck— passbun run test(vitest, matches CIcheckjob) — 346 pass, contract lane cleanly excludedSUPABASE_DB_URLat an unreachable port and confirmedbun test packages/billing/test/checkout-lifecycle.contract.test.tsskips all 22 tests with the expected log messagesupabase db reset(fresh disposable DB, matching CI'ssupabase db start)PICKFORGE_ALLOW_LOCAL_DB_TESTS=1 SUPABASE_DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres bun run test:supabase(matches CI'sdatabasejob) — all SQL fixture tests, the welcome-credits concurrency script, and all 20 contract tests passsupabase db advisors --local --type security --fail-on warn— no issuesbun run build— passRefs #37 (CAND-1). Unblocks CAND-2 (consolidate checkout/deletion orchestration).