172 lines
6.9 KiB
TypeScript
172 lines
6.9 KiB
TypeScript
import { Logger } from '@nestjs/common';
|
|
import { ProductHiboutikService } from './product.hiboutik.service';
|
|
import { ProductConverter } from '../../models/converter/product.converter';
|
|
import { ProductHiboutikDto } from '../../models/hiboutikDTO/product.hiboutik.dto';
|
|
import { StockHiboutikDto } from '../../models/hiboutikDTO/stock.hiboutik.dto';
|
|
|
|
const hiboutikProduct = (id: number, overrides: Partial<ProductHiboutikDto> = {}): ProductHiboutikDto => ({
|
|
product_id: id,
|
|
product_model: `Product ${id}`,
|
|
product_barcode: `barcode-${id}`,
|
|
product_brand: 0,
|
|
product_supplier: 0,
|
|
product_price: '10.00',
|
|
product_discount_price: '0.00',
|
|
product_supply_price: '5.00',
|
|
product_category: 0,
|
|
product_display_www: 1,
|
|
product_arch: 0,
|
|
products_desc: '',
|
|
updated_at: '2026-06-28 17:02:55',
|
|
product_specific_rules: [],
|
|
...overrides,
|
|
});
|
|
|
|
const stock = (productId: number, size: number, available: string): StockHiboutikDto => ({
|
|
product_id: productId,
|
|
product_size: size,
|
|
stock_available: available,
|
|
product_barcode: '',
|
|
product_supplier_reference: '',
|
|
});
|
|
|
|
describe('ProductHiboutikService', () => {
|
|
let service: ProductHiboutikService;
|
|
let calls: string[];
|
|
const productHiboutikRepository = { getProducts: jest.fn() };
|
|
const stockHiboutikRepository = { getStocks: jest.fn() };
|
|
const productDatabaseRepository = { getAll: jest.fn(), upsert: jest.fn(), deleteAll: jest.fn() };
|
|
const productVersionDatabaseRepository = { upsert: jest.fn(), deleteMissing: jest.fn() };
|
|
|
|
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();
|
|
calls = [];
|
|
stockHiboutikRepository.getStocks.mockResolvedValue([]);
|
|
productDatabaseRepository.getAll.mockResolvedValue([]);
|
|
productDatabaseRepository.deleteAll.mockImplementation((ids: string[]) => {
|
|
calls.push('deleteProducts');
|
|
return Promise.resolve(ids.length);
|
|
});
|
|
// the saved product mirrors what was upserted, like the database would
|
|
productDatabaseRepository.upsert.mockImplementation((product: { hiboutikId: number }) => {
|
|
calls.push('upsertProduct');
|
|
return Promise.resolve({ ...product, id: `uuid-${product.hiboutikId}` });
|
|
});
|
|
productVersionDatabaseRepository.deleteMissing.mockImplementation(() => {
|
|
calls.push('deleteVersions');
|
|
return Promise.resolve(0);
|
|
});
|
|
productVersionDatabaseRepository.upsert.mockImplementation(() => {
|
|
calls.push('upsertVersion');
|
|
return Promise.resolve({});
|
|
});
|
|
|
|
service = new ProductHiboutikService(
|
|
productHiboutikRepository as never,
|
|
productDatabaseRepository as never,
|
|
productVersionDatabaseRepository as never,
|
|
new ProductConverter(),
|
|
stockHiboutikRepository as never,
|
|
);
|
|
});
|
|
|
|
it('deletes products missing from Hiboutik, before upserting', async () => {
|
|
productHiboutikRepository.getProducts.mockResolvedValue([hiboutikProduct(1)]);
|
|
productDatabaseRepository.getAll.mockResolvedValue([
|
|
{ id: 'uuid-1', hiboutikId: 1 },
|
|
{ id: 'uuid-2', hiboutikId: 2 },
|
|
]);
|
|
|
|
await service.syncProducts();
|
|
|
|
expect(productDatabaseRepository.deleteAll).toHaveBeenCalledWith(['uuid-2']);
|
|
expect(calls[0]).toBe('deleteProducts');
|
|
});
|
|
|
|
it('deletes every product when Hiboutik answers with an empty list', async () => {
|
|
productHiboutikRepository.getProducts.mockResolvedValue([]);
|
|
productDatabaseRepository.getAll.mockResolvedValue([{ id: 'uuid-1', hiboutikId: 1 }]);
|
|
|
|
await service.syncProducts();
|
|
|
|
expect(productDatabaseRepository.deleteAll).toHaveBeenCalledWith(['uuid-1']);
|
|
});
|
|
|
|
it('deletes nothing when the Hiboutik call fails', async () => {
|
|
productHiboutikRepository.getProducts.mockRejectedValue(new Error('Request failed with status code 500'));
|
|
productDatabaseRepository.getAll.mockResolvedValue([{ id: 'uuid-1', hiboutikId: 1 }]);
|
|
|
|
await expect(service.syncProducts()).rejects.toThrow('500');
|
|
|
|
expect(productDatabaseRepository.deleteAll).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('creates a single version for a product without sizes', async () => {
|
|
productHiboutikRepository.getProducts.mockResolvedValue([hiboutikProduct(1)]);
|
|
stockHiboutikRepository.getStocks.mockResolvedValue([stock(1, 0, '4'), stock(2, 0, '99')]);
|
|
|
|
await service.syncProducts();
|
|
|
|
expect(productDatabaseRepository.upsert).toHaveBeenCalledWith(expect.objectContaining({ hiboutikId: 1, globalStock: 4 }));
|
|
expect(productVersionDatabaseRepository.deleteMissing).toHaveBeenCalledWith('uuid-1', [0]);
|
|
expect(productVersionDatabaseRepository.upsert).toHaveBeenCalledTimes(1);
|
|
expect(productVersionDatabaseRepository.upsert).toHaveBeenCalledWith(
|
|
'uuid-1',
|
|
expect.objectContaining({ hiboutikSizeId: 0, barcode: 'barcode-1', stock: 4, versionName: 'Product 1', discountPrice: null }),
|
|
);
|
|
});
|
|
|
|
it('creates one version per size, with specific rules and stocks, after deleting removed sizes', async () => {
|
|
productHiboutikRepository.getProducts.mockResolvedValue([
|
|
hiboutikProduct(1, {
|
|
product_size_details: [
|
|
{ size_id: 10, size_name: 'S', barcode: 'b-s' },
|
|
{ size_id: 11, size_name: 'M', barcode: '' },
|
|
],
|
|
product_specific_rules: [{ size_id: 10, barcode: 'b-s', product_price: '12.00', product_discount_price: '0.00' }],
|
|
}),
|
|
]);
|
|
stockHiboutikRepository.getStocks.mockResolvedValue([stock(1, 10, '3'), stock(1, 11, '2')]);
|
|
|
|
await service.syncProducts();
|
|
|
|
expect(productDatabaseRepository.upsert).toHaveBeenCalledWith(expect.objectContaining({ globalStock: 5 }));
|
|
expect(productVersionDatabaseRepository.deleteMissing).toHaveBeenCalledWith('uuid-1', [10, 11]);
|
|
expect(calls).toEqual(['deleteProducts', 'upsertProduct', 'deleteVersions', 'upsertVersion', 'upsertVersion']);
|
|
expect(productVersionDatabaseRepository.upsert).toHaveBeenNthCalledWith(1, 'uuid-1', {
|
|
barcode: 'b-s',
|
|
discountPrice: null,
|
|
hiboutikSizeId: 10,
|
|
price: '12.00',
|
|
product: { connect: { id: 'uuid-1' } },
|
|
stock: 3,
|
|
versionName: 'S',
|
|
});
|
|
expect(productVersionDatabaseRepository.upsert).toHaveBeenNthCalledWith(2, 'uuid-1', {
|
|
barcode: null,
|
|
discountPrice: null,
|
|
hiboutikSizeId: 11,
|
|
price: null,
|
|
product: { connect: { id: 'uuid-1' } },
|
|
stock: 2,
|
|
versionName: 'M',
|
|
});
|
|
});
|
|
|
|
it('keeps syncing the other products when one fails', async () => {
|
|
productHiboutikRepository.getProducts.mockResolvedValue([hiboutikProduct(1), hiboutikProduct(2)]);
|
|
productDatabaseRepository.upsert.mockRejectedValueOnce(new Error('boom'));
|
|
|
|
await expect(service.syncProducts()).resolves.toBeUndefined();
|
|
|
|
expect(productDatabaseRepository.upsert).toHaveBeenCalledTimes(2);
|
|
expect(productVersionDatabaseRepository.upsert).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|