template-saas/apps/backend/dist/modules/billing/__tests__/billing.service.spec.js
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

416 lines
20 KiB
JavaScript

"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