[SYNC] test: Add service unit tests

- billing-usage: invoices.service.spec.ts, subscriptions.service.spec.ts
- financial: payments.service.spec.ts
- inventory: warehouses-new.service.spec.ts
- partners: partners.service.spec.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-24 18:13:32 -06:00
parent 7d3ad15968
commit 7ae745e509
5 changed files with 1714 additions and 0 deletions

View File

@ -0,0 +1,360 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DataSource, Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { InvoicesService } from '../services/invoices.service';
import { Invoice, InvoiceItem, InvoiceStatus, PaymentStatus } from '../entities';
import { CreateInvoiceDto, UpdateInvoiceDto } from '../dto';
describe('InvoicesService', () => {
let service: InvoicesService;
let invoiceRepository: Repository<Invoice>;
let invoiceItemRepository: Repository<InvoiceItem>;
let dataSource: DataSource;
const mockInvoice = {
id: 'uuid-1',
tenantId: 'tenant-1',
customerId: 'customer-1',
number: 'INV-2024-001',
status: InvoiceStatus.DRAFT,
paymentStatus: PaymentStatus.PENDING,
issueDate: new Date('2024-01-01'),
dueDate: new Date('2024-01-15'),
subtotal: 1000,
taxAmount: 160,
totalAmount: 1160,
currency: 'USD',
notes: null,
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
const mockInvoiceItem = {
id: 'item-1',
invoiceId: 'uuid-1',
productId: 'product-1',
description: 'Test Product',
quantity: 2,
unitPrice: 500,
discount: 0,
taxRate: 0.08,
total: 1080,
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
InvoicesService,
{
provide: DataSource,
useValue: {
getRepository: jest.fn(),
query: jest.fn(),
},
},
],
}).compile();
service = module.get<InvoicesService>(InvoicesService);
dataSource = module.get<DataSource>(DataSource);
invoiceRepository = module.get<Repository<Invoice>>(
getRepositoryToken(Invoice),
);
invoiceItemRepository = module.get<Repository<InvoiceItem>>(
getRepositoryToken(InvoiceItem),
);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new invoice successfully', async () => {
const dto: CreateInvoiceDto = {
customerId: 'customer-1',
issueDate: new Date('2024-01-01'),
dueDate: new Date('2024-01-15'),
items: [
{
productId: 'product-1',
description: 'Test Product',
quantity: 2,
unitPrice: 500,
discount: 0,
taxRate: 0.08,
},
],
notes: 'Test invoice',
};
jest.spyOn(invoiceRepository, 'create').mockReturnValue(mockInvoice as any);
jest.spyOn(invoiceRepository, 'save').mockResolvedValue(mockInvoice);
jest.spyOn(invoiceItemRepository, 'create').mockReturnValue(mockInvoiceItem as any);
jest.spyOn(invoiceItemRepository, 'save').mockResolvedValue(mockInvoiceItem);
const result = await service.create(dto);
expect(invoiceRepository.create).toHaveBeenCalled();
expect(invoiceRepository.save).toHaveBeenCalled();
expect(invoiceItemRepository.create).toHaveBeenCalled();
expect(invoiceItemRepository.save).toHaveBeenCalled();
expect(result).toEqual(mockInvoice);
});
it('should calculate totals correctly', async () => {
const dto: CreateInvoiceDto = {
customerId: 'customer-1',
issueDate: new Date('2024-01-01'),
dueDate: new Date('2024-01-15'),
items: [
{
productId: 'product-1',
description: 'Test Product 1',
quantity: 2,
unitPrice: 500,
discount: 50,
taxRate: 0.08,
},
{
productId: 'product-2',
description: 'Test Product 2',
quantity: 1,
unitPrice: 300,
discount: 0,
taxRate: 0.08,
},
],
};
const expectedInvoice = {
...mockInvoice,
subtotal: 1000,
taxAmount: 120,
totalAmount: 1120,
};
jest.spyOn(invoiceRepository, 'create').mockReturnValue(expectedInvoice as any);
jest.spyOn(invoiceRepository, 'save').mockResolvedValue(expectedInvoice);
jest.spyOn(invoiceItemRepository, 'create').mockReturnValue(mockInvoiceItem as any);
jest.spyOn(invoiceItemRepository, 'save').mockResolvedValue(mockInvoiceItem);
const result = await service.create(dto);
expect(result.subtotal).toBe(1000);
expect(result.taxAmount).toBe(120);
expect(result.totalAmount).toBe(1120);
});
});
describe('findById', () => {
it('should find invoice by id', async () => {
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
const result = await service.findById('uuid-1');
expect(invoiceRepository.findOne).toHaveBeenCalledWith({
where: { id: 'uuid-1' },
relations: ['items'],
});
expect(result).toEqual(mockInvoice);
});
it('should return null if invoice not found', async () => {
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(null);
const result = await service.findById('invalid-id');
expect(result).toBeNull();
});
});
describe('findByTenant', () => {
it('should find invoices by tenant', async () => {
const mockInvoices = [mockInvoice, { ...mockInvoice, id: 'uuid-2' }];
jest.spyOn(invoiceRepository, 'find').mockResolvedValue(mockInvoices as any);
const result = await service.findByTenant('tenant-1', {
page: 1,
limit: 10,
});
expect(invoiceRepository.find).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1' },
relations: ['items'],
order: { createdAt: 'DESC' },
skip: 0,
take: 10,
});
expect(result).toEqual(mockInvoices);
});
});
describe('update', () => {
it('should update invoice successfully', async () => {
const dto: UpdateInvoiceDto = {
status: InvoiceStatus.SENT,
notes: 'Updated notes',
};
const updatedInvoice = { ...mockInvoice, status: InvoiceStatus.SENT, notes: 'Updated notes' };
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
jest.spyOn(invoiceRepository, 'save').mockResolvedValue(updatedInvoice as any);
const result = await service.update('uuid-1', dto);
expect(invoiceRepository.findOne).toHaveBeenCalledWith({
where: { id: 'uuid-1' },
});
expect(invoiceRepository.save).toHaveBeenCalled();
expect(result.status).toBe(InvoiceStatus.SENT);
expect(result.notes).toBe('Updated notes');
});
it('should throw error if invoice not found', async () => {
const dto: UpdateInvoiceDto = {
status: InvoiceStatus.SENT,
};
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(null);
await expect(service.update('invalid-id', dto)).rejects.toThrow('Invoice not found');
});
});
describe('updateStatus', () => {
it('should update invoice status', async () => {
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
...mockInvoice,
status: InvoiceStatus.PAID,
} as any);
const result = await service.updateStatus('uuid-1', InvoiceStatus.PAID);
expect(result.status).toBe(InvoiceStatus.PAID);
});
});
describe('updatePaymentStatus', () => {
it('should update payment status', async () => {
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
...mockInvoice,
paymentStatus: PaymentStatus.PAID,
} as any);
const result = await service.updatePaymentStatus('uuid-1', PaymentStatus.PAID);
expect(result.paymentStatus).toBe(PaymentStatus.PAID);
});
});
describe('delete', () => {
it('should delete invoice successfully', async () => {
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
jest.spyOn(invoiceRepository, 'remove').mockResolvedValue(undefined);
await service.delete('uuid-1');
expect(invoiceRepository.remove).toHaveBeenCalledWith(mockInvoice);
});
it('should throw error if invoice not found', async () => {
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(null);
await expect(service.delete('invalid-id')).rejects.toThrow('Invoice not found');
});
});
describe('addItem', () => {
it('should add item to invoice', async () => {
const itemDto = {
productId: 'product-2',
description: 'New Product',
quantity: 1,
unitPrice: 300,
discount: 0,
taxRate: 0.08,
};
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
jest.spyOn(invoiceItemRepository, 'create').mockReturnValue(mockInvoiceItem as any);
jest.spyOn(invoiceItemRepository, 'save').mockResolvedValue(mockInvoiceItem);
jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
...mockInvoice,
subtotal: 1500,
taxAmount: 120,
totalAmount: 1620,
} as any);
const result = await service.addItem('uuid-1', itemDto);
expect(invoiceItemRepository.create).toHaveBeenCalled();
expect(invoiceItemRepository.save).toHaveBeenCalled();
expect(result.totalAmount).toBe(1620);
});
});
describe('removeItem', () => {
it('should remove item from invoice', async () => {
jest.spyOn(invoiceItemRepository, 'findOne').mockResolvedValue(mockInvoiceItem as any);
jest.spyOn(invoiceItemRepository, 'remove').mockResolvedValue(undefined);
jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
...mockInvoice,
subtotal: 500,
taxAmount: 40,
totalAmount: 540,
} as any);
const result = await service.removeItem('uuid-1', 'item-1');
expect(invoiceItemRepository.remove).toHaveBeenCalled();
expect(result.totalAmount).toBe(540);
});
});
describe('sendInvoice', () => {
it('should mark invoice as sent', async () => {
jest.spyOn(invoiceRepository, 'findOne').mockResolvedValue(mockInvoice as any);
jest.spyOn(invoiceRepository, 'save').mockResolvedValue({
...mockInvoice,
status: InvoiceStatus.SENT,
sentAt: new Date(),
} as any);
const result = await service.sendInvoice('uuid-1');
expect(result.status).toBe(InvoiceStatus.SENT);
expect(result.sentAt).toBeDefined();
});
});
describe('calculateTotals', () => {
it('should calculate totals from items', () => {
const items = [
{ quantity: 2, unitPrice: 500, discount: 50, taxRate: 0.08 },
{ quantity: 1, unitPrice: 300, discount: 0, taxRate: 0.08 },
];
const totals = service.calculateTotals(items);
expect(totals.subtotal).toBe(1000);
expect(totals.taxAmount).toBe(120);
expect(totals.totalAmount).toBe(1120);
});
it('should handle empty items array', () => {
const totals = service.calculateTotals([]);
expect(totals.subtotal).toBe(0);
expect(totals.taxAmount).toBe(0);
expect(totals.totalAmount).toBe(0);
});
});
});

View File

@ -0,0 +1,307 @@
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');
});
});
});

View File

@ -0,0 +1,335 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PaymentsService } from '../payments.service';
import { Payment, PaymentMethod, PaymentStatus } from '../entities';
import { CreatePaymentDto, UpdatePaymentDto } from '../dto';
describe('PaymentsService', () => {
let service: PaymentsService;
let paymentRepository: Repository<Payment>;
let paymentMethodRepository: Repository<PaymentMethod>;
const mockPayment = {
id: 'uuid-1',
tenantId: 'tenant-1',
invoiceId: 'invoice-1',
paymentMethodId: 'method-1',
amount: 1000,
currency: 'USD',
status: PaymentStatus.PENDING,
paymentDate: new Date('2024-01-15'),
reference: 'REF-001',
notes: 'Payment for invoice #001',
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPaymentMethod = {
id: 'method-1',
tenantId: 'tenant-1',
name: 'Bank Transfer',
type: 'BANK_TRANSFER',
isActive: true,
config: {},
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PaymentsService,
{
provide: getRepositoryToken(Payment),
useValue: {
findOne: jest.fn(),
find: jest.fn(),
create: jest.fn(),
save: jest.fn(),
remove: jest.fn(),
createQueryBuilder: jest.fn(),
},
},
{
provide: getRepositoryToken(PaymentMethod),
useValue: {
findOne: jest.fn(),
},
},
],
}).compile();
service = module.get<PaymentsService>(PaymentsService);
paymentRepository = module.get<Repository<Payment>>(getRepositoryToken(Payment));
paymentMethodRepository = module.get<Repository<PaymentMethod>>(getRepositoryToken(PaymentMethod));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new payment successfully', async () => {
const dto: CreatePaymentDto = {
invoiceId: 'invoice-1',
paymentMethodId: 'method-1',
amount: 1000,
currency: 'USD',
paymentDate: new Date('2024-01-15'),
reference: 'REF-001',
notes: 'Payment for invoice #001',
};
jest.spyOn(paymentMethodRepository, 'findOne').mockResolvedValue(mockPaymentMethod as any);
jest.spyOn(paymentRepository, 'create').mockReturnValue(mockPayment as any);
jest.spyOn(paymentRepository, 'save').mockResolvedValue(mockPayment);
const result = await service.create(dto);
expect(paymentMethodRepository.findOne).toHaveBeenCalledWith({
where: { id: dto.paymentMethodId },
});
expect(paymentRepository.create).toHaveBeenCalled();
expect(paymentRepository.save).toHaveBeenCalled();
expect(result).toEqual(mockPayment);
});
it('should throw error if payment method not found', async () => {
const dto: CreatePaymentDto = {
invoiceId: 'invoice-1',
paymentMethodId: 'invalid-method',
amount: 1000,
currency: 'USD',
};
jest.spyOn(paymentMethodRepository, 'findOne').mockResolvedValue(null);
await expect(service.create(dto)).rejects.toThrow('Payment method not found');
});
it('should throw error if payment method is inactive', async () => {
const inactiveMethod = { ...mockPaymentMethod, isActive: false };
const dto: CreatePaymentDto = {
invoiceId: 'invoice-1',
paymentMethodId: 'method-1',
amount: 1000,
currency: 'USD',
};
jest.spyOn(paymentMethodRepository, 'findOne').mockResolvedValue(inactiveMethod as any);
await expect(service.create(dto)).rejects.toThrow('Payment method is not active');
});
});
describe('findById', () => {
it('should find payment by id', async () => {
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
const result = await service.findById('uuid-1');
expect(paymentRepository.findOne).toHaveBeenCalledWith({
where: { id: 'uuid-1' },
relations: ['paymentMethod', 'invoice'],
});
expect(result).toEqual(mockPayment);
});
it('should return null if payment not found', async () => {
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(null);
const result = await service.findById('invalid-id');
expect(result).toBeNull();
});
});
describe('findByInvoice', () => {
it('should find payments by invoice', async () => {
const mockPayments = [mockPayment, { ...mockPayment, id: 'uuid-2' }];
jest.spyOn(paymentRepository, 'find').mockResolvedValue(mockPayments as any);
const result = await service.findByInvoice('invoice-1');
expect(paymentRepository.find).toHaveBeenCalledWith({
where: { invoiceId: 'invoice-1' },
relations: ['paymentMethod'],
order: { createdAt: 'DESC' },
});
expect(result).toEqual(mockPayments);
});
});
describe('update', () => {
it('should update payment successfully', async () => {
const dto: UpdatePaymentDto = {
status: PaymentStatus.COMPLETED,
notes: 'Payment completed',
};
const updatedPayment = { ...mockPayment, status: PaymentStatus.COMPLETED };
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
jest.spyOn(paymentRepository, 'save').mockResolvedValue(updatedPayment as any);
const result = await service.update('uuid-1', dto);
expect(paymentRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
expect(paymentRepository.save).toHaveBeenCalled();
expect(result.status).toBe(PaymentStatus.COMPLETED);
});
it('should throw error if payment not found', async () => {
const dto: UpdatePaymentDto = { status: PaymentStatus.COMPLETED };
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(null);
await expect(service.update('invalid-id', dto)).rejects.toThrow('Payment not found');
});
});
describe('updateStatus', () => {
it('should update payment status', async () => {
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
jest.spyOn(paymentRepository, 'save').mockResolvedValue({
...mockPayment,
status: PaymentStatus.COMPLETED,
} as any);
const result = await service.updateStatus('uuid-1', PaymentStatus.COMPLETED);
expect(result.status).toBe(PaymentStatus.COMPLETED);
});
});
describe('delete', () => {
it('should delete payment successfully', async () => {
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
jest.spyOn(paymentRepository, 'remove').mockResolvedValue(undefined);
await service.delete('uuid-1');
expect(paymentRepository.remove).toHaveBeenCalledWith(mockPayment);
});
it('should throw error if payment not found', async () => {
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(null);
await expect(service.delete('invalid-id')).rejects.toThrow('Payment not found');
});
it('should throw error if payment is completed', async () => {
const completedPayment = { ...mockPayment, status: PaymentStatus.COMPLETED };
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(completedPayment as any);
await expect(service.delete('uuid-1')).rejects.toThrow('Cannot delete completed payment');
});
});
describe('getTotalPaid', () => {
it('should get total paid for invoice', async () => {
jest.spyOn(paymentRepository, 'createQueryBuilder').mockReturnValue({
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue({ total: 1500 }),
} as any);
const result = await service.getTotalPaid('invoice-1');
expect(result).toBe(1500);
});
it('should return 0 if no payments found', async () => {
jest.spyOn(paymentRepository, 'createQueryBuilder').mockReturnValue({
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue(null),
} as any);
const result = await service.getTotalPaid('invoice-1');
expect(result).toBe(0);
});
});
describe('processRefund', () => {
it('should process refund successfully', async () => {
const refundAmount = 500;
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
jest.spyOn(paymentRepository, 'save').mockResolvedValue({
...mockPayment,
status: PaymentStatus.REFUNDED,
refundedAmount: refundAmount,
} as any);
const result = await service.processRefund('uuid-1', refundAmount, 'Customer request');
expect(result.status).toBe(PaymentStatus.REFUNDED);
expect(result.refundedAmount).toBe(refundAmount);
});
it('should throw error if refund amount exceeds payment amount', async () => {
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
await expect(service.processRefund('uuid-1', 1500, 'Over refund')).rejects.toThrow('Refund amount cannot exceed payment amount');
});
it('should throw error if payment is not completed', async () => {
jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
await expect(service.processRefund('uuid-1', 500, 'Refund pending payment')).rejects.toThrow('Can only refund completed payments');
});
});
describe('getPaymentsByDateRange', () => {
it('should get payments by date range', async () => {
const startDate = new Date('2024-01-01');
const endDate = new Date('2024-01-31');
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
leftJoinAndSelect: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue([mockPayment]),
};
jest.spyOn(paymentRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);
const result = await service.getPaymentsByDateRange('tenant-1', startDate, endDate);
expect(paymentRepository.createQueryBuilder).toHaveBeenCalledWith('payment');
expect(mockQueryBuilder.where).toHaveBeenCalledWith('payment.tenantId = :tenantId', { tenantId: 'tenant-1' });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('payment.paymentDate >= :startDate', { startDate });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('payment.paymentDate <= :endDate', { endDate });
expect(result).toEqual([mockPayment]);
});
});
describe('getPaymentSummary', () => {
it('should get payment summary for tenant', async () => {
const mockSummary = {
totalPayments: 10,
totalAmount: 10000,
completedPayments: 8,
completedAmount: 8500,
pendingPayments: 2,
pendingAmount: 1500,
};
jest.spyOn(paymentRepository, 'createQueryBuilder').mockReturnValue({
where: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue({ total: 10, amount: 10000 }),
} as any);
const result = await service.getPaymentSummary('tenant-1', new Date('2024-01-01'), new Date('2024-01-31'));
expect(result.totalPayments).toBe(10);
expect(result.totalAmount).toBe(10000);
});
});
});

View File

@ -0,0 +1,319 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { WarehousesService } from '../warehouses.service';
import { Warehouse, WarehouseStatus } from '../entities';
import { CreateWarehouseDto, UpdateWarehouseDto } from '../dto';
describe('WarehousesService', () => {
let service: WarehousesService;
let warehouseRepository: Repository<Warehouse>;
const mockWarehouse = {
id: 'uuid-1',
tenantId: 'tenant-1',
code: 'WH-001',
name: 'Main Warehouse',
description: 'Primary storage facility',
address: {
street: '123 Storage St',
city: 'Storage City',
state: 'SC',
zipCode: '12345',
country: 'US',
},
contact: {
name: 'John Manager',
email: 'john@company.com',
phone: '+1234567890',
},
status: WarehouseStatus.ACTIVE,
capacity: 10000,
currentOccupancy: 3500,
operatingHours: {
monday: { open: '08:00', close: '18:00' },
tuesday: { open: '08:00', close: '18:00' },
wednesday: { open: '08:00', close: '18:00' },
thursday: { open: '08:00', close: '18:00' },
friday: { open: '08:00', close: '18:00' },
saturday: { open: '09:00', close: '14:00' },
sunday: { open: null, close: null },
},
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WarehousesService,
{
provide: getRepositoryToken(Warehouse),
useValue: {
findOne: jest.fn(),
find: jest.fn(),
create: jest.fn(),
save: jest.fn(),
remove: jest.fn(),
createQueryBuilder: jest.fn(),
},
},
],
}).compile();
service = module.get<WarehousesService>(WarehousesService);
warehouseRepository = module.get<Repository<Warehouse>>(getRepositoryToken(Warehouse));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new warehouse successfully', async () => {
const dto: CreateWarehouseDto = {
code: 'WH-001',
name: 'Main Warehouse',
description: 'Primary storage facility',
address: {
street: '123 Storage St',
city: 'Storage City',
state: 'SC',
zipCode: '12345',
country: 'US',
},
contact: {
name: 'John Manager',
email: 'john@company.com',
phone: '+1234567890',
},
capacity: 10000,
operatingHours: {
monday: { open: '08:00', close: '18:00' },
tuesday: { open: '08:00', close: '18:00' },
},
};
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(null);
jest.spyOn(warehouseRepository, 'create').mockReturnValue(mockWarehouse as any);
jest.spyOn(warehouseRepository, 'save').mockResolvedValue(mockWarehouse);
const result = await service.create(dto);
expect(warehouseRepository.findOne).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1', code: dto.code },
});
expect(warehouseRepository.create).toHaveBeenCalled();
expect(warehouseRepository.save).toHaveBeenCalled();
expect(result).toEqual(mockWarehouse);
});
it('should throw error if warehouse code already exists', async () => {
const dto: CreateWarehouseDto = {
code: 'WH-001',
name: 'Main Warehouse',
};
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(mockWarehouse as any);
await expect(service.create(dto)).rejects.toThrow('Warehouse code already exists');
});
});
describe('findById', () => {
it('should find warehouse by id', async () => {
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(mockWarehouse as any);
const result = await service.findById('uuid-1');
expect(warehouseRepository.findOne).toHaveBeenCalledWith({
where: { id: 'uuid-1' },
});
expect(result).toEqual(mockWarehouse);
});
it('should return null if warehouse not found', async () => {
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(null);
const result = await service.findById('invalid-id');
expect(result).toBeNull();
});
});
describe('findByTenant', () => {
it('should find warehouses by tenant', async () => {
const mockWarehouses = [mockWarehouse, { ...mockWarehouse, id: 'uuid-2' }];
jest.spyOn(warehouseRepository, 'find').mockResolvedValue(mockWarehouses as any);
const result = await service.findByTenant('tenant-1');
expect(warehouseRepository.find).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1' },
order: { code: 'ASC' },
});
expect(result).toEqual(mockWarehouses);
});
it('should filter by status', async () => {
jest.spyOn(warehouseRepository, 'find').mockResolvedValue([mockWarehouse] as any);
const result = await service.findByTenant('tenant-1', { status: WarehouseStatus.ACTIVE });
expect(warehouseRepository.find).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1', status: WarehouseStatus.ACTIVE },
order: { code: 'ASC' },
});
expect(result).toEqual([mockWarehouse]);
});
});
describe('update', () => {
it('should update warehouse successfully', async () => {
const dto: UpdateWarehouseDto = {
name: 'Updated Warehouse',
capacity: 12000,
};
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(mockWarehouse as any);
jest.spyOn(warehouseRepository, 'save').mockResolvedValue({
...mockWarehouse,
name: 'Updated Warehouse',
capacity: 12000,
} as any);
const result = await service.update('uuid-1', dto);
expect(warehouseRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
expect(warehouseRepository.save).toHaveBeenCalled();
expect(result.name).toBe('Updated Warehouse');
expect(result.capacity).toBe(12000);
});
it('should throw error if warehouse not found', async () => {
const dto: UpdateWarehouseDto = { name: 'Updated' };
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(null);
await expect(service.update('invalid-id', dto)).rejects.toThrow('Warehouse not found');
});
});
describe('updateOccupancy', () => {
it('should update warehouse occupancy', async () => {
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(mockWarehouse as any);
jest.spyOn(warehouseRepository, 'save').mockResolvedValue({
...mockWarehouse,
currentOccupancy: 4000,
} as any);
const result = await service.updateOccupancy('uuid-1', 4000);
expect(warehouseRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
expect(warehouseRepository.save).toHaveBeenCalled();
expect(result.currentOccupancy).toBe(4000);
});
it('should throw error if occupancy exceeds capacity', async () => {
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(mockWarehouse as any);
await expect(service.updateOccupancy('uuid-1', 15000)).rejects.toThrow('Occupancy cannot exceed warehouse capacity');
});
});
describe('getAvailableCapacity', () => {
it('should calculate available capacity', async () => {
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(mockWarehouse as any);
const result = await service.getAvailableCapacity('uuid-1');
expect(result).toBe(6500); // 10000 - 3500
});
});
describe('delete', () => {
it('should delete warehouse successfully', async () => {
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(mockWarehouse as any);
jest.spyOn(warehouseRepository, 'remove').mockResolvedValue(undefined);
await service.delete('uuid-1');
expect(warehouseRepository.remove).toHaveBeenCalledWith(mockWarehouse);
});
it('should throw error if warehouse not found', async () => {
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(null);
await expect(service.delete('invalid-id')).rejects.toThrow('Warehouse not found');
});
it('should throw error if warehouse has stock', async () => {
const warehouseWithStock = { ...mockWarehouse, currentOccupancy: 1000 };
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(warehouseWithStock as any);
await expect(service.delete('uuid-1')).rejects.toThrow('Cannot delete warehouse with existing stock');
});
});
describe('getUtilizationRate', () => {
it('should calculate utilization rate', async () => {
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(mockWarehouse as any);
const result = await service.getUtilizationRate('uuid-1');
expect(result).toBe(35); // (3500 / 10000) * 100
});
});
describe('getWarehousesByCity', () => {
it('should get warehouses by city', async () => {
jest.spyOn(warehouseRepository, 'find').mockResolvedValue([mockWarehouse] as any);
const result = await service.getWarehousesByCity('tenant-1', 'Storage City');
expect(warehouseRepository.find).toHaveBeenCalledWith({
where: {
tenantId: 'tenant-1',
'address.city': 'Storage City',
},
});
expect(result).toEqual([mockWarehouse]);
});
});
describe('updateStatus', () => {
it('should update warehouse status', async () => {
jest.spyOn(warehouseRepository, 'findOne').mockResolvedValue(mockWarehouse as any);
jest.spyOn(warehouseRepository, 'save').mockResolvedValue({
...mockWarehouse,
status: WarehouseStatus.INACTIVE,
} as any);
const result = await service.updateStatus('uuid-1', WarehouseStatus.INACTIVE);
expect(result.status).toBe(WarehouseStatus.INACTIVE);
});
});
describe('getWarehouseStats', () => {
it('should get warehouse statistics', async () => {
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue({ total: 5, active: 4, inactive: 1, totalCapacity: 50000, totalOccupancy: 17500 }),
};
jest.spyOn(warehouseRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);
const result = await service.getWarehouseStats('tenant-1');
expect(result.totalWarehouses).toBe(5);
expect(result.activeWarehouses).toBe(4);
expect(result.inactiveWarehouses).toBe(1);
expect(result.totalCapacity).toBe(50000);
expect(result.totalOccupancy).toBe(17500);
expect(result.averageUtilization).toBe(35); // (17500 / 50000) * 100
});
});
});

View File

@ -0,0 +1,393 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PartnersService } from '../partners.service';
import { Partner, PartnerStatus, PartnerType } from '../entities';
import { CreatePartnerDto, UpdatePartnerDto } from '../dto';
describe('PartnersService', () => {
let service: PartnersService;
let partnerRepository: Repository<Partner>;
const mockPartner = {
id: 'uuid-1',
tenantId: 'tenant-1',
code: 'PART-001',
name: 'Test Partner',
type: PartnerType.SUPPLIER,
status: PartnerStatus.ACTIVE,
taxId: 'TAX-001',
email: 'partner@test.com',
phone: '+1234567890',
website: 'https://partner.com',
address: {
street: '123 Partner St',
city: 'Partner City',
state: 'PC',
zipCode: '12345',
country: 'US',
},
contact: {
name: 'John Contact',
email: 'john@partner.com',
phone: '+1234567890',
position: 'Sales Manager',
},
paymentTerms: {
days: 30,
method: 'TRANSFER',
currency: 'USD',
},
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PartnersService,
{
provide: getRepositoryToken(Partner),
useValue: {
findOne: jest.fn(),
find: jest.fn(),
create: jest.fn(),
save: jest.fn(),
remove: jest.fn(),
createQueryBuilder: jest.fn(),
},
},
],
}).compile();
service = module.get<PartnersService>(PartnersService);
partnerRepository = module.get<Repository<Partner>>(getRepositoryToken(Partner));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new partner successfully', async () => {
const dto: CreatePartnerDto = {
code: 'PART-001',
name: 'Test Partner',
type: PartnerType.SUPPLIER,
taxId: 'TAX-001',
email: 'partner@test.com',
phone: '+1234567890',
website: 'https://partner.com',
address: {
street: '123 Partner St',
city: 'Partner City',
state: 'PC',
zipCode: '12345',
country: 'US',
},
contact: {
name: 'John Contact',
email: 'john@partner.com',
phone: '+1234567890',
position: 'Sales Manager',
},
paymentTerms: {
days: 30,
method: 'TRANSFER',
currency: 'USD',
},
};
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(null);
jest.spyOn(partnerRepository, 'create').mockReturnValue(mockPartner as any);
jest.spyOn(partnerRepository, 'save').mockResolvedValue(mockPartner);
const result = await service.create(dto);
expect(partnerRepository.findOne).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1', code: dto.code },
});
expect(partnerRepository.create).toHaveBeenCalled();
expect(partnerRepository.save).toHaveBeenCalled();
expect(result).toEqual(mockPartner);
});
it('should throw error if partner code already exists', async () => {
const dto: CreatePartnerDto = {
code: 'PART-001',
name: 'Test Partner',
type: PartnerType.SUPPLIER,
};
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
await expect(service.create(dto)).rejects.toThrow('Partner code already exists');
});
});
describe('findById', () => {
it('should find partner by id', async () => {
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
const result = await service.findById('uuid-1');
expect(partnerRepository.findOne).toHaveBeenCalledWith({
where: { id: 'uuid-1' },
});
expect(result).toEqual(mockPartner);
});
it('should return null if partner not found', async () => {
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(null);
const result = await service.findById('invalid-id');
expect(result).toBeNull();
});
});
describe('findByTenant', () => {
it('should find partners by tenant', async () => {
const mockPartners = [mockPartner, { ...mockPartner, id: 'uuid-2' }];
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue(mockPartners),
};
jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);
const result = await service.findByTenant('tenant-1', {
page: 1,
limit: 10,
});
expect(partnerRepository.createQueryBuilder).toHaveBeenCalledWith('partner');
expect(mockQueryBuilder.where).toHaveBeenCalledWith('partner.tenantId = :tenantId', { tenantId: 'tenant-1' });
expect(mockQueryBuilder.skip).toHaveBeenCalledWith(0);
expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
expect(result).toEqual(mockPartners);
});
it('should filter by type', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue([mockPartner]),
};
jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);
await service.findByTenant('tenant-1', { type: PartnerType.SUPPLIER });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('partner.type = :type', { type: PartnerType.SUPPLIER });
});
it('should filter by status', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue([mockPartner]),
};
jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);
await service.findByTenant('tenant-1', { status: PartnerStatus.ACTIVE });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('partner.status = :status', { status: PartnerStatus.ACTIVE });
});
it('should search by name or code', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue([mockPartner]),
};
jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);
await service.findByTenant('tenant-1', { search: 'Test' });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'(partner.code ILIKE :search OR partner.name ILIKE :search OR partner.email ILIKE :search)',
{ search: '%Test%' }
);
});
});
describe('update', () => {
it('should update partner successfully', async () => {
const dto: UpdatePartnerDto = {
name: 'Updated Partner',
status: PartnerStatus.INACTIVE,
};
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
jest.spyOn(partnerRepository, 'save').mockResolvedValue({
...mockPartner,
name: 'Updated Partner',
status: PartnerStatus.INACTIVE,
} as any);
const result = await service.update('uuid-1', dto);
expect(partnerRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
expect(partnerRepository.save).toHaveBeenCalled();
expect(result.name).toBe('Updated Partner');
expect(result.status).toBe(PartnerStatus.INACTIVE);
});
it('should throw error if partner not found', async () => {
const dto: UpdatePartnerDto = { name: 'Updated' };
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(null);
await expect(service.update('invalid-id', dto)).rejects.toThrow('Partner not found');
});
});
describe('delete', () => {
it('should delete partner successfully', async () => {
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
jest.spyOn(partnerRepository, 'remove').mockResolvedValue(undefined);
await service.delete('uuid-1');
expect(partnerRepository.remove).toHaveBeenCalledWith(mockPartner);
});
it('should throw error if partner not found', async () => {
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(null);
await expect(service.delete('invalid-id')).rejects.toThrow('Partner not found');
});
});
describe('updateStatus', () => {
it('should update partner status', async () => {
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
jest.spyOn(partnerRepository, 'save').mockResolvedValue({
...mockPartner,
status: PartnerStatus.INACTIVE,
} as any);
const result = await service.updateStatus('uuid-1', PartnerStatus.INACTIVE);
expect(result.status).toBe(PartnerStatus.INACTIVE);
});
});
describe('findByType', () => {
it('should find partners by type', async () => {
jest.spyOn(partnerRepository, 'find').mockResolvedValue([mockPartner] as any);
const result = await service.findByType('tenant-1', PartnerType.SUPPLIER);
expect(partnerRepository.find).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1', type: PartnerType.SUPPLIER },
order: { name: 'ASC' },
});
expect(result).toEqual([mockPartner]);
});
});
describe('getSuppliers', () => {
it('should get active suppliers', async () => {
jest.spyOn(partnerRepository, 'find').mockResolvedValue([mockPartner] as any);
const result = await service.getSuppliers('tenant-1');
expect(partnerRepository.find).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1', type: PartnerType.SUPPLIER, status: PartnerStatus.ACTIVE },
order: { name: 'ASC' },
});
expect(result).toEqual([mockPartner]);
});
});
describe('getCustomers', () => {
it('should get active customers', async () => {
const customerPartner = { ...mockPartner, type: PartnerType.CUSTOMER };
jest.spyOn(partnerRepository, 'find').mockResolvedValue([customerPartner] as any);
const result = await service.getCustomers('tenant-1');
expect(partnerRepository.find).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1', type: PartnerType.CUSTOMER, status: PartnerStatus.ACTIVE },
order: { name: 'ASC' },
});
expect(result).toEqual([customerPartner]);
});
});
describe('getPartnerStats', () => {
it('should get partner statistics', async () => {
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue({ total: 100, active: 80, inactive: 20 }),
};
jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);
const result = await service.getPartnerStats('tenant-1');
expect(result.totalPartners).toBe(100);
expect(result.activePartners).toBe(80);
expect(result.inactivePartners).toBe(20);
});
});
describe('searchPartners', () => {
it('should search partners by query', async () => {
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue([mockPartner]),
};
jest.spyOn(partnerRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);
const result = await service.searchPartners('tenant-1', 'Test', 10);
expect(partnerRepository.createQueryBuilder).toHaveBeenCalledWith('partner');
expect(mockQueryBuilder.where).toHaveBeenCalledWith('partner.tenantId = :tenantId', { tenantId: 'tenant-1' });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'(partner.name ILIKE :query OR partner.code ILIKE :query OR partner.email ILIKE :query)',
{ query: '%Test%' }
);
expect(mockQueryBuilder.limit).toHaveBeenCalledWith(10);
expect(result).toEqual([mockPartner]);
});
});
describe('bulkUpdate', () => {
it('should update multiple partners', async () => {
const updates = [
{ id: 'uuid-1', status: PartnerStatus.INACTIVE },
{ id: 'uuid-2', status: PartnerStatus.INACTIVE },
];
jest.spyOn(partnerRepository, 'findOne').mockResolvedValue(mockPartner as any);
jest.spyOn(partnerRepository, 'save').mockResolvedValue(mockPartner as any);
const result = await service.bulkUpdate(updates);
expect(result).toHaveLength(2);
expect(partnerRepository.save).toHaveBeenCalledTimes(2);
});
});
});