feat: Feature 3.4 — Stripe integration with founder code system
Subscription billing: - POST /api/stripe/checkout — creates Stripe Checkout session - POST /api/stripe/webhook — handles lifecycle events (raw body) - POST /api/stripe/portal — customer self-service management - GET /api/stripe/status — current subscription info Founder code system: - Validates codes against env var list with expiry date - Routes to founder Stripe Price IDs (locked rate for life) - 4 price objects: analyst, analyst-founder, desk, desk-founder Webhook events handled: - checkout.session.completed → tier update + founder status - customer.subscription.deleted → revert to free tier - invoice.payment_failed → logged Lazy Stripe init (no API key required at import time). Raw body middleware for webhook signature verification. 16 new tests, 237 total (210 Node.js + 27 Python), all passing. Phase 3 Web MVP COMPLETE. All roadmap features shipped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
process.env.STRIPE_SECRET_KEY = 'sk_test_dummy';
|
||||
|
||||
jest.mock('../../src/utils/supabase', () => ({
|
||||
getSupabaseServiceClient: () => ({ from: jest.fn() }),
|
||||
}));
|
||||
|
||||
const { isFounderCodeValid, getPriceId } = require('../../src/services/stripeService');
|
||||
|
||||
describe('stripeService', () => {
|
||||
describe('isFounderCodeValid', () => {
|
||||
test('valid founder code returns true', () => {
|
||||
expect(isFounderCodeValid('FOUNDER2026')).toBe(true);
|
||||
expect(isFounderCodeValid('BETONBLK')).toBe(true);
|
||||
});
|
||||
|
||||
test('case insensitive', () => {
|
||||
expect(isFounderCodeValid('founder2026')).toBe(true);
|
||||
});
|
||||
|
||||
test('invalid code returns false', () => {
|
||||
expect(isFounderCodeValid('INVALID')).toBe(false);
|
||||
});
|
||||
|
||||
test('null/empty returns false', () => {
|
||||
expect(isFounderCodeValid(null)).toBe(false);
|
||||
expect(isFounderCodeValid('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPriceId', () => {
|
||||
test('analyst without founder code returns standard price', () => {
|
||||
const id = getPriceId('analyst', null);
|
||||
expect(id).toContain('analyst');
|
||||
expect(id).not.toContain('founder');
|
||||
});
|
||||
|
||||
test('analyst with valid founder code returns founder price', () => {
|
||||
const id = getPriceId('analyst', 'FOUNDER2026');
|
||||
expect(id).toContain('founder');
|
||||
});
|
||||
|
||||
test('desk without founder code returns standard price', () => {
|
||||
const id = getPriceId('desk', null);
|
||||
expect(id).toContain('desk');
|
||||
expect(id).not.toContain('founder');
|
||||
});
|
||||
|
||||
test('desk with valid founder code returns founder price', () => {
|
||||
const id = getPriceId('desk', 'BETONBLK');
|
||||
expect(id).toContain('founder');
|
||||
});
|
||||
|
||||
test('invalid tier throws', () => {
|
||||
expect(() => getPriceId('gold', null)).toThrow('Invalid tier');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user