const logger = require('./logger'); const { publicConfig, resolveSyncDateRange } = require('./config'); const { mapProductionOrder } = require('./mapper'); const { TinyClient, findArrayPaths, getTopLevelShape } = require('./tinyClient'); const { ProductionOrderRepository } = require('./db'); const safeRawPreview = (raw, maxKeys = 25) => { if (!raw || typeof raw !== 'object') return raw; const keys = Object.keys(raw).slice(0, maxKeys); return keys.reduce((preview, key) => { preview[key] = raw[key]; return preview; }, {}); }; const orderWritePreview = (order, markers) => ({ production_orders: { tiny_id: order.tinyId, number: order.number, status: order.status, order_reference: order.orderReference, issue_date: order.issueDate, expected_date: order.expectedDate, product_sku: order.productSku, product_description: order.productDescription, quantity: order.quantity, unit: order.unit, integration_status: order.integrationStatus, tiny_payload_keys: order.tinyPayload && typeof order.tinyPayload === 'object' ? Object.keys(order.tinyPayload) : [] }, production_order_markers: markers }); const runSync = async (config) => { const syncDateRange = resolveSyncDateRange(config); logger.info('Start config', { ...publicConfig(config), resolvedDateRange: syncDateRange }); const tinyClient = new TinyClient(config); const repository = config.dryRun ? null : new ProductionOrderRepository(config.databaseUrl); const stats = { pagesFetched: 0, ordersFound: 0, rowsMapped: 0, rowsWouldUpsert: 0, rowsUpserted: 0, skippedInvalidRows: 0, detailErrors: 0 }; if (repository) await repository.connect(); try { let page = 1; let totalPages = 1; do { const pageResult = await tinyClient.listProductionOrders(page, { startDate: syncDateRange.startDate, endDate: syncDateRange.endDate }); stats.pagesFetched += 1; totalPages = pageResult.totalPages; logger.info('Tiny production orders page fetched', { page, totalPages, dateRange: { startDate: syncDateRange.startDate, endDate: syncDateRange.endDate, mode: syncDateRange.mode, source: syncDateRange.source }, endpoint: config.tinyListEndpoint, ordersFound: pageResult.orders.length }); if (config.dryRun) { logger.info('DRY_RUN Tiny payload shape', { page, endpoint: config.tinyListEndpoint, shape: getTopLevelShape(pageResult.raw), arrayPaths: findArrayPaths(pageResult.raw) }); logger.info('DRY_RUN extracted order preview', { page, extractedOrders: pageResult.orders.length, firstOrderRawPreview: safeRawPreview(pageResult.orders[0] || null) }); } for (const summaryRecord of pageResult.orders) { stats.ordersFound += 1; let record = summaryRecord; if (config.tinyFetchDetails) { const mappedSummary = mapProductionOrder(summaryRecord); const tinyId = mappedSummary.order?.tinyId; if (tinyId) { try { record = await tinyClient.getProductionOrderDetail(tinyId); } catch (error) { stats.detailErrors += 1; logger.warn('Tiny detail fetch failed, using list summary payload', { tinyId, error: error.message }); } } } const mapped = mapProductionOrder(record); if (!mapped.order) { stats.skippedInvalidRows += 1; logger.warn('Skipped invalid Tiny production order row', { reason: mapped.reason, rawPreview: safeRawPreview(mapped.raw || record) }); continue; } stats.rowsMapped += 1; if (config.dryRun) { stats.rowsWouldUpsert += 1; logger.info('DRY_RUN would upsert production order', { tinyId: mapped.order.tinyId, number: mapped.order.number, status: mapped.order.status, markers: mapped.markers.length, writePreview: orderWritePreview(mapped.order, mapped.markers), rawPreview: safeRawPreview(mapped.raw) }); continue; } await repository.upsertOrder(mapped.order, mapped.markers); stats.rowsUpserted += 1; } logger.info('Page sync complete', { page, rowsMappedSoFar: stats.rowsMapped, rowsWouldUpsertSoFar: stats.rowsWouldUpsert, rowsUpsertedSoFar: stats.rowsUpserted, skippedInvalidRowsSoFar: stats.skippedInvalidRows }); page += 1; } while (page <= totalPages); logger.info('End summary', stats); return stats; } finally { if (repository) await repository.close(); } }; module.exports = { runSync };