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