feat(sync): validate Hiboutik payloads, read French dates and guard deletions
- Hiboutik payloads are zod schemas, validated by HiboutikClient (getOne added for single resources: null on 404 or empty answer). - Hiboutik dates are read as Europe/Paris whatever the server time zone. - Paginated lists driving deletions are walked twice when they span several pages (brands, categories, products); categories are sorted by id. - A customer missing from the list is deleted only once Hiboutik confirms it; completed orders that lost their customer are linked back. - Each product and its versions are written in one transaction. - An email removed in Hiboutik is erased in database. - A 429 is retried with backoff, then stops the synchronization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
87
src/models/hiboutikDTO/hiboutik.schema.spec.ts
Normal file
87
src/models/hiboutikDTO/hiboutik.schema.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { parseHiboutik } from './hiboutik.schema';
|
||||
import { customerAddressHiboutikSchema, fullCustomerHiboutikSchema } from './customer.hiboutik.dto';
|
||||
import { productHiboutikSchema } from './product.hiboutik.dto';
|
||||
import { saleHiboutikSchema } from './sale.hiboutik.dto';
|
||||
import { stockHiboutikSchema } from './stock.hiboutik.dto';
|
||||
|
||||
const parse = <T extends Parameters<typeof parseHiboutik>[0]>(schema: T, data: unknown) => parseHiboutik(schema, data, '/x');
|
||||
|
||||
describe('Hiboutik schemas', () => {
|
||||
const product = {
|
||||
product_id: 1,
|
||||
product_model: 'BT21 bottle',
|
||||
product_barcode: '8435497280048',
|
||||
product_brand: 1,
|
||||
product_supplier: 2,
|
||||
product_price: '13.99',
|
||||
product_discount_price: '0.00',
|
||||
product_category: 19,
|
||||
product_display_www: 1,
|
||||
product_arch: 0,
|
||||
products_desc: '',
|
||||
updated_at: '2026-06-28 17:02:55',
|
||||
product_specific_rules: [],
|
||||
};
|
||||
|
||||
it('accepts ids, references and flags sent as strings', () => {
|
||||
const parsed = parse(customerAddressHiboutikSchema, { address_id: ' 8 ', customers_id: '43', default: '0' });
|
||||
expect(parsed.address_id).toBe(8);
|
||||
expect(parsed.customers_id).toBe(43);
|
||||
expect(parsed.default).toBe(0);
|
||||
});
|
||||
|
||||
it('turns empty references into 0 and empty text into ""', () => {
|
||||
const parsed = parse(productHiboutikSchema, { ...product, product_brand: '', product_category: null, products_desc: null });
|
||||
expect(parsed.product_brand).toBe(0);
|
||||
expect(parsed.product_category).toBe(0);
|
||||
expect(parsed.products_desc).toBe('');
|
||||
});
|
||||
|
||||
it('keeps numeric text (zip code) and prices as strings', () => {
|
||||
expect(parse(customerAddressHiboutikSchema, { address_id: 1, zip_code: 75002 }).zip_code).toBe('75002');
|
||||
expect(parse(productHiboutikSchema, { ...product, product_price: 13.5 }).product_price).toBe('13.5');
|
||||
});
|
||||
|
||||
it('defaults missing lists to [] and strips unknown fields', () => {
|
||||
const parsed = parse(productHiboutikSchema, { ...product, product_specific_rules: '', product_supply_price: '7.49' });
|
||||
expect(parsed.product_specific_rules).toEqual([]);
|
||||
expect(parsed.product_size_details).toEqual([]);
|
||||
expect(parsed).not.toHaveProperty('product_supply_price');
|
||||
expect(parse(saleHiboutikSchema, { sale_id: 5, total: '0.00' }).line_items).toEqual([]);
|
||||
});
|
||||
|
||||
it('leaves addresses and sales undefined when they are not arrays, so that stored data is kept', () => {
|
||||
const parsed = parse(fullCustomerHiboutikSchema, { customers_id: 1, addresses: '', sales: { error: 'x' } });
|
||||
expect(parsed.addresses).toBeUndefined();
|
||||
expect(parsed.sales).toBeUndefined();
|
||||
});
|
||||
|
||||
it('parses stocks sent as decimal strings', () => {
|
||||
expect(parse(stockHiboutikSchema, { product_id: '1', product_size: '0', stock_available: '3.000' })).toEqual({
|
||||
product_id: 1,
|
||||
product_size: 0,
|
||||
stock_available: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a missing id', { ...product, product_id: undefined }],
|
||||
['a zero id', { ...product, product_id: 0 }],
|
||||
['a non-numeric id', { ...product, product_id: 'abc' }],
|
||||
['a negative reference', { ...product, product_brand: -1 }],
|
||||
['an invalid price', { ...product, product_price: 'free' }],
|
||||
['a list that is not an array', { ...product, product_size_details: { size_id: 1 } }],
|
||||
])('rejects %s', (_, data) => {
|
||||
expect(() => parse(productHiboutikSchema, data)).toThrow('Unexpected answer from Hiboutik /x');
|
||||
});
|
||||
|
||||
it('names the faulty fields, never their values', () => {
|
||||
expect.assertions(2);
|
||||
try {
|
||||
parse(fullCustomerHiboutikSchema, { customers_id: 'jane@example.com', addresses: [{ address_id: 'Jane Doe' }] });
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toMatch(/customers_id.*addresses\.0\.address_id/);
|
||||
expect((error as Error).message).not.toMatch(/jane|Doe/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user