Files
Momento/memento-note/tests/unit/billing-sync.test.ts
Antigravity bd495be965
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
- Sidebar: dynamic brand-accent colors, brainstorm section restyled
- AI chat general: popup panel with expand/collapse, hides when contextual AI open
- AI chat contextual: tabs reordered (Actions first), X close button, height fix
- Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.)
- Global color cleanup: emerald/orange hardcoded → brand-accent dynamic
- Brainstorm page: orange → brand-accent throughout
- PageEntry animation component added to key pages
- Floating AI button: bg-brand-accent instead of hardcoded black
- i18n: all 15 locales updated with new AI/billing keys
- Billing: freemium quota tracking, BYOK, stripe subscription scaffolding
- Admin: integrated into new design
- AGENTS.md + CLAUDE.md project rules added
2026-05-16 12:59:30 +00:00

176 lines
6.6 KiB
TypeScript

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> = {}): 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();
});
});