import { Logger } from '@nestjs/common'; import { CustomerHiboutikService } from './customer.hiboutik.service'; describe('CustomerHiboutikService', () => { const customerHiboutikRepository = { getCustomers: jest.fn(), getCustomerById: jest.fn(), findCustomerById: jest.fn(), getAddressById: jest.fn(), }; const customerDatabaseRepository = { upsert: jest.fn(), getAllHiboutikIds: jest.fn(), deleteByHiboutikIds: jest.fn() }; const customerConverter = { fromHiboutik: jest.fn() }; const customerAddressDatabaseRepository = { upsert: jest.fn(), deleteMissing: jest.fn() }; const customerAddressConverter = { fromHiboutik: jest.fn((dto: unknown) => dto) }; const saleHiboutikRepository = { getSaleById: jest.fn() }; const orderDatabaseRepository = { upsert: jest.fn(), getCompletedSaleIds: jest.fn() }; const orderConverter = { fromHiboutik: jest.fn((dto: unknown) => dto) }; let service: CustomerHiboutikService; beforeAll(() => { jest.spyOn(Logger.prototype, 'log').mockImplementation(() => undefined); jest.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined); jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); }); beforeEach(() => { jest.clearAllMocks(); customerHiboutikRepository.getCustomers.mockResolvedValue([]); customerHiboutikRepository.getCustomerById.mockResolvedValue({ customers_id: 1, addresses: [] }); customerDatabaseRepository.upsert.mockResolvedValue({ id: 'customer-uuid' }); customerDatabaseRepository.getAllHiboutikIds.mockResolvedValue([]); customerDatabaseRepository.deleteByHiboutikIds.mockImplementation((ids: number[]) => Promise.resolve(ids.length)); customerHiboutikRepository.findCustomerById.mockResolvedValue(null); customerAddressDatabaseRepository.deleteMissing.mockResolvedValue(0); orderDatabaseRepository.getCompletedSaleIds.mockResolvedValue(new Set()); saleHiboutikRepository.getSaleById.mockImplementation((id: number) => Promise.resolve({ sale_id: id, quotation: 0 })); service = new CustomerHiboutikService( customerHiboutikRepository as never, customerDatabaseRepository as never, customerConverter as never, customerAddressDatabaseRepository as never, customerAddressConverter as never, saleHiboutikRepository as never, orderDatabaseRepository as never, orderConverter as never, ); }); describe('deletions', () => { it('deletes the stored customers missing from the list once Hiboutik confirms they no longer exist', async () => { customerHiboutikRepository.getCustomers.mockResolvedValue([{ customers_id: 1 }, { customers_id: 2 }]); customerDatabaseRepository.getAllHiboutikIds.mockResolvedValue([1, 2, 3, 4]); await service.syncCustomers(); expect(customerHiboutikRepository.findCustomerById.mock.calls).toEqual([[3], [4]]); expect(customerDatabaseRepository.deleteByHiboutikIds).toHaveBeenCalledWith([3, 4]); expect(customerDatabaseRepository.upsert).toHaveBeenCalledTimes(2); }); it('keeps and syncs a customer missing from the list (page shift) that still exists in Hiboutik', async () => { customerHiboutikRepository.getCustomers.mockResolvedValue([{ customers_id: 1 }]); customerDatabaseRepository.getAllHiboutikIds.mockResolvedValue([1, 2]); customerHiboutikRepository.findCustomerById.mockResolvedValue({ customers_id: 2 }); await service.syncCustomers(); expect(customerDatabaseRepository.deleteByHiboutikIds).toHaveBeenCalledWith([]); expect(customerHiboutikRepository.getCustomerById.mock.calls).toEqual([[1], [2]]); }); it('keeps a customer whose check fails', async () => { customerDatabaseRepository.getAllHiboutikIds.mockResolvedValue([3, 4]); customerHiboutikRepository.findCustomerById.mockRejectedValueOnce(new Error('Request failed with status code 500')); await service.syncCustomers(); expect(customerDatabaseRepository.deleteByHiboutikIds).toHaveBeenCalledWith([4]); }); it('deletes every confirmed customer when Hiboutik answers with an empty list', async () => { customerDatabaseRepository.getAllHiboutikIds.mockResolvedValue([1, 2]); await service.syncCustomers(); expect(customerDatabaseRepository.deleteByHiboutikIds).toHaveBeenCalledWith([1, 2]); }); it('deletes nothing when the Hiboutik list call fails', async () => { customerHiboutikRepository.getCustomers.mockRejectedValue(new Error('Request failed with status code 500')); await expect(service.syncCustomers()).rejects.toThrow('500'); expect(customerDatabaseRepository.deleteByHiboutikIds).not.toHaveBeenCalled(); }); it('syncs once a customer listed twice', async () => { customerHiboutikRepository.getCustomers.mockResolvedValue([{ customers_id: 1 }, { customers_id: 1 }]); await service.syncCustomers(); expect(customerHiboutikRepository.getCustomerById).toHaveBeenCalledTimes(1); }); }); it('keeps syncing the other customers when one fails', async () => { customerHiboutikRepository.getCustomers.mockResolvedValue([{ customers_id: 1 }, { customers_id: 2 }]); customerHiboutikRepository.getCustomerById.mockRejectedValueOnce(new Error('boom')); await expect(service.syncCustomers()).resolves.toBeUndefined(); expect(customerDatabaseRepository.upsert).toHaveBeenCalledTimes(1); }); describe('addresses', () => { beforeEach(() => { customerHiboutikRepository.getCustomers.mockResolvedValue([{ customers_id: 1 }]); }); it('deletes the missing addresses of the customer, then upserts the others linked to it', async () => { customerHiboutikRepository.getCustomerById.mockResolvedValue({ customers_id: 1, addresses: [ { address_id: 10, customers_id: 1, company: '' }, { address_id: 11, customers_id: 999, company: '' }, ], }); await service.syncCustomers(); expect(customerAddressDatabaseRepository.deleteMissing).toHaveBeenCalledWith('customer-uuid', [10, 11]); expect(customerAddressDatabaseRepository.upsert).toHaveBeenCalledTimes(2); expect(customerAddressConverter.fromHiboutik).toHaveBeenLastCalledWith({ address_id: 11, customers_id: 1, company: '' }); expect(customerAddressDatabaseRepository.deleteMissing.mock.invocationCallOrder[0]).toBeLessThan( customerAddressDatabaseRepository.upsert.mock.invocationCallOrder[0], ); }); it('deletes all the addresses of a customer that has none left', async () => { await service.syncCustomers(); expect(customerAddressDatabaseRepository.deleteMissing).toHaveBeenCalledWith('customer-uuid', []); expect(customerAddressDatabaseRepository.upsert).not.toHaveBeenCalled(); }); it('keeps the stored addresses when the payload has no addresses array', async () => { customerHiboutikRepository.getCustomerById.mockResolvedValue({ customers_id: 1 }); await service.syncCustomers(); expect(customerAddressDatabaseRepository.deleteMissing).not.toHaveBeenCalled(); expect(customerAddressDatabaseRepository.upsert).not.toHaveBeenCalled(); }); it('fetches the company details of addresses with a company only', async () => { customerHiboutikRepository.getCustomerById.mockResolvedValue({ customers_id: 1, addresses: [ { address_id: 10, company: 'ACME' }, { address_id: 11, company: ' ' }, ], }); customerHiboutikRepository.getAddressById.mockResolvedValue({ address_id: 10, tax_number: 'FR123', company_number: '123', legal_status: 'SAS', updated_at: '2026-11-01 10:00:00', }); await service.syncCustomers(); expect(customerHiboutikRepository.getAddressById).toHaveBeenCalledTimes(1); expect(customerHiboutikRepository.getAddressById).toHaveBeenCalledWith(10); expect(customerAddressConverter.fromHiboutik).toHaveBeenCalledWith({ address_id: 10, company: 'ACME', customers_id: 1, tax_number: 'FR123', company_number: '123', legal_status: 'SAS', updated_at: '2026-11-01 10:00:00', }); }); it('still syncs an address whose company details cannot be fetched', async () => { customerHiboutikRepository.getCustomerById.mockResolvedValue({ customers_id: 1, addresses: [{ address_id: 10, company: 'ACME' }] }); customerHiboutikRepository.getAddressById.mockRejectedValue(new Error('boom')); await service.syncCustomers(); expect(customerAddressConverter.fromHiboutik).toHaveBeenCalledWith({ address_id: 10, company: 'ACME', customers_id: 1 }); expect(customerAddressDatabaseRepository.upsert).toHaveBeenCalledTimes(1); }); it('keeps syncing the other addresses when one fails', async () => { customerHiboutikRepository.getCustomerById.mockResolvedValue({ customers_id: 1, addresses: [ { address_id: 10, company: '' }, { address_id: 11, company: '' }, ], }); customerAddressDatabaseRepository.upsert.mockRejectedValueOnce(new Error('boom')); await service.syncCustomers(); expect(customerAddressDatabaseRepository.upsert).toHaveBeenCalledTimes(2); }); }); describe('orders', () => { beforeEach(() => { customerHiboutikRepository.getCustomers.mockResolvedValue([{ customers_id: 1 }]); customerHiboutikRepository.getCustomerById.mockResolvedValue({ customers_id: 1, addresses: [], sales: [{ sale_id: 5 }, { sale_id: 6 }, { sale_id: 7 }], }); }); it('fetches and upserts the sales not yet stored as completed, linked to the customer', async () => { orderDatabaseRepository.getCompletedSaleIds.mockResolvedValue(new Set([6])); await service.syncCustomers(); expect(orderDatabaseRepository.getCompletedSaleIds).toHaveBeenCalledWith(1, [5, 6, 7]); expect(saleHiboutikRepository.getSaleById.mock.calls.map((call: unknown[]) => call[0])).toEqual([5, 7]); expect(orderDatabaseRepository.upsert).toHaveBeenCalledTimes(2); expect(orderConverter.fromHiboutik).toHaveBeenCalledWith({ sale_id: 5, quotation: 0, customer_id: 1 }); }); it('ignores quotations', async () => { saleHiboutikRepository.getSaleById.mockResolvedValueOnce({ sale_id: 5, quotation: 1 }); await service.syncCustomers(); expect(orderDatabaseRepository.upsert).toHaveBeenCalledTimes(2); }); it('keeps syncing the other sales when one fails', async () => { saleHiboutikRepository.getSaleById.mockRejectedValueOnce(new Error('boom')); await service.syncCustomers(); expect(orderDatabaseRepository.upsert).toHaveBeenCalledTimes(2); }); it('skips orders when the payload has no sales array', async () => { customerHiboutikRepository.getCustomerById.mockResolvedValue({ customers_id: 1, addresses: [] }); await service.syncCustomers(); expect(saleHiboutikRepository.getSaleById).not.toHaveBeenCalled(); expect(orderDatabaseRepository.upsert).not.toHaveBeenCalled(); }); }); });