feat(sync): synchronize Hiboutik data through POST /sync
POST /sync starts the full synchronization in the background (202, or 409 when one is already running). Entities are synced in dependency order: brands, categories, suppliers, products (with versions and stocks), customers. Each row is synced independently so that a failure is logged without stopping the rest. Replaces the default AppService.
This commit is contained in:
51
src/services/hiboutik/category.hiboutik.service.ts
Normal file
51
src/services/hiboutik/category.hiboutik.service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { CategoryHiboutikRepository } from '../../hiboutik/category.hiboutik.repository';
|
||||
import { CategoryDatabaseRepository } from '../../database/category.database.repository';
|
||||
import { CategoryConverter } from '../../models/converter/category.converter';
|
||||
import { forEachSafely } from '../../utils/sync';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryHiboutikService {
|
||||
private readonly logger = new Logger('CategoryHiboutikService');
|
||||
|
||||
constructor(
|
||||
private readonly categoryHiboutikRepository: CategoryHiboutikRepository,
|
||||
private readonly categoryDatabaseRepository: CategoryDatabaseRepository,
|
||||
private readonly categoryConverter: CategoryConverter,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Mirrors Hiboutik categories and their tree: upserts each of them, links them to their parent,
|
||||
* then deletes the categories missing from Hiboutik.
|
||||
*/
|
||||
async syncCategories() {
|
||||
const hiboutikCategories = await this.categoryHiboutikRepository.getCategories();
|
||||
|
||||
// Pass 1: categories themselves. One upsert per category: an invalid category must not prevent the others from being saved
|
||||
const nbFailed = await forEachSafely(
|
||||
this.categoryConverter.fromHiboutikList(hiboutikCategories),
|
||||
(category) => this.categoryDatabaseRepository.upsert(category),
|
||||
this.logger,
|
||||
(category) => `Hiboutik category ${category.hiboutikId}`,
|
||||
);
|
||||
|
||||
// Pass 2: parent links, once every category exists (a child can come before its parent in Hiboutik's list)
|
||||
const nbParentsFailed = await forEachSafely(
|
||||
hiboutikCategories,
|
||||
(dto) => this.categoryDatabaseRepository.setParent(Number(dto.category_id), Number(dto.category_id_parent) || null),
|
||||
this.logger,
|
||||
(dto) => `parent of Hiboutik category ${dto.category_id}`,
|
||||
);
|
||||
|
||||
// Safety net: an empty answer from Hiboutik most likely means an API issue
|
||||
if (hiboutikCategories.length === 0) {
|
||||
this.logger.warn('Hiboutik returned no categories: deletion skipped.');
|
||||
return;
|
||||
}
|
||||
const nbDeleted = await this.categoryDatabaseRepository.deleteMissing(hiboutikCategories.map((dto) => Number(dto.category_id)));
|
||||
this.logger.log(
|
||||
`Categories sync ended: ${hiboutikCategories.length - nbFailed}/${hiboutikCategories.length} synced, ${nbFailed} failed, ` +
|
||||
`${nbParentsFailed} parent links failed, ${nbDeleted} deleted.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user