full-sp-api-sync.mjs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. const parseUrl = process.env.PARSE_URL || 'http://127.0.0.1:3000/parse';
  2. const apiUrl = process.env.API_URL || 'http://127.0.0.1:3000/api/amazon';
  3. const appId = process.env.PARSE_APP_ID;
  4. const masterKey = process.env.PARSE_MASTER_KEY;
  5. const targetSellerId = process.env.SELLER_ID;
  6. if (!appId || !masterKey || !targetSellerId) {
  7. throw new Error('Missing PARSE_APP_ID, PARSE_MASTER_KEY, or SELLER_ID');
  8. }
  9. const parseHeaders = {
  10. 'Content-Type': 'application/json',
  11. 'X-Parse-Application-Id': appId,
  12. 'X-Parse-Master-Key': masterKey,
  13. };
  14. async function parseRequest(path, method = 'GET', body) {
  15. const response = await fetch(`${parseUrl}${path}`, {
  16. method,
  17. headers: parseHeaders,
  18. body: body === undefined ? undefined : JSON.stringify(body),
  19. });
  20. const text = await response.text();
  21. let result = {};
  22. try { result = JSON.parse(text); } catch {}
  23. if (!response.ok || result.error) {
  24. throw new Error(`${method} ${path}: ${result.error?.message || result.error || text.slice(0, 300) || response.status}`);
  25. }
  26. return result;
  27. }
  28. async function countRows(className, shopId) {
  29. const where = encodeURIComponent(JSON.stringify({
  30. shop: { __type: 'Pointer', className: 'Shop', objectId: shopId },
  31. }));
  32. let total = 0;
  33. let skip = 0;
  34. while (true) {
  35. const page = await parseRequest(`/classes/${className}?where=${where}&keys=objectId&limit=1000&skip=${skip}`);
  36. const length = page.results?.length || 0;
  37. total += length;
  38. if (length < 1000) return total;
  39. skip += length;
  40. }
  41. }
  42. async function forward(shopId, payload, timeoutMs = 45 * 60 * 1000) {
  43. const response = await fetch(`${apiUrl}/forward`, {
  44. method: 'POST',
  45. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  46. body: JSON.stringify(payload),
  47. signal: AbortSignal.timeout(timeoutMs),
  48. });
  49. const text = await response.text();
  50. let result = {};
  51. try { result = JSON.parse(text); } catch {}
  52. if (!response.ok || result.success === false) {
  53. throw new Error(result.message || result.errors?.[0]?.message || text.slice(0, 300) || `HTTP ${response.status}`);
  54. }
  55. return result;
  56. }
  57. function log(event, data = {}) {
  58. console.log(JSON.stringify({ at: new Date().toISOString(), event, ...data }));
  59. }
  60. const shopResult = await parseRequest('/classes/Shop?limit=1000');
  61. const shops = (shopResult.results || []).filter(shop => {
  62. const config = shop.config?.SpApiConfig;
  63. return shop.platform === 'amazon'
  64. && shop.status === 'active'
  65. && shop.disable !== true
  66. && shop.deleted !== true
  67. && config?.sellerID === targetSellerId;
  68. });
  69. const startedAt = new Date();
  70. const ordersStart = new Date(startedAt.getTime() - 729 * 24 * 60 * 60 * 1000).toISOString();
  71. const listingsStart = new Date(startedAt.getTime() - 365 * 24 * 60 * 60 * 1000).toISOString();
  72. const summary = [];
  73. log('sync_started', {
  74. sellerId: targetSellerId,
  75. shops: shops.length,
  76. ordersStart,
  77. listingsStart,
  78. });
  79. for (const shop of shops) {
  80. const config = shop.config.SpApiConfig;
  81. const item = {
  82. shopId: shop.objectId,
  83. shopName: shop.name,
  84. marketplaceId: shop.marketplaceId,
  85. listingEnabled: config.listingEnabled !== false,
  86. orders: {},
  87. listings: {},
  88. errors: [],
  89. };
  90. summary.push(item);
  91. log('shop_started', item);
  92. const orderBefore = await countRows('Order', shop.objectId);
  93. try {
  94. await forward(shop.objectId, {
  95. path: `/orders/v0/orders?MarketplaceIds=${encodeURIComponent(shop.marketplaceId)}&CreatedAfter=${encodeURIComponent(ordersStart)}`,
  96. method: 'GET',
  97. functionName: 'cleanOrders',
  98. });
  99. const orderAfter = await countRows('Order', shop.objectId);
  100. item.orders = { success: true, before: orderBefore, after: orderAfter, added: orderAfter - orderBefore };
  101. log('orders_completed', { shopId: shop.objectId, shopName: shop.name, ...item.orders });
  102. } catch (error) {
  103. item.orders = { success: false, before: orderBefore, after: await countRows('Order', shop.objectId), error: error.message };
  104. item.errors.push(`orders: ${error.message}`);
  105. log('orders_failed', { shopId: shop.objectId, shopName: shop.name, ...item.orders });
  106. }
  107. const listingBefore = await countRows('Listing', shop.objectId);
  108. if (config.listingEnabled === false) {
  109. item.listings = { success: false, skipped: true, before: listingBefore, after: listingBefore, reason: 'regional seller ID not authorized' };
  110. log('listings_skipped', { shopId: shop.objectId, shopName: shop.name, ...item.listings });
  111. } else {
  112. try {
  113. const path = `/listings/2021-08-01/items/${encodeURIComponent(targetSellerId)}`
  114. + `?marketplaceIds=${encodeURIComponent(shop.marketplaceId)}`
  115. + '&includedData=summaries,attributes,issues,fulfillmentAvailability'
  116. + '&withStatus=BUYABLE,DISCOVERABLE'
  117. + '&sortBy=lastUpdatedDate&sortOrder=ASC&pageSize=10'
  118. + `&lastUpdatedAfter=${encodeURIComponent(listingsStart)}`;
  119. await forward(shop.objectId, { path, method: 'GET', functionName: 'cleanListings' });
  120. const listingAfter = await countRows('Listing', shop.objectId);
  121. item.listings = { success: true, before: listingBefore, after: listingAfter, added: listingAfter - listingBefore };
  122. log('listings_completed', { shopId: shop.objectId, shopName: shop.name, ...item.listings });
  123. } catch (error) {
  124. item.listings = { success: false, before: listingBefore, after: await countRows('Listing', shop.objectId), error: error.message };
  125. item.errors.push(`listings: ${error.message}`);
  126. log('listings_failed', { shopId: shop.objectId, shopName: shop.name, ...item.listings });
  127. }
  128. }
  129. const completed = item.errors.length === 0;
  130. await parseRequest(`/classes/Shop/${shop.objectId}`, 'PUT', {
  131. lastSyncTime: { __type: 'Date', iso: new Date().toISOString() },
  132. last_sync_at: { __type: 'Date', iso: new Date().toISOString() },
  133. syncStatus: completed ? 'completed' : 'partial',
  134. sync_status: completed ? 'completed' : 'partial',
  135. sync_error: item.errors.join('; '),
  136. });
  137. log('shop_completed', { shopId: shop.objectId, shopName: shop.name, success: completed });
  138. }
  139. const result = {
  140. sellerId: targetSellerId,
  141. startedAt: startedAt.toISOString(),
  142. finishedAt: new Date().toISOString(),
  143. shops: summary.length,
  144. orderSuccesses: summary.filter(item => item.orders.success).length,
  145. listingSuccesses: summary.filter(item => item.listings.success).length,
  146. listingSkips: summary.filter(item => item.listings.skipped).length,
  147. errors: summary.flatMap(item => item.errors.map(error => `${item.shopName}: ${error}`)),
  148. details: summary,
  149. };
  150. log('sync_finished', result);