feat: KEEP-1512 Stripe billing implementation#446
Open
feat: KEEP-1512 Stripe billing implementation#446
Conversation
…tion Stripe portal cancellations set cancel_at instead of cancel_at_period_end. Derive cancelAtPeriodEnd from both flags so the UI correctly reflects cancellation and un-cancellation state. Also use current_period_end instead of cancel_at for the billing period end date, which is always present regardless of cancellation state.
Show loading skeleton while fetching proration data, display "No charge" instead of negative amounts when the user has a credit from unused time, and use "Proration breakdown" as the section header for credit scenarios.
Use Stripe's amount_due instead of manually summing line items, which correctly accounts for customer credit balance. Show applied credit in the proration breakdown and display "No charge" when nothing is owed.
Add all 16 Stripe environment variables (secret key, webhook secret, and 14 price IDs) to both staging and prod values.yaml using AWS Parameter Store references.
Guard all billing API routes, UI pages, and navigation behind a build-time feature flag so billing is disabled by default. - Add feature-flag.ts module with isBillingEnabled() check - Make stripe.ts return null instead of throwing when key is missing - Add getStripe() helper in stripe provider for null-safe access - Add isBillingEnabled() guard to getBillingProvider() - Add early 404 return to all 7 billing API routes when disabled - Hide billing page (notFound) and nav item when disabled - Add NEXT_PUBLIC_BILLING_ENABLED to Dockerfile and .env.example - Remove console.log statements from webhook route
Add NEXT_PUBLIC_BILLING_ENABLED and Stripe price ID env vars to tests/setup.ts so billing tests have required configuration.
Unit tests (84 tests): - feature-flag: env var parsing - plans: getPriceId, resolvePriceId, getPlanLimits, PLANS structure - handle-billing-event: all 7 webhook event handlers - stripe-provider: all provider methods and UnknownEventTypeError - plans-server: getOrgSubscription, getOrgPlan, checkFeatureAccess, checkExecutionLimit Integration tests (22 tests): - checkout route: auth, validation, checkout/update flows - subscription route: plan data with limits, free fallback - cancel route: auth, ownership, cancellation flow - webhook route: signature validation, dedup, event processing E2E tests (6 tests): - Billing page rendering, pricing, checkout flow, subscription status, cancel flow, invoice history (all with mocked API routes)
Enable billing in staging, disable in prod.
- Refuse unknown price IDs instead of defaulting to "pro" (handle-billing-event) - Fix isCurrentPlan wildcard matching on null tier/interval (pricing-table/utils) - Use unique React keys for proration line items (confirm-plan-change-dialog) - Add owner role check to invoices and preview-proration routes - Return generic error message from webhook handler (no internal details) - Use event periodEnd before DB fallback in subscription.deleted - Clear checkout query param after showing toast (billing-page) - Fix dead ternary in price display (confirm-plan-change-dialog) - Add error logging to silent fetch failures (billing-page, billing-status, billing-history) - Add warning log when checkout update affects zero rows - Extract requireOrgOwner helper to reduce complexity in preview-proration route - Add integration tests for invoices and preview-proration routes - Update webhook and handle-event tests for changed behavior
- Make billing page a Server Component so notFound() works correctly - Add database index on provider_subscription_id for webhook query performance - Fix webhook idempotency TOCTOU race with atomic INSERT ON CONFLICT DO NOTHING - Add server-side same-plan guard to reject redundant checkout requests - Fix isCurrentPlan returning false for free plan users viewing free card - Extract shared requireOrgOwner() helper to deduplicate auth logic and eliminate redundant headers() calls across all 5 billing routes - Fix upgrade-prompt text color invisible in light mode
…e customers - Move PRICE_IDS, getPriceId, resolvePriceId from plans.ts to plans-server.ts to prevent accidental client-side usage of server-only env vars - Replace TOCTOU check-then-insert in ensureProviderCustomer with atomic onConflictDoUpdate using COALESCE to prevent duplicate Stripe customers on concurrent checkout requests - Add onConflictDoNothing to afterCreateOrganization hook to handle race between org creation and checkout insert
…ions Add enforcement of monthly execution limits at all workflow entry points (API, webhook, internal). Paid plans that exceed limits are allowed but tracked for overage billing via Stripe invoice items. Free plans are hard-blocked. Includes upgrade suggestion API and UI banner, overage billing records table, and comprehensive tests.
… allowance When a paid org exceeds its monthly execution limit, overage is billed via Stripe invoice items. If the invoice remains unpaid after a 15-day grace period, the unpaid overage executions are deducted from the next month's allowance (minimum floor of 100 executions). Debt clears automatically when the invoice is paid. - Add execution_debt table and migration - Add debt scan endpoint for daily scheduler runs - Extend BillingProvider with invoice status methods - Handle Stripe 404 for consumed invoice items - Update checkExecutionLimit to apply debt deduction - Clear debt on invoice.paid and subscription.deleted events - Guard against concurrent scans with onConflictDoNothing
The guard response body was re-wrapped into a new NextResponse losing Content-Type: application/json. Use NextResponse.json instead.
Add doc comments explaining the check-then-act race condition is intentional and bounded. Add tests for resolvePriceId round-trip and unknown price ID handling.
Production code should not use console.log per project lint rules. All informational billing event logs now use console.warn. Also adds comment explaining why invoice.paid fallback omits status.
Track org+period keys processed in the subscription scan loop and skip them in the failed-records retry loop to prevent misleading double entries in the response.
The projection used local-time getDate/getMonth while the usage query used UTC, causing mismatches near month boundaries. Also suppresses suggestions when fewer than 3 days of data exist to avoid extreme projections on day 1-2.
The error string was duplicated between execution-guard.ts and the webhook route. Extract to a shared constant so changes stay in sync.
Add comment explaining why debtExecutions is hardcoded to 0 for unlimited plans. Use it.skipIf for resolvePriceId tests that depend on Stripe env vars so skips are visible in test output.
Track actual retry attempts instead of reporting fetched record count. Include canceled subscriptions in the scan query so their final period overage is not missed.
Replace loose optional fields with SuggestionNoUpgrade | SuggestionUpgrade discriminated union so all fields are guaranteed present when shouldUpgrade is true, eliminating potential undefined rendering in JSX.
- Show execution usage progress bar on billing status page - Show proper "Monthly execution limit exceeded" toast on 429 - Add overage charges section with pending/billed status - Add "View Plans" CTA on upgrade suggestion banner - Softer amber tones for paid plan overage (not destructive red) - Fix tier suggestion to prefer same-plan upgrades over cross-plan jumps - Return usage and overage data from subscription API endpoint
- Charge per exact execution instead of rounding up to nearest 1,000 - Add overage bar extension showing excess usage beyond plan limit - Show unpaid overage records using providerInvoiceId instead of status - Bold per-execution rate in overage message - Consistent bar colors: green (safe), yellow (near limit), muted+red (over)
…id on invoice - Block paid plan executions when active debt exists (unpaid overage past 15-day grace) - Add debt-specific error message for suspended executions - Mark overage billing records as paid when invoice.paid webhook fires - Move debt check before usage comparison to prevent early allow bypass
- Fix overage test: 1,500 executions at $2/1000 = $3.00 (per-execution math) - Update debt tests: paid plans with active debt are now fully blocked
Add db.execute mock for usage count query and overageBillingRecords mock for overage charges in subscription route test. Add db.select, execution-debt, and overageBillingRecords schema mocks to webhook route test for invoice.paid handler changes.
…uards feat: KEEP-1515 add billing execution guards, overage billing, and execution debt
- execute/webhook routes: keep both billing guard and concurrency limit - navigation-sidebar: keep billing, org hooks, and anonymous user imports - drizzle migrations: renumber billing migrations 0026-0029 to 0027-0030 to accommodate staging's 0026_puzzling_thunderball - update snapshot prevId chain and add missing execution status index
Migration 0028_add_provider_sub_id_index already creates idx_org_subscriptions_provider_sub with IF NOT EXISTS. The duplicate CREATE INDEX in 0029_opposite_human_cannonball (without IF NOT EXISTS) fails on fresh databases.
Lexicographic string comparison incorrectly ranks "99.99%" below "99.9%". Switch to parseFloat for correct numeric comparison.
The checkout and cancel tests wrapped assertions in catch(() => false) guards, causing tests to pass green even if buttons never rendered. Replace with proper toBeVisible assertions that fail on missing elements.
Replace unnecessary dynamic import of drizzle-orm eq with a static import, consistent with all other files in the codebase.
console.warn was used for expected operational events (checkout completed, invoice paid, subscription updated, etc.), muddying log severity. Reserve console.warn for actual warnings (missing rows, billing failures).
Replace unsafe type assertions (as PlanName, as TierKey) with runtime validation via parsePlanName/parseTierKey helpers. Falls back to safe defaults (free plan, null tier) on invalid data instead of silently producing incorrect behavior.
pendingTotal was calculated from the full charges array but the displayed list filters to charges without a providerInvoiceId. This could show a total exceeding what the user sees on screen.
Hardcoded "5,000" string would drift if the free tier limit changes. Use PLANS.free.features.maxExecutionsPerMonth.toLocaleString() instead.
The useBillingData hook silently swallowed fetch errors, leaving a blank card with no user feedback. Add error state and render a fallback message when the subscription API fails.
Replace type assertions with parsePlanName/parseTierKey helpers for consistent validation of server response data.
The field represents the per-month price when billed annually, not the annual total. The old name was misleading and could cause confusion when reading annualTotal = yearlyPrice * 12.
|
@joelorzet plan is to merge #472 then verify all the testing for this PR, do a PR Deploy, manual test and then merge |
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
Add Stripe-based billing system with plan management, proration previews, invoice history, and deployment configuration for staging and production environments.
Details
NEXT_PUBLIC_BILLING_ENABLEDfeature flag: all billing API routes, UI pages, and navigation are guarded behind this build-time flag (enabled in staging, disabled in prod)NEXT_PUBLIC_BILLING_ENABLED(kv: "true" staging, "false" prod)STRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETSTRIPE_PRICE_PRO_25K_MONTHLYSTRIPE_PRICE_PRO_25K_YEARLYSTRIPE_PRICE_PRO_50K_MONTHLYSTRIPE_PRICE_PRO_50K_YEARLYSTRIPE_PRICE_PRO_100K_MONTHLYSTRIPE_PRICE_PRO_100K_YEARLYSTRIPE_PRICE_BUSINESS_250K_MONTHLYSTRIPE_PRICE_BUSINESS_250K_YEARLYSTRIPE_PRICE_BUSINESS_500K_MONTHLYSTRIPE_PRICE_BUSINESS_500K_YEARLYSTRIPE_PRICE_BUSINESS_1M_MONTHLYSTRIPE_PRICE_BUSINESS_1M_YEARLYSTRIPE_PRICE_ENTERPRISE_MONTHLYSTRIPE_PRICE_ENTERPRISE_YEARLYTest coverage