import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SubscriptionStatus, SubscriptionTier } from '@prisma/client'; vi.mock('@/lib/prisma', () => ({ prisma: { subscription: { upsert: vi.fn().mockResolvedValue({}), findUnique: vi.fn(), findFirst: vi.fn(), update: vi.fn().mockResolvedValue({}), }, }, })); vi.mock('@/lib/billing/stripe-prices', () => ({ priceIdToTier: vi.fn((priceId: string) => { if (priceId === 'price_pro_monthly_test') return 'PRO'; if (priceId === 'price_business_monthly_test') return 'BUSINESS'; return null; }), })); import { syncSubscriptionFromStripe, handleSubscriptionDeleted } from '@/lib/billing/sync-subscription-from-stripe'; import { prisma } from '@/lib/prisma'; import type Stripe from 'stripe'; function makeSubscription(overrides: Partial = {}): Stripe.Subscription { return { id: 'sub_test_123', object: 'subscription', status: 'active', customer: 'cus_test_123', current_period_start: 1700000000, current_period_end: 1702678400, cancel_at_period_end: false, canceled_at: null, trial_end: null, metadata: { userId: 'user_test_123', tier: 'PRO' }, items: { object: 'list', data: [ { id: 'si_test', object: 'subscription_item', price: { id: 'price_pro_monthly_test', object: 'price', } as Stripe.Price, } as Stripe.SubscriptionItem, ], has_more: false, url: '', }, ...overrides, } as unknown as Stripe.Subscription; } describe('syncSubscriptionFromStripe', () => { beforeEach(() => { vi.clearAllMocks(); }); it('upserts subscription with correct PRO tier from price ID', async () => { const sub = makeSubscription(); await syncSubscriptionFromStripe(sub, 'user_test_123'); expect(prisma.subscription.upsert).toHaveBeenCalledOnce(); const call = vi.mocked(prisma.subscription.upsert).mock.calls[0][0]; expect(call.where).toEqual({ stripeSubscriptionId: 'sub_test_123' }); expect(call.create.tier).toBe(SubscriptionTier.PRO); expect(call.create.status).toBe(SubscriptionStatus.ACTIVE); expect(call.create.userId).toBe('user_test_123'); expect(call.create.stripeCustomerId).toBe('cus_test_123'); expect(call.create.stripeSubscriptionId).toBe('sub_test_123'); expect(call.create.stripePriceId).toBe('price_pro_monthly_test'); }); it('maps stripe active status to ACTIVE', async () => { const sub = makeSubscription({ status: 'active' }); await syncSubscriptionFromStripe(sub, 'user_test_123'); const call = vi.mocked(prisma.subscription.upsert).mock.calls[0][0]; expect(call.create.status).toBe(SubscriptionStatus.ACTIVE); }); it('maps stripe past_due status to PAST_DUE', async () => { const sub = makeSubscription({ status: 'past_due' }); await syncSubscriptionFromStripe(sub, 'user_test_123'); const call = vi.mocked(prisma.subscription.upsert).mock.calls[0][0]; expect(call.create.status).toBe(SubscriptionStatus.PAST_DUE); }); it('maps stripe canceled status to CANCELED', async () => { const sub = makeSubscription({ status: 'canceled', canceled_at: 1700100000 }); await syncSubscriptionFromStripe(sub, 'user_test_123'); const call = vi.mocked(prisma.subscription.upsert).mock.calls[0][0]; expect(call.create.status).toBe(SubscriptionStatus.CANCELED); expect(call.create.canceledAt).toBeInstanceOf(Date); }); it('maps stripe trialing status to TRIALING', async () => { const sub = makeSubscription({ status: 'trialing', trial_end: 1700500000 }); await syncSubscriptionFromStripe(sub, 'user_test_123'); const call = vi.mocked(prisma.subscription.upsert).mock.calls[0][0]; expect(call.create.status).toBe(SubscriptionStatus.TRIALING); expect(call.create.trialEndsAt).toBeInstanceOf(Date); }); it('maps stripe incomplete_expired to INACTIVE', async () => { const sub = makeSubscription({ status: 'incomplete_expired' }); await syncSubscriptionFromStripe(sub, 'user_test_123'); const call = vi.mocked(prisma.subscription.upsert).mock.calls[0][0]; expect(call.create.status).toBe(SubscriptionStatus.INACTIVE); }); it('falls back to metadata tier when price ID not in map', async () => { const sub = makeSubscription({ metadata: { userId: 'user_test_123', tier: 'BUSINESS' }, items: { object: 'list', data: [{ id: 'si_test', object: 'subscription_item', price: { id: 'price_unknown_abc', object: 'price' } as Stripe.Price } as Stripe.SubscriptionItem], has_more: false, url: '', }, }); await syncSubscriptionFromStripe(sub, 'user_test_123'); const call = vi.mocked(prisma.subscription.upsert).mock.calls[0][0]; expect(call.create.tier).toBe(SubscriptionTier.BUSINESS); }); it('sets currentPeriodStart and currentPeriodEnd from Stripe timestamps', async () => { const sub = makeSubscription({ current_period_start: 1700000000, current_period_end: 1702678400, }); await syncSubscriptionFromStripe(sub, 'user_test_123'); const call = vi.mocked(prisma.subscription.upsert).mock.calls[0][0]; expect(call.create.currentPeriodStart).toEqual(new Date(1700000000 * 1000)); expect(call.create.currentPeriodEnd).toEqual(new Date(1702678400 * 1000)); }); it('sets cancelAtPeriodEnd correctly', async () => { const sub = makeSubscription({ cancel_at_period_end: true }); await syncSubscriptionFromStripe(sub, 'user_test_123'); const call = vi.mocked(prisma.subscription.upsert).mock.calls[0][0]; expect(call.create.cancelAtPeriodEnd).toBe(true); }); }); describe('handleSubscriptionDeleted', () => { beforeEach(() => { vi.clearAllMocks(); }); it('marks subscription as CANCELED when found', async () => { vi.mocked(prisma.subscription.findUnique).mockResolvedValue({ id: 'local_id', stripeSubscriptionId: 'sub_test_123', } as any); const sub = makeSubscription({ status: 'canceled', canceled_at: 1700100000 }); await handleSubscriptionDeleted(sub); expect(prisma.subscription.update).toHaveBeenCalledOnce(); const call = vi.mocked(prisma.subscription.update).mock.calls[0][0]; expect(call.data.status).toBe(SubscriptionStatus.CANCELED); expect(call.data.cancelAtPeriodEnd).toBe(false); }); it('does nothing when subscription not found locally', async () => { vi.mocked(prisma.subscription.findUnique).mockResolvedValue(null); const sub = makeSubscription({ status: 'canceled' }); await handleSubscriptionDeleted(sub); expect(prisma.subscription.update).not.toHaveBeenCalled(); }); });