const parseUrl = process.env.PARSE_URL || 'http://127.0.0.1:3000/parse'; const apiUrl = process.env.API_URL || 'http://127.0.0.1:3000/api/amazon'; const appId = process.env.PARSE_APP_ID; const masterKey = process.env.PARSE_MASTER_KEY; const targetSellerId = process.env.SELLER_ID; if (!appId || !masterKey || !targetSellerId) { throw new Error('Missing PARSE_APP_ID, PARSE_MASTER_KEY, or SELLER_ID'); } const parseHeaders = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': appId, 'X-Parse-Master-Key': masterKey, }; async function parseRequest(path, method = 'GET', body) { const response = await fetch(`${parseUrl}${path}`, { method, headers: parseHeaders, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await response.text(); let result = {}; try { result = JSON.parse(text); } catch {} if (!response.ok || result.error) { throw new Error(`${method} ${path}: ${result.error?.message || result.error || text.slice(0, 300) || response.status}`); } return result; } async function countRows(className, shopId) { const where = encodeURIComponent(JSON.stringify({ shop: { __type: 'Pointer', className: 'Shop', objectId: shopId }, })); let total = 0; let skip = 0; while (true) { const page = await parseRequest(`/classes/${className}?where=${where}&keys=objectId&limit=1000&skip=${skip}`); const length = page.results?.length || 0; total += length; if (length < 1000) return total; skip += length; } } async function forward(shopId, payload, timeoutMs = 45 * 60 * 1000) { const response = await fetch(`${apiUrl}/forward`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId }, body: JSON.stringify(payload), signal: AbortSignal.timeout(timeoutMs), }); const text = await response.text(); let result = {}; try { result = JSON.parse(text); } catch {} if (!response.ok || result.success === false) { throw new Error(result.message || result.errors?.[0]?.message || text.slice(0, 300) || `HTTP ${response.status}`); } return result; } function log(event, data = {}) { console.log(JSON.stringify({ at: new Date().toISOString(), event, ...data })); } const shopResult = await parseRequest('/classes/Shop?limit=1000'); const shops = (shopResult.results || []).filter(shop => { const config = shop.config?.SpApiConfig; return shop.platform === 'amazon' && shop.status === 'active' && shop.disable !== true && shop.deleted !== true && config?.sellerID === targetSellerId; }); const startedAt = new Date(); const ordersStart = new Date(startedAt.getTime() - 729 * 24 * 60 * 60 * 1000).toISOString(); const listingsStart = new Date(startedAt.getTime() - 365 * 24 * 60 * 60 * 1000).toISOString(); const summary = []; log('sync_started', { sellerId: targetSellerId, shops: shops.length, ordersStart, listingsStart, }); for (const shop of shops) { const config = shop.config.SpApiConfig; const item = { shopId: shop.objectId, shopName: shop.name, marketplaceId: shop.marketplaceId, listingEnabled: config.listingEnabled !== false, orders: {}, listings: {}, errors: [], }; summary.push(item); log('shop_started', item); const orderBefore = await countRows('Order', shop.objectId); try { await forward(shop.objectId, { path: `/orders/v0/orders?MarketplaceIds=${encodeURIComponent(shop.marketplaceId)}&CreatedAfter=${encodeURIComponent(ordersStart)}`, method: 'GET', functionName: 'cleanOrders', }); const orderAfter = await countRows('Order', shop.objectId); item.orders = { success: true, before: orderBefore, after: orderAfter, added: orderAfter - orderBefore }; log('orders_completed', { shopId: shop.objectId, shopName: shop.name, ...item.orders }); } catch (error) { item.orders = { success: false, before: orderBefore, after: await countRows('Order', shop.objectId), error: error.message }; item.errors.push(`orders: ${error.message}`); log('orders_failed', { shopId: shop.objectId, shopName: shop.name, ...item.orders }); } const listingBefore = await countRows('Listing', shop.objectId); if (config.listingEnabled === false) { item.listings = { success: false, skipped: true, before: listingBefore, after: listingBefore, reason: 'regional seller ID not authorized' }; log('listings_skipped', { shopId: shop.objectId, shopName: shop.name, ...item.listings }); } else { try { const path = `/listings/2021-08-01/items/${encodeURIComponent(targetSellerId)}` + `?marketplaceIds=${encodeURIComponent(shop.marketplaceId)}` + '&includedData=summaries,attributes,issues,fulfillmentAvailability' + '&withStatus=BUYABLE,DISCOVERABLE' + '&sortBy=lastUpdatedDate&sortOrder=ASC&pageSize=10' + `&lastUpdatedAfter=${encodeURIComponent(listingsStart)}`; await forward(shop.objectId, { path, method: 'GET', functionName: 'cleanListings' }); const listingAfter = await countRows('Listing', shop.objectId); item.listings = { success: true, before: listingBefore, after: listingAfter, added: listingAfter - listingBefore }; log('listings_completed', { shopId: shop.objectId, shopName: shop.name, ...item.listings }); } catch (error) { item.listings = { success: false, before: listingBefore, after: await countRows('Listing', shop.objectId), error: error.message }; item.errors.push(`listings: ${error.message}`); log('listings_failed', { shopId: shop.objectId, shopName: shop.name, ...item.listings }); } } const completed = item.errors.length === 0; await parseRequest(`/classes/Shop/${shop.objectId}`, 'PUT', { lastSyncTime: { __type: 'Date', iso: new Date().toISOString() }, last_sync_at: { __type: 'Date', iso: new Date().toISOString() }, syncStatus: completed ? 'completed' : 'partial', sync_status: completed ? 'completed' : 'partial', sync_error: item.errors.join('; '), }); log('shop_completed', { shopId: shop.objectId, shopName: shop.name, success: completed }); } const result = { sellerId: targetSellerId, startedAt: startedAt.toISOString(), finishedAt: new Date().toISOString(), shops: summary.length, orderSuccesses: summary.filter(item => item.orders.success).length, listingSuccesses: summary.filter(item => item.listings.success).length, listingSkips: summary.filter(item => item.listings.skipped).length, errors: summary.flatMap(item => item.errors.map(error => `${item.shopName}: ${error}`)), details: summary, }; log('sync_finished', result);