For each customer sale not yet stored as completed, fetch the full sale (GET /sales/:id) and upsert it as a STORE order keyed on hiboutikSaleId. Lines are replaced on each upsert and linked to the matching product version when it still exists. Quotations are skipped, orders are never deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
207 lines
8.6 KiB
TypeScript
207 lines
8.6 KiB
TypeScript
import { Logger } from '@nestjs/common';
|
|
import { CustomerHiboutikService } from './customer.hiboutik.service';
|
|
|
|
describe('CustomerHiboutikService', () => {
|
|
const customerHiboutikRepository = { getCustomers: jest.fn(), getCustomerById: jest.fn(), getAddressById: jest.fn() };
|
|
const customerDatabaseRepository = { upsert: jest.fn(), deleteMissing: 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.deleteMissing.mockResolvedValue(0);
|
|
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,
|
|
);
|
|
});
|
|
|
|
it('deletes customers missing from Hiboutik', async () => {
|
|
customerHiboutikRepository.getCustomers.mockResolvedValue([{ customers_id: 1 }, { customers_id: '2' }]);
|
|
|
|
await service.syncCustomers();
|
|
|
|
expect(customerDatabaseRepository.deleteMissing).toHaveBeenCalledWith([1, 2]);
|
|
expect(customerDatabaseRepository.upsert).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('does not delete customers when Hiboutik returns none', async () => {
|
|
await service.syncCustomers();
|
|
|
|
expect(customerDatabaseRepository.deleteMissing).not.toHaveBeenCalled();
|
|
});
|
|
|
|
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 },
|
|
{ address_id: '11', customers_id: 999 },
|
|
],
|
|
});
|
|
|
|
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 });
|
|
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 }, { address_id: 11 }],
|
|
});
|
|
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([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();
|
|
});
|
|
});
|
|
});
|