151 lines
7.0 KiB
TypeScript
151 lines
7.0 KiB
TypeScript
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();
|
|
|
|
// Safety net: an empty answer from Hiboutik most likely means an API issue
|
|
let nbDeleted = 0;
|
|
if (hiboutikCustomers.length === 0) {
|
|
this.logger.warn('Hiboutik returned no customers: deletion skipped.');
|
|
} else {
|
|
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<number> {
|
|
// 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<CustomerAddressHiboutikDto | CustomerAddressDetailsHiboutikDto> {
|
|
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<number> {
|
|
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}`,
|
|
);
|
|
}
|
|
}
|