feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- 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
This commit is contained in:
175
memento-note/tests/unit/billing-sync.test.ts
Normal file
175
memento-note/tests/unit/billing-sync.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user