import { Test, TestingModule } from '@nestjs/testing'; import { DataSource, Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { SubscriptionsService } from '../services/subscriptions.service'; import { TenantSubscription, SubscriptionPlan, BillingCycle, SubscriptionStatus } from '../entities'; import { CreateTenantSubscriptionDto, UpdateTenantSubscriptionDto, CancelSubscriptionDto, ChangePlanDto } from '../dto'; describe('SubscriptionsService', () => { let service: SubscriptionsService; let subscriptionRepository: Repository; let planRepository: Repository; let dataSource: DataSource; const mockSubscription = { id: 'uuid-1', tenantId: 'tenant-1', planId: 'plan-1', status: SubscriptionStatus.ACTIVE, billingCycle: BillingCycle.MONTHLY, currentPeriodStart: new Date('2024-01-01'), currentPeriodEnd: new Date('2024-02-01'), trialEnd: null, cancelledAt: null, paymentMethodId: 'pm-1', metadata: {}, createdAt: new Date(), updatedAt: new Date(), }; const mockPlan = { id: 'plan-1', name: 'Basic Plan', description: 'Basic subscription plan', price: 9.99, billingCycle: BillingCycle.MONTHLY, features: ['feature1', 'feature2'], isActive: true, createdAt: new Date(), updatedAt: new Date(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ SubscriptionsService, { provide: DataSource, useValue: { getRepository: jest.fn(), }, }, ], }).compile(); service = module.get(SubscriptionsService); dataSource = module.get(DataSource); subscriptionRepository = module.get>( getRepositoryToken(TenantSubscription), ); planRepository = module.get>( getRepositoryToken(SubscriptionPlan), ); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('create', () => { it('should create a new subscription successfully', async () => { const dto: CreateTenantSubscriptionDto = { tenantId: 'tenant-1', planId: 'plan-1', billingCycle: BillingCycle.MONTHLY, paymentMethodId: 'pm-1', }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null); jest.spyOn(planRepository, 'findOne').mockResolvedValue(mockPlan as any); jest.spyOn(subscriptionRepository, 'create').mockReturnValue(mockSubscription as any); jest.spyOn(subscriptionRepository, 'save').mockResolvedValue(mockSubscription); const result = await service.create(dto); expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { tenantId: dto.tenantId }, }); expect(planRepository.findOne).toHaveBeenCalledWith({ where: { id: dto.planId } }); expect(subscriptionRepository.create).toHaveBeenCalled(); expect(subscriptionRepository.save).toHaveBeenCalled(); expect(result).toEqual(mockSubscription); }); it('should throw error if tenant already has subscription', async () => { const dto: CreateTenantSubscriptionDto = { tenantId: 'tenant-1', planId: 'plan-1', billingCycle: BillingCycle.MONTHLY, paymentMethodId: 'pm-1', }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any); await expect(service.create(dto)).rejects.toThrow('Tenant already has a subscription'); }); it('should throw error if plan not found', async () => { const dto: CreateTenantSubscriptionDto = { tenantId: 'tenant-1', planId: 'invalid-plan', billingCycle: BillingCycle.MONTHLY, paymentMethodId: 'pm-1', }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null); jest.spyOn(planRepository, 'findOne').mockResolvedValue(null); await expect(service.create(dto)).rejects.toThrow('Plan not found'); }); }); describe('findByTenant', () => { it('should find subscription by tenant id', async () => { jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any); const result = await service.findByTenant('tenant-1'); expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1' }, }); expect(result).toEqual(mockSubscription); }); it('should return null if no subscription found', async () => { jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null); const result = await service.findByTenant('invalid-tenant'); expect(result).toBeNull(); }); }); describe('update', () => { it('should update subscription successfully', async () => { const dto: UpdateTenantSubscriptionDto = { paymentMethodId: 'pm-2', }; const updatedSubscription = { ...mockSubscription, paymentMethodId: 'pm-2' }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any); jest.spyOn(subscriptionRepository, 'save').mockResolvedValue(updatedSubscription as any); const result = await service.update('uuid-1', dto); expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } }); expect(subscriptionRepository.save).toHaveBeenCalled(); expect(result).toEqual(updatedSubscription); }); it('should throw error if subscription not found', async () => { const dto: UpdateTenantSubscriptionDto = { paymentMethodId: 'pm-2', }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null); await expect(service.update('invalid-id', dto)).rejects.toThrow('Subscription not found'); }); }); describe('cancel', () => { it('should cancel subscription successfully', async () => { const dto: CancelSubscriptionDto = { reason: 'Customer request', effectiveImmediately: false, }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any); jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({ ...mockSubscription, status: SubscriptionStatus.CANCELLED, cancelledAt: new Date(), } as any); const result = await service.cancel('uuid-1', dto); expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } }); expect(subscriptionRepository.save).toHaveBeenCalled(); expect(result.status).toBe(SubscriptionStatus.CANCELLED); expect(result.cancelledAt).toBeDefined(); }); it('should cancel subscription immediately if requested', async () => { const dto: CancelSubscriptionDto = { reason: 'Customer request', effectiveImmediately: true, }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any); jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({ ...mockSubscription, status: SubscriptionStatus.CANCELLED, cancelledAt: new Date(), currentPeriodEnd: new Date(), } as any); const result = await service.cancel('uuid-1', dto); expect(result.status).toBe(SubscriptionStatus.CANCELLED); expect(result.cancelledAt).toBeDefined(); }); }); describe('changePlan', () => { it('should change subscription plan successfully', async () => { const newPlan = { ...mockPlan, id: 'plan-2', price: 19.99 }; const dto: ChangePlanDto = { newPlanId: 'plan-2', billingCycle: BillingCycle.YEARLY, prorate: true, }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any); jest.spyOn(planRepository, 'findOne').mockResolvedValue(newPlan as any); jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({ ...mockSubscription, planId: 'plan-2', billingCycle: BillingCycle.YEARLY, } as any); const result = await service.changePlan('uuid-1', dto); expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } }); expect(planRepository.findOne).toHaveBeenCalledWith({ where: { id: 'plan-2' } }); expect(subscriptionRepository.save).toHaveBeenCalled(); expect(result.planId).toBe('plan-2'); expect(result.billingCycle).toBe(BillingCycle.YEARLY); }); it('should throw error if new plan not found', async () => { const dto: ChangePlanDto = { newPlanId: 'invalid-plan', billingCycle: BillingCycle.MONTHLY, prorate: false, }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any); jest.spyOn(planRepository, 'findOne').mockResolvedValue(null); await expect(service.changePlan('uuid-1', dto)).rejects.toThrow('New plan not found'); }); }); describe('getUsage', () => { it('should get subscription usage', async () => { const mockUsage = { currentUsage: 850, limits: { apiCalls: 1000, storage: 5368709120, // 5GB in bytes users: 10, }, periodStart: new Date('2024-01-01'), periodEnd: new Date('2024-02-01'), }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any); jest.spyOn(dataSource, 'query').mockResolvedValue([{ current_usage: 850 }]); const result = await service.getUsage('uuid-1'); expect(result.currentUsage).toBe(850); expect(result.limits).toBeDefined(); }); }); describe('reactivate', () => { it('should reactivate cancelled subscription', async () => { const cancelledSubscription = { ...mockSubscription, status: SubscriptionStatus.CANCELLED, cancelledAt: new Date(), }; jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(cancelledSubscription as any); jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({ ...cancelledSubscription, status: SubscriptionStatus.ACTIVE, cancelledAt: null, } as any); const result = await service.reactivate('uuid-1'); expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } }); expect(subscriptionRepository.save).toHaveBeenCalled(); expect(result.status).toBe(SubscriptionStatus.ACTIVE); expect(result.cancelledAt).toBeNull(); }); it('should throw error if subscription is not cancelled', async () => { jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any); await expect(service.reactivate('uuid-1')).rejects.toThrow('Cannot reactivate active subscription'); }); }); });