test(fase2): Add unit tests for FASE 2 modules

Add comprehensive unit tests for Business Core modules:
- Partners: 22 tests (CRUD, filters, tenant isolation)
- Products: 23 tests (CRUD, categories, search)
- Invoices: 31 tests (CRUD, payments, state transitions)
- Warehouses: 24 tests (CRUD, locations, default handling)
- HR Employees: 24 tests (CRUD, terminate/reactivate, subordinates)

Test infrastructure:
- jest.config.js with ts-jest and ESM support
- setup.ts with mocks for AppDataSource and database
- helpers.ts with factory functions for test data

Total: 114 tests passing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-18 04:18:40 -06:00
parent a127a4a424
commit 301628f759
8 changed files with 1883 additions and 0 deletions

32
jest.config.js Normal file
View File

@ -0,0 +1,32 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: [
'**/__tests__/**/*.test.ts',
'**/__tests__/**/*.spec.ts',
],
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: 'tsconfig.json',
useESM: true,
isolatedModules: true,
}],
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/index.ts',
'!src/config/**',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts'],
testTimeout: 30000,
verbose: true,
extensionsToTreatAsEsm: ['.ts'],
};

168
src/__tests__/helpers.ts Normal file
View File

@ -0,0 +1,168 @@
// Test helpers and mock factories
import { jest } from '@jest/globals';
// Mock repository factory
export function createMockRepository<T>() {
return {
find: jest.fn(),
findOne: jest.fn(),
findAndCount: jest.fn(),
create: jest.fn((data: Partial<T>) => data as T),
save: jest.fn((entity: T) => Promise.resolve(entity)),
update: jest.fn(),
delete: jest.fn(),
softDelete: jest.fn(),
createQueryBuilder: jest.fn(() => createMockQueryBuilder()),
count: jest.fn(),
merge: jest.fn((entity: T, ...sources: Partial<T>[]) => Object.assign(entity, ...sources)),
};
}
// Mock query builder
export function createMockQueryBuilder() {
const qb = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orWhere: jest.fn().mockReturnThis(),
leftJoin: jest.fn().mockReturnThis(),
leftJoinAndSelect: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
innerJoinAndSelect: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
addOrderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getOne: jest.fn(),
getMany: jest.fn(),
getManyAndCount: jest.fn(),
getCount: jest.fn(),
execute: jest.fn(),
setParameter: jest.fn().mockReturnThis(),
setParameters: jest.fn().mockReturnThis(),
};
return qb;
}
// Partner factory
export function createMockPartner(overrides = {}) {
return {
id: 'partner-uuid-1',
tenantId: global.testTenantId,
name: 'Test Partner',
email: 'partner@test.com',
phone: '+1234567890',
isCustomer: true,
isSupplier: false,
isActive: true,
creditLimit: 10000,
currentBalance: 0,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
...overrides,
};
}
// Product factory
export function createMockProduct(overrides = {}) {
return {
id: 'product-uuid-1',
tenantId: global.testTenantId,
sku: 'PROD-001',
name: 'Test Product',
description: 'Test product description',
productType: 'product',
salePrice: 100,
costPrice: 50,
currency: 'MXN',
taxRate: 16,
isActive: true,
isSellable: true,
isPurchasable: true,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
...overrides,
};
}
// Warehouse factory
export function createMockWarehouse(overrides = {}) {
return {
id: 'warehouse-uuid-1',
tenantId: global.testTenantId,
code: 'WH-001',
name: 'Test Warehouse',
address: '123 Test St',
city: 'Test City',
state: 'Test State',
country: 'MEX',
isActive: true,
isDefault: true,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
...overrides,
};
}
// Invoice factory
export function createMockInvoice(overrides = {}) {
return {
id: 'invoice-uuid-1',
tenantId: global.testTenantId,
invoiceNumber: 'INV-001',
invoiceType: 'sale',
partnerId: 'partner-uuid-1',
status: 'draft',
subtotal: 1000,
taxAmount: 160,
total: 1160,
currency: 'MXN',
invoiceDate: new Date(),
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
...overrides,
};
}
// Employee factory
export function createMockEmployee(overrides = {}) {
return {
id: 'employee-uuid-1',
tenantId: global.testTenantId,
employeeNumber: 'EMP-001',
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@test.com',
departmentId: 'dept-uuid-1',
position: 'Developer',
hireDate: new Date(),
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
...overrides,
};
}
// Account factory
export function createMockAccount(overrides = {}) {
return {
id: 'account-uuid-1',
tenantId: global.testTenantId,
code: '1000',
name: 'Cash',
accountType: 'asset',
isActive: true,
balance: 0,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
...overrides,
};
}

32
src/__tests__/setup.ts Normal file
View File

@ -0,0 +1,32 @@
// Test setup file for Jest
import { jest } from '@jest/globals';
// Mock AppDataSource
jest.mock('../config/typeorm.js', () => ({
AppDataSource: {
getRepository: jest.fn(),
isInitialized: true,
initialize: jest.fn(() => Promise.resolve()),
destroy: jest.fn(() => Promise.resolve()),
},
}));
// Global test utilities
global.testTenantId = 'test-tenant-uuid';
global.testUserId = 'test-user-uuid';
// Extend global types for tests
declare global {
var testTenantId: string;
var testUserId: string;
}
// Clean up mocks after each test
afterEach(() => {
jest.clearAllMocks();
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
});

View File

@ -0,0 +1,333 @@
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockEmployee } from '../../../__tests__/helpers.js';
// Mock query functions
const mockQuery = jest.fn();
const mockQueryOne = jest.fn();
jest.mock('../../../config/database.js', () => ({
query: (...args: any[]) => mockQuery(...args),
queryOne: (...args: any[]) => mockQueryOne(...args),
}));
// Import after mocking
import { employeesService } from '../employees.service.js';
import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js';
describe('EmployeesService', () => {
const tenantId = 'test-tenant-uuid';
const userId = 'test-user-uuid';
beforeEach(() => {
jest.clearAllMocks();
});
describe('findAll', () => {
it('should return employees with pagination', async () => {
const mockEmployees = [
createMockEmployee({ id: '1', firstName: 'John' }),
createMockEmployee({ id: '2', firstName: 'Jane' }),
];
mockQueryOne.mockResolvedValue({ count: '2' });
mockQuery.mockResolvedValue(mockEmployees);
const result = await employeesService.findAll(tenantId, { page: 1, limit: 20 });
expect(result.data).toHaveLength(2);
expect(result.total).toBe(2);
});
it('should filter by company_id', async () => {
mockQueryOne.mockResolvedValue({ count: '0' });
mockQuery.mockResolvedValue([]);
await employeesService.findAll(tenantId, { company_id: 'company-uuid' });
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('e.company_id = $'),
expect.arrayContaining([tenantId, 'company-uuid'])
);
});
it('should filter by department_id', async () => {
mockQueryOne.mockResolvedValue({ count: '0' });
mockQuery.mockResolvedValue([]);
await employeesService.findAll(tenantId, { department_id: 'dept-uuid' });
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('e.department_id = $'),
expect.arrayContaining([tenantId, 'dept-uuid'])
);
});
it('should filter by status', async () => {
mockQueryOne.mockResolvedValue({ count: '0' });
mockQuery.mockResolvedValue([]);
await employeesService.findAll(tenantId, { status: 'active' });
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('e.status = $'),
expect.arrayContaining([tenantId, 'active'])
);
});
it('should filter by search term', async () => {
mockQueryOne.mockResolvedValue({ count: '0' });
mockQuery.mockResolvedValue([]);
await employeesService.findAll(tenantId, { search: 'John' });
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('e.first_name ILIKE'),
expect.arrayContaining([tenantId, '%John%'])
);
});
it('should apply pagination correctly', async () => {
mockQueryOne.mockResolvedValue({ count: '50' });
mockQuery.mockResolvedValue([]);
await employeesService.findAll(tenantId, { page: 3, limit: 10 });
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('LIMIT'),
expect.arrayContaining([10, 20]) // limit=10, offset=20 (page 3)
);
});
});
describe('findById', () => {
it('should return employee when found', async () => {
const mockEmployee = createMockEmployee();
mockQueryOne.mockResolvedValue(mockEmployee);
const result = await employeesService.findById('employee-uuid-1', tenantId);
expect(result).toEqual(mockEmployee);
});
it('should throw NotFoundError when employee not found', async () => {
mockQueryOne.mockResolvedValue(null);
await expect(
employeesService.findById('nonexistent-id', tenantId)
).rejects.toThrow(NotFoundError);
});
});
describe('create', () => {
const createDto = {
company_id: 'company-uuid',
employee_number: 'EMP-001',
first_name: 'John',
last_name: 'Doe',
hire_date: '2024-01-15',
};
it('should create employee successfully', async () => {
// No existing employee with same number
mockQueryOne.mockResolvedValueOnce(null);
// INSERT returns new employee
mockQueryOne.mockResolvedValueOnce({ id: 'new-uuid', ...createDto });
// findById for return value
mockQueryOne.mockResolvedValueOnce(createMockEmployee({ ...createDto }));
const result = await employeesService.create(createDto, tenantId, userId);
expect(result.first_name).toBe('John');
});
it('should throw ConflictError when employee number already exists', async () => {
mockQueryOne.mockResolvedValue({ id: 'existing-uuid' });
await expect(
employeesService.create(createDto, tenantId, userId)
).rejects.toThrow(ConflictError);
});
});
describe('update', () => {
it('should update employee successfully', async () => {
const existingEmployee = createMockEmployee();
mockQueryOne.mockResolvedValue(existingEmployee);
mockQuery.mockResolvedValue([]);
const result = await employeesService.update(
'employee-uuid-1',
{ first_name: 'Updated' },
tenantId,
userId
);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('UPDATE hr.employees SET'),
expect.any(Array)
);
});
it('should throw NotFoundError when employee not found', async () => {
mockQueryOne.mockResolvedValue(null);
await expect(
employeesService.update('nonexistent-id', { first_name: 'Test' }, tenantId, userId)
).rejects.toThrow(NotFoundError);
});
it('should return unchanged employee when no fields to update', async () => {
const existingEmployee = createMockEmployee();
mockQueryOne.mockResolvedValue(existingEmployee);
const result = await employeesService.update(
'employee-uuid-1',
{},
tenantId,
userId
);
expect(result).toEqual(existingEmployee);
});
});
describe('terminate', () => {
it('should terminate active employee', async () => {
const activeEmployee = createMockEmployee({ status: 'active' });
mockQueryOne.mockResolvedValue(activeEmployee);
mockQuery.mockResolvedValue([]);
await employeesService.terminate('employee-uuid-1', '2024-12-31', tenantId, userId);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("status = 'terminated'"),
expect.any(Array)
);
});
it('should throw ValidationError when employee already terminated', async () => {
const terminatedEmployee = createMockEmployee({ status: 'terminated' });
mockQueryOne.mockResolvedValue(terminatedEmployee);
await expect(
employeesService.terminate('employee-uuid-1', '2024-12-31', tenantId, userId)
).rejects.toThrow(ValidationError);
});
it('should also terminate active contracts', async () => {
const activeEmployee = createMockEmployee({ status: 'active' });
mockQueryOne.mockResolvedValue(activeEmployee);
mockQuery.mockResolvedValue([]);
await employeesService.terminate('employee-uuid-1', '2024-12-31', tenantId, userId);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('UPDATE hr.contracts SET'),
expect.any(Array)
);
});
});
describe('reactivate', () => {
it('should reactivate terminated employee', async () => {
const terminatedEmployee = createMockEmployee({ status: 'terminated' });
mockQueryOne.mockResolvedValue(terminatedEmployee);
mockQuery.mockResolvedValue([]);
await employeesService.reactivate('employee-uuid-1', tenantId, userId);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("status = 'active'"),
expect.any(Array)
);
});
it('should reactivate inactive employee', async () => {
const inactiveEmployee = createMockEmployee({ status: 'inactive' });
mockQueryOne.mockResolvedValue(inactiveEmployee);
mockQuery.mockResolvedValue([]);
await employeesService.reactivate('employee-uuid-1', tenantId, userId);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("status = 'active'"),
expect.any(Array)
);
});
it('should throw ValidationError when employee is already active', async () => {
const activeEmployee = createMockEmployee({ status: 'active' });
mockQueryOne.mockResolvedValue(activeEmployee);
await expect(
employeesService.reactivate('employee-uuid-1', tenantId, userId)
).rejects.toThrow(ValidationError);
});
});
describe('delete', () => {
it('should delete employee without contracts or subordinates', async () => {
const employee = createMockEmployee();
mockQueryOne
.mockResolvedValueOnce(employee) // findById
.mockResolvedValueOnce({ count: '0' }) // hasContracts
.mockResolvedValueOnce({ count: '0' }); // isManager
mockQuery.mockResolvedValue([]);
await employeesService.delete('employee-uuid-1', tenantId);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('DELETE FROM hr.employees'),
expect.any(Array)
);
});
it('should throw ConflictError when employee has contracts', async () => {
const employee = createMockEmployee();
mockQueryOne
.mockResolvedValueOnce(employee)
.mockResolvedValueOnce({ count: '5' }); // hasContracts
await expect(
employeesService.delete('employee-uuid-1', tenantId)
).rejects.toThrow(ConflictError);
});
it('should throw ConflictError when employee is a manager', async () => {
const employee = createMockEmployee();
mockQueryOne
.mockResolvedValueOnce(employee)
.mockResolvedValueOnce({ count: '0' }) // hasContracts
.mockResolvedValueOnce({ count: '3' }); // isManager
await expect(
employeesService.delete('employee-uuid-1', tenantId)
).rejects.toThrow(ConflictError);
});
});
describe('getSubordinates', () => {
it('should return subordinates of a manager', async () => {
const manager = createMockEmployee();
const subordinates = [
createMockEmployee({ id: 'sub-1', firstName: 'Sub 1' }),
createMockEmployee({ id: 'sub-2', firstName: 'Sub 2' }),
];
mockQueryOne.mockResolvedValue(manager);
mockQuery.mockResolvedValue(subordinates);
const result = await employeesService.getSubordinates('manager-uuid', tenantId);
expect(result).toHaveLength(2);
});
it('should throw NotFoundError when manager not found', async () => {
mockQueryOne.mockResolvedValue(null);
await expect(
employeesService.getSubordinates('nonexistent-id', tenantId)
).rejects.toThrow(NotFoundError);
});
});
});

View File

@ -0,0 +1,346 @@
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockRepository, createMockInvoice } from '../../../__tests__/helpers.js';
// Mock dependencies before importing service
const mockInvoiceRepository = createMockRepository();
const mockPaymentRepository = createMockRepository();
jest.mock('../../../config/typeorm.js', () => ({
AppDataSource: {
getRepository: jest.fn((entity: any) => {
if (entity.name === 'Invoice') return mockInvoiceRepository;
if (entity.name === 'Payment') return mockPaymentRepository;
return mockInvoiceRepository;
}),
},
}));
// Import after mocking
import { invoicesService } from '../services/index.js';
describe('InvoicesService', () => {
const tenantId = 'test-tenant-uuid';
const userId = 'test-user-uuid';
beforeEach(() => {
jest.clearAllMocks();
});
describe('findAllInvoices', () => {
it('should return invoices with pagination', async () => {
const mockInvoices = [
createMockInvoice({ id: '1', invoiceNumber: 'FAC-000001' }),
createMockInvoice({ id: '2', invoiceNumber: 'FAC-000002' }),
];
mockInvoiceRepository.findAndCount.mockResolvedValue([mockInvoices, 2]);
const result = await invoicesService.findAllInvoices({ tenantId, limit: 50, offset: 0 });
expect(result.data).toHaveLength(2);
expect(result.total).toBe(2);
});
it('should filter by invoice type', async () => {
mockInvoiceRepository.findAndCount.mockResolvedValue([[], 0]);
await invoicesService.findAllInvoices({ tenantId, invoiceType: 'sale' });
expect(mockInvoiceRepository.findAndCount).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
invoiceType: 'sale',
}),
})
);
});
it('should filter by partner', async () => {
mockInvoiceRepository.findAndCount.mockResolvedValue([[], 0]);
await invoicesService.findAllInvoices({ tenantId, partnerId: 'partner-uuid' });
expect(mockInvoiceRepository.findAndCount).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
partnerId: 'partner-uuid',
}),
})
);
});
it('should filter by status', async () => {
mockInvoiceRepository.findAndCount.mockResolvedValue([[], 0]);
await invoicesService.findAllInvoices({ tenantId, status: 'validated' });
expect(mockInvoiceRepository.findAndCount).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
status: 'validated',
}),
})
);
});
});
describe('findInvoice', () => {
it('should return invoice when found', async () => {
const mockInvoice = createMockInvoice();
mockInvoiceRepository.findOne.mockResolvedValue(mockInvoice);
const result = await invoicesService.findInvoice('invoice-uuid-1', tenantId);
expect(result).toEqual(mockInvoice);
});
it('should return null when invoice not found', async () => {
mockInvoiceRepository.findOne.mockResolvedValue(null);
const result = await invoicesService.findInvoice('nonexistent-id', tenantId);
expect(result).toBeNull();
});
});
describe('createInvoice', () => {
const createDto = {
invoiceType: 'sale' as const,
partnerId: 'partner-uuid',
subtotal: 1000,
taxAmount: 160,
totalAmount: 1160,
};
it('should create invoice with auto-generated number', async () => {
mockInvoiceRepository.count.mockResolvedValue(5);
const savedInvoice = createMockInvoice({ ...createDto, invoiceNumber: 'FAC-000006' });
mockInvoiceRepository.create.mockReturnValue(savedInvoice);
mockInvoiceRepository.save.mockResolvedValue(savedInvoice);
const result = await invoicesService.createInvoice(tenantId, createDto, userId);
expect(result.invoiceNumber).toBe('FAC-000006');
expect(mockInvoiceRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
invoiceNumber: 'FAC-000006',
status: 'draft',
})
);
});
it('should use correct prefix for purchase invoices', async () => {
mockInvoiceRepository.count.mockResolvedValue(3);
const purchaseDto = { ...createDto, invoiceType: 'purchase' as const };
mockInvoiceRepository.create.mockReturnValue(createMockInvoice({ invoiceNumber: 'FP-000004' }));
mockInvoiceRepository.save.mockResolvedValue(createMockInvoice({ invoiceNumber: 'FP-000004' }));
await invoicesService.createInvoice(tenantId, purchaseDto, userId);
expect(mockInvoiceRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
invoiceNumber: 'FP-000004',
})
);
});
it('should use correct prefix for credit notes', async () => {
mockInvoiceRepository.count.mockResolvedValue(1);
const creditDto = { ...createDto, invoiceType: 'credit_note' as const };
mockInvoiceRepository.create.mockReturnValue(createMockInvoice({ invoiceNumber: 'NC-000002' }));
mockInvoiceRepository.save.mockResolvedValue(createMockInvoice({ invoiceNumber: 'NC-000002' }));
await invoicesService.createInvoice(tenantId, creditDto, userId);
expect(mockInvoiceRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
invoiceNumber: 'NC-000002',
})
);
});
it('should set createdBy field', async () => {
mockInvoiceRepository.count.mockResolvedValue(0);
mockInvoiceRepository.create.mockReturnValue(createMockInvoice());
mockInvoiceRepository.save.mockResolvedValue(createMockInvoice());
await invoicesService.createInvoice(tenantId, createDto, userId);
expect(mockInvoiceRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
createdBy: userId,
})
);
});
});
describe('updateInvoice', () => {
it('should update invoice successfully', async () => {
const existingInvoice = createMockInvoice();
mockInvoiceRepository.findOne.mockResolvedValue(existingInvoice);
mockInvoiceRepository.save.mockResolvedValue({ ...existingInvoice, notes: 'Updated notes' });
const result = await invoicesService.updateInvoice(
'invoice-uuid-1',
tenantId,
{ notes: 'Updated notes' },
userId
);
expect(result?.notes).toBe('Updated notes');
});
it('should return null when invoice not found', async () => {
mockInvoiceRepository.findOne.mockResolvedValue(null);
const result = await invoicesService.updateInvoice(
'nonexistent-id',
tenantId,
{ notes: 'Test' },
userId
);
expect(result).toBeNull();
});
});
describe('deleteInvoice', () => {
it('should soft delete invoice', async () => {
mockInvoiceRepository.softDelete.mockResolvedValue({ affected: 1 });
const result = await invoicesService.deleteInvoice('invoice-uuid-1', tenantId);
expect(result).toBe(true);
});
it('should return false when invoice not found', async () => {
mockInvoiceRepository.softDelete.mockResolvedValue({ affected: 0 });
const result = await invoicesService.deleteInvoice('nonexistent-id', tenantId);
expect(result).toBe(false);
});
});
describe('validateInvoice', () => {
it('should validate draft invoice', async () => {
const draftInvoice = createMockInvoice({ status: 'draft' });
mockInvoiceRepository.findOne.mockResolvedValue(draftInvoice);
mockInvoiceRepository.save.mockResolvedValue({ ...draftInvoice, status: 'validated' });
const result = await invoicesService.validateInvoice('invoice-uuid-1', tenantId, userId);
expect(result?.status).toBe('validated');
});
it('should return null when invoice not in draft status', async () => {
const validatedInvoice = createMockInvoice({ status: 'validated' });
mockInvoiceRepository.findOne.mockResolvedValue(validatedInvoice);
const result = await invoicesService.validateInvoice('invoice-uuid-1', tenantId, userId);
expect(result).toBeNull();
});
it('should return null when invoice not found', async () => {
mockInvoiceRepository.findOne.mockResolvedValue(null);
const result = await invoicesService.validateInvoice('nonexistent-id', tenantId, userId);
expect(result).toBeNull();
});
});
describe('cancelInvoice', () => {
it('should cancel invoice', async () => {
const invoice = createMockInvoice({ status: 'validated' });
mockInvoiceRepository.findOne.mockResolvedValue(invoice);
mockInvoiceRepository.save.mockResolvedValue({ ...invoice, status: 'cancelled' });
const result = await invoicesService.cancelInvoice('invoice-uuid-1', tenantId, userId);
expect(result?.status).toBe('cancelled');
});
it('should return null when invoice already cancelled', async () => {
const cancelledInvoice = createMockInvoice({ status: 'cancelled' });
mockInvoiceRepository.findOne.mockResolvedValue(cancelledInvoice);
const result = await invoicesService.cancelInvoice('invoice-uuid-1', tenantId, userId);
expect(result).toBeNull();
});
});
describe('Payment operations', () => {
const createPaymentDto = {
partnerId: 'partner-uuid',
paymentType: 'received' as const,
paymentMethod: 'transfer',
amount: 1000,
};
it('should find all payments', async () => {
const mockPayments = [{ id: '1', amount: 1000 }];
mockPaymentRepository.findAndCount.mockResolvedValue([mockPayments, 1]);
const result = await invoicesService.findAllPayments(tenantId);
expect(result.data).toHaveLength(1);
});
it('should find all payments filtered by partner', async () => {
mockPaymentRepository.findAndCount.mockResolvedValue([[], 0]);
await invoicesService.findAllPayments(tenantId, 'partner-uuid');
expect(mockPaymentRepository.findAndCount).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
partnerId: 'partner-uuid',
}),
})
);
});
it('should create payment with auto-generated number', async () => {
mockPaymentRepository.count.mockResolvedValue(10);
const savedPayment = { id: '1', paymentNumber: 'PAG-000011', ...createPaymentDto };
mockPaymentRepository.create.mockReturnValue(savedPayment);
mockPaymentRepository.save.mockResolvedValue(savedPayment);
const result = await invoicesService.createPayment(tenantId, createPaymentDto, userId);
expect(result.paymentNumber).toBe('PAG-000011');
});
it('should confirm payment', async () => {
const draftPayment = { id: '1', status: 'draft' };
mockPaymentRepository.findOne.mockResolvedValue(draftPayment);
mockPaymentRepository.save.mockResolvedValue({ ...draftPayment, status: 'confirmed' });
const result = await invoicesService.confirmPayment('payment-uuid', tenantId, userId);
expect(result?.status).toBe('confirmed');
});
it('should not confirm already confirmed payment', async () => {
const confirmedPayment = { id: '1', status: 'confirmed' };
mockPaymentRepository.findOne.mockResolvedValue(confirmedPayment);
const result = await invoicesService.confirmPayment('payment-uuid', tenantId, userId);
expect(result).toBeNull();
});
it('should cancel payment', async () => {
const payment = { id: '1', status: 'confirmed' };
mockPaymentRepository.findOne.mockResolvedValue(payment);
mockPaymentRepository.save.mockResolvedValue({ ...payment, status: 'cancelled' });
const result = await invoicesService.cancelPayment('payment-uuid', tenantId, userId);
expect(result?.status).toBe('cancelled');
});
});
});

View File

@ -0,0 +1,325 @@
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockRepository, createMockQueryBuilder, createMockPartner } from '../../../__tests__/helpers.js';
// Mock dependencies before importing service
const mockRepository = createMockRepository();
const mockQueryBuilder = createMockQueryBuilder();
jest.mock('../../../config/typeorm.js', () => ({
AppDataSource: {
getRepository: jest.fn(() => mockRepository),
},
}));
jest.mock('../../../shared/utils/logger.js', () => ({
logger: {
info: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
},
}));
// Import after mocking
import { partnersService } from '../partners.service.js';
import { NotFoundError, ValidationError } from '../../../shared/types/index.js';
describe('PartnersService', () => {
const tenantId = 'test-tenant-uuid';
const userId = 'test-user-uuid';
beforeEach(() => {
jest.clearAllMocks();
mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
});
describe('findAll', () => {
it('should return partners with pagination', async () => {
const mockPartners = [
createMockPartner({ id: '1', displayName: 'Partner A' }),
createMockPartner({ id: '2', displayName: 'Partner B' }),
];
mockQueryBuilder.getCount.mockResolvedValue(2);
mockQueryBuilder.getMany.mockResolvedValue(mockPartners);
const result = await partnersService.findAll(tenantId, { page: 1, limit: 20 });
expect(result.data).toHaveLength(2);
expect(result.total).toBe(2);
expect(mockQueryBuilder.where).toHaveBeenCalledWith(
'partner.tenantId = :tenantId',
{ tenantId }
);
});
it('should filter by search term', async () => {
mockQueryBuilder.getCount.mockResolvedValue(1);
mockQueryBuilder.getMany.mockResolvedValue([createMockPartner()]);
await partnersService.findAll(tenantId, { search: 'test' });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
expect.stringContaining('partner.displayName ILIKE :search'),
{ search: '%test%' }
);
});
it('should filter by partner type', async () => {
mockQueryBuilder.getCount.mockResolvedValue(1);
mockQueryBuilder.getMany.mockResolvedValue([createMockPartner()]);
await partnersService.findAll(tenantId, { partnerType: 'customer' });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'partner.partnerType = :partnerType',
{ partnerType: 'customer' }
);
});
it('should filter by active status', async () => {
mockQueryBuilder.getCount.mockResolvedValue(1);
mockQueryBuilder.getMany.mockResolvedValue([createMockPartner()]);
await partnersService.findAll(tenantId, { isActive: true });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'partner.isActive = :isActive',
{ isActive: true }
);
});
it('should apply pagination correctly', async () => {
mockQueryBuilder.getCount.mockResolvedValue(50);
mockQueryBuilder.getMany.mockResolvedValue([]);
await partnersService.findAll(tenantId, { page: 3, limit: 10 });
expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20);
expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
});
});
describe('findById', () => {
it('should return partner when found', async () => {
const mockPartner = createMockPartner();
mockRepository.findOne.mockResolvedValue(mockPartner);
const result = await partnersService.findById('partner-uuid-1', tenantId);
expect(result).toEqual(mockPartner);
expect(mockRepository.findOne).toHaveBeenCalledWith({
where: {
id: 'partner-uuid-1',
tenantId,
deletedAt: expect.anything(),
},
});
});
it('should throw NotFoundError when partner not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(
partnersService.findById('nonexistent-id', tenantId)
).rejects.toThrow(NotFoundError);
});
it('should enforce tenant isolation', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(
partnersService.findById('partner-uuid-1', 'different-tenant')
).rejects.toThrow(NotFoundError);
});
});
describe('create', () => {
const createDto = {
code: 'PART-001',
displayName: 'New Partner',
email: 'new@partner.com',
partnerType: 'customer' as const,
};
it('should create partner successfully', async () => {
mockRepository.findOne.mockResolvedValue(null); // No existing partner
const savedPartner = createMockPartner({ ...createDto });
mockRepository.create.mockReturnValue(savedPartner);
mockRepository.save.mockResolvedValue(savedPartner);
const result = await partnersService.create(createDto, tenantId, userId);
expect(result).toEqual(savedPartner);
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
tenantId,
code: createDto.code,
displayName: createDto.displayName,
createdBy: userId,
})
);
});
it('should throw ValidationError when code already exists', async () => {
mockRepository.findOne.mockResolvedValue(createMockPartner());
await expect(
partnersService.create(createDto, tenantId, userId)
).rejects.toThrow(ValidationError);
});
it('should normalize email to lowercase', async () => {
mockRepository.findOne.mockResolvedValue(null);
const savedPartner = createMockPartner();
mockRepository.create.mockReturnValue(savedPartner);
mockRepository.save.mockResolvedValue(savedPartner);
await partnersService.create(
{ ...createDto, email: 'TEST@PARTNER.COM' },
tenantId,
userId
);
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
email: 'test@partner.com',
})
);
});
it('should set default values correctly', async () => {
mockRepository.findOne.mockResolvedValue(null);
const savedPartner = createMockPartner();
mockRepository.create.mockReturnValue(savedPartner);
mockRepository.save.mockResolvedValue(savedPartner);
await partnersService.create(
{ code: 'PART-001', displayName: 'Partner' },
tenantId,
userId
);
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
partnerType: 'customer',
paymentTermDays: 0,
creditLimit: 0,
discountPercent: 0,
isActive: true,
isVerified: false,
})
);
});
});
describe('update', () => {
it('should update partner successfully', async () => {
const existingPartner = createMockPartner();
mockRepository.findOne.mockResolvedValue(existingPartner);
mockRepository.save.mockResolvedValue({ ...existingPartner, displayName: 'Updated Name' });
const result = await partnersService.update(
'partner-uuid-1',
{ displayName: 'Updated Name' },
tenantId,
userId
);
expect(mockRepository.save).toHaveBeenCalled();
expect(result.displayName).toBe('Updated Name');
});
it('should throw NotFoundError when partner not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(
partnersService.update('nonexistent-id', { displayName: 'Test' }, tenantId, userId)
).rejects.toThrow(NotFoundError);
});
it('should update credit limit', async () => {
const existingPartner = createMockPartner({ creditLimit: 1000 });
mockRepository.findOne.mockResolvedValue(existingPartner);
mockRepository.save.mockResolvedValue({ ...existingPartner, creditLimit: 5000 });
await partnersService.update(
'partner-uuid-1',
{ creditLimit: 5000 },
tenantId,
userId
);
expect(existingPartner.creditLimit).toBe(5000);
});
it('should set updatedBy field', async () => {
const existingPartner = createMockPartner();
mockRepository.findOne.mockResolvedValue(existingPartner);
mockRepository.save.mockResolvedValue(existingPartner);
await partnersService.update(
'partner-uuid-1',
{ displayName: 'Updated' },
tenantId,
userId
);
expect(existingPartner.updatedBy).toBe(userId);
});
});
describe('delete', () => {
it('should soft delete partner', async () => {
const existingPartner = createMockPartner();
mockRepository.findOne.mockResolvedValue(existingPartner);
mockRepository.save.mockResolvedValue(existingPartner);
await partnersService.delete('partner-uuid-1', tenantId, userId);
expect(existingPartner.deletedAt).toBeInstanceOf(Date);
expect(existingPartner.isActive).toBe(false);
expect(mockRepository.save).toHaveBeenCalled();
});
it('should throw NotFoundError when partner not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(
partnersService.delete('nonexistent-id', tenantId, userId)
).rejects.toThrow(NotFoundError);
});
});
describe('findCustomers', () => {
it('should return only customers', async () => {
const mockCustomers = [createMockPartner({ partnerType: 'customer' })];
mockQueryBuilder.getCount.mockResolvedValue(1);
mockQueryBuilder.getMany.mockResolvedValue(mockCustomers);
const result = await partnersService.findCustomers(tenantId, {});
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'partner.partnerType = :partnerType',
{ partnerType: 'customer' }
);
expect(result.data).toHaveLength(1);
});
});
describe('findSuppliers', () => {
it('should return only suppliers', async () => {
const mockSuppliers = [createMockPartner({ partnerType: 'supplier' })];
mockQueryBuilder.getCount.mockResolvedValue(1);
mockQueryBuilder.getMany.mockResolvedValue(mockSuppliers);
const result = await partnersService.findSuppliers(tenantId, {});
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'partner.partnerType = :partnerType',
{ partnerType: 'supplier' }
);
expect(result.data).toHaveLength(1);
});
});
});

View File

@ -0,0 +1,302 @@
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockRepository, createMockProduct } from '../../../__tests__/helpers.js';
// Mock dependencies before importing service
const mockProductRepository = createMockRepository();
const mockCategoryRepository = createMockRepository();
jest.mock('../../../config/typeorm.js', () => ({
AppDataSource: {
getRepository: jest.fn((entity: any) => {
if (entity.name === 'Product') return mockProductRepository;
if (entity.name === 'ProductCategory') return mockCategoryRepository;
return mockProductRepository;
}),
},
}));
// Import after mocking
import { productsService } from '../products.service.js';
describe('ProductsService', () => {
const tenantId = 'test-tenant-uuid';
beforeEach(() => {
jest.clearAllMocks();
});
describe('findAll', () => {
it('should return products with pagination', async () => {
const mockProducts = [
createMockProduct({ id: '1', name: 'Product A' }),
createMockProduct({ id: '2', name: 'Product B' }),
];
mockProductRepository.findAndCount.mockResolvedValue([mockProducts, 2]);
const result = await productsService.findAll({ tenantId, limit: 50, offset: 0 });
expect(result.data).toHaveLength(2);
expect(result.total).toBe(2);
});
it('should filter by search term', async () => {
const mockProducts = [createMockProduct({ name: 'Test Product' })];
mockProductRepository.findAndCount.mockResolvedValue([mockProducts, 1]);
const result = await productsService.findAll({ tenantId, search: 'Test' });
expect(result.data).toHaveLength(1);
expect(mockProductRepository.findAndCount).toHaveBeenCalled();
});
it('should filter by category', async () => {
mockProductRepository.findAndCount.mockResolvedValue([[], 0]);
await productsService.findAll({ tenantId, categoryId: 'cat-uuid' });
expect(mockProductRepository.findAndCount).toHaveBeenCalled();
});
it('should filter by product type', async () => {
mockProductRepository.findAndCount.mockResolvedValue([[], 0]);
await productsService.findAll({ tenantId, productType: 'service' });
expect(mockProductRepository.findAndCount).toHaveBeenCalled();
});
it('should filter by sellable status', async () => {
mockProductRepository.findAndCount.mockResolvedValue([[], 0]);
await productsService.findAll({ tenantId, isSellable: true });
expect(mockProductRepository.findAndCount).toHaveBeenCalled();
});
it('should filter by purchasable status', async () => {
mockProductRepository.findAndCount.mockResolvedValue([[], 0]);
await productsService.findAll({ tenantId, isPurchasable: true });
expect(mockProductRepository.findAndCount).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should return product when found', async () => {
const mockProduct = createMockProduct();
mockProductRepository.findOne.mockResolvedValue(mockProduct);
const result = await productsService.findOne('product-uuid-1', tenantId);
expect(result).toEqual(mockProduct);
expect(mockProductRepository.findOne).toHaveBeenCalledWith({
where: {
id: 'product-uuid-1',
tenantId,
deletedAt: expect.anything(),
},
relations: ['category'],
});
});
it('should return null when product not found', async () => {
mockProductRepository.findOne.mockResolvedValue(null);
const result = await productsService.findOne('nonexistent-id', tenantId);
expect(result).toBeNull();
});
});
describe('findBySku', () => {
it('should return product by SKU', async () => {
const mockProduct = createMockProduct({ sku: 'PROD-001' });
mockProductRepository.findOne.mockResolvedValue(mockProduct);
const result = await productsService.findBySku('PROD-001', tenantId);
expect(result).toEqual(mockProduct);
expect(mockProductRepository.findOne).toHaveBeenCalledWith({
where: {
sku: 'PROD-001',
tenantId,
deletedAt: expect.anything(),
},
relations: ['category'],
});
});
});
describe('findByBarcode', () => {
it('should return product by barcode', async () => {
const mockProduct = createMockProduct({ barcode: '1234567890123' });
mockProductRepository.findOne.mockResolvedValue(mockProduct);
const result = await productsService.findByBarcode('1234567890123', tenantId);
expect(result).toEqual(mockProduct);
});
});
describe('create', () => {
const createDto = {
sku: 'PROD-001',
name: 'New Product',
salePrice: 100,
costPrice: 50,
};
it('should create product successfully', async () => {
const savedProduct = createMockProduct({ ...createDto });
mockProductRepository.create.mockReturnValue(savedProduct);
mockProductRepository.save.mockResolvedValue(savedProduct);
const result = await productsService.create(tenantId, createDto, 'user-uuid');
expect(result).toEqual(savedProduct);
expect(mockProductRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
tenantId,
sku: createDto.sku,
name: createDto.name,
})
);
});
it('should set createdBy field', async () => {
const savedProduct = createMockProduct();
mockProductRepository.create.mockReturnValue(savedProduct);
mockProductRepository.save.mockResolvedValue(savedProduct);
await productsService.create(tenantId, createDto, 'user-uuid');
expect(mockProductRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
createdBy: 'user-uuid',
})
);
});
});
describe('update', () => {
it('should update product successfully', async () => {
const existingProduct = createMockProduct();
const updatedProduct = { ...existingProduct, name: 'Updated Name' };
mockProductRepository.findOne.mockResolvedValue(existingProduct);
mockProductRepository.save.mockResolvedValue(updatedProduct);
const result = await productsService.update(
'product-uuid-1',
tenantId,
{ name: 'Updated Name' },
'user-uuid'
);
expect(result?.name).toBe('Updated Name');
});
it('should return null when product not found', async () => {
mockProductRepository.findOne.mockResolvedValue(null);
const result = await productsService.update(
'nonexistent-id',
tenantId,
{ name: 'Test' },
'user-uuid'
);
expect(result).toBeNull();
});
it('should update prices', async () => {
const existingProduct = createMockProduct({ salePrice: 100 });
mockProductRepository.findOne.mockResolvedValue(existingProduct);
mockProductRepository.save.mockResolvedValue({ ...existingProduct, salePrice: 150 });
await productsService.update(
'product-uuid-1',
tenantId,
{ salePrice: 150 },
'user-uuid'
);
expect(mockProductRepository.save).toHaveBeenCalled();
});
});
describe('delete', () => {
it('should soft delete product', async () => {
mockProductRepository.softDelete.mockResolvedValue({ affected: 1 });
const result = await productsService.delete('product-uuid-1', tenantId);
expect(result).toBe(true);
expect(mockProductRepository.softDelete).toHaveBeenCalledWith({
id: 'product-uuid-1',
tenantId,
});
});
it('should return false when product not found', async () => {
mockProductRepository.softDelete.mockResolvedValue({ affected: 0 });
const result = await productsService.delete('nonexistent-id', tenantId);
expect(result).toBe(false);
});
});
describe('getSellableProducts', () => {
it('should return only sellable products', async () => {
const mockProducts = [createMockProduct({ isSellable: true })];
mockProductRepository.findAndCount.mockResolvedValue([mockProducts, 1]);
const result = await productsService.getSellableProducts(tenantId);
expect(result.data).toHaveLength(1);
});
});
describe('getPurchasableProducts', () => {
it('should return only purchasable products', async () => {
const mockProducts = [createMockProduct({ isPurchasable: true })];
mockProductRepository.findAndCount.mockResolvedValue([mockProducts, 1]);
const result = await productsService.getPurchasableProducts(tenantId);
expect(result.data).toHaveLength(1);
});
});
describe('Category operations', () => {
it('should find all categories', async () => {
const mockCategories = [{ id: '1', name: 'Category A' }];
mockCategoryRepository.findAndCount.mockResolvedValue([mockCategories, 1]);
const result = await productsService.findAllCategories({ tenantId });
expect(result.data).toHaveLength(1);
});
it('should create category', async () => {
const categoryDto = { code: 'CAT-001', name: 'New Category' };
const savedCategory = { id: '1', ...categoryDto };
mockCategoryRepository.create.mockReturnValue(savedCategory);
mockCategoryRepository.save.mockResolvedValue(savedCategory);
const result = await productsService.createCategory(tenantId, categoryDto);
expect(result).toEqual(savedCategory);
});
it('should delete category', async () => {
mockCategoryRepository.softDelete.mockResolvedValue({ affected: 1 });
const result = await productsService.deleteCategory('cat-uuid', tenantId);
expect(result).toBe(true);
});
});
});

View File

@ -0,0 +1,345 @@
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockRepository, createMockQueryBuilder, createMockWarehouse } from '../../../__tests__/helpers.js';
// Mock dependencies before importing service
const mockWarehouseRepository = createMockRepository();
const mockLocationRepository = createMockRepository();
const mockQueryBuilder = createMockQueryBuilder();
jest.mock('../../../config/typeorm.js', () => ({
AppDataSource: {
getRepository: jest.fn((entity: any) => {
if (entity.name === 'Warehouse') return mockWarehouseRepository;
if (entity.name === 'WarehouseLocation') return mockLocationRepository;
return mockWarehouseRepository;
}),
},
}));
// Import after mocking
import { warehousesService } from '../warehouses.service.js';
describe('WarehousesService', () => {
const tenantId = 'test-tenant-uuid';
const userId = 'test-user-uuid';
beforeEach(() => {
jest.clearAllMocks();
mockLocationRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
});
describe('findAll', () => {
it('should return warehouses with pagination', async () => {
const mockWarehouses = [
createMockWarehouse({ id: '1', name: 'Warehouse A' }),
createMockWarehouse({ id: '2', name: 'Warehouse B' }),
];
mockWarehouseRepository.findAndCount.mockResolvedValue([mockWarehouses, 2]);
const result = await warehousesService.findAll({ tenantId, limit: 50, offset: 0 });
expect(result.data).toHaveLength(2);
expect(result.total).toBe(2);
});
it('should filter by search term', async () => {
const mockWarehouses = [createMockWarehouse({ name: 'Main Warehouse' })];
mockWarehouseRepository.findAndCount.mockResolvedValue([mockWarehouses, 1]);
const result = await warehousesService.findAll({ tenantId, search: 'Main' });
expect(result.data).toHaveLength(1);
});
it('should filter by active status', async () => {
mockWarehouseRepository.findAndCount.mockResolvedValue([[], 0]);
await warehousesService.findAll({ tenantId, isActive: true });
expect(mockWarehouseRepository.findAndCount).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should return warehouse when found', async () => {
const mockWarehouse = createMockWarehouse();
mockWarehouseRepository.findOne.mockResolvedValue(mockWarehouse);
const result = await warehousesService.findOne('warehouse-uuid-1', tenantId);
expect(result).toEqual(mockWarehouse);
expect(mockWarehouseRepository.findOne).toHaveBeenCalledWith({
where: {
id: 'warehouse-uuid-1',
tenantId,
deletedAt: expect.anything(),
},
});
});
it('should return null when warehouse not found', async () => {
mockWarehouseRepository.findOne.mockResolvedValue(null);
const result = await warehousesService.findOne('nonexistent-id', tenantId);
expect(result).toBeNull();
});
});
describe('findByCode', () => {
it('should return warehouse by code', async () => {
const mockWarehouse = createMockWarehouse({ code: 'WH-001' });
mockWarehouseRepository.findOne.mockResolvedValue(mockWarehouse);
const result = await warehousesService.findByCode('WH-001', tenantId);
expect(result).toEqual(mockWarehouse);
expect(mockWarehouseRepository.findOne).toHaveBeenCalledWith({
where: {
code: 'WH-001',
tenantId,
deletedAt: expect.anything(),
},
});
});
});
describe('getDefault', () => {
it('should return default warehouse', async () => {
const mockWarehouse = createMockWarehouse({ isDefault: true });
mockWarehouseRepository.findOne.mockResolvedValue(mockWarehouse);
const result = await warehousesService.getDefault(tenantId);
expect(result?.isDefault).toBe(true);
});
it('should return null when no default warehouse exists', async () => {
mockWarehouseRepository.findOne.mockResolvedValue(null);
const result = await warehousesService.getDefault(tenantId);
expect(result).toBeNull();
});
});
describe('getActive', () => {
it('should return only active warehouses', async () => {
const mockWarehouses = [
createMockWarehouse({ isActive: true }),
createMockWarehouse({ isActive: true }),
];
mockWarehouseRepository.find.mockResolvedValue(mockWarehouses);
const result = await warehousesService.getActive(tenantId);
expect(result).toHaveLength(2);
});
});
describe('create', () => {
const createDto = {
code: 'WH-001',
name: 'New Warehouse',
address: '123 Main St',
city: 'Test City',
};
it('should create warehouse successfully', async () => {
const savedWarehouse = createMockWarehouse({ ...createDto });
mockWarehouseRepository.create.mockReturnValue(savedWarehouse);
mockWarehouseRepository.save.mockResolvedValue(savedWarehouse);
const result = await warehousesService.create(tenantId, createDto, userId);
expect(result).toEqual(savedWarehouse);
expect(mockWarehouseRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
tenantId,
code: createDto.code,
name: createDto.name,
})
);
});
it('should unset other defaults when creating default warehouse', async () => {
const createDtoWithDefault = { ...createDto, isDefault: true };
mockWarehouseRepository.update.mockResolvedValue({ affected: 1 });
mockWarehouseRepository.create.mockReturnValue(createMockWarehouse(createDtoWithDefault));
mockWarehouseRepository.save.mockResolvedValue(createMockWarehouse(createDtoWithDefault));
await warehousesService.create(tenantId, createDtoWithDefault, userId);
expect(mockWarehouseRepository.update).toHaveBeenCalledWith(
{ tenantId, isDefault: true },
{ isDefault: false }
);
});
});
describe('update', () => {
it('should update warehouse successfully', async () => {
const existingWarehouse = createMockWarehouse();
mockWarehouseRepository.findOne.mockResolvedValue(existingWarehouse);
mockWarehouseRepository.save.mockResolvedValue({ ...existingWarehouse, name: 'Updated Name' });
const result = await warehousesService.update(
'warehouse-uuid-1',
tenantId,
{ name: 'Updated Name' },
userId
);
expect(result?.name).toBe('Updated Name');
});
it('should return null when warehouse not found', async () => {
mockWarehouseRepository.findOne.mockResolvedValue(null);
const result = await warehousesService.update(
'nonexistent-id',
tenantId,
{ name: 'Test' },
userId
);
expect(result).toBeNull();
});
it('should unset other defaults when setting as default', async () => {
const existingWarehouse = createMockWarehouse({ isDefault: false });
mockWarehouseRepository.findOne.mockResolvedValue(existingWarehouse);
mockWarehouseRepository.update.mockResolvedValue({ affected: 1 });
mockWarehouseRepository.save.mockResolvedValue({ ...existingWarehouse, isDefault: true });
await warehousesService.update(
'warehouse-uuid-1',
tenantId,
{ isDefault: true },
userId
);
expect(mockWarehouseRepository.update).toHaveBeenCalledWith(
{ tenantId, isDefault: true },
{ isDefault: false }
);
});
});
describe('delete', () => {
it('should soft delete warehouse', async () => {
mockWarehouseRepository.softDelete.mockResolvedValue({ affected: 1 });
const result = await warehousesService.delete('warehouse-uuid-1', tenantId);
expect(result).toBe(true);
});
it('should return false when warehouse not found', async () => {
mockWarehouseRepository.softDelete.mockResolvedValue({ affected: 0 });
const result = await warehousesService.delete('nonexistent-id', tenantId);
expect(result).toBe(false);
});
});
describe('Location operations', () => {
const createLocationDto = {
warehouseId: 'warehouse-uuid-1',
code: 'LOC-001',
name: 'Location A',
locationType: 'shelf' as const,
};
beforeEach(() => {
mockQueryBuilder.getOne.mockResolvedValue(null);
mockQueryBuilder.getMany.mockResolvedValue([]);
mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]);
});
it('should find all locations with tenant filtering via join', async () => {
mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]);
await warehousesService.findAllLocations({ tenantId, limit: 50, offset: 0 });
expect(mockQueryBuilder.where).toHaveBeenCalledWith(
'warehouse.tenantId = :tenantId',
{ tenantId }
);
});
it('should filter locations by warehouse', async () => {
mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]);
await warehousesService.findAllLocations({
tenantId,
warehouseId: 'warehouse-uuid',
});
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'location.warehouseId = :warehouseId',
{ warehouseId: 'warehouse-uuid' }
);
});
it('should filter locations by type', async () => {
mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]);
await warehousesService.findAllLocations({
tenantId,
locationType: 'shelf',
});
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'location.locationType = :locationType',
{ locationType: 'shelf' }
);
});
it('should create location', async () => {
const savedLocation = { id: '1', ...createLocationDto };
mockLocationRepository.create.mockReturnValue(savedLocation);
mockLocationRepository.save.mockResolvedValue(savedLocation);
const result = await warehousesService.createLocation(tenantId, createLocationDto, userId);
expect(result).toEqual(savedLocation);
});
it('should update location', async () => {
const existingLocation = { id: '1', ...createLocationDto };
mockQueryBuilder.getOne.mockResolvedValue(existingLocation);
mockLocationRepository.save.mockResolvedValue({ ...existingLocation, name: 'Updated Location' });
const result = await warehousesService.updateLocation(
'location-uuid',
tenantId,
{ name: 'Updated Location' },
userId
);
expect(result?.name).toBe('Updated Location');
});
it('should delete location', async () => {
const existingLocation = { id: '1', ...createLocationDto };
mockQueryBuilder.getOne.mockResolvedValue(existingLocation);
mockLocationRepository.softDelete.mockResolvedValue({ affected: 1 });
const result = await warehousesService.deleteLocation('location-uuid', tenantId);
expect(result).toBe(true);
});
it('should get locations by warehouse', async () => {
const mockLocations = [{ id: '1', code: 'LOC-001' }];
mockQueryBuilder.getMany.mockResolvedValue(mockLocations);
const result = await warehousesService.getLocationsByWarehouse('warehouse-uuid', tenantId);
expect(result).toHaveLength(1);
});
});
});