Skip to content

Commit 6fe5e25

Browse files
committed
feat(web): Temporarily disable org billing with feature flag
- Add centralized ORG_BILLING_ENABLED flag in billing-config.ts - Add 503 guards to all org billing API routes (setup, status, subscription, credits) - Add org event rejection to Stripe webhook with isOrgBillingEvent helper - Add "Feature Unavailable" UI to billing pages (purchase, setup) - Comment out billing UI in org dashboard and settings pages - Hide auto-topup banner in credit-monitor component - Add comprehensive tests for feature flag and webhook helper - Personal billing (Strong subscriptions) remains fully functional To re-enable: Set ORG_BILLING_ENABLED=true and search for BILLING_DISABLED
1 parent 6b210a3 commit 6fe5e25

File tree

18 files changed

+694
-193
lines changed

18 files changed

+694
-193
lines changed

web/jest.config.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const createJestConfig = nextJest({
77
const config = {
88
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
99
testEnvironment: 'jest-environment-jsdom',
10-
testPathIgnorePatterns: ['<rootDir>/src/__tests__/e2e'],
1110
moduleNameMapper: {
1211
'^@/(.*)$': '<rootDir>/src/$1',
1312
'^common/(.*)$': '<rootDir>/../common/src/$1',
@@ -17,13 +16,17 @@ const config = {
1716
'^react$': '<rootDir>/node_modules/react',
1817
'^react-dom$': '<rootDir>/node_modules/react-dom',
1918
},
19+
// Bun-specific tests that use top-level await or bun:test features
2020
testPathIgnorePatterns: [
2121
'<rootDir>/src/__tests__/e2e',
2222
'<rootDir>/src/__tests__/playwright-runner.e2e.ts',
2323
'<rootDir>/src/lib/__tests__/ban-conditions.test.ts',
24+
'<rootDir>/src/lib/__tests__/billing-config.test.ts',
2425
'<rootDir>/src/app/api/v1/.*/__tests__',
2526
'<rootDir>/src/app/api/agents/publish/__tests__',
2627
'<rootDir>/src/app/api/healthz/__tests__',
28+
'<rootDir>/src/app/api/stripe/webhook/__tests__',
29+
'<rootDir>/src/app/api/orgs/.*/billing/__tests__',
2730
],
2831
}
2932

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import { ORG_BILLING_ENABLED } from '@/lib/billing-config'
4+
5+
/**
6+
* Tests for the org billing feature flag.
7+
*
8+
* These tests verify the feature flag state and document expected behavior.
9+
* Direct route testing is difficult due to Next.js dependencies, so we verify:
10+
* 1. The feature flag is in the expected state
11+
* 2. The flag is properly exported and importable
12+
*
13+
* The actual route behavior (503 responses) is tested via the integration tests
14+
* and verified by the isOrgBillingEvent tests in the webhook test file.
15+
*/
16+
describe('Org Billing Feature Flag', () => {
17+
describe('ORG_BILLING_ENABLED', () => {
18+
test('is exported and accessible', () => {
19+
expect(typeof ORG_BILLING_ENABLED).toBe('boolean')
20+
})
21+
22+
test('is currently set to false (org billing disabled)', () => {
23+
// This test documents the current state of the feature flag.
24+
// When re-enabling org billing, update this test to expect true.
25+
expect(ORG_BILLING_ENABLED).toBe(false)
26+
})
27+
28+
test('when false, billing routes have appropriate fallback behavior', () => {
29+
// This is a documentation test that describes expected behavior.
30+
// Actual route testing is done via integration/E2E tests.
31+
if (!ORG_BILLING_ENABLED) {
32+
// Expected behavior when org billing is disabled:
33+
// - GET /api/orgs/[orgId]/billing/setup returns 200 with { is_setup: false, disabled: true }
34+
// - POST /api/orgs/[orgId]/billing/setup returns 503 (can't set up new billing)
35+
// - GET /api/orgs/[orgId]/billing/status returns 503
36+
// - POST /api/orgs/[orgId]/credits returns 503
37+
// - DELETE /api/orgs/[orgId]/billing/subscription is ALLOWED (users can cancel)
38+
// - Stripe webhook returns 200 for org events (prevents retry storms)
39+
expect(true).toBe(true)
40+
}
41+
})
42+
})
43+
44+
describe('Feature flag integration', () => {
45+
test('flag can be used in conditional logic', () => {
46+
const message = ORG_BILLING_ENABLED
47+
? 'Billing is enabled'
48+
: 'Organization billing is temporarily disabled'
49+
50+
expect(message).toBe('Organization billing is temporarily disabled')
51+
})
52+
53+
test('flag value is consistent across imports', async () => {
54+
// Verify the flag value is the same when imported multiple times
55+
const { ORG_BILLING_ENABLED: flag1 } = await import('@/lib/billing-config')
56+
const { ORG_BILLING_ENABLED: flag2 } = await import('@/lib/billing-config')
57+
58+
expect(flag1).toBe(flag2)
59+
expect(flag1).toBe(ORG_BILLING_ENABLED)
60+
})
61+
})
62+
})

web/src/app/api/orgs/[orgId]/billing/setup/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getServerSession } from 'next-auth'
1010
import type { NextRequest } from 'next/server'
1111

1212
import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
13+
import { ORG_BILLING_ENABLED } from '@/lib/billing-config'
1314
import { logger } from '@/util/logger'
1415

1516
interface RouteParams {
@@ -19,6 +20,15 @@ interface RouteParams {
1920
}
2021

2122
export async function GET(req: NextRequest, { params }: RouteParams) {
23+
// BILLING_DISABLED: Return stub response for GET to not break org pages
24+
// The useOrganizationData hook calls this endpoint, and 503 causes loading spinners
25+
if (!ORG_BILLING_ENABLED) {
26+
return NextResponse.json({
27+
is_setup: false,
28+
disabled: true,
29+
})
30+
}
31+
2232
const session = await getServerSession(authOptions)
2333
if (!session?.user?.id) {
2434
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
@@ -105,6 +115,10 @@ export async function GET(req: NextRequest, { params }: RouteParams) {
105115
}
106116

107117
export async function POST(req: NextRequest, { params }: RouteParams) {
118+
if (!ORG_BILLING_ENABLED) {
119+
return NextResponse.json({ error: 'Organization billing is temporarily disabled' }, { status: 503 })
120+
}
121+
108122
const session = await getServerSession(authOptions)
109123
if (!session?.user?.id) {
110124
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

web/src/app/api/orgs/[orgId]/billing/status/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getServerSession } from 'next-auth'
99
import type { NextRequest } from 'next/server'
1010

1111
import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
12+
import { ORG_BILLING_ENABLED } from '@/lib/billing-config'
1213
import { logger } from '@/util/logger'
1314

1415
interface RouteParams {
@@ -18,6 +19,10 @@ interface RouteParams {
1819
}
1920

2021
export async function GET(req: NextRequest, { params }: RouteParams) {
22+
if (!ORG_BILLING_ENABLED) {
23+
return NextResponse.json({ error: 'Organization billing is temporarily disabled' }, { status: 503 })
24+
}
25+
2126
const session = await getServerSession(authOptions)
2227
if (!session?.user?.id) {
2328
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

web/src/app/api/orgs/[orgId]/billing/subscription/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ interface RouteParams {
1717
}
1818

1919
export async function DELETE(req: NextRequest, { params }: RouteParams) {
20+
// NOTE: Subscription cancellation is allowed even when org billing is disabled
21+
// Users must be able to cancel existing subscriptions
2022
const session = await getServerSession(authOptions)
2123
if (!session?.user?.id) {
2224
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

web/src/app/api/orgs/[orgId]/credits/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getServerSession } from 'next-auth'
1212
import type { NextRequest } from 'next/server'
1313

1414
import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
15+
import { ORG_BILLING_ENABLED } from '@/lib/billing-config'
1516
import { logger } from '@/util/logger'
1617

1718
interface RouteParams {
@@ -21,6 +22,10 @@ interface RouteParams {
2122
const ORG_MIN_PURCHASE_CREDITS = 5000 // $50 minimum for organizations
2223

2324
export async function POST(request: NextRequest, { params }: RouteParams) {
25+
if (!ORG_BILLING_ENABLED) {
26+
return NextResponse.json({ error: 'Organization billing is temporarily disabled' }, { status: 503 })
27+
}
28+
2429
const session = await getServerSession(authOptions)
2530
if (!session?.user?.id) {
2631
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

0 commit comments

Comments
 (0)