import { ConfigService } from '@nestjs/config'; import { SubscriptionService } from './subscription.service'; import { Subscription } from '@domain/entities/subscription.entity'; import { SubscriptionRepository } from '@domain/ports/out/subscription.repository'; import { LicenseRepository } from '@domain/ports/out/license.repository'; import { OrganizationRepository } from '@domain/ports/out/organization.repository'; import { UserRepository } from '@domain/ports/out/user.repository'; import { StripePort, StripeCheckoutSessionData, StripeSubscriptionData, } from '@domain/ports/out/stripe.port'; import { SubscriptionOverviewResponseDto } from '../dto/subscription.dto'; describe('Stripe checkout organization binding', () => { let subscription: Subscription; let session: StripeCheckoutSessionData; let stripeData: StripeSubscriptionData; let save: jest.Mock; let getSubscription: jest.Mock; let service: SubscriptionService; beforeEach(() => { subscription = Subscription.create({ id: 'local-sub', organizationId: 'org-caller' }); session = { sessionId: 'cs_fixture', customerId: 'cus_fixture', subscriptionId: 'sub_fixture', status: 'complete', metadata: { organizationId: 'org-caller' }, }; stripeData = { subscriptionId: 'sub_fixture', customerId: 'cus_fixture', status: 'active', planId: 'price_fixture', currentPeriodStart: new Date(), currentPeriodEnd: new Date(), cancelAtPeriodEnd: false, }; // No local row owns the Stripe subscription yet: models checkout before webhook delivery. save = jest.fn(async (value: Subscription) => { subscription = value; return value; }); getSubscription = jest.fn(async () => stripeData); service = new SubscriptionService( { findByOrganizationId: async () => subscription, save } as unknown as SubscriptionRepository, { countActiveBySubscriptionIdExcludingAdmins: async () => 0 } as unknown as LicenseRepository, {} as OrganizationRepository, {} as UserRepository, { getCheckoutSession: async () => session, getSubscription, mapPriceIdToPlan: () => 'GOLD', } as unknown as StripePort, new ConfigService() ); jest .spyOn(service, 'getSubscriptionOverview') .mockResolvedValue({} as SubscriptionOverviewResponseDto); }); it.each(['org-victim', undefined])( 'rejects a checkout whose organization is %j before fetching or saving its subscription', async owner => { session.metadata = owner ? { organizationId: owner } : {}; await expect(service.syncFromStripe('org-caller', session.sessionId)).rejects.toMatchObject({ status: 403, }); expect(getSubscription).not.toHaveBeenCalled(); expect(save).not.toHaveBeenCalled(); } ); it('preserves checkout synchronization for its authenticated organization', async () => { await service.syncFromStripe('org-caller', session.sessionId); expect(save).toHaveBeenCalledTimes(1); expect(subscription.stripeSubscriptionId).toBe('sub_fixture'); expect(subscription.stripeCustomerId).toBe('cus_fixture'); expect(subscription.plan.value).toBe('GOLD'); }); it('does not accept a foreign checkout merely because the customer matches', async () => { subscription = subscription.updateStripeCustomerId('cus_fixture'); session.metadata = { organizationId: 'org-victim' }; await expect(service.syncFromStripe('org-caller', session.sessionId)).rejects.toMatchObject({ status: 403, }); expect(save).not.toHaveBeenCalled(); }); it('permits an owned upgrade to replace the existing Stripe subscription ID', async () => { subscription = subscription.updateStripeCustomerId('cus_fixture').updateStripeSubscription({ stripeSubscriptionId: 'sub_old', currentPeriodStart: new Date(), currentPeriodEnd: new Date(), cancelAtPeriodEnd: false, }); await service.syncFromStripe('org-caller', session.sessionId); expect(subscription.stripeSubscriptionId).toBe('sub_fixture'); expect(save).toHaveBeenCalledTimes(1); }); it('preserves sessionless refresh of an already linked subscription', async () => { subscription = subscription.updateStripeCustomerId('cus_fixture').updateStripeSubscription({ stripeSubscriptionId: 'sub_fixture', currentPeriodStart: new Date(), currentPeriodEnd: new Date(), cancelAtPeriodEnd: false, }); await service.syncFromStripe('org-caller'); expect(getSubscription).toHaveBeenCalledWith('sub_fixture'); expect(save).toHaveBeenCalledTimes(1); }); });