- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
659 lines
33 KiB
JavaScript
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
|