import { Injectable, Logger } from '@nestjs/common'; import { CustomerHiboutikRepository } from '../../hiboutik/customer.hiboutik.repository'; import { CustomerDatabaseRepository } from '../../database/customer.database.repository'; import { CustomerConverter } from '../../models/converter/customer.converter'; import { CustomerAddressDatabaseRepository } from '../../database/customerAddress.database.repository'; import { CustomerAddressConverter } from '../../models/converter/customerAddress.converter'; import { CustomerAddressDetailsHiboutikDto, CustomerAddressHiboutikDto, FullCustomerHiboutikDto, } from '../../models/hiboutikDTO/customer.hiboutik.dto'; import { forEachSafely } from '../../utils/sync'; import { SaleHiboutikRepository } from '../../hiboutik/sale.hiboutik.repository'; import { OrderDatabaseRepository } from '../../database/order.database.repository'; import { OrderConverter } from '../../models/converter/order.converter'; @Injectable() export class CustomerHiboutikService { private readonly logger = new Logger('CustomerHiboutikService'); constructor( private readonly customerHiboutikRepository: CustomerHiboutikRepository, private readonly customerDatabaseRepository: CustomerDatabaseRepository, private readonly customerConverter: CustomerConverter, private readonly customerAddressDatabaseRepository: CustomerAddressDatabaseRepository, private readonly customerAddressConverter: CustomerAddressConverter, private readonly saleHiboutikRepository: SaleHiboutikRepository, private readonly orderDatabaseRepository: OrderDatabaseRepository, private readonly orderConverter: OrderConverter, ) {} /** * Mirrors Hiboutik customers: deletes the customers missing from Hiboutik, then fetches and upserts each of them * with its addresses and orders (one API call per customer, as the list endpoint lacks addresses and sales, plus one * per sale not yet stored as completed). */ async syncCustomers() { const hiboutikCustomers = await this.customerHiboutikRepository.getCustomers(); // Hiboutik is trusted: a successful (2xx) answer, even an empty list, is the truth. An HTTP error throws before this point. const nbDeleted = await this.customerDatabaseRepository.deleteMissing(hiboutikCustomers.map((dto) => Number(dto.customers_id))); let nbFailed = 0; let nbFailedAddresses = 0; let nbFailedOrders = 0; for (const hiboutikCustomer of hiboutikCustomers) { try { const hiboutikFullCustomer = await this.customerHiboutikRepository.getCustomerById(hiboutikCustomer.customers_id); const customer = await this.customerDatabaseRepository.upsert(this.customerConverter.fromHiboutik(hiboutikFullCustomer)); nbFailedAddresses += await this.syncAddresses(customer.id, hiboutikFullCustomer); nbFailedOrders += await this.syncOrders(hiboutikFullCustomer); } catch (error) { nbFailed++; this.logger.error(`Failed to sync Hiboutik customer ${hiboutikCustomer.customers_id}`, (error as Error)?.stack); } } this.logger.log( `Customers sync ended: ${hiboutikCustomers.length - nbFailed}/${hiboutikCustomers.length} synced, ${nbFailed} failed, ${nbDeleted} deleted, ${nbFailedAddresses} addresses failed, ${nbFailedOrders} orders failed.`, ); } /** * Mirrors the addresses embedded in a full Hiboutik customer: deletes the ones missing from Hiboutik, then upserts * each of them (a failing address is logged and skipped). * @returns the number of failed addresses. */ private async syncAddresses(customerId: string, hiboutikCustomer: FullCustomerHiboutikDto): Promise { // A missing array means an unexpected payload, not a customer without addresses: keep the stored ones if (!Array.isArray(hiboutikCustomer.addresses)) { this.logger.warn(`Hiboutik customer ${hiboutikCustomer.customers_id} has no addresses array: addresses sync skipped.`); return 0; } await this.customerAddressDatabaseRepository.deleteMissing( customerId, hiboutikCustomer.addresses.map((dto) => Number(dto.address_id)), ); return forEachSafely( hiboutikCustomer.addresses, // Linked to the parent customer, whatever the embedded customers_id says async (dto) => { const address = dto.company?.trim() ? await this.withCompanyDetails(dto) : dto; await this.customerAddressDatabaseRepository.upsert( this.customerAddressConverter.fromHiboutik({ ...address, customers_id: hiboutikCustomer.customers_id }), ); }, this.logger, (dto) => `Hiboutik address ${dto.address_id} of customer ${hiboutikCustomer.customers_id}`, ); } /** * Completes an embedded address with its company details (one API call). On failure, the embedded address is returned * as is, so the address is still synced and its stored company details are kept. */ private async withCompanyDetails( dto: CustomerAddressHiboutikDto, ): Promise { try { const details = await this.customerHiboutikRepository.getAddressById(dto.address_id); return { ...dto, tax_number: details.tax_number, company_number: details.company_number, legal_status: details.legal_status, updated_at: details.updated_at, }; } catch (error) { this.logger.warn(`Failed to fetch company details of Hiboutik address ${dto.address_id}: ${(error as Error)?.message}`); return dto; } } /** * Mirrors the sales of a full Hiboutik customer as orders: each sale not yet stored as completed is fetched (for its * lines) and upserted (a failing sale is logged and skipped). Quotations are ignored. Orders are never deleted. * @returns the number of failed sales. */ private async syncOrders(hiboutikCustomer: FullCustomerHiboutikDto): Promise { if (!Array.isArray(hiboutikCustomer.sales)) { this.logger.warn(`Hiboutik customer ${hiboutikCustomer.customers_id} has no sales array: orders sync skipped.`); return 0; } // A completed sale no longer changes in Hiboutik: no need to fetch it again const completedSaleIds = await this.orderDatabaseRepository.getCompletedSaleIds( hiboutikCustomer.sales.map((dto) => Number(dto.sale_id)), ); const salesToSync = hiboutikCustomer.sales.filter((dto) => !completedSaleIds.has(Number(dto.sale_id))); return forEachSafely( salesToSync, async (dto) => { const sale = await this.saleHiboutikRepository.getSaleById(dto.sale_id); if (Number(sale.quotation) === 1) return; // Linked to the parent customer, whatever the sale says await this.orderDatabaseRepository.upsert( this.orderConverter.fromHiboutik({ ...sale, customer_id: hiboutikCustomer.customers_id }), ); }, this.logger, (dto) => `Hiboutik sale ${dto.sale_id} of customer ${hiboutikCustomer.customers_id}`, ); } }