template-saas/apps/backend/dist/modules/billing/__tests__/billing-edge-cases.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

659 lines
33 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 - Edge Cases', () => {
let service;
let subscriptionRepo;
let invoiceRepo;
let paymentMethodRepo;
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
const mockPlanId = '550e8400-e29b-41d4-a716-446655440002';
const createMockSubscription = (overrides = {}) => ({
id: 'sub-001',
tenant_id: mockTenantId,
plan_id: mockPlanId,
plan: null,
status: subscription_entity_1.SubscriptionStatus.ACTIVE,
current_period_start: new Date('2026-01-01'),
current_period_end: new Date('2026-02-01'),
trial_end: null,
cancelled_at: null,
external_subscription_id: '',
payment_provider: 'stripe',
metadata: {},
created_at: new Date(),
updated_at: new Date(),
...overrides,
});
const createMockInvoice = (overrides = {}) => ({
id: 'inv-001',
tenant_id: mockTenantId,
subscription_id: 'sub-001',
invoice_number: 'INV-202601-000001',
status: invoice_entity_1.InvoiceStatus.OPEN,
currency: 'MXN',
subtotal: 100,
tax: 16,
total: 116,
due_date: new Date('2026-01-15'),
paid_at: null,
external_invoice_id: '',
pdf_url: null,
line_items: [{ description: 'Pro Plan', quantity: 1, unit_price: 100, amount: 100 }],
billing_details: null,
created_at: new Date(),
updated_at: new Date(),
...overrides,
});
const createMockPaymentMethod = (overrides = {}) => ({
id: 'pm-001',
tenant_id: mockTenantId,
type: payment_method_entity_1.PaymentMethodType.CARD,
card_last_four: '4242',
card_brand: 'visa',
card_exp_month: 12,
card_exp_year: 2026,
external_payment_method_id: '',
payment_provider: 'stripe',
is_default: true,
is_active: true,
metadata: {},
created_at: new Date(),
updated_at: new Date(),
...overrides,
});
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 - Edge Cases', () => {
it('should create subscription with far future trial end date', async () => {
const farFutureTrial = new Date();
farFutureTrial.setFullYear(farFutureTrial.getFullYear() + 1);
const trialSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.TRIAL,
trial_end: farFutureTrial,
});
subscriptionRepo.create.mockReturnValue(trialSub);
subscriptionRepo.save.mockResolvedValue(trialSub);
const dto = {
tenant_id: mockTenantId,
plan_id: mockPlanId,
payment_provider: 'stripe',
trial_end: farFutureTrial.toISOString(),
};
const result = await service.createSubscription(dto);
expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.TRIAL);
expect(result.trial_end).toEqual(farFutureTrial);
});
it('should create subscription without optional payment_provider', async () => {
const subscription = createMockSubscription({
payment_provider: undefined,
});
subscriptionRepo.create.mockReturnValue(subscription);
subscriptionRepo.save.mockResolvedValue(subscription);
const dto = {
tenant_id: mockTenantId,
plan_id: mockPlanId,
};
await service.createSubscription(dto);
expect(subscriptionRepo.create).toHaveBeenCalled();
});
it('should set period end one month from creation', async () => {
const now = new Date('2026-01-15T10:00:00Z');
jest.useFakeTimers();
jest.setSystemTime(now);
const subscription = createMockSubscription({
current_period_start: now,
current_period_end: new Date('2026-02-15T10:00:00Z'),
});
subscriptionRepo.create.mockReturnValue(subscription);
subscriptionRepo.save.mockResolvedValue(subscription);
await service.createSubscription({
tenant_id: mockTenantId,
plan_id: mockPlanId,
payment_provider: 'stripe',
});
expect(subscriptionRepo.create).toHaveBeenCalledWith(expect.objectContaining({
current_period_start: now,
}));
jest.useRealTimers();
});
});
describe('cancelSubscription - Edge Cases', () => {
it('should cancel with custom reason in metadata', async () => {
const existingSub = createMockSubscription({
metadata: { existing_key: 'existing_value' },
});
const cancelledSub = createMockSubscription({
cancelled_at: new Date(),
metadata: {
existing_key: 'existing_value',
cancellation_reason: 'Too expensive',
},
});
subscriptionRepo.findOne.mockResolvedValue(existingSub);
subscriptionRepo.save.mockResolvedValue(cancelledSub);
const result = await service.cancelSubscription(mockTenantId, {
immediately: false,
reason: 'Too expensive',
});
expect(result.metadata.cancellation_reason).toBe('Too expensive');
expect(result.metadata.existing_key).toBe('existing_value');
});
it('should cancel trial subscription immediately', async () => {
const trialSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.TRIAL,
trial_end: new Date('2026-01-20'),
});
const cancelledSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.CANCELLED,
cancelled_at: new Date(),
});
subscriptionRepo.findOne.mockResolvedValue(trialSub);
subscriptionRepo.save.mockResolvedValue(cancelledSub);
const result = await service.cancelSubscription(mockTenantId, {
immediately: true,
});
expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.CANCELLED);
});
it('should preserve active status when scheduling end-of-period cancellation', async () => {
const activeSub = createMockSubscription({
current_period_end: new Date('2026-02-01'),
});
subscriptionRepo.findOne.mockResolvedValue(activeSub);
subscriptionRepo.save.mockImplementation((sub) => Promise.resolve({ ...sub, cancelled_at: new Date() }));
const result = await service.cancelSubscription(mockTenantId, {
immediately: false,
});
expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.ACTIVE);
expect(result.cancelled_at).toBeDefined();
});
});
describe('changePlan - Edge Cases (Upgrade/Downgrade)', () => {
it('should upgrade from basic to pro plan', async () => {
const basicSub = createMockSubscription({
plan_id: 'plan-basic',
});
const upgradedSub = createMockSubscription({
plan_id: 'plan-pro',
metadata: { plan_changed_at: expect.any(String) },
});
subscriptionRepo.findOne.mockResolvedValue(basicSub);
subscriptionRepo.save.mockResolvedValue(upgradedSub);
const result = await service.changePlan(mockTenantId, 'plan-pro');
expect(result.plan_id).toBe('plan-pro');
expect(result.metadata.plan_changed_at).toBeDefined();
});
it('should downgrade from pro to basic plan', async () => {
const proSub = createMockSubscription({
plan_id: 'plan-pro',
});
const downgradedSub = createMockSubscription({
plan_id: 'plan-basic',
metadata: { plan_changed_at: expect.any(String) },
});
subscriptionRepo.findOne.mockResolvedValue(proSub);
subscriptionRepo.save.mockResolvedValue(downgradedSub);
const result = await service.changePlan(mockTenantId, 'plan-basic');
expect(result.plan_id).toBe('plan-basic');
});
it('should preserve existing metadata when changing plan', async () => {
const existingSub = createMockSubscription({
plan_id: 'plan-basic',
metadata: {
original_signup: '2025-01-01',
referral_code: 'ABC123',
},
});
subscriptionRepo.findOne.mockResolvedValue(existingSub);
subscriptionRepo.save.mockImplementation((sub) => Promise.resolve(sub));
const result = await service.changePlan(mockTenantId, 'plan-pro');
expect(result.metadata.original_signup).toBe('2025-01-01');
expect(result.metadata.referral_code).toBe('ABC123');
expect(result.metadata.plan_changed_at).toBeDefined();
});
});
describe('renewSubscription - Edge Cases', () => {
it('should renew expired subscription', async () => {
const expiredSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.EXPIRED,
current_period_start: new Date('2025-12-01'),
current_period_end: new Date('2026-01-01'),
});
const renewedSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.ACTIVE,
current_period_start: new Date('2026-01-01'),
current_period_end: new Date('2026-02-01'),
});
subscriptionRepo.findOne.mockResolvedValue(expiredSub);
subscriptionRepo.save.mockResolvedValue(renewedSub);
const result = await service.renewSubscription(mockTenantId);
expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.ACTIVE);
});
it('should renew past_due subscription after payment', async () => {
const pastDueSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.PAST_DUE,
current_period_start: new Date('2025-12-01'),
current_period_end: new Date('2026-01-01'),
});
const renewedSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.ACTIVE,
current_period_start: new Date('2026-01-01'),
current_period_end: new Date('2026-02-01'),
});
subscriptionRepo.findOne.mockResolvedValue(pastDueSub);
subscriptionRepo.save.mockResolvedValue(renewedSub);
const result = await service.renewSubscription(mockTenantId);
expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.ACTIVE);
});
it('should throw NotFoundException when renewing non-existent subscription', async () => {
subscriptionRepo.findOne.mockResolvedValue(null);
await expect(service.renewSubscription(mockTenantId)).rejects.toThrow(common_1.NotFoundException);
});
it('should correctly calculate new period end across year boundary', async () => {
const decemberSub = createMockSubscription({
current_period_start: new Date('2025-12-15'),
current_period_end: new Date('2026-01-15'),
});
subscriptionRepo.findOne.mockResolvedValue(decemberSub);
subscriptionRepo.save.mockImplementation((sub) => Promise.resolve(sub));
const result = await service.renewSubscription(mockTenantId);
expect(result.current_period_start).toEqual(new Date('2026-01-15'));
const expectedEnd = new Date('2026-01-15');
expectedEnd.setMonth(expectedEnd.getMonth() + 1);
expect(result.current_period_end).toEqual(expectedEnd);
});
});
describe('createInvoice - Edge Cases', () => {
it('should calculate tax correctly (16% IVA)', async () => {
invoiceRepo.count.mockResolvedValue(0);
invoiceRepo.create.mockImplementation((data) => data);
invoiceRepo.save.mockImplementation((invoice) => Promise.resolve(invoice));
await service.createInvoice(mockTenantId, 'sub-001', [
{ description: 'Pro Plan', quantity: 1, unit_price: 1000 },
]);
expect(invoiceRepo.create).toHaveBeenCalledWith(expect.objectContaining({
subtotal: 1000,
tax: 160,
total: 1160,
}));
});
it('should calculate totals for multiple line items', async () => {
invoiceRepo.count.mockResolvedValue(0);
invoiceRepo.create.mockImplementation((data) => data);
invoiceRepo.save.mockImplementation((invoice) => Promise.resolve(invoice));
await service.createInvoice(mockTenantId, 'sub-001', [
{ description: 'Pro Plan', quantity: 1, unit_price: 100 },
{ description: 'Extra Users', quantity: 5, unit_price: 10 },
{ description: 'Storage Add-on', quantity: 2, unit_price: 25 },
]);
expect(invoiceRepo.create).toHaveBeenCalledWith(expect.objectContaining({
subtotal: 200,
tax: 32,
total: 232,
}));
});
it('should generate unique invoice number', async () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-03-15'));
invoiceRepo.count.mockResolvedValue(42);
invoiceRepo.create.mockImplementation((data) => data);
invoiceRepo.save.mockImplementation((invoice) => Promise.resolve(invoice));
await service.createInvoice(mockTenantId, 'sub-001', [
{ description: 'Pro Plan', quantity: 1, unit_price: 100 },
]);
expect(invoiceRepo.create).toHaveBeenCalledWith(expect.objectContaining({
invoice_number: 'INV-202603-000043',
}));
jest.useRealTimers();
});
it('should set due date 15 days from creation', async () => {
const now = new Date('2026-01-10T12:00:00Z');
jest.useFakeTimers();
jest.setSystemTime(now);
invoiceRepo.count.mockResolvedValue(0);
invoiceRepo.create.mockImplementation((data) => data);
invoiceRepo.save.mockImplementation((invoice) => Promise.resolve(invoice));
await service.createInvoice(mockTenantId, 'sub-001', [
{ description: 'Pro Plan', quantity: 1, unit_price: 100 },
]);
const expectedDueDate = new Date('2026-01-25T12:00:00Z');
expect(invoiceRepo.create).toHaveBeenCalledWith(expect.objectContaining({
due_date: expectedDueDate,
}));
jest.useRealTimers();
});
it('should handle zero quantity line items', async () => {
invoiceRepo.count.mockResolvedValue(0);
invoiceRepo.create.mockImplementation((data) => data);
invoiceRepo.save.mockImplementation((invoice) => Promise.resolve(invoice));
await service.createInvoice(mockTenantId, 'sub-001', [
{ description: 'Pro Plan', quantity: 0, unit_price: 100 },
]);
expect(invoiceRepo.create).toHaveBeenCalledWith(expect.objectContaining({
subtotal: 0,
tax: 0,
total: 0,
}));
});
it('should handle empty line items array', async () => {
invoiceRepo.count.mockResolvedValue(0);
invoiceRepo.create.mockImplementation((data) => data);
invoiceRepo.save.mockImplementation((invoice) => Promise.resolve(invoice));
await service.createInvoice(mockTenantId, 'sub-001', []);
expect(invoiceRepo.create).toHaveBeenCalledWith(expect.objectContaining({
subtotal: 0,
tax: 0,
total: 0,
line_items: [],
}));
});
});
describe('getInvoices - Edge Cases', () => {
it('should return empty array when no invoices exist', async () => {
invoiceRepo.findAndCount.mockResolvedValue([[], 0]);
const result = await service.getInvoices(mockTenantId);
expect(result.data).toHaveLength(0);
expect(result.total).toBe(0);
});
it('should handle high page numbers with no results', async () => {
invoiceRepo.findAndCount.mockResolvedValue([[], 0]);
const result = await service.getInvoices(mockTenantId, { page: 999, limit: 10 });
expect(result.data).toHaveLength(0);
expect(result.page).toBe(999);
});
it('should handle custom limit values', async () => {
const invoices = Array(50)
.fill(null)
.map((_, i) => createMockInvoice({ id: `inv-${i}` }));
invoiceRepo.findAndCount.mockResolvedValue([invoices.slice(0, 25), 50]);
const result = await service.getInvoices(mockTenantId, { page: 1, limit: 25 });
expect(result.data).toHaveLength(25);
expect(result.limit).toBe(25);
expect(result.total).toBe(50);
});
});
describe('voidInvoice - Edge Cases', () => {
it('should void draft invoice', async () => {
const draftInvoice = createMockInvoice({ status: invoice_entity_1.InvoiceStatus.DRAFT });
invoiceRepo.findOne.mockResolvedValue(draftInvoice);
invoiceRepo.save.mockResolvedValue(createMockInvoice({ 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 void open invoice', async () => {
const openInvoice = createMockInvoice({ status: invoice_entity_1.InvoiceStatus.OPEN });
invoiceRepo.findOne.mockResolvedValue(openInvoice);
invoiceRepo.save.mockResolvedValue(createMockInvoice({ 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 when voiding already voided invoice', async () => {
const voidedInvoice = createMockInvoice({ status: invoice_entity_1.InvoiceStatus.VOID });
invoiceRepo.findOne.mockResolvedValue(voidedInvoice);
invoiceRepo.save.mockResolvedValue(voidedInvoice);
const result = await service.voidInvoice('inv-001', mockTenantId);
expect(result.status).toBe(invoice_entity_1.InvoiceStatus.VOID);
});
});
describe('markInvoicePaid - Edge Cases', () => {
it('should mark draft invoice as paid', async () => {
const draftInvoice = createMockInvoice({ status: invoice_entity_1.InvoiceStatus.DRAFT });
invoiceRepo.findOne.mockResolvedValue(draftInvoice);
invoiceRepo.save.mockResolvedValue(createMockInvoice({ 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();
});
it('should update paid_at timestamp', async () => {
const now = new Date('2026-01-15T14:30:00Z');
jest.useFakeTimers();
jest.setSystemTime(now);
const openInvoice = createMockInvoice({ status: invoice_entity_1.InvoiceStatus.OPEN });
invoiceRepo.findOne.mockResolvedValue(openInvoice);
invoiceRepo.save.mockImplementation((invoice) => Promise.resolve(invoice));
const result = await service.markInvoicePaid('inv-001', mockTenantId);
expect(result.paid_at).toEqual(now);
jest.useRealTimers();
});
});
describe('addPaymentMethod - Edge Cases', () => {
it('should add non-default payment method without updating others', async () => {
const newMethod = createMockPaymentMethod({
id: 'pm-002',
is_default: false,
});
paymentMethodRepo.create.mockReturnValue(newMethod);
paymentMethodRepo.save.mockResolvedValue(newMethod);
const dto = {
type: payment_method_entity_1.PaymentMethodType.CARD,
card_last_four: '1234',
card_brand: 'mastercard',
is_default: false,
};
await service.addPaymentMethod(mockTenantId, dto);
expect(paymentMethodRepo.update).not.toHaveBeenCalled();
});
it('should handle bank_transfer payment method type', async () => {
const bankMethod = createMockPaymentMethod({
id: 'pm-003',
type: payment_method_entity_1.PaymentMethodType.BANK_TRANSFER,
});
paymentMethodRepo.create.mockReturnValue(bankMethod);
paymentMethodRepo.save.mockResolvedValue(bankMethod);
const dto = {
type: payment_method_entity_1.PaymentMethodType.BANK_TRANSFER,
is_default: false,
};
await service.addPaymentMethod(mockTenantId, dto);
expect(paymentMethodRepo.create).toHaveBeenCalledWith(expect.objectContaining({
type: payment_method_entity_1.PaymentMethodType.BANK_TRANSFER,
}));
});
});
describe('getPaymentMethods - Edge Cases', () => {
it('should return empty array when no payment methods exist', async () => {
paymentMethodRepo.find.mockResolvedValue([]);
const result = await service.getPaymentMethods(mockTenantId);
expect(result).toHaveLength(0);
});
it('should return only active payment methods', async () => {
const activeMethod = createMockPaymentMethod({ is_active: true });
paymentMethodRepo.find.mockResolvedValue([activeMethod]);
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' },
});
});
it('should order payment methods with default first', async () => {
const methods = [
createMockPaymentMethod({ id: 'pm-001', is_default: false, created_at: new Date('2026-01-01') }),
createMockPaymentMethod({ id: 'pm-002', is_default: true, created_at: new Date('2026-01-02') }),
createMockPaymentMethod({ id: 'pm-003', is_default: false, created_at: new Date('2026-01-03') }),
];
paymentMethodRepo.find.mockResolvedValue(methods);
await service.getPaymentMethods(mockTenantId);
expect(paymentMethodRepo.find).toHaveBeenCalledWith(expect.objectContaining({
order: { is_default: 'DESC', created_at: 'DESC' },
}));
});
});
describe('setDefaultPaymentMethod - Edge Cases', () => {
it('should unset previous default when setting new default', async () => {
const newDefault = createMockPaymentMethod({
id: 'pm-002',
is_default: false,
});
paymentMethodRepo.findOne.mockResolvedValue(newDefault);
paymentMethodRepo.update.mockResolvedValue({ affected: 1 });
paymentMethodRepo.save.mockResolvedValue(createMockPaymentMethod({ id: 'pm-002', is_default: true }));
await service.setDefaultPaymentMethod('pm-002', mockTenantId);
expect(paymentMethodRepo.update).toHaveBeenCalledWith({ tenant_id: mockTenantId, is_default: true }, { is_default: false });
});
it('should handle setting already default payment method as default', async () => {
const alreadyDefault = createMockPaymentMethod({ is_default: true });
paymentMethodRepo.findOne.mockResolvedValue(alreadyDefault);
paymentMethodRepo.update.mockResolvedValue({ affected: 1 });
paymentMethodRepo.save.mockResolvedValue(alreadyDefault);
const result = await service.setDefaultPaymentMethod('pm-001', mockTenantId);
expect(result.is_default).toBe(true);
});
});
describe('removePaymentMethod - Edge Cases', () => {
it('should deactivate instead of delete payment method', async () => {
const nonDefaultMethod = createMockPaymentMethod({
id: 'pm-002',
is_default: false,
is_active: true,
});
paymentMethodRepo.findOne.mockResolvedValue(nonDefaultMethod);
paymentMethodRepo.save.mockImplementation((pm) => Promise.resolve(pm));
await service.removePaymentMethod('pm-002', mockTenantId);
expect(paymentMethodRepo.save).toHaveBeenCalledWith(expect.objectContaining({
is_active: false,
}));
});
it('should throw when trying to remove default payment method', async () => {
const defaultMethod = createMockPaymentMethod({
is_default: true,
is_active: true,
});
paymentMethodRepo.findOne.mockResolvedValue(defaultMethod);
await expect(service.removePaymentMethod('pm-001', mockTenantId)).rejects.toThrow(common_1.BadRequestException);
});
});
describe('getBillingSummary - Edge Cases', () => {
it('should return null values when no subscription or payment method', async () => {
subscriptionRepo.findOne.mockResolvedValue(null);
paymentMethodRepo.findOne.mockResolvedValue(null);
invoiceRepo.find.mockResolvedValue([]);
const result = await service.getBillingSummary(mockTenantId);
expect(result.subscription).toBeNull();
expect(result.defaultPaymentMethod).toBeNull();
expect(result.pendingInvoices).toBe(0);
expect(result.totalDue).toBe(0);
});
it('should calculate total due from multiple pending invoices', async () => {
const pendingInvoices = [
createMockInvoice({ id: 'inv-001', total: 116, status: invoice_entity_1.InvoiceStatus.OPEN }),
createMockInvoice({ id: 'inv-002', total: 58, status: invoice_entity_1.InvoiceStatus.OPEN }),
createMockInvoice({ id: 'inv-003', total: 232, status: invoice_entity_1.InvoiceStatus.OPEN }),
];
subscriptionRepo.findOne.mockResolvedValue(createMockSubscription());
paymentMethodRepo.findOne.mockResolvedValue(createMockPaymentMethod());
invoiceRepo.find.mockResolvedValue(pendingInvoices);
const result = await service.getBillingSummary(mockTenantId);
expect(result.pendingInvoices).toBe(3);
expect(result.totalDue).toBe(406);
});
it('should handle decimal totals correctly', async () => {
const pendingInvoices = [
createMockInvoice({ id: 'inv-001', total: 116.5, status: invoice_entity_1.InvoiceStatus.OPEN }),
createMockInvoice({ id: 'inv-002', total: 58.25, status: invoice_entity_1.InvoiceStatus.OPEN }),
];
subscriptionRepo.findOne.mockResolvedValue(createMockSubscription());
paymentMethodRepo.findOne.mockResolvedValue(createMockPaymentMethod());
invoiceRepo.find.mockResolvedValue(pendingInvoices);
const result = await service.getBillingSummary(mockTenantId);
expect(result.totalDue).toBe(174.75);
});
});
describe('checkSubscriptionStatus - Edge Cases', () => {
it('should return zero days remaining when period has ended', async () => {
const expiredSub = createMockSubscription({
current_period_end: new Date('2025-12-01'),
});
subscriptionRepo.findOne.mockResolvedValue(expiredSub);
const result = await service.checkSubscriptionStatus(mockTenantId);
expect(result.daysRemaining).toBe(0);
});
it('should calculate days remaining correctly', async () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-01-10'));
const activeSub = createMockSubscription({
current_period_end: new Date('2026-01-25'),
});
subscriptionRepo.findOne.mockResolvedValue(activeSub);
const result = await service.checkSubscriptionStatus(mockTenantId);
expect(result.daysRemaining).toBe(15);
expect(result.isActive).toBe(true);
jest.useRealTimers();
});
it('should return inactive for past_due subscription', async () => {
const pastDueSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.PAST_DUE,
current_period_end: new Date('2026-02-01'),
});
subscriptionRepo.findOne.mockResolvedValue(pastDueSub);
const result = await service.checkSubscriptionStatus(mockTenantId);
expect(result.isActive).toBe(false);
expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.PAST_DUE);
});
it('should return inactive for cancelled subscription', async () => {
const cancelledSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.CANCELLED,
current_period_end: new Date('2026-02-01'),
});
subscriptionRepo.findOne.mockResolvedValue(cancelledSub);
const result = await service.checkSubscriptionStatus(mockTenantId);
expect(result.isActive).toBe(false);
expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.CANCELLED);
});
it('should return active for trial subscription', async () => {
const trialSub = createMockSubscription({
status: subscription_entity_1.SubscriptionStatus.TRIAL,
current_period_end: new Date('2026-02-01'),
trial_end: new Date('2026-01-15'),
});
subscriptionRepo.findOne.mockResolvedValue(trialSub);
const result = await service.checkSubscriptionStatus(mockTenantId);
expect(result.isActive).toBe(true);
expect(result.status).toBe(subscription_entity_1.SubscriptionStatus.TRIAL);
});
});
});
//# sourceMappingURL=billing-edge-cases.spec.js.map