feat(sync): harden the Hiboutik sync, share rate limits across instances, update dependencies (#4)

This commit is contained in:
2026-11-08 13:45:08 +00:00
parent 04960379e8
commit d07c4df52d
62 changed files with 1546 additions and 639 deletions

View 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);
}
});
});