erp-transportistas-backend-v2/src/modules/billing-usage/__tests__/subscriptions.service.spec.ts
Adrian Flores Cortes 95c6b58449 feat: Add base modules from erp-core following SIMCO-REUSE directive
Phase 0 - Base modules (100% copy):
- shared/ (errors, middleware, services, utils, types)
- auth, users, tenants (multi-tenancy)
- ai, audit, notifications, mcp, payment-terminals
- billing-usage, branches, companies, core

Phase 1 - Modules to adapt (70-95%):
- partners (for shippers/consignees)
- inventory (for refacciones)
- financial (for transport costing)

Phase 2 - Pattern modules (50-70%):
- ordenes-transporte (from sales)
- gestion-flota (from products)
- viajes (from projects)

Phase 3 - New transport-specific modules:
- tracking (GPS, events, alerts)
- tarifas-transporte (pricing, surcharges)
- combustible-gastos (fuel, tolls, expenses)
- carta-porte (CFDI complement 3.1)

Estimated token savings: ~65% (~10,675 lines)

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

308 lines
11 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { DataSource, Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { SubscriptionsService } from '../services/subscriptions.service';
import { TenantSubscription, SubscriptionPlan, BillingCycle, SubscriptionStatus } from '../entities';
import { CreateTenantSubscriptionDto, UpdateTenantSubscriptionDto, CancelSubscriptionDto, ChangePlanDto } from '../dto';
describe('SubscriptionsService', () => {
let service: SubscriptionsService;
let subscriptionRepository: Repository<TenantSubscription>;
let planRepository: Repository<SubscriptionPlan>;
let dataSource: DataSource;
const mockSubscription = {
id: 'uuid-1',
tenantId: 'tenant-1',
planId: 'plan-1',
status: SubscriptionStatus.ACTIVE,
billingCycle: BillingCycle.MONTHLY,
currentPeriodStart: new Date('2024-01-01'),
currentPeriodEnd: new Date('2024-02-01'),
trialEnd: null,
cancelledAt: null,
paymentMethodId: 'pm-1',
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPlan = {
id: 'plan-1',
name: 'Basic Plan',
description: 'Basic subscription plan',
price: 9.99,
billingCycle: BillingCycle.MONTHLY,
features: ['feature1', 'feature2'],
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SubscriptionsService,
{
provide: DataSource,
useValue: {
getRepository: jest.fn(),
},
},
],
}).compile();
service = module.get<SubscriptionsService>(SubscriptionsService);
dataSource = module.get<DataSource>(DataSource);
subscriptionRepository = module.get<Repository<TenantSubscription>>(
getRepositoryToken(TenantSubscription),
);
planRepository = module.get<Repository<SubscriptionPlan>>(
getRepositoryToken(SubscriptionPlan),
);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new subscription successfully', async () => {
const dto: CreateTenantSubscriptionDto = {
tenantId: 'tenant-1',
planId: 'plan-1',
billingCycle: BillingCycle.MONTHLY,
paymentMethodId: 'pm-1',
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null);
jest.spyOn(planRepository, 'findOne').mockResolvedValue(mockPlan as any);
jest.spyOn(subscriptionRepository, 'create').mockReturnValue(mockSubscription as any);
jest.spyOn(subscriptionRepository, 'save').mockResolvedValue(mockSubscription);
const result = await service.create(dto);
expect(subscriptionRepository.findOne).toHaveBeenCalledWith({
where: { tenantId: dto.tenantId },
});
expect(planRepository.findOne).toHaveBeenCalledWith({ where: { id: dto.planId } });
expect(subscriptionRepository.create).toHaveBeenCalled();
expect(subscriptionRepository.save).toHaveBeenCalled();
expect(result).toEqual(mockSubscription);
});
it('should throw error if tenant already has subscription', async () => {
const dto: CreateTenantSubscriptionDto = {
tenantId: 'tenant-1',
planId: 'plan-1',
billingCycle: BillingCycle.MONTHLY,
paymentMethodId: 'pm-1',
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
await expect(service.create(dto)).rejects.toThrow('Tenant already has a subscription');
});
it('should throw error if plan not found', async () => {
const dto: CreateTenantSubscriptionDto = {
tenantId: 'tenant-1',
planId: 'invalid-plan',
billingCycle: BillingCycle.MONTHLY,
paymentMethodId: 'pm-1',
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null);
jest.spyOn(planRepository, 'findOne').mockResolvedValue(null);
await expect(service.create(dto)).rejects.toThrow('Plan not found');
});
});
describe('findByTenant', () => {
it('should find subscription by tenant id', async () => {
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
const result = await service.findByTenant('tenant-1');
expect(subscriptionRepository.findOne).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1' },
});
expect(result).toEqual(mockSubscription);
});
it('should return null if no subscription found', async () => {
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null);
const result = await service.findByTenant('invalid-tenant');
expect(result).toBeNull();
});
});
describe('update', () => {
it('should update subscription successfully', async () => {
const dto: UpdateTenantSubscriptionDto = {
paymentMethodId: 'pm-2',
};
const updatedSubscription = { ...mockSubscription, paymentMethodId: 'pm-2' };
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
jest.spyOn(subscriptionRepository, 'save').mockResolvedValue(updatedSubscription as any);
const result = await service.update('uuid-1', dto);
expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
expect(subscriptionRepository.save).toHaveBeenCalled();
expect(result).toEqual(updatedSubscription);
});
it('should throw error if subscription not found', async () => {
const dto: UpdateTenantSubscriptionDto = {
paymentMethodId: 'pm-2',
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(null);
await expect(service.update('invalid-id', dto)).rejects.toThrow('Subscription not found');
});
});
describe('cancel', () => {
it('should cancel subscription successfully', async () => {
const dto: CancelSubscriptionDto = {
reason: 'Customer request',
effectiveImmediately: false,
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({
...mockSubscription,
status: SubscriptionStatus.CANCELLED,
cancelledAt: new Date(),
} as any);
const result = await service.cancel('uuid-1', dto);
expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
expect(subscriptionRepository.save).toHaveBeenCalled();
expect(result.status).toBe(SubscriptionStatus.CANCELLED);
expect(result.cancelledAt).toBeDefined();
});
it('should cancel subscription immediately if requested', async () => {
const dto: CancelSubscriptionDto = {
reason: 'Customer request',
effectiveImmediately: true,
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({
...mockSubscription,
status: SubscriptionStatus.CANCELLED,
cancelledAt: new Date(),
currentPeriodEnd: new Date(),
} as any);
const result = await service.cancel('uuid-1', dto);
expect(result.status).toBe(SubscriptionStatus.CANCELLED);
expect(result.cancelledAt).toBeDefined();
});
});
describe('changePlan', () => {
it('should change subscription plan successfully', async () => {
const newPlan = { ...mockPlan, id: 'plan-2', price: 19.99 };
const dto: ChangePlanDto = {
newPlanId: 'plan-2',
billingCycle: BillingCycle.YEARLY,
prorate: true,
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
jest.spyOn(planRepository, 'findOne').mockResolvedValue(newPlan as any);
jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({
...mockSubscription,
planId: 'plan-2',
billingCycle: BillingCycle.YEARLY,
} as any);
const result = await service.changePlan('uuid-1', dto);
expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
expect(planRepository.findOne).toHaveBeenCalledWith({ where: { id: 'plan-2' } });
expect(subscriptionRepository.save).toHaveBeenCalled();
expect(result.planId).toBe('plan-2');
expect(result.billingCycle).toBe(BillingCycle.YEARLY);
});
it('should throw error if new plan not found', async () => {
const dto: ChangePlanDto = {
newPlanId: 'invalid-plan',
billingCycle: BillingCycle.MONTHLY,
prorate: false,
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
jest.spyOn(planRepository, 'findOne').mockResolvedValue(null);
await expect(service.changePlan('uuid-1', dto)).rejects.toThrow('New plan not found');
});
});
describe('getUsage', () => {
it('should get subscription usage', async () => {
const mockUsage = {
currentUsage: 850,
limits: {
apiCalls: 1000,
storage: 5368709120, // 5GB in bytes
users: 10,
},
periodStart: new Date('2024-01-01'),
periodEnd: new Date('2024-02-01'),
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
jest.spyOn(dataSource, 'query').mockResolvedValue([{ current_usage: 850 }]);
const result = await service.getUsage('uuid-1');
expect(result.currentUsage).toBe(850);
expect(result.limits).toBeDefined();
});
});
describe('reactivate', () => {
it('should reactivate cancelled subscription', async () => {
const cancelledSubscription = {
...mockSubscription,
status: SubscriptionStatus.CANCELLED,
cancelledAt: new Date(),
};
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(cancelledSubscription as any);
jest.spyOn(subscriptionRepository, 'save').mockResolvedValue({
...cancelledSubscription,
status: SubscriptionStatus.ACTIVE,
cancelledAt: null,
} as any);
const result = await service.reactivate('uuid-1');
expect(subscriptionRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
expect(subscriptionRepository.save).toHaveBeenCalled();
expect(result.status).toBe(SubscriptionStatus.ACTIVE);
expect(result.cancelledAt).toBeNull();
});
it('should throw error if subscription is not cancelled', async () => {
jest.spyOn(subscriptionRepository, 'findOne').mockResolvedValue(mockSubscription as any);
await expect(service.reactivate('uuid-1')).rejects.toThrow('Cannot reactivate active subscription');
});
});
});