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:
@@ -2,8 +2,11 @@ import { Injectable } from '@nestjs/common';
|
||||
import { HiboutikClient } from './hiboutik.client';
|
||||
import {
|
||||
CustomerAddressDetailsHiboutikDto,
|
||||
customerAddressDetailsHiboutikSchema,
|
||||
CustomerHiboutikDto,
|
||||
customerHiboutikSchema,
|
||||
FullCustomerHiboutikDto,
|
||||
fullCustomerHiboutikSchema,
|
||||
} from '../models/hiboutikDTO/customer.hiboutik.dto';
|
||||
|
||||
/** Reads customers from the Hiboutik API. */
|
||||
@@ -11,29 +14,39 @@ import {
|
||||
export class CustomerHiboutikRepository {
|
||||
constructor(private readonly hiboutikClient: HiboutikClient) {}
|
||||
|
||||
/** Customer summaries only: use {@link getCustomerById} for addresses and sales. */
|
||||
/**
|
||||
* Customer summaries only: use {@link getCustomerById} for addresses and sales. Sorted by id so that the pages are
|
||||
* stable, but a customer created or deleted while they are walked can still shift the following ones.
|
||||
*/
|
||||
async getCustomers(): Promise<CustomerHiboutikDto[]> {
|
||||
return this.hiboutikClient.getAllPages<CustomerHiboutikDto>('/customers');
|
||||
return this.hiboutikClient.getAllPages('/customers', customerHiboutikSchema, {
|
||||
order_by: 'customers_id',
|
||||
sort: 'ASC',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Full customer, with its addresses and sales (Hiboutik answers with a one-element array).
|
||||
* @throws if the customer does not exist.
|
||||
* Full customer, with its addresses and sales.
|
||||
* @returns `null` when Hiboutik says the customer does not exist (404 or empty answer).
|
||||
* @throws on any other failure: an unknown state is never taken for a deleted customer.
|
||||
*/
|
||||
async findCustomerById(id: number): Promise<FullCustomerHiboutikDto | null> {
|
||||
return this.hiboutikClient.getOne(`/customer/${id}`, fullCustomerHiboutikSchema);
|
||||
}
|
||||
|
||||
/** Same as {@link findCustomerById}, but a missing customer throws. */
|
||||
async getCustomerById(id: number): Promise<FullCustomerHiboutikDto> {
|
||||
const data = await this.hiboutikClient.get<FullCustomerHiboutikDto[]>('/customer/' + String(id));
|
||||
const customer = data[0];
|
||||
const customer = await this.findCustomerById(id);
|
||||
if (!customer) throw new Error(`Hiboutik customer ${id} not found`);
|
||||
return customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address with its company details (Hiboutik answers with a one-element array).
|
||||
* Address with its company details.
|
||||
* @throws if the address does not exist.
|
||||
*/
|
||||
async getAddressById(id: number): Promise<CustomerAddressDetailsHiboutikDto> {
|
||||
const data = await this.hiboutikClient.get<CustomerAddressDetailsHiboutikDto[]>('/customers_addresses/' + String(id));
|
||||
const address = data[0];
|
||||
const address = await this.hiboutikClient.getOne(`/customers_addresses/${id}`, customerAddressDetailsHiboutikSchema);
|
||||
if (!address) throw new Error(`Hiboutik address ${id} not found`);
|
||||
return address;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user