"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const typeorm_1 = require("@nestjs/typeorm"); const common_1 = require("@nestjs/common"); const billing_service_1 = require("../services/billing.service"); const subscription_entity_1 = require("../entities/subscription.entity"); const invoice_entity_1 = require("../entities/invoice.entity"); const payment_method_entity_1 = require("../entities/payment-method.entity"); describe('BillingService', () => { let service; let subscriptionRepo; let invoiceRepo; let paymentMethodRepo; const mockTenantId = '550e8400-e29b-41d4-a716-446655440001'; const mockPlanId = '550e8400-e29b-41d4-a716-446655440002'; const mockSubscription = { id: 'sub-001', tenant_id: mockTenantId, plan_id: mockPlanId, status: subscription_entity_1.SubscriptionStatus.ACTIVE, current_period_start: new Date('2026-01-01'), current_period_end: new Date('2026-02-01'), payment_provider: 'stripe', metadata: {}, }; const mockInvoice = { id: 'inv-001', tenant_id: mockTenantId, subscription_id: 'sub-001', invoice_number: 'INV-202601-000001', status: invoice_entity_1.InvoiceStatus.OPEN, subtotal: 100, tax: 16, total: 116, due_date: new Date('2026-01-15'), line_items: [{ description: 'Pro Plan', quantity: 1, unit_price: 100, amount: 100 }], }; const mockPaymentMethod = { id: 'pm-001', tenant_id: mockTenantId, type: payment_method_entity_1.PaymentMethodType.CARD, card_last_four: '4242', card_brand: 'visa', is_default: true, is_active: true, }; beforeEach(async () => { const mockSubscriptionRepo = { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), find: jest.fn(), update: jest.fn(), }; const mockInvoiceRepo = { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), find: jest.fn(), findAndCount: jest.fn(), count: jest.fn(), }; const mockPaymentMethodRepo = { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), find: jest.fn(), update: jest.fn(), }; const module = await testing_1.Test.createTestingModule({ providers: [ billing_service_1.BillingService, { provide: (0, typeorm_1.getRepositoryToken)(subscription_entity_1.Subscription), useValue: mockSubscriptionRepo }, { provide: (0, typeorm_1.getRepositoryToken)(invoice_entity_1.Invoice), useValue: mockInvoiceRepo }, { provide: (0, typeorm_1.getRepositoryToken)(payment_method_entity_1.PaymentMethod), useValue: mockPaymentMethodRepo }, ], }).compile(); service = module.get(billing_service_1.BillingService); subscriptionRepo = module.get((0, typeorm_1.getRepositoryToken)(subscription_entity_1.Subscription)); invoiceRepo = module.get((0, typeorm_1.getRepositoryToken)(invoice_entity_1.Invoice)); paymentMethodRepo = module.get((0, typeorm_1.getRepositoryToken)(payment_method_entity_1.PaymentMethod)); }); afterEach(() => { jest.clearAllMocks(); }); describe('createSubscription', () => { it('should create a subscription successfully', async () => { subscriptionRepo.create.mockReturnValue(mockSubscription); subscriptionRepo.save.mockResolvedValue(mockSubscription); const dto = { tenant_id: mockTenantId, plan_id: mockPlanId, payment_provider: 'stripe', }; const result = await service.createSubscription(dto); expect(result).toEqual(mockSubscription); expect(subscriptionRepo.create).toHaveBeenCalled(); expect(subscriptionRepo.save).toHaveBeenCalled(); }); it('should create trial subscription when trial_end provided', async () => { const trialSub = { ...mockSubscription, status: subscription_entity_1.SubscriptionStatus.TRIAL, trial_end: new Date('2026-01-15'), }; subscriptionRepo.create.mockReturnValue(trialSub); subscriptionRepo.save.mockResolvedValue(trialSub); const dto = { tenant_id: mockTenantId, plan_id: mockPlanId, payment_provider: 'stripe', trial_end: '2026-01-15', }; const result = await service.createSubscription(dto); expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.TRIAL); }); }); describe('getSubscription', () => { it('should return subscription for tenant', async () => { subscriptionRepo.findOne.mockResolvedValue(mockSubscription); const result = await service.getSubscription(mockTenantId); expect(result).toEqual(mockSubscription); expect(subscriptionRepo.findOne).toHaveBeenCalledWith({ where: { tenant_id: mockTenantId }, order: { created_at: 'DESC' }, }); }); it('should return null if no subscription found', async () => { subscriptionRepo.findOne.mockResolvedValue(null); const result = await service.getSubscription(mockTenantId); expect(result).toBeNull(); }); }); describe('updateSubscription', () => { it('should update subscription successfully', async () => { subscriptionRepo.findOne.mockResolvedValue(mockSubscription); subscriptionRepo.save.mockResolvedValue({ ...mockSubscription, status: subscription_entity_1.SubscriptionStatus.PAST_DUE, }); const result = await service.updateSubscription(mockTenantId, { status: subscription_entity_1.SubscriptionStatus.PAST_DUE, }); expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.PAST_DUE); }); it('should throw NotFoundException if subscription not found', async () => { subscriptionRepo.findOne.mockResolvedValue(null); await expect(service.updateSubscription(mockTenantId, { status: subscription_entity_1.SubscriptionStatus.ACTIVE })).rejects.toThrow(common_1.NotFoundException); }); }); describe('cancelSubscription', () => { it('should cancel subscription immediately', async () => { subscriptionRepo.findOne.mockResolvedValue(mockSubscription); subscriptionRepo.save.mockResolvedValue({ ...mockSubscription, status: subscription_entity_1.SubscriptionStatus.CANCELLED, cancelled_at: new Date(), }); const result = await service.cancelSubscription(mockTenantId, { immediately: true, reason: 'User requested', }); expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.CANCELLED); expect(result.cancelled_at).toBeDefined(); }); it('should schedule cancellation at period end', async () => { const activeSub = { ...mockSubscription, status: subscription_entity_1.SubscriptionStatus.ACTIVE }; const savedSub = { ...activeSub, cancelled_at: new Date() }; subscriptionRepo.findOne.mockResolvedValue(activeSub); subscriptionRepo.save.mockResolvedValue(savedSub); const result = await service.cancelSubscription(mockTenantId, { immediately: false, }); expect(result.cancelled_at).toBeDefined(); expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.ACTIVE); }); it('should throw NotFoundException if subscription not found', async () => { subscriptionRepo.findOne.mockResolvedValue(null); await expect(service.cancelSubscription(mockTenantId, { immediately: true })).rejects.toThrow(common_1.NotFoundException); }); }); describe('changePlan', () => { it('should change plan successfully', async () => { const newPlanId = 'new-plan-id'; subscriptionRepo.findOne.mockResolvedValue(mockSubscription); subscriptionRepo.save.mockResolvedValue({ ...mockSubscription, plan_id: newPlanId, }); const result = await service.changePlan(mockTenantId, newPlanId); expect(result.plan_id).toBe(newPlanId); }); it('should throw NotFoundException if subscription not found', async () => { subscriptionRepo.findOne.mockResolvedValue(null); await expect(service.changePlan(mockTenantId, 'new-plan')).rejects.toThrow(common_1.NotFoundException); }); }); describe('renewSubscription', () => { it('should renew subscription successfully', async () => { subscriptionRepo.findOne.mockResolvedValue(mockSubscription); subscriptionRepo.save.mockResolvedValue({ ...mockSubscription, current_period_start: mockSubscription.current_period_end, current_period_end: new Date('2026-03-01'), status: subscription_entity_1.SubscriptionStatus.ACTIVE, }); const result = await service.renewSubscription(mockTenantId); expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.ACTIVE); }); }); describe('createInvoice', () => { it('should create invoice with correct calculations', async () => { invoiceRepo.count.mockResolvedValue(0); invoiceRepo.create.mockReturnValue(mockInvoice); invoiceRepo.save.mockResolvedValue(mockInvoice); const result = await service.createInvoice(mockTenantId, 'sub-001', [ { description: 'Pro Plan', quantity: 1, unit_price: 100 }, ]); expect(result).toEqual(mockInvoice); expect(invoiceRepo.create).toHaveBeenCalled(); }); }); describe('getInvoices', () => { it('should return paginated invoices', async () => { invoiceRepo.findAndCount.mockResolvedValue([[mockInvoice], 1]); const result = await service.getInvoices(mockTenantId, { page: 1, limit: 10 }); expect(result.data).toHaveLength(1); expect(result.total).toBe(1); expect(result.page).toBe(1); expect(result.limit).toBe(10); }); it('should use default pagination values', async () => { invoiceRepo.findAndCount.mockResolvedValue([[mockInvoice], 1]); const result = await service.getInvoices(mockTenantId); expect(result.page).toBe(1); expect(result.limit).toBe(10); }); }); describe('getInvoice', () => { it('should return invoice by id', async () => { invoiceRepo.findOne.mockResolvedValue(mockInvoice); const result = await service.getInvoice('inv-001', mockTenantId); expect(result).toEqual(mockInvoice); }); it('should throw NotFoundException if invoice not found', async () => { invoiceRepo.findOne.mockResolvedValue(null); await expect(service.getInvoice('invalid-id', mockTenantId)).rejects.toThrow(common_1.NotFoundException); }); }); describe('markInvoicePaid', () => { it('should mark invoice as paid', async () => { const openInvoice = { ...mockInvoice, status: invoice_entity_1.InvoiceStatus.OPEN }; invoiceRepo.findOne.mockResolvedValue(openInvoice); invoiceRepo.save.mockResolvedValue({ ...openInvoice, status: invoice_entity_1.InvoiceStatus.PAID, paid_at: new Date(), }); const result = await service.markInvoicePaid('inv-001', mockTenantId); expect(result.status).toBe(invoice_entity_1.InvoiceStatus.PAID); expect(result.paid_at).toBeDefined(); }); }); describe('voidInvoice', () => { it('should void open invoice', async () => { const openInvoice = { ...mockInvoice, status: invoice_entity_1.InvoiceStatus.OPEN }; invoiceRepo.findOne.mockResolvedValue(openInvoice); invoiceRepo.save.mockResolvedValue({ ...openInvoice, status: invoice_entity_1.InvoiceStatus.VOID, }); const result = await service.voidInvoice('inv-001', mockTenantId); expect(result.status).toBe(invoice_entity_1.InvoiceStatus.VOID); }); it('should throw BadRequestException for paid invoice', async () => { invoiceRepo.findOne.mockResolvedValue({ ...mockInvoice, status: invoice_entity_1.InvoiceStatus.PAID, }); await expect(service.voidInvoice('inv-001', mockTenantId)).rejects.toThrow(common_1.BadRequestException); }); }); describe('addPaymentMethod', () => { it('should add payment method successfully', async () => { paymentMethodRepo.update.mockResolvedValue({ affected: 1 }); paymentMethodRepo.create.mockReturnValue(mockPaymentMethod); paymentMethodRepo.save.mockResolvedValue(mockPaymentMethod); const dto = { type: payment_method_entity_1.PaymentMethodType.CARD, card_last_four: '4242', card_brand: 'visa', is_default: true, }; const result = await service.addPaymentMethod(mockTenantId, dto); expect(result).toEqual(mockPaymentMethod); }); it('should unset other defaults when adding default', async () => { paymentMethodRepo.update.mockResolvedValue({ affected: 1 }); paymentMethodRepo.create.mockReturnValue(mockPaymentMethod); paymentMethodRepo.save.mockResolvedValue(mockPaymentMethod); const dto = { type: payment_method_entity_1.PaymentMethodType.CARD, card_last_four: '4242', card_brand: 'visa', is_default: true, }; await service.addPaymentMethod(mockTenantId, dto); expect(paymentMethodRepo.update).toHaveBeenCalledWith({ tenant_id: mockTenantId, is_default: true }, { is_default: false }); }); }); describe('getPaymentMethods', () => { it('should return active payment methods', async () => { paymentMethodRepo.find.mockResolvedValue([mockPaymentMethod]); const result = await service.getPaymentMethods(mockTenantId); expect(result).toHaveLength(1); expect(paymentMethodRepo.find).toHaveBeenCalledWith({ where: { tenant_id: mockTenantId, is_active: true }, order: { is_default: 'DESC', created_at: 'DESC' }, }); }); }); describe('getDefaultPaymentMethod', () => { it('should return default payment method', async () => { paymentMethodRepo.findOne.mockResolvedValue(mockPaymentMethod); const result = await service.getDefaultPaymentMethod(mockTenantId); expect(result).toEqual(mockPaymentMethod); }); it('should return null if no default', async () => { paymentMethodRepo.findOne.mockResolvedValue(null); const result = await service.getDefaultPaymentMethod(mockTenantId); expect(result).toBeNull(); }); }); describe('setDefaultPaymentMethod', () => { it('should set payment method as default', async () => { paymentMethodRepo.findOne.mockResolvedValue(mockPaymentMethod); paymentMethodRepo.update.mockResolvedValue({ affected: 1 }); paymentMethodRepo.save.mockResolvedValue({ ...mockPaymentMethod, is_default: true, }); const result = await service.setDefaultPaymentMethod('pm-001', mockTenantId); expect(result.is_default).toBe(true); }); it('should throw NotFoundException if not found', async () => { paymentMethodRepo.findOne.mockResolvedValue(null); await expect(service.setDefaultPaymentMethod('invalid-id', mockTenantId)).rejects.toThrow(common_1.NotFoundException); }); }); describe('removePaymentMethod', () => { it('should deactivate non-default payment method', async () => { paymentMethodRepo.findOne.mockResolvedValue({ ...mockPaymentMethod, is_default: false, }); paymentMethodRepo.save.mockResolvedValue({}); await service.removePaymentMethod('pm-001', mockTenantId); expect(paymentMethodRepo.save).toHaveBeenCalled(); }); it('should throw BadRequestException for default payment method', async () => { paymentMethodRepo.findOne.mockResolvedValue(mockPaymentMethod); await expect(service.removePaymentMethod('pm-001', mockTenantId)).rejects.toThrow(common_1.BadRequestException); }); it('should throw NotFoundException if not found', async () => { paymentMethodRepo.findOne.mockResolvedValue(null); await expect(service.removePaymentMethod('invalid-id', mockTenantId)).rejects.toThrow(common_1.NotFoundException); }); }); describe('getBillingSummary', () => { it('should return billing summary', async () => { subscriptionRepo.findOne.mockResolvedValue(mockSubscription); paymentMethodRepo.findOne.mockResolvedValue(mockPaymentMethod); invoiceRepo.find.mockResolvedValue([mockInvoice]); const result = await service.getBillingSummary(mockTenantId); expect(result.subscription).toEqual(mockSubscription); expect(result.defaultPaymentMethod).toEqual(mockPaymentMethod); expect(result.pendingInvoices).toBe(1); expect(result.totalDue).toBe(116); }); }); describe('checkSubscriptionStatus', () => { it('should return active subscription status', async () => { const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + 15); subscriptionRepo.findOne.mockResolvedValue({ ...mockSubscription, current_period_end: futureDate, }); const result = await service.checkSubscriptionStatus(mockTenantId); expect(result.isActive).toBe(true); expect(result.daysRemaining).toBeGreaterThan(0); expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.ACTIVE); }); it('should return expired status when no subscription', async () => { subscriptionRepo.findOne.mockResolvedValue(null); const result = await service.checkSubscriptionStatus(mockTenantId); expect(result.isActive).toBe(false); expect(result.daysRemaining).toBe(0); expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.EXPIRED); }); it('should return active for trial subscription', async () => { const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + 10); subscriptionRepo.findOne.mockResolvedValue({ ...mockSubscription, status: subscription_entity_1.SubscriptionStatus.TRIAL, current_period_end: futureDate, }); const result = await service.checkSubscriptionStatus(mockTenantId); expect(result.isActive).toBe(true); expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.TRIAL); }); }); }); //# sourceMappingURL=billing.service.spec.js.map