260 better checkout flow#261
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 📝 WalkthroughWalkthroughThis PR introduces a comprehensive Stripe-first billing redesign featuring KV-backed subscription caching, stateless plan resolution, config-driven entitlements, promo code validation, and usage metrics endpoints. The implementation spans shared error handling, backend billing routes, frontend UI components, and middleware integration with webhook-driven cache invalidation. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Frontend
participant API as Backend API
participant KV as KV Cache
participant Stripe as Stripe API
participant DB as Database
Client->>API: GET /api/billing/usage
API->>DB: Query projects & collaborators for org
DB-->>API: Return counts
API-->>Client: { projects, collaborators }
Client->>API: resolve plan for entitlements
API->>KV: Check subscription cache
alt Cache Hit
KV-->>API: Cached subscription
else Cache Miss
API->>Stripe: Fetch subscription by customer ID
Stripe-->>API: Active subscription
API->>KV: Store with TTL
end
API->>API: Map price ID to plan tier
API-->>Client: Plan tier + entitlements
sequenceDiagram
participant Client as Frontend
participant Checkout as Checkout Route
participant Stripe as Stripe API
participant Webhook as Webhook Handler
participant KV as KV Cache
Client->>Checkout: POST /validate-coupon { code }
Checkout->>Stripe: Query promotionCodes
Stripe-->>Checkout: Promo details or error
Checkout-->>Client: { valid, discount, duration }
Stripe->>Webhook: Event (subscription.created/updated)
Webhook->>KV: invalidateSubscription(customerId)
KV-->>Webhook: Cache cleared
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
corates | 57d88a3 | Commit Preview URL | Jan 09 2026, 05:49 PM |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In @packages/docs/plans/flowglad-features-plan.md:
- Around line 780-792: The PRICE_TO_PLAN mapping currently references
process.env (e.g., process.env.STRIPE_PRICE_TEAM_MONTHLY) which is incorrect for
Cloudflare Workers; update the docs to show using the Workers environment
bindings (e.g., the c.env or binding names) instead of process.env, and replace
or annotate every occurrence in the example for PRICE_TO_PLAN so it reads that
price IDs come from the Workers bindings (or pass them into the module) —
mention the exact symbol PRICE_TO_PLAN and show that values should be read from
the Worker binding names (or injected config) rather than process.env.
In @packages/docs/README.md:
- Line 5: The phrase "AI generated plans and audits" should use a hyphenated
compound modifier; update the README entry that currently reads "audits/ and
plans/ are for AI generated plans and audits of the codebase and features" to
"audits/ and plans/ are for AI-generated plans and audits of the codebase and
features" so that "AI-generated" correctly modifies "plans and audits."
In @packages/shared/src/errors/stripe.ts:
- Around line 78-97: The type guards isStripeCardError and
isStripeRateLimitError use wrong string literals; update them to check for
Stripe's actual error type values ('card_error' and 'rate_limit_error') instead
of 'StripeCardError'/'StripeRateLimitError', or alternatively import the Stripe
SDK and switch the guards to instanceof checks against
Stripe.errors.StripeCardError and Stripe.errors.StripeRateLimitError (choose one
approach and apply it to both functions).
In @packages/workers/src/routes/billing/checkout.js:
- Around line 23-32: Replace the manual body parsing and ad-hoc checks in the
billingCheckoutRoutes.post('/validate-coupon') handler with the validateRequest
middleware using a schema (e.g., validateCouponSchema) defined in the validation
config (validateCouponSchema = z.object({ code: z.string().min(1, 'Please enter
a promo code').trim() })). Attach validateRequest(validateCouponSchema) to the
route before the async handler, remove the c.req.json() and manual string/trim
checks, and read the validated value (from the request context provided by
validateRequest) inside the handler to continue the existing coupon validation
logic.
- Around line 39-41: Update the Stripe API version in the other billing route so
all Stripe clients use the same version; locate the Stripe client instantiation
(the new Stripe(...) call) in packages/workers/src/routes/billing/invoices.js
and change its apiVersion option from '2024-06-20' to '2025-11-17.clover' so it
matches the instantiation in packages/workers/src/routes/billing/checkout.js
(const stripe = new Stripe(..., { apiVersion: '2025-11-17.clover' })).
🧹 Nitpick comments (4)
packages/workers/src/routes/billing/checkout.js (1)
23-23: Consider adding rate limiting to prevent abuse.The
/validate-couponendpoint lacks rate limiting, unlike the other checkout routes that usebillingCheckoutRateLimit. This could allow attackers to enumerate valid promo codes or abuse the Stripe API.🛡️ Suggested fix
-billingCheckoutRoutes.post('/validate-coupon', requireAuth, async c => { +billingCheckoutRoutes.post('/validate-coupon', billingCheckoutRateLimit, requireAuth, async c => {packages/docs/plans/phase-4-embedded-checkout.md (1)
39-56: Add language specifiers to ASCII diagram code blocks.The code blocks for architecture diagrams should have a language specifier (e.g.,
textorplaintext) to satisfy linting and improve rendering consistency.📝 Suggested fix
-``` +```text User clicks "Subscribe" | vpackages/web/src/components/settings/pages/BillingSettings.jsx (1)
101-101: Consider adding a comment explaining the side-effect initialization.The
useMembers()call without using its return value may confuse future maintainers. A brief comment would clarify intent.📝 Suggested comment
- useMembers(); // Initialize members context + // Initialize members context for side effects (populates shared state used by child components) + useMembers();packages/docs/plans/flowglad-features-plan.md (1)
1190-1245: Add language specifiers to migration task code blocks.Several code blocks in the migration plan section are missing language specifiers, which affects linting and rendering.
📝 Suggested fix example
-``` +```text Morning: - [ ] Create KV namespace...Apply similar fix to code blocks at lines 1207, 1221, 1235, and 1413.
| const PRICE_TO_PLAN = { | ||
| // Team Monthly | ||
| [process.env.STRIPE_PRICE_TEAM_MONTHLY]: 'team', | ||
| // Team Yearly | ||
| [process.env.STRIPE_PRICE_TEAM_YEARLY]: 'team', | ||
| // Unlimited Team Monthly | ||
| [process.env.STRIPE_PRICE_UNLIMITED_MONTHLY]: 'unlimited_team', | ||
| // Unlimited Team Yearly | ||
| [process.env.STRIPE_PRICE_UNLIMITED_YEARLY]: 'unlimited_team', | ||
| // Starter Team (if exists) | ||
| [process.env.STRIPE_PRICE_STARTER_MONTHLY]: 'starter_team', | ||
| [process.env.STRIPE_PRICE_STARTER_YEARLY]: 'starter_team', | ||
| }; |
There was a problem hiding this comment.
Note: The PRICE_TO_PLAN mapping uses process.env which won't work in Cloudflare Workers.
The code sample shows process.env.STRIPE_PRICE_TEAM_MONTHLY but Cloudflare Workers use c.env or bindings, not process.env. Update the documentation to reflect the Workers environment.
📝 Suggested documentation fix
-// Price IDs from Stripe Dashboard
-// Format: price_xxx for live, price_test_xxx for test mode
-const PRICE_TO_PLAN = {
- // Team Monthly
- [process.env.STRIPE_PRICE_TEAM_MONTHLY]: 'team',
+// Price IDs from Stripe Dashboard (passed via env binding)
+// Format: price_xxx for live, price_test_xxx for test mode
+export function createPriceToPlanMap(env) {
+ return {
+ // Team Monthly
+ [env.STRIPE_PRICE_TEAM_MONTHLY]: 'team',
+ // ... rest
+ };
+}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @packages/docs/plans/flowglad-features-plan.md around lines 780 - 792, The
PRICE_TO_PLAN mapping currently references process.env (e.g.,
process.env.STRIPE_PRICE_TEAM_MONTHLY) which is incorrect for Cloudflare
Workers; update the docs to show using the Workers environment bindings (e.g.,
the c.env or binding names) instead of process.env, and replace or annotate
every occurrence in the example for PRICE_TO_PLAN so it reads that price IDs
come from the Workers bindings (or pass them into the module) — mention the
exact symbol PRICE_TO_PLAN and show that values should be read from the Worker
binding names (or injected config) rather than process.env.
|
|
||
| This package contains the VitePress documentation site for CoRATES. | ||
|
|
||
| Note the `audits/` and `plans/` are for AI generated plans and audits of the codebase and features. |
There was a problem hiding this comment.
Fix hyphenation in compound modifier.
"AI generated" should be hyphenated when used as a compound adjective modifying "plans and audits."
📝 Proposed fix
-Note the `audits/` and `plans/` are for AI generated plans and audits of the codebase and features.
+Note the `audits/` and `plans/` are for AI-generated plans and audits of the codebase and features.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Note the `audits/` and `plans/` are for AI generated plans and audits of the codebase and features. | |
| Note the `audits/` and `plans/` are for AI-generated plans and audits of the codebase and features. |
🧰 Tools
🪛 LanguageTool
[grammar] ~5-~5: Use a hyphen to join words.
Context: ...te the audits/ and plans/ are for AI generated plans and audits of the codeba...
(QB_NEW_EN_HYPHEN)
🤖 Prompt for AI Agents
In @packages/docs/README.md at line 5, The phrase "AI generated plans and
audits" should use a hyphenated compound modifier; update the README entry that
currently reads "audits/ and plans/ are for AI generated plans and audits of the
codebase and features" to "audits/ and plans/ are for AI-generated plans and
audits of the codebase and features" so that "AI-generated" correctly modifies
"plans and audits."
| export function isStripeCardError(error: unknown): boolean { | ||
| return ( | ||
| typeof error === 'object' && | ||
| error !== null && | ||
| 'type' in error && | ||
| (error as { type: string }).type === 'StripeCardError' | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Check if an error is a Stripe rate limit error | ||
| */ | ||
| export function isStripeRateLimitError(error: unknown): boolean { | ||
| return ( | ||
| typeof error === 'object' && | ||
| error !== null && | ||
| 'type' in error && | ||
| (error as { type: string }).type === 'StripeRateLimitError' | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Stripe Node.js SDK v20 error type StripeCardError StripeRateLimitError
💡 Result:
Short answer: In stripe-node v20 the API still exposes specific Stripe error classes (e.g., StripeCardError, StripeRateLimitError) under Stripe.errors, and Stripe's error objects also include a type/code you can inspect (e.g., "card_error" / "rate_limit"). Catch them with try/catch and either check instanceof Stripe.errors. or inspect err.type / err.code.
Example (Node.js / v20):
-
Using instanceof:
try {
await stripe.paymentIntents.create({...});
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
// handle card/payment error
} else if (err instanceof Stripe.errors.StripeRateLimitError) {
// handle rate limit
} else {
// other Stripe errors
}
} -
Or inspect properties:
catch (err) {
if (err.type === 'card_error') { /* ... / }
if (err.type === 'rate_limit_error') { / ... */ }
}
Sources: Stripe Error handling docs and stripe-node error-handling docs. [1][2]
Sources
[1] Stripe — Error handling (official docs).
[2] stripe-node — Error Handling (GitHub / wiki).
Fix incorrect Stripe error type strings.
The type guards check for 'StripeCardError' and 'StripeRateLimitError', but the actual Stripe SDK error type property values are 'card_error' and 'rate_limit_error' respectively. These functions will never match real Stripe errors. Either use instanceof checks with Stripe.errors.StripeCardError and Stripe.errors.StripeRateLimitError, or correct the type string comparisons to use the actual error type values.
🤖 Prompt for AI Agents
In @packages/shared/src/errors/stripe.ts around lines 78 - 97, The type guards
isStripeCardError and isStripeRateLimitError use wrong string literals; update
them to check for Stripe's actual error type values ('card_error' and
'rate_limit_error') instead of 'StripeCardError'/'StripeRateLimitError', or
alternatively import the Stripe SDK and switch the guards to instanceof checks
against Stripe.errors.StripeCardError and Stripe.errors.StripeRateLimitError
(choose one approach and apply it to both functions).
| billingCheckoutRoutes.post('/validate-coupon', requireAuth, async c => { | ||
| const logger = createLogger({ c, service: 'billing', env: c.env }); | ||
|
|
||
| try { | ||
| const body = await c.req.json(); | ||
| const { code } = body; | ||
|
|
||
| if (!code || typeof code !== 'string' || code.trim().length === 0) { | ||
| return c.json({ valid: false, error: 'Please enter a promo code' }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use validateRequest middleware instead of manual body parsing.
As per coding guidelines, request bodies should be validated using validateRequest middleware rather than manual validation. This ensures consistent validation behavior across routes.
♻️ Suggested refactor
First, add a validation schema (in your validation config):
// In validation config
export const validateCouponSchema = z.object({
code: z.string().min(1, 'Please enter a promo code').trim(),
});Then refactor the route:
-billingCheckoutRoutes.post('/validate-coupon', requireAuth, async c => {
+billingCheckoutRoutes.post('/validate-coupon', requireAuth, validateRequest(validateCouponSchema), async c => {
const logger = createLogger({ c, service: 'billing', env: c.env });
try {
- const body = await c.req.json();
- const { code } = body;
-
- if (!code || typeof code !== 'string' || code.trim().length === 0) {
- return c.json({ valid: false, error: 'Please enter a promo code' });
- }
+ const { code } = c.req.valid('json');Based on learnings, validateRequest middleware should always be used for request body validation in API routes.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @packages/workers/src/routes/billing/checkout.js around lines 23 - 32,
Replace the manual body parsing and ad-hoc checks in the
billingCheckoutRoutes.post('/validate-coupon') handler with the validateRequest
middleware using a schema (e.g., validateCouponSchema) defined in the validation
config (validateCouponSchema = z.object({ code: z.string().min(1, 'Please enter
a promo code').trim() })). Attach validateRequest(validateCouponSchema) to the
route before the async handler, remove the c.req.json() and manual string/trim
checks, and read the validated value (from the request context provided by
validateRequest) inside the handler to continue the existing coupon validation
logic.
| const stripe = new Stripe(c.env.STRIPE_SECRET_KEY, { | ||
| apiVersion: '2025-11-17.clover', | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check Stripe API version usage across the codebase
rg -n "apiVersion" --type js --type ts -A 1 -B 1Repository: InfinityBowman/corates
Length of output: 6581
🏁 Script executed:
#!/bin/bash
# Check for all Stripe instantiations to see version patterns
rg -n "new Stripe" --type js --type ts -A 2Repository: InfinityBowman/corates
Length of output: 3753
🏁 Script executed:
#!/bin/bash
# Check the specific file for context
cat -n packages/workers/src/routes/billing/checkout.js | head -50Repository: InfinityBowman/corates
Length of output: 2228
🌐 Web query:
Stripe API version 2025-11-17.clover valid
💡 Result:
Yes — 2025-11-17.clover is a valid Stripe API version (a Clover release). Use it by sending the request header Stripe-Version: 2025-11-17.clover (or upgrade your account in Workbench). [1][2]
Sources:
- Clover overview (Stripe changelog). [1]
- Example changelog entry showing usage and Stripe-Version header. [2]
🌐 Web query:
Stripe API versioning format clover suffix
💡 Result:
Stripe now uses date-based releases with a named “major” suffix (e.g., 2025-09-30.clover). The format is:
- YYYY-MM-DD. — the date is the release epoch and the suffix (like clover) is the major release name. Monthly updates under the same major keep the same suffix; a new major name indicates a breaking-change release. [1][2]
References:
- Stripe changelog / "Clover" release notes. [1]
- Stripe API Versioning documentation (explains YYYY-MM-DD. and major vs monthly releases). [2]
Standardize Stripe API version across all routes.
The API version 2025-11-17.clover is valid and current. However, packages/workers/src/routes/billing/invoices.js uses the older version 2024-06-20. Align all Stripe client instantiations to use the same API version (2025-11-17.clover) for consistent behavior across billing operations.
🤖 Prompt for AI Agents
In @packages/workers/src/routes/billing/checkout.js around lines 39 - 41, Update
the Stripe API version in the other billing route so all Stripe clients use the
same version; locate the Stripe client instantiation (the new Stripe(...) call)
in packages/workers/src/routes/billing/invoices.js and change its apiVersion
option from '2024-06-20' to '2025-11-17.clover' so it matches the instantiation
in packages/workers/src/routes/billing/checkout.js (const stripe = new
Stripe(..., { apiVersion: '2025-11-17.clover' })).
Summary by CodeRabbit
Release Notes
New Features
Style
✏️ Tip: You can customize this high-level summary in your review settings.