Add Olist V3 product structure sync
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import crypto from 'crypto';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@@ -47,6 +48,9 @@ type ProductStructurePayload = {
|
||||
|
||||
type SyncState = {
|
||||
status?: 'running' | 'stopped' | 'completed';
|
||||
sourceSignature?: string;
|
||||
totalProducts?: number;
|
||||
nextIndex?: number;
|
||||
processedProducts?: number;
|
||||
skippedProducts?: number;
|
||||
failedProducts?: number;
|
||||
@@ -120,9 +124,11 @@ const REQUEST_DELAY_MS = numberEnv('PRODUCT_STRUCTURES_REQUEST_DELAY_MS', 2500);
|
||||
const GRAPHS_RETRY_DELAY_MS = numberEnv('PRODUCT_STRUCTURES_GRAPHS_RETRY_DELAY_MS', 30000);
|
||||
const GRAPHS_MAX_RETRIES = numberEnv('PRODUCT_STRUCTURES_GRAPHS_MAX_RETRIES', 3);
|
||||
const MAX_PRODUCTS = numberEnv('PRODUCT_STRUCTURES_MAX_PRODUCTS', 0);
|
||||
const RESUME = process.env.PRODUCT_STRUCTURES_RESUME !== 'false';
|
||||
|
||||
let nextRequestAt = 0;
|
||||
const productDetailCache = new Map<string, TinyProduct | null>();
|
||||
let latestState: SyncState | null = null;
|
||||
|
||||
class SyncStoppedError extends Error {
|
||||
constructor() {
|
||||
@@ -159,10 +165,23 @@ async function waitForRequestSlot(reason: string) {
|
||||
}
|
||||
|
||||
function saveState(state: SyncState) {
|
||||
fs.writeFileSync(STATE_FILE, JSON.stringify({
|
||||
latestState = {
|
||||
...state,
|
||||
updatedAt: new Date().toISOString()
|
||||
}, null, 2));
|
||||
};
|
||||
fs.writeFileSync(STATE_FILE, JSON.stringify(latestState, null, 2));
|
||||
}
|
||||
|
||||
function readState(): SyncState | null {
|
||||
if (!fs.existsSync(STATE_FILE)) return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
||||
return parsed && typeof parsed === 'object' ? parsed : null;
|
||||
} catch (error: any) {
|
||||
console.warn(`[state] Could not read previous state; starting from the beginning: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function describeRequestError(error: any) {
|
||||
@@ -457,29 +476,58 @@ async function runSync() {
|
||||
console.log(`[config] Graphs URL: ${GRAPHS_PRODUCTION_ORDERS_URL}`);
|
||||
console.log(`[config] Dry run: ${DRY_RUN ? 'yes' : 'no'}`);
|
||||
console.log(`[config] Request pace: 1 request every ${REQUEST_DELAY_MS}ms`);
|
||||
console.log(`[config] Resume: ${RESUME ? 'yes' : 'no'}`);
|
||||
console.log(`[config] State file: ${STATE_FILE}`);
|
||||
console.log(`[config] Stop file: ${STOP_FILE}`);
|
||||
console.log(`[source] Found ${requestedProducts.length} requested product(s).`);
|
||||
|
||||
const sourceSignature = crypto
|
||||
.createHash('sha256')
|
||||
.update(requestedProducts.map(product => `${product.id}|${product.sku}`).join('\n'))
|
||||
.digest('hex');
|
||||
const previousState = readState();
|
||||
const canResume = RESUME
|
||||
&& !SKUS.length
|
||||
&& !PRODUCT_IDS.length
|
||||
&& previousState?.status !== 'completed'
|
||||
&& previousState?.sourceSignature === sourceSignature
|
||||
&& Number.isInteger(previousState?.nextIndex)
|
||||
&& (previousState?.nextIndex || 0) > 0
|
||||
&& (previousState?.nextIndex || 0) < requestedProducts.length;
|
||||
const startIndex = canResume ? previousState!.nextIndex! : 0;
|
||||
let processedProducts = canResume ? previousState?.processedProducts || 0 : 0;
|
||||
let skippedProducts = canResume ? previousState?.skippedProducts || 0 : 0;
|
||||
let failedProducts = canResume ? previousState?.failedProducts || 0 : 0;
|
||||
let handledThisRun = 0;
|
||||
let nextIndex = startIndex;
|
||||
let reachedLimit = false;
|
||||
|
||||
if (canResume) {
|
||||
console.log(`[state] Resuming at ${startIndex + 1}/${requestedProducts.length} after ${previousState?.lastSku || 'the last checkpoint'}.`);
|
||||
}
|
||||
|
||||
saveState({
|
||||
status: 'running',
|
||||
processedProducts: 0,
|
||||
skippedProducts: 0,
|
||||
failedProducts: 0,
|
||||
lastMessage: 'Product structures sync started'
|
||||
sourceSignature,
|
||||
totalProducts: requestedProducts.length,
|
||||
nextIndex: startIndex,
|
||||
processedProducts,
|
||||
skippedProducts,
|
||||
failedProducts,
|
||||
lastSku: canResume ? previousState?.lastSku : undefined,
|
||||
lastMessage: canResume ? 'Product structures sync resumed' : 'Product structures sync started'
|
||||
});
|
||||
|
||||
let processedProducts = 0;
|
||||
let skippedProducts = 0;
|
||||
let failedProducts = 0;
|
||||
|
||||
for (const requested of requestedProducts) {
|
||||
for (let index = startIndex; index < requestedProducts.length; index += 1) {
|
||||
assertNotStopped();
|
||||
if (MAX_PRODUCTS && processedProducts + skippedProducts + failedProducts >= MAX_PRODUCTS) {
|
||||
if (MAX_PRODUCTS && handledThisRun >= MAX_PRODUCTS) {
|
||||
console.log(`[limit] Stopped after ${MAX_PRODUCTS} product(s).`);
|
||||
reachedLimit = true;
|
||||
break;
|
||||
}
|
||||
|
||||
nextIndex = index + 1;
|
||||
const requested = requestedProducts[index];
|
||||
const label = requested.sku || requested.id;
|
||||
try {
|
||||
const product = await resolveProduct(requested);
|
||||
@@ -488,12 +536,16 @@ async function runSync() {
|
||||
failedProducts += 1;
|
||||
saveState({
|
||||
status: 'running',
|
||||
sourceSignature,
|
||||
totalProducts: requestedProducts.length,
|
||||
nextIndex,
|
||||
processedProducts,
|
||||
skippedProducts,
|
||||
failedProducts,
|
||||
lastFailedSku: label,
|
||||
lastMessage: `Product not found for ${label}`
|
||||
});
|
||||
handledThisRun += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -502,12 +554,16 @@ async function runSync() {
|
||||
failedProducts += 1;
|
||||
saveState({
|
||||
status: 'running',
|
||||
sourceSignature,
|
||||
totalProducts: requestedProducts.length,
|
||||
nextIndex,
|
||||
processedProducts,
|
||||
skippedProducts,
|
||||
failedProducts,
|
||||
lastFailedSku: product.codigo || product.id,
|
||||
lastMessage: `Could not get structure for ${product.codigo || product.id}`
|
||||
});
|
||||
handledThisRun += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -516,12 +572,16 @@ async function runSync() {
|
||||
console.log(`[Tiny] No structure components for ${payload.order.productSku}; skipping Graphs send.`);
|
||||
saveState({
|
||||
status: 'running',
|
||||
sourceSignature,
|
||||
totalProducts: requestedProducts.length,
|
||||
nextIndex,
|
||||
processedProducts,
|
||||
skippedProducts,
|
||||
failedProducts,
|
||||
lastSku: payload.order.productSku,
|
||||
lastMessage: `Skipped ${payload.order.productSku}; no structure components`
|
||||
});
|
||||
handledThisRun += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -534,6 +594,9 @@ async function runSync() {
|
||||
|
||||
saveState({
|
||||
status: 'running',
|
||||
sourceSignature,
|
||||
totalProducts: requestedProducts.length,
|
||||
nextIndex,
|
||||
processedProducts,
|
||||
skippedProducts,
|
||||
failedProducts,
|
||||
@@ -541,35 +604,46 @@ async function runSync() {
|
||||
lastFailedSku: sent ? undefined : payload.order.productSku,
|
||||
lastMessage: sent ? `Synced structure ${payload.order.productSku}` : `Failed structure ${payload.order.productSku}; continuing`
|
||||
});
|
||||
handledThisRun += 1;
|
||||
} catch (error: any) {
|
||||
if (error instanceof SyncStoppedError) throw error;
|
||||
failedProducts += 1;
|
||||
console.error(`[sync] Failed product ${label}: ${describeRequestError(error)}`);
|
||||
saveState({
|
||||
status: 'running',
|
||||
sourceSignature,
|
||||
totalProducts: requestedProducts.length,
|
||||
nextIndex,
|
||||
processedProducts,
|
||||
skippedProducts,
|
||||
failedProducts,
|
||||
lastFailedSku: label,
|
||||
lastMessage: `Failed product ${label}; continuing`
|
||||
});
|
||||
handledThisRun += 1;
|
||||
}
|
||||
}
|
||||
|
||||
saveState({
|
||||
status: 'completed',
|
||||
status: reachedLimit ? 'stopped' : 'completed',
|
||||
sourceSignature,
|
||||
totalProducts: requestedProducts.length,
|
||||
nextIndex,
|
||||
processedProducts,
|
||||
skippedProducts,
|
||||
failedProducts,
|
||||
lastMessage: failedProducts ? `Product structures sync completed with ${failedProducts} failed product(s)` : 'Product structures sync completed'
|
||||
lastMessage: reachedLimit
|
||||
? `Product structures sync paused after ${handledThisRun} product(s)`
|
||||
: (failedProducts ? `Product structures sync completed with ${failedProducts} failed product(s)` : 'Product structures sync completed')
|
||||
});
|
||||
console.log(`Done. Synced ${processedProducts} product structure(s). Skipped products: ${skippedProducts}. Failed products: ${failedProducts}.`);
|
||||
console.log(`${reachedLimit ? 'Paused' : 'Done'}. Synced ${processedProducts} product structure(s). Skipped products: ${skippedProducts}. Failed products: ${failedProducts}.`);
|
||||
}
|
||||
|
||||
runSync().catch((error: any) => {
|
||||
if (error instanceof SyncStoppedError) {
|
||||
console.log('Product structures sync stopped by request.');
|
||||
saveState({
|
||||
...latestState,
|
||||
status: 'stopped',
|
||||
lastMessage: 'Product structures sync stopped by request'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user