import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundException } from '@nestjs/common'; import { PaymentsService } from './payments.service'; import { StripeService } from './services/stripe.service'; import { SubscriptionService } from './services/subscription.service'; import { PaymentRepository } from '../database/repositories/payment.repository'; import { UserRepository } from '../database/repositories/user.repository'; import { Plan } from '@prisma/client'; describe('PaymentsService', () => { let service: PaymentsService; let stripeService: jest.Mocked; let subscriptionService: jest.Mocked; let paymentRepository: jest.Mocked; let userRepository: jest.Mocked; const mockUser = { id: 'user-123', email: 'test@example.com', plan: Plan.BASIC, quotaRemaining: 50, quotaResetDate: new Date(), isActive: true, stripeCustomerId: 'cus_123', createdAt: new Date(), updatedAt: new Date(), }; const mockSubscription = { id: 'sub-123', userId: 'user-123', stripeSubscriptionId: 'sub_stripe_123', stripeCustomerId: 'cus_123', stripePriceId: 'price_123', status: 'ACTIVE', plan: Plan.PRO, currentPeriodStart: new Date(), currentPeriodEnd: new Date(), cancelAtPeriodEnd: false, createdAt: new Date(), updatedAt: new Date(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ PaymentsService, { provide: StripeService, useValue: { createCheckoutSession: jest.fn(), cancelSubscription: jest.fn(), reactivateSubscription: jest.fn(), scheduleSubscriptionChange: jest.fn(), }, }, { provide: SubscriptionService, useValue: { getActiveSubscription: jest.fn(), getCancelledSubscription: jest.fn(), markAsCancelled: jest.fn(), markAsActive: jest.fn(), create: jest.fn(), update: jest.fn(), findByStripeId: jest.fn(), markAsDeleted: jest.fn(), }, }, { provide: PaymentRepository, useValue: { findByUserId: jest.fn(), create: jest.fn(), }, }, { provide: UserRepository, useValue: { findById: jest.fn(), findByStripeCustomerId: jest.fn(), updatePlan: jest.fn(), resetQuota: jest.fn(), }, }, ], }).compile(); service = module.get(PaymentsService); stripeService = module.get(StripeService); subscriptionService = module.get(SubscriptionService); paymentRepository = module.get(PaymentRepository); userRepository = module.get(UserRepository); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('getUserSubscription', () => { it('should return user subscription details', async () => { userRepository.findById.mockResolvedValue(mockUser); subscriptionService.getActiveSubscription.mockResolvedValue(mockSubscription); paymentRepository.findByUserId.mockResolvedValue([]); const result = await service.getUserSubscription('user-123'); expect(result).toEqual({ currentPlan: Plan.BASIC, quotaRemaining: 50, quotaLimit: 50, quotaResetDate: mockUser.quotaResetDate, subscription: { id: 'sub_stripe_123', status: 'ACTIVE', currentPeriodStart: mockSubscription.currentPeriodStart, currentPeriodEnd: mockSubscription.currentPeriodEnd, cancelAtPeriodEnd: false, }, recentPayments: [], }); }); it('should throw NotFoundException if user not found', async () => { userRepository.findById.mockResolvedValue(null); await expect(service.getUserSubscription('user-123')).rejects.toThrow( NotFoundException ); }); }); describe('cancelSubscription', () => { it('should cancel active subscription', async () => { subscriptionService.getActiveSubscription.mockResolvedValue(mockSubscription); stripeService.cancelSubscription.mockResolvedValue({} as any); subscriptionService.markAsCancelled.mockResolvedValue({} as any); await service.cancelSubscription('user-123'); expect(stripeService.cancelSubscription).toHaveBeenCalledWith('sub_stripe_123'); expect(subscriptionService.markAsCancelled).toHaveBeenCalledWith('sub-123'); }); it('should throw NotFoundException if no active subscription found', async () => { subscriptionService.getActiveSubscription.mockResolvedValue(null); await expect(service.cancelSubscription('user-123')).rejects.toThrow( NotFoundException ); }); }); describe('upgradePlan', () => { it('should create checkout session for plan upgrade', async () => { userRepository.findById.mockResolvedValue(mockUser); const mockSession = { id: 'cs_123', url: 'https://checkout.stripe.com' }; stripeService.createCheckoutSession.mockResolvedValue(mockSession); const result = await service.upgradePlan( 'user-123', Plan.PRO, 'https://success.com', 'https://cancel.com' ); expect(result).toEqual(mockSession); expect(stripeService.createCheckoutSession).toHaveBeenCalledWith( 'user-123', Plan.PRO, 'https://success.com', 'https://cancel.com', true ); }); it('should throw error for invalid upgrade path', async () => { userRepository.findById.mockResolvedValue({ ...mockUser, plan: Plan.MAX }); await expect( service.upgradePlan('user-123', Plan.PRO, 'success', 'cancel') ).rejects.toThrow('Invalid upgrade path'); }); }); describe('processSuccessfulPayment', () => { it('should process successful payment and update user', async () => { userRepository.findByStripeCustomerId.mockResolvedValue(mockUser); paymentRepository.create.mockResolvedValue({} as any); userRepository.updatePlan.mockResolvedValue({} as any); userRepository.resetQuota.mockResolvedValue({} as any); await service.processSuccessfulPayment( 'pi_123', 'cus_123', 900, 'usd', Plan.PRO ); expect(paymentRepository.create).toHaveBeenCalledWith({ userId: 'user-123', stripePaymentIntentId: 'pi_123', stripeCustomerId: 'cus_123', amount: 900, currency: 'usd', status: 'succeeded', planUpgrade: Plan.PRO, }); expect(userRepository.updatePlan).toHaveBeenCalledWith('user-123', Plan.PRO); expect(userRepository.resetQuota).toHaveBeenCalledWith('user-123', Plan.PRO); }); it('should throw NotFoundException if user not found', async () => { userRepository.findByStripeCustomerId.mockResolvedValue(null); await expect( service.processSuccessfulPayment('pi_123', 'cus_123', 900, 'usd', Plan.PRO) ).rejects.toThrow(NotFoundException); }); }); describe('handleSubscriptionCreated', () => { const stripeSubscription = { id: 'sub_stripe_123', customer: 'cus_123', status: 'active', current_period_start: Math.floor(Date.now() / 1000), current_period_end: Math.floor(Date.now() / 1000) + 86400 * 30, items: { data: [ { price: { id: 'price_pro_monthly', }, }, ], }, }; it('should create subscription and update user plan', async () => { userRepository.findByStripeCustomerId.mockResolvedValue(mockUser); subscriptionService.create.mockResolvedValue({} as any); userRepository.updatePlan.mockResolvedValue({} as any); userRepository.resetQuota.mockResolvedValue({} as any); await service.handleSubscriptionCreated(stripeSubscription); expect(subscriptionService.create).toHaveBeenCalledWith({ userId: 'user-123', stripeSubscriptionId: 'sub_stripe_123', stripeCustomerId: 'cus_123', stripePriceId: 'price_pro_monthly', status: 'active', currentPeriodStart: expect.any(Date), currentPeriodEnd: expect.any(Date), plan: Plan.BASIC, // Default mapping }); }); }); describe('plan validation', () => { it('should validate upgrade paths correctly', () => { // Access private method for testing const isValidUpgrade = (service as any).isValidUpgrade; expect(isValidUpgrade(Plan.BASIC, Plan.PRO)).toBe(true); expect(isValidUpgrade(Plan.PRO, Plan.MAX)).toBe(true); expect(isValidUpgrade(Plan.PRO, Plan.BASIC)).toBe(false); expect(isValidUpgrade(Plan.MAX, Plan.PRO)).toBe(false); }); it('should validate downgrade paths correctly', () => { const isValidDowngrade = (service as any).isValidDowngrade; expect(isValidDowngrade(Plan.PRO, Plan.BASIC)).toBe(true); expect(isValidDowngrade(Plan.MAX, Plan.PRO)).toBe(true); expect(isValidDowngrade(Plan.BASIC, Plan.PRO)).toBe(false); expect(isValidDowngrade(Plan.PRO, Plan.MAX)).toBe(false); }); }); describe('quota limits', () => { it('should return correct quota limits for each plan', () => { const getQuotaLimit = (service as any).getQuotaLimit; expect(getQuotaLimit(Plan.BASIC)).toBe(50); expect(getQuotaLimit(Plan.PRO)).toBe(500); expect(getQuotaLimit(Plan.MAX)).toBe(1000); }); }); });