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:
MastersGame
2026-11-05 23:00:49 +01:00
parent 27d0e0149d
commit c5f65dcb62
17 changed files with 878 additions and 34 deletions

View File

@@ -0,0 +1,69 @@
import { Injectable, Logger } from '@nestjs/common';
import { BrandHiboutikService } from './brand.hiboutik.service';
import { CategoryHiboutikService } from './category.hiboutik.service';
import { CustomerHiboutikService } from './customer.hiboutik.service';
import { SupplierHiboutikService } from './supplier.hiboutik.service';
import { ProductHiboutikService } from './product.hiboutik.service';
/** Entry point of the Hiboutik → database synchronization: runs each entity sync in dependency order. */
@Injectable()
export class HiboutikService {
private readonly logger = new Logger('HiboutikService');
/** In-memory lock: only valid while the app runs as a single instance. */
private syncInProgress = false;
constructor(
private readonly brandHiboutikService: BrandHiboutikService,
private readonly categoryHiboutikService: CategoryHiboutikService,
private readonly customerHiboutikService: CustomerHiboutikService,
private readonly productHiboutikService: ProductHiboutikService,
private readonly supplierHiboutikService: SupplierHiboutikService,
) {}
get isSyncInProgress(): boolean {
return this.syncInProgress;
}
/**
* Starts a full synchronization in the background.
* @returns false if a synchronization is already running (nothing is started).
*/
startSync(): boolean {
if (this.syncInProgress) return false;
this.syncInProgress = true;
void this.syncAll()
.catch((error) => this.logger.error('Global Hiboutik synchronization crashed', (error as Error)?.stack))
.finally(() => (this.syncInProgress = false));
return true;
}
/** Runs every sync step in order. A failing step is logged and does not stop the next ones. */
async syncAll() {
this.logger.log('Starting global Hiboutik synchronization');
// Order matters: products connect to brands, categories and suppliers by hiboutikId.
const steps: [string, () => Promise<void>][] = [
['brands', () => this.brandHiboutikService.syncBrands()],
['categories', () => this.categoryHiboutikService.syncCategories()],
['suppliers', () => this.supplierHiboutikService.syncSuppliers()],
['products', () => this.productHiboutikService.syncProducts()],
['customers', () => this.customerHiboutikService.syncCustomers()],
];
const failedSteps: string[] = [];
for (const [name, step] of steps) {
try {
await step();
} catch (error) {
failedSteps.push(name);
this.logger.error(`Hiboutik ${name} sync failed`, (error as Error)?.stack);
}
}
if (failedSteps.length > 0) {
this.logger.warn(`Global Hiboutik synchronization ended with errors (failed steps: ${failedSteps.join(', ')})`);
} else {
this.logger.log('Global Hiboutik synchronization ended successfully');
}
}
}