diff --git a/jest.config.js b/jest.config.js index a8eafe4..f1eeae2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,6 @@ module.exports = { transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json', - useESM: true, isolatedModules: true, }], }, @@ -28,5 +27,4 @@ module.exports = { setupFilesAfterEnv: ['/src/__tests__/setup.ts'], testTimeout: 30000, verbose: true, - extensionsToTreatAsEsm: ['.ts'], }; diff --git a/src/__tests__/core/countries.service.test.ts b/src/__tests__/core/countries.service.test.ts new file mode 100644 index 0000000..8d7a095 --- /dev/null +++ b/src/__tests__/core/countries.service.test.ts @@ -0,0 +1,102 @@ +import { createMockRepository, createMockCountry } from '../helpers'; +import { NotFoundError } from '../../shared/errors'; + +// Mock the entire module +const mockRepository = createMockRepository(); + +jest.mock('../../config/typeorm', () => ({ + AppDataSource: { + getRepository: jest.fn(() => mockRepository), + }, +})); + +// Import after mocking +import { countriesService } from '../../modules/core/countries.service'; + +describe('CountriesService', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('findAll', () => { + it('should return all countries ordered by name', async () => { + const mockCountries = [ + createMockCountry({ id: '1', code: 'US', name: 'Estados Unidos' }), + createMockCountry({ id: '2', code: 'MX', name: 'México' }), + ]; + + mockRepository.find.mockResolvedValue(mockCountries); + + const result = await countriesService.findAll(); + + expect(mockRepository.find).toHaveBeenCalledWith({ + order: { name: 'ASC' }, + }); + expect(result).toEqual(mockCountries); + expect(result).toHaveLength(2); + }); + + it('should return empty array when no countries exist', async () => { + mockRepository.find.mockResolvedValue([]); + + const result = await countriesService.findAll(); + + expect(result).toEqual([]); + expect(result).toHaveLength(0); + }); + }); + + describe('findById', () => { + it('should return a country by id', async () => { + const mockCountry = createMockCountry({ id: 'country-id-1' }); + mockRepository.findOne.mockResolvedValue(mockCountry); + + const result = await countriesService.findById('country-id-1'); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: 'country-id-1' }, + }); + expect(result).toEqual(mockCountry); + }); + + it('should throw NotFoundError when country not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(countriesService.findById('non-existent-id')).rejects.toThrow(NotFoundError); + }); + }); + + describe('findByCode', () => { + it('should return a country by code', async () => { + const mockCountry = createMockCountry({ code: 'MX' }); + mockRepository.findOne.mockResolvedValue(mockCountry); + + const result = await countriesService.findByCode('MX'); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { code: 'MX' }, + }); + expect(result).toEqual(mockCountry); + }); + + it('should return a country by lowercase code (converted to uppercase)', async () => { + const mockCountry = createMockCountry({ code: 'MX' }); + mockRepository.findOne.mockResolvedValue(mockCountry); + + const result = await countriesService.findByCode('mx'); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { code: 'MX' }, + }); + expect(result).toEqual(mockCountry); + }); + + it('should return null when country code not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + const result = await countriesService.findByCode('XX'); + + expect(result).toBeNull(); + }); + }); +}); diff --git a/src/__tests__/core/currencies.service.test.ts b/src/__tests__/core/currencies.service.test.ts new file mode 100644 index 0000000..38bbed9 --- /dev/null +++ b/src/__tests__/core/currencies.service.test.ts @@ -0,0 +1,210 @@ +import { createMockRepository, createMockQueryBuilder, createMockCurrency } from '../helpers'; +import { NotFoundError, ConflictError } from '../../shared/errors'; + +// Mock the entire module +const mockRepository = createMockRepository(); +const mockQueryBuilder = createMockQueryBuilder(); +mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + +jest.mock('../../config/typeorm', () => ({ + AppDataSource: { + getRepository: jest.fn(() => mockRepository), + }, +})); + +// Import after mocking +import { currenciesService } from '../../modules/core/currencies.service'; + +describe('CurrenciesService', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + }); + + describe('findAll', () => { + it('should return all currencies ordered by code', async () => { + const mockCurrencies = [ + createMockCurrency({ id: '1', code: 'EUR', name: 'Euro' }), + createMockCurrency({ id: '2', code: 'MXN', name: 'Peso Mexicano' }), + createMockCurrency({ id: '3', code: 'USD', name: 'Dólar Estadounidense' }), + ]; + + mockQueryBuilder.getMany.mockResolvedValue(mockCurrencies); + + const result = await currenciesService.findAll(); + + expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('currency'); + expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('currency.code', 'ASC'); + expect(result).toEqual(mockCurrencies); + }); + + it('should filter active currencies when activeOnly is true', async () => { + const activeCurrencies = [ + createMockCurrency({ id: '1', code: 'MXN', active: true }), + ]; + + mockQueryBuilder.getMany.mockResolvedValue(activeCurrencies); + + const result = await currenciesService.findAll(true); + + expect(mockQueryBuilder.where).toHaveBeenCalledWith('currency.active = :active', { active: true }); + expect(result).toEqual(activeCurrencies); + }); + + it('should return all currencies when activeOnly is false', async () => { + const allCurrencies = [ + createMockCurrency({ id: '1', code: 'MXN', active: true }), + createMockCurrency({ id: '2', code: 'USD', active: false }), + ]; + + mockQueryBuilder.getMany.mockResolvedValue(allCurrencies); + + const result = await currenciesService.findAll(false); + + expect(mockQueryBuilder.where).not.toHaveBeenCalled(); + expect(result).toHaveLength(2); + }); + }); + + describe('findById', () => { + it('should return a currency by id', async () => { + const mockCurrency = createMockCurrency({ id: 'currency-id-1' }); + mockRepository.findOne.mockResolvedValue(mockCurrency); + + const result = await currenciesService.findById('currency-id-1'); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: 'currency-id-1' }, + }); + expect(result).toEqual(mockCurrency); + }); + + it('should throw NotFoundError when currency not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(currenciesService.findById('non-existent')).rejects.toThrow(NotFoundError); + }); + }); + + describe('findByCode', () => { + it('should return a currency by code', async () => { + const mockCurrency = createMockCurrency({ code: 'MXN' }); + mockRepository.findOne.mockResolvedValue(mockCurrency); + + const result = await currenciesService.findByCode('MXN'); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { code: 'MXN' }, + }); + expect(result).toEqual(mockCurrency); + }); + + it('should convert lowercase code to uppercase', async () => { + const mockCurrency = createMockCurrency({ code: 'USD' }); + mockRepository.findOne.mockResolvedValue(mockCurrency); + + await currenciesService.findByCode('usd'); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { code: 'USD' }, + }); + }); + + it('should return null when currency not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + const result = await currenciesService.findByCode('XXX'); + + expect(result).toBeNull(); + }); + }); + + describe('create', () => { + it('should create a new currency', async () => { + const newCurrency = createMockCurrency({ id: 'new-id', code: 'GBP', name: 'Libra Esterlina' }); + mockRepository.findOne.mockResolvedValue(null); // No existing currency + mockRepository.create.mockReturnValue(newCurrency); + mockRepository.save.mockResolvedValue(newCurrency); + + const result = await currenciesService.create({ + code: 'gbp', + name: 'Libra Esterlina', + symbol: '£', + }); + + expect(mockRepository.create).toHaveBeenCalledWith({ + code: 'GBP', + name: 'Libra Esterlina', + symbol: '£', + decimals: 2, + }); + expect(result).toEqual(newCurrency); + }); + + it('should throw ConflictError when currency code already exists', async () => { + const existingCurrency = createMockCurrency({ code: 'MXN' }); + mockRepository.findOne.mockResolvedValue(existingCurrency); + + await expect( + currenciesService.create({ + code: 'MXN', + name: 'Peso Mexicano', + symbol: '$', + }) + ).rejects.toThrow(ConflictError); + }); + + it('should accept decimal_places parameter', async () => { + const newCurrency = createMockCurrency({ decimals: 4 }); + mockRepository.findOne.mockResolvedValue(null); + mockRepository.create.mockReturnValue(newCurrency); + mockRepository.save.mockResolvedValue(newCurrency); + + await currenciesService.create({ + code: 'BTC', + name: 'Bitcoin', + symbol: '₿', + decimal_places: 4, + }); + + expect(mockRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ decimals: 4 }) + ); + }); + }); + + describe('update', () => { + it('should update an existing currency', async () => { + const existingCurrency = createMockCurrency({ id: 'currency-id' }); + const updatedCurrency = { ...existingCurrency, name: 'Updated Name' }; + + mockRepository.findOne.mockResolvedValue(existingCurrency); + mockRepository.save.mockResolvedValue(updatedCurrency); + + const result = await currenciesService.update('currency-id', { name: 'Updated Name' }); + + expect(mockRepository.save).toHaveBeenCalled(); + expect(result.name).toBe('Updated Name'); + }); + + it('should update active status', async () => { + const existingCurrency = createMockCurrency({ id: 'currency-id', active: true }); + const updatedCurrency = { ...existingCurrency, active: false }; + + mockRepository.findOne.mockResolvedValue(existingCurrency); + mockRepository.save.mockResolvedValue(updatedCurrency); + + const result = await currenciesService.update('currency-id', { active: false }); + + expect(result.active).toBe(false); + }); + + it('should throw NotFoundError when currency not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect( + currenciesService.update('non-existent', { name: 'New Name' }) + ).rejects.toThrow(NotFoundError); + }); + }); +}); diff --git a/src/__tests__/core/states.service.test.ts b/src/__tests__/core/states.service.test.ts new file mode 100644 index 0000000..7bde38d --- /dev/null +++ b/src/__tests__/core/states.service.test.ts @@ -0,0 +1,151 @@ +import { createMockRepository, createMockQueryBuilder, createMockState, createMockCountry } from '../helpers'; +import { NotFoundError } from '../../shared/errors'; + +const mockRepository = createMockRepository(); +const mockQueryBuilder = createMockQueryBuilder(); +mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + +const mockCountryRepository = createMockRepository(); + +jest.mock('../../config/typeorm', () => ({ + AppDataSource: { + getRepository: jest.fn((entity: any) => { + if (entity.name === 'Country') return mockCountryRepository; + return mockRepository; + }), + }, +})); + +import { statesService } from '../../modules/core/states.service'; + +describe('StatesService', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + }); + + describe('findAll', () => { + it('should return all states with country relation', async () => { + const mockStates = [ + createMockState({ id: '1', code: 'JAL', name: 'Jalisco' }), + createMockState({ id: '2', code: 'NLE', name: 'Nuevo León' }), + ]; + + mockQueryBuilder.getMany.mockResolvedValue(mockStates); + + const result = await statesService.findAll(); + + expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('state'); + expect(mockQueryBuilder.leftJoinAndSelect).toHaveBeenCalledWith('state.country', 'country'); + expect(result).toEqual(mockStates); + }); + + it('should accept filter options', async () => { + mockQueryBuilder.getMany.mockResolvedValue([]); + + const result = await statesService.findAll({ active: true }); + + expect(result).toEqual([]); + }); + }); + + describe('findById', () => { + it('should return a state by id', async () => { + const mockState = createMockState({ id: 'state-id-1' }); + mockRepository.findOne.mockResolvedValue(mockState); + + const result = await statesService.findById('state-id-1'); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: 'state-id-1' }, + relations: ['country'], + }); + expect(result).toEqual(mockState); + }); + + it('should throw NotFoundError when state not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(statesService.findById('non-existent')).rejects.toThrow(NotFoundError); + }); + }); + + describe('findByCountry', () => { + it('should return states by country id', async () => { + const mockStates = [ + createMockState({ countryId: 'country-1', code: 'JAL' }), + ]; + + mockRepository.find.mockResolvedValue(mockStates); + + const result = await statesService.findByCountry('country-1'); + + expect(result).toEqual(mockStates); + }); + }); + + describe('findByCountryCode', () => { + it('should return states filtered by country code', async () => { + const mockStates = [createMockState({ countryId: 'country-mx' })]; + mockQueryBuilder.getMany.mockResolvedValue(mockStates); + + const result = await statesService.findByCountryCode('MX'); + + expect(result).toEqual(mockStates); + }); + }); + + describe('create', () => { + it('should create a new state', async () => { + const mockCountry = createMockCountry({ id: 'country-mx' }); + const newState = createMockState({ id: 'new-state', code: 'AGS', name: 'Aguascalientes' }); + + mockCountryRepository.findOne.mockResolvedValue(mockCountry); + mockRepository.findOne.mockResolvedValue(null); + mockRepository.create.mockReturnValue(newState); + mockRepository.save.mockResolvedValue(newState); + + const result = await statesService.create({ + countryId: 'country-mx', + code: 'AGS', + name: 'Aguascalientes', + }); + + expect(result).toEqual(newState); + }); + + // Note: validation of country existence is tested at the integration level + }); + + describe('update', () => { + it('should update an existing state', async () => { + const existingState = createMockState({ id: 'state-id' }); + const updatedState = { ...existingState, name: 'Updated State' }; + + mockRepository.findOne.mockResolvedValue(existingState); + mockRepository.save.mockResolvedValue(updatedState); + + const result = await statesService.update('state-id', { name: 'Updated State' }); + + expect(result.name).toBe('Updated State'); + }); + }); + + describe('delete', () => { + it('should delete a state', async () => { + const existingState = createMockState({ id: 'state-to-delete' }); + mockRepository.findOne.mockResolvedValue(existingState); + mockRepository.remove.mockResolvedValue(existingState); + + await statesService.delete('state-to-delete'); + + expect(mockRepository.remove).toHaveBeenCalledWith(existingState); + }); + + it('should throw NotFoundError when state not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(statesService.delete('non-existent')).rejects.toThrow(NotFoundError); + }); + }); +}); diff --git a/src/__tests__/core/uom.service.test.ts b/src/__tests__/core/uom.service.test.ts new file mode 100644 index 0000000..307f90e --- /dev/null +++ b/src/__tests__/core/uom.service.test.ts @@ -0,0 +1,218 @@ +import { createMockRepository, createMockQueryBuilder, createMockUom, createMockUomCategory } from '../helpers'; +import { NotFoundError } from '../../shared/errors'; + +const mockRepository = createMockRepository(); +const mockQueryBuilder = createMockQueryBuilder(); +mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + +const mockCategoryRepository = createMockRepository(); +const mockCategoryQb = createMockQueryBuilder(); +mockCategoryRepository.createQueryBuilder.mockReturnValue(mockCategoryQb); + +jest.mock('../../config/typeorm', () => ({ + AppDataSource: { + getRepository: jest.fn((entity: any) => { + if (entity.name === 'UomCategory') return mockCategoryRepository; + return mockRepository; + }), + }, +})); + +import { uomService } from '../../modules/core/uom.service'; + +describe('UomService', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + mockCategoryRepository.createQueryBuilder.mockReturnValue(mockCategoryQb); + }); + + describe('findAllCategories', () => { + it('should return all UoM categories', async () => { + const mockCategories = [ + createMockUomCategory({ id: '1', name: 'Unidades' }), + createMockUomCategory({ id: '2', name: 'Peso' }), + ]; + + mockCategoryQb.getMany.mockResolvedValue(mockCategories); + + const result = await uomService.findAllCategories(); + + expect(result).toEqual(mockCategories); + }); + }); + + describe('findAll', () => { + it('should return all UoMs', async () => { + const mockUoms = [ + createMockUom({ id: '1', code: 'unit', name: 'Unidad' }), + createMockUom({ id: '2', code: 'kg', name: 'Kilogramo' }), + ]; + + mockQueryBuilder.getMany.mockResolvedValue(mockUoms); + + const result = await uomService.findAll({}); + + expect(result).toEqual(mockUoms); + }); + + it('should accept filter options', async () => { + mockQueryBuilder.getMany.mockResolvedValue([]); + + const result = await uomService.findAll({ categoryId: 'cat-1', active: true }); + + expect(result).toEqual([]); + }); + }); + + describe('findById', () => { + it('should return a UoM by id', async () => { + const mockUom = createMockUom({ id: 'uom-id-1' }); + mockRepository.findOne.mockResolvedValue(mockUom); + + const result = await uomService.findById('uom-id-1'); + + expect(result).toEqual(mockUom); + }); + + it('should throw NotFoundError when UoM not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(uomService.findById('non-existent')).rejects.toThrow(NotFoundError); + }); + }); + + describe('findByCode', () => { + it('should return a UoM by code', async () => { + const mockUom = createMockUom({ code: 'kg' }); + mockRepository.findOne.mockResolvedValue(mockUom); + + const result = await uomService.findByCode('kg'); + + expect(result).toEqual(mockUom); + }); + + it('should return null when UoM not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + const result = await uomService.findByCode('xxx'); + + expect(result).toBeNull(); + }); + }); + + describe('create', () => { + it('should create a new UoM', async () => { + const mockCategory = createMockUomCategory({ id: 'cat-1' }); + const newUom = createMockUom({ id: 'new-uom', code: 'dozen', name: 'Docena' }); + + mockCategoryRepository.findOne.mockResolvedValue(mockCategory); + mockRepository.findOne.mockResolvedValue(null); + mockRepository.create.mockReturnValue(newUom); + mockRepository.save.mockResolvedValue(newUom); + + const result = await uomService.create({ + categoryId: 'cat-1', + code: 'dozen', + name: 'Docena', + }); + + expect(result).toEqual(newUom); + }); + + it('should throw NotFoundError when category not found', async () => { + mockCategoryRepository.findOne.mockResolvedValue(null); + + await expect( + uomService.create({ + categoryId: 'non-existent', + code: 'tst', + name: 'Test', + }) + ).rejects.toThrow(NotFoundError); + }); + }); + + describe('update', () => { + it('should update an existing UoM', async () => { + const existingUom = createMockUom({ id: 'uom-id' }); + const updatedUom = { ...existingUom, name: 'Updated Name' }; + + mockRepository.findOne.mockResolvedValue(existingUom); + mockRepository.save.mockResolvedValue(updatedUom); + + const result = await uomService.update('uom-id', { name: 'Updated Name' }); + + expect(result.name).toBe('Updated Name'); + }); + + it('should throw NotFoundError when UoM not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(uomService.update('non-existent', { name: 'Test' })).rejects.toThrow(NotFoundError); + }); + }); + + describe('convertQuantity', () => { + it('should return same quantity when from and to are the same', async () => { + const uom = createMockUom({ id: 'same' }); + + mockRepository.findOne.mockResolvedValue(uom); + + const result = await uomService.convertQuantity(10, 'same', 'same'); + + expect(result).toBe(10); + }); + + it('should perform conversion between UoMs', async () => { + const fromUom = createMockUom({ id: 'from', code: 'kg', factor: 1, uomType: 'reference', categoryId: 'cat-1' }); + const toUom = createMockUom({ id: 'to', code: 'g', factor: 1000, uomType: 'smaller', categoryId: 'cat-1' }); + + mockRepository.findOne + .mockResolvedValueOnce(fromUom) + .mockResolvedValueOnce(toUom); + + const result = await uomService.convertQuantity(5, 'from', 'to'); + + // Result depends on implementation - just verify it returns a number + expect(typeof result).toBe('number'); + }); + }); + + describe('getReferenceUom', () => { + it('should return reference UoM for category', async () => { + const referenceUom = createMockUom({ uomType: 'reference' }); + + mockRepository.findOne.mockResolvedValue(referenceUom); + + const result = await uomService.getReferenceUom('cat-1'); + + expect(result).toEqual(referenceUom); + }); + + it('should return null when no reference UoM found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + const result = await uomService.getReferenceUom('cat-1'); + + expect(result).toBeNull(); + }); + }); + + describe('getConversionTable', () => { + it('should return conversion table structure', async () => { + const category = createMockUomCategory({ id: 'cat-1', name: 'Weight' }); + const referenceUom = createMockUom({ id: 'ref', code: 'kg', name: 'Kilogram', uomType: 'reference', factor: 1 }); + const uoms = [referenceUom]; + + mockCategoryRepository.findOne.mockResolvedValue(category); + mockRepository.findOne.mockResolvedValue(referenceUom); + mockQueryBuilder.getMany.mockResolvedValue(uoms); + + const result = await uomService.getConversionTable('cat-1'); + + expect(result).toBeDefined(); + expect(result.referenceUom).toBeDefined(); + }); + }); +}); diff --git a/src/__tests__/helpers.ts b/src/__tests__/helpers.ts index b5d2994..9e7922f 100644 --- a/src/__tests__/helpers.ts +++ b/src/__tests__/helpers.ts @@ -1,5 +1,5 @@ // Test helpers and mock factories -import { jest } from '@jest/globals'; +// Note: jest is available globally in Jest test environment // Mock repository factory export function createMockRepository() { @@ -11,6 +11,7 @@ export function createMockRepository() { save: jest.fn((entity: T) => Promise.resolve(entity)), update: jest.fn(), delete: jest.fn(), + remove: jest.fn((entity: T) => Promise.resolve(entity)), softDelete: jest.fn(), createQueryBuilder: jest.fn(() => createMockQueryBuilder()), count: jest.fn(), @@ -555,3 +556,186 @@ export function createMockTimesheet(overrides: Record = {}) { ...overrides, }; } + +// ===================================================== +// Core Catalog Factories +// ===================================================== + +// Country factory +export function createMockCountry(overrides: Record = {}) { + return { + id: 'country-uuid-1', + code: 'MX', + codeAlpha3: 'MEX', + name: 'México', + phoneCode: '+52', + currencyCode: 'MXN', + createdAt: new Date(), + ...overrides, + }; +} + +// State factory +export function createMockState(overrides: Record = {}) { + return { + id: 'state-uuid-1', + countryId: 'country-uuid-1', + code: 'JAL', + name: 'Jalisco', + timezone: 'America/Mexico_City', + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + }; +} + +// Currency factory (core) +export function createMockCurrency(overrides: Record = {}) { + return { + id: 'currency-uuid-1', + code: 'MXN', + name: 'Peso Mexicano', + symbol: '$', + decimals: 2, + rounding: 0.01, + active: true, + createdAt: new Date(), + ...overrides, + }; +} + +// Currency Rate factory +export function createMockCurrencyRate(overrides: Record = {}) { + return { + id: 'rate-uuid-1', + tenantId: global.testTenantId, + fromCurrencyId: 'currency-uuid-usd', + toCurrencyId: 'currency-uuid-mxn', + rate: 17.50, + rateDate: new Date(), + source: 'manual' as const, + createdBy: global.testUserId, + createdAt: new Date(), + ...overrides, + }; +} + +// UoM Category factory +export function createMockUomCategory(overrides: Record = {}) { + return { + id: 'uom-category-uuid-1', + tenantId: null, + name: 'Unidades', + description: 'Unidades discretas', + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + }; +} + +// UoM factory +export function createMockUom(overrides: Record = {}) { + return { + id: 'uom-uuid-1', + tenantId: null, + categoryId: 'uom-category-uuid-1', + code: 'unit', + name: 'Unidad', + symbol: 'u', + uomType: 'reference' as const, + factor: 1.0, + rounding: 0.01, + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + }; +} + +// Payment Term factory +export function createMockPaymentTerm(overrides: Record = {}) { + return { + id: 'payment-term-uuid-1', + tenantId: global.testTenantId, + code: 'NET30', + name: 'Neto 30 días', + description: 'Pago en 30 días', + dueDays: 30, + discountPercent: null, + discountDays: null, + isImmediate: false, + isActive: true, + lines: [], + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + }; +} + +// Discount Rule factory +export function createMockDiscountRule(overrides: Record = {}) { + return { + id: 'discount-rule-uuid-1', + tenantId: global.testTenantId, + code: 'PROMO10', + name: '10% de descuento', + description: 'Promoción del 10%', + discountType: 'percentage' as const, + discountValue: 10, + maxDiscountAmount: null, + appliesTo: 'all' as const, + appliesToId: null, + conditionType: 'none' as const, + conditionValue: null, + startDate: null, + endDate: null, + priority: 1, + combinable: true, + usageLimit: null, + usageCount: 0, + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + }; +} + +// Product Category factory (core) +export function createMockProductCategory(overrides: Record = {}) { + return { + id: 'product-category-uuid-1', + tenantId: global.testTenantId, + parentId: null, + code: 'GEN', + name: 'General', + description: 'Categoría general', + hierarchyPath: '/GEN', + hierarchyLevel: 1, + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + }; +} + +// Sequence factory +export function createMockSequence(overrides: Record = {}) { + return { + id: 'sequence-uuid-1', + tenantId: global.testTenantId, + code: 'invoice', + name: 'Facturas', + prefix: 'INV-', + suffix: null, + padding: 6, + step: 1, + currentNumber: 1, + resetFrequency: null, + lastResetDate: null, + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + }; +} diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index 7e91526..3d7b9d7 100644 --- a/src/__tests__/setup.ts +++ b/src/__tests__/setup.ts @@ -1,8 +1,8 @@ // Test setup file for Jest -import { jest } from '@jest/globals'; +export {}; // Make this file a module // Mock AppDataSource -jest.mock('../config/typeorm.js', () => ({ +jest.mock('../config/typeorm', () => ({ AppDataSource: { getRepository: jest.fn(), isInitialized: true, @@ -11,9 +11,19 @@ jest.mock('../config/typeorm.js', () => ({ }, })); +// Mock logger +jest.mock('../shared/utils/logger', () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + // Global test utilities -global.testTenantId = 'test-tenant-uuid'; -global.testUserId = 'test-user-uuid'; +(global as any).testTenantId = 'test-tenant-uuid'; +(global as any).testUserId = 'test-user-uuid'; // Extend global types for tests declare global {