Make product structure sync use product export
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m23s

This commit is contained in:
Cauê Faleiros
2026-07-23 14:03:43 -03:00
parent bda1e22be3
commit 12aa5c7f83
6 changed files with 7681 additions and 5 deletions

2
.gitignore vendored
View File

@@ -3,3 +3,5 @@ dist
.env
graphs_backfill_state.json
graphs_backfill_stop.json
product_structures_sync_state.json
product_structures_sync_stop.json

View File

@@ -22,6 +22,7 @@ RUN npm install --omit=dev
# Copy built code from the builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/data ./data
# Expose the port the app runs on
EXPOSE 3000

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,10 @@ services:
- GRAPHS_BACKFILL_ORDER_DELAY_MS=${GRAPHS_BACKFILL_ORDER_DELAY_MS:-0}
- GRAPHS_BACKFILL_TINY_BLOCK_DELAY_MS=${GRAPHS_BACKFILL_TINY_BLOCK_DELAY_MS:-600000}
- GRAPHS_BACKFILL_MAX_TINY_BLOCK_RETRIES=${GRAPHS_BACKFILL_MAX_TINY_BLOCK_RETRIES:-0}
- PRODUCT_STRUCTURES_GRAPHS_API_URL=${PRODUCT_STRUCTURES_GRAPHS_API_URL}
- PRODUCT_STRUCTURES_GRAPHS_API_KEY=${PRODUCT_STRUCTURES_GRAPHS_API_KEY}
- PRODUCT_STRUCTURES_REQUEST_DELAY_MS=${PRODUCT_STRUCTURES_REQUEST_DELAY_MS:-2500}
- PRODUCT_STRUCTURES_FETCH_COMPONENT_UNITS=${PRODUCT_STRUCTURES_FETCH_COMPONENT_UNITS:-true}
- TINY_FETCH_SELLER_DETAILS=${TINY_FETCH_SELLER_DETAILS:-false}
- TINY_ORDER_DETAILS_CACHE_TTL_MS=${TINY_ORDER_DETAILS_CACHE_TTL_MS:-120000}
- TINY_LIVE_REQUEST_DELAY_MS=${TINY_LIVE_REQUEST_DELAY_MS:-6000}

View File

@@ -10,6 +10,18 @@ dotenv.config();
const app: Express = express();
const port = process.env.PORT || 3000;
function deriveProductionOrdersUrl(env: NodeJS.ProcessEnv) {
const raw = env.GRAPHS_API_URL || env.NEXSTAR_GRAPHS_API_URL || '';
if (!raw) return null;
try {
const parsed = new URL(raw);
return `${parsed.origin}/api/production-orders/tiny-sync`;
} catch {
return raw.replace(/\/api\/data.*$/, '/api/production-orders/tiny-sync');
}
}
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
@@ -228,8 +240,8 @@ app.get('/api/trigger-product-structures-sync', (req: Request, res: Response) =>
options: {
skus: env.PRODUCT_STRUCTURE_SKUS || null,
productIds: env.PRODUCT_STRUCTURE_PRODUCT_IDS || null,
inputFile: env.PRODUCT_STRUCTURES_INPUT_FILE || null,
graphsUrl: env.PRODUCT_STRUCTURES_GRAPHS_API_URL || env.PRODUCTION_ORDERS_GRAPHS_API_URL || env.GRAPHS_PRODUCTION_ORDERS_API_URL || null,
inputFile: env.PRODUCT_STRUCTURES_INPUT_FILE || '/app/data/product-structure-products.csv',
graphsUrl: env.PRODUCT_STRUCTURES_GRAPHS_API_URL || env.PRODUCTION_ORDERS_GRAPHS_API_URL || env.GRAPHS_PRODUCTION_ORDERS_API_URL || deriveProductionOrdersUrl(env),
dryRun: env.PRODUCT_STRUCTURES_DRY_RUN || 'false',
maxProducts: env.PRODUCT_STRUCTURES_MAX_PRODUCTS || null,
requestDelayMs: env.PRODUCT_STRUCTURES_REQUEST_DELAY_MS || '2500',

View File

@@ -48,6 +48,7 @@ type ProductStructurePayload = {
type SyncState = {
status?: 'running' | 'stopped' | 'completed';
processedProducts?: number;
skippedProducts?: number;
failedProducts?: number;
lastSku?: string;
lastFailedSku?: string;
@@ -60,12 +61,25 @@ type RequestedProduct = {
sku: string;
};
function deriveProductionOrdersUrl() {
const raw = process.env.GRAPHS_API_URL || process.env.NEXSTAR_GRAPHS_API_URL || '';
if (!raw) return '';
try {
const parsed = new URL(raw);
return `${parsed.origin}/api/production-orders/tiny-sync`;
} catch {
return raw.replace(/\/api\/data.*$/, '/api/production-orders/tiny-sync');
}
}
const TINY_API_TOKEN = process.env.TINY_API_TOKEN;
const GRAPHS_PRODUCTION_ORDERS_URL = (
process.env.PRODUCT_STRUCTURES_GRAPHS_API_URL ||
process.env.PRODUCTION_ORDERS_GRAPHS_API_URL ||
process.env.GRAPHS_PRODUCTION_ORDERS_API_URL ||
(process.env.GRAPHS_API_BASE_URL ? `${process.env.GRAPHS_API_BASE_URL.replace(/\/+$/, '')}/api/production-orders/tiny-sync` : '') ||
deriveProductionOrdersUrl() ||
'http://localhost:3004/api/production-orders/tiny-sync'
);
const GRAPHS_PRODUCTION_ORDERS_API_KEY = (
@@ -78,7 +92,8 @@ const GRAPHS_PRODUCTION_ORDERS_API_KEY = (
process.env.API_KEY ||
'nexstar_secret_key_123'
);
const INPUT_FILE = process.env.PRODUCT_STRUCTURES_INPUT_FILE || '';
const DEFAULT_INPUT_FILE = path.join(process.cwd(), 'data', 'product-structure-products.csv');
const INPUT_FILE = process.env.PRODUCT_STRUCTURES_INPUT_FILE || (fs.existsSync(DEFAULT_INPUT_FILE) ? DEFAULT_INPUT_FILE : '');
const SKUS = String(process.env.PRODUCT_STRUCTURE_SKUS || '')
.split(',')
.map(value => value.trim())
@@ -447,16 +462,18 @@ async function runSync() {
saveState({
status: 'running',
processedProducts: 0,
skippedProducts: 0,
failedProducts: 0,
lastMessage: 'Product structures sync started'
});
let processedProducts = 0;
let skippedProducts = 0;
let failedProducts = 0;
for (const requested of requestedProducts) {
assertNotStopped();
if (MAX_PRODUCTS && processedProducts + failedProducts >= MAX_PRODUCTS) {
if (MAX_PRODUCTS && processedProducts + skippedProducts + failedProducts >= MAX_PRODUCTS) {
console.log(`[limit] Stopped after ${MAX_PRODUCTS} product(s).`);
break;
}
@@ -470,6 +487,7 @@ async function runSync() {
saveState({
status: 'running',
processedProducts,
skippedProducts,
failedProducts,
lastFailedSku: label,
lastMessage: `Product not found for ${label}`
@@ -483,6 +501,7 @@ async function runSync() {
saveState({
status: 'running',
processedProducts,
skippedProducts,
failedProducts,
lastFailedSku: product.codigo || product.id,
lastMessage: `Could not get structure for ${product.codigo || product.id}`
@@ -490,6 +509,20 @@ async function runSync() {
continue;
}
if (!payload.components.length) {
skippedProducts += 1;
console.log(`[Tiny] No structure components for ${payload.order.productSku}; skipping Graphs send.`);
saveState({
status: 'running',
processedProducts,
skippedProducts,
failedProducts,
lastSku: payload.order.productSku,
lastMessage: `Skipped ${payload.order.productSku}; no structure components`
});
continue;
}
const sent = await sendStructure(payload);
if (sent) {
processedProducts += 1;
@@ -500,6 +533,7 @@ async function runSync() {
saveState({
status: 'running',
processedProducts,
skippedProducts,
failedProducts,
lastSku: sent ? payload.order.productSku : undefined,
lastFailedSku: sent ? undefined : payload.order.productSku,
@@ -512,6 +546,7 @@ async function runSync() {
saveState({
status: 'running',
processedProducts,
skippedProducts,
failedProducts,
lastFailedSku: label,
lastMessage: `Failed product ${label}; continuing`
@@ -522,10 +557,11 @@ async function runSync() {
saveState({
status: 'completed',
processedProducts,
skippedProducts,
failedProducts,
lastMessage: failedProducts ? `Product structures sync completed with ${failedProducts} failed product(s)` : 'Product structures sync completed'
});
console.log(`Done. Synced ${processedProducts} product structure(s). Failed products: ${failedProducts}.`);
console.log(`Done. Synced ${processedProducts} product structure(s). Skipped products: ${skippedProducts}. Failed products: ${failedProducts}.`);
}
runSync().catch((error: any) => {