Add checkout.session.completed webhook validation and tests #591

Open
pook wants to merge 59 commits from agent-task/588 into main
Owner

Summary

  • handleCheckoutCompleted now throws explicit errors when userId or subscription is missing from the Stripe session, instead of silently returning. This ensures malformed events surface as failures rather than being swallowed.
  • The billing webhook route returns 400 for malformed payloads (missing required fields) and 500 for unexpected handler errors.
  • Extracts customerId and priceId from the session/subscription objects for future use.
  • Adds 7 unit tests with mocked Stripe payloads covering: successful insertion, missing subscription ID, missing userId, duplicate event idempotency, and agency-to-enterprise plan mapping.

Depends on #586 (subscriptions table migration).

Test plan

  • bun test packages/api/tests/unit/checkout-webhook.test.ts — 7/7 pass
  • TypeScript compiles with zero errors in changed files
  • Verify against live Stripe test mode webhook delivery

🤖 Generated with Claude Code

## Summary - `handleCheckoutCompleted` now throws explicit errors when `userId` or `subscription` is missing from the Stripe session, instead of silently returning. This ensures malformed events surface as failures rather than being swallowed. - The billing webhook route returns 400 for malformed payloads (missing required fields) and 500 for unexpected handler errors. - Extracts `customerId` and `priceId` from the session/subscription objects for future use. - Adds 7 unit tests with mocked Stripe payloads covering: successful insertion, missing subscription ID, missing userId, duplicate event idempotency, and agency-to-enterprise plan mapping. Depends on #586 (subscriptions table migration). ## Test plan - [x] `bun test packages/api/tests/unit/checkout-webhook.test.ts` — 7/7 pass - [x] TypeScript compiles with zero errors in changed files - [ ] Verify against live Stripe test mode webhook delivery 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add checkout.session.completed webhook validation and unit tests
Some checks are pending
CI Quality Gate / Lint / Typecheck / Test / Build (pull_request) Waiting to run
99785d7daf
handleCheckoutCompleted now throws on missing userId or subscription ID
instead of silently returning, and the billing route returns 400 for
malformed event payloads. Adds 7 unit tests covering successful checkout
insertion, missing subscription ID, missing userId, duplicate event
idempotency, and agency-to-enterprise plan mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: issue #588 create-a-new-webhook-handler-for-stripe (agent task liancebot588)
Some checks failed
CI Quality Gate / Lint / Typecheck / Test / Build (pull_request) Has been cancelled
8666b669fe
pook left a comment
Author
Owner

Review: Checkout Webhook Handler

Tests: 7/7 pass (bun test) — covers happy path, missing fields, idempotency, and plan mapping.

What's good

Signature verification (billing.ts:67-81): Uses arrayBuffer() to preserve raw bytes for HMAC verification instead of re-encoding through UTF-8. This is the correct approach — text() can silently alter bytes and break Stripe signature checks.

Explicit errors over silent returns (stripe.ts:217-222): The old code did if (!userId || !session.subscription) return which would silently swallow malformed checkout events. Throwing explicit errors means these surface as failures in logs and the webhookFailures table. Right call for billing-critical code.

Idempotency (stripe.ts:232-257): Checks for existing subscription by stripeSubId before inserting. Duplicate webhook deliveries (which Stripe does) will update rather than create duplicates.

Subscription type handling (stripe.ts:225): Correctly handles session.subscription being either a string ID or an expanded object.

Failure persistence (billing.ts:115-125): Handler errors are logged to webhookFailures table with event ID, type, customer, and error message. The .catch() on the insert prevents a DB failure from masking the original error.

Nits (non-blocking)

  1. Unused variables (stripe.ts:224,229): customerId and priceId are extracted but never used. Consider removing until needed.

  2. Fragile error classification (billing.ts:127): errorMessage.includes("missing") to decide 400 vs 500 could misfire if a Stripe API error contains "missing". A custom error class would be more reliable.

  3. Duplicate test assertion (checkout-webhook.test.ts:188-194): The "extracts price ID" test only checks mockRetrieve was called, already asserted in the first test.

  4. Unused userRows (checkout-webhook.test.ts:10-12): Declared but never read.

Verdict

Approved. The critical path (signature verification, idempotent upsert, explicit error handling) is solid. Nits are non-blocking for a follow-up.

## Review: Checkout Webhook Handler **Tests:** 7/7 pass (`bun test`) — covers happy path, missing fields, idempotency, and plan mapping. ### What's good **Signature verification** (`billing.ts:67-81`): Uses `arrayBuffer()` to preserve raw bytes for HMAC verification instead of re-encoding through UTF-8. This is the correct approach — `text()` can silently alter bytes and break Stripe signature checks. **Explicit errors over silent returns** (`stripe.ts:217-222`): The old code did `if (!userId || !session.subscription) return` which would silently swallow malformed checkout events. Throwing explicit errors means these surface as failures in logs and the `webhookFailures` table. Right call for billing-critical code. **Idempotency** (`stripe.ts:232-257`): Checks for existing subscription by `stripeSubId` before inserting. Duplicate webhook deliveries (which Stripe does) will update rather than create duplicates. **Subscription type handling** (`stripe.ts:225`): Correctly handles `session.subscription` being either a string ID or an expanded object. **Failure persistence** (`billing.ts:115-125`): Handler errors are logged to `webhookFailures` table with event ID, type, customer, and error message. The `.catch()` on the insert prevents a DB failure from masking the original error. ### Nits (non-blocking) 1. **Unused variables** (`stripe.ts:224,229`): `customerId` and `priceId` are extracted but never used. Consider removing until needed. 2. **Fragile error classification** (`billing.ts:127`): `errorMessage.includes("missing")` to decide 400 vs 500 could misfire if a Stripe API error contains "missing". A custom error class would be more reliable. 3. **Duplicate test assertion** (`checkout-webhook.test.ts:188-194`): The "extracts price ID" test only checks `mockRetrieve` was called, already asserted in the first test. 4. **Unused `userRows`** (`checkout-webhook.test.ts:10-12`): Declared but never read. ### Verdict Approved. The critical path (signature verification, idempotent upsert, explicit error handling) is solid. Nits are non-blocking for a follow-up.
Some checks failed
CI Quality Gate / Lint / Typecheck / Test / Build (pull_request) Has been cancelled
This pull request has changes conflicting with the target branch.
  • .forgejo/workflows/ci.yml
  • bun.lock
  • package.json
  • packages/api/src/db/schema.ts
  • packages/api/src/index.ts
  • packages/api/src/middleware/csrf.ts
  • packages/api/src/middleware/rate-limit.ts
  • packages/api/src/middleware/security-headers.ts
  • packages/api/src/routes/admin.ts
  • packages/api/src/routes/billing.ts
  • packages/api/src/routes/generate-tos.ts
  • packages/api/src/routes/generate.ts
  • packages/api/src/routes/health.ts
  • packages/api/src/routes/questionnaire.ts
  • packages/api/src/services/document-generator.ts
  • packages/api/src/services/llm.ts
  • packages/api/src/templates/index.ts
  • packages/api/tsconfig.json
  • packages/shared/src/types.ts
  • packages/web/src/app/questionnaire/page.tsx
  • packages/web/src/components/documents/DocumentList.tsx
  • packages/web/src/components/questionnaire/ReviewStep.tsx
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin agent-task/588:agent-task/588
git switch agent-task/588
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pook/compliancebot!591
No description provided.