process-sp-api-reports.mjs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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 rounds = Number(process.env.REPORT_POLL_ROUNDS || 12);
  6. const roundDelayMs = Number(process.env.REPORT_POLL_DELAY_MS || 30000);
  7. if (!appId || !masterKey) throw new Error('Missing PARSE_APP_ID or PARSE_MASTER_KEY');
  8. const headers = {
  9. 'Content-Type': 'application/json',
  10. 'X-Parse-Application-Id': appId,
  11. 'X-Parse-Master-Key': masterKey,
  12. };
  13. const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
  14. function log(event, data = {}) {
  15. console.log(JSON.stringify({ at: new Date().toISOString(), event, ...data }));
  16. }
  17. async function parseRequest(path, method = 'GET', body) {
  18. const response = await fetch(`${parseUrl}${path}`, {
  19. method,
  20. headers,
  21. body: body === undefined ? undefined : JSON.stringify(body),
  22. });
  23. const text = await response.text();
  24. let result = {};
  25. try { result = JSON.parse(text); } catch {}
  26. if (!response.ok || result.error) {
  27. throw new Error(`${method} ${path}: ${result.error?.message || result.error || text.slice(0, 300) || response.status}`);
  28. }
  29. return result;
  30. }
  31. async function amazonRequest(shopId, path, body) {
  32. for (let attempt = 1; attempt <= 5; attempt++) {
  33. try {
  34. const response = await fetch(`${apiUrl}${path}`, {
  35. method: 'POST',
  36. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  37. body: JSON.stringify(body),
  38. signal: AbortSignal.timeout(180000),
  39. });
  40. const text = await response.text();
  41. let result = {};
  42. try { result = JSON.parse(text); } catch {}
  43. if (!response.ok || result.success === false) {
  44. throw new Error(result.message || result.errors?.[0]?.message || text.slice(0, 300) || `HTTP ${response.status}`);
  45. }
  46. return result.data || result;
  47. } catch (error) {
  48. if (attempt === 5) throw error;
  49. const delay = attempt * 15000;
  50. log('amazon_retry', { shopId, path, attempt, delay, error: error.message });
  51. await sleep(delay);
  52. }
  53. }
  54. }
  55. async function getReports() {
  56. const reports = [];
  57. let skip = 0;
  58. while (true) {
  59. const page = await parseRequest(`/classes/Reports?limit=1000&skip=${skip}&order=createdAt`);
  60. reports.push(...(page.results || []));
  61. const length = page.results?.length || 0;
  62. if (length < 1000) return reports;
  63. skip += length;
  64. }
  65. }
  66. async function updateReport(reportId, body) {
  67. return parseRequest(`/classes/Reports/${reportId}`, 'PUT', body);
  68. }
  69. const finalStates = new Set(['DONE', 'CANCELLED', 'FATAL']);
  70. const processedReports = new Set();
  71. let totalParsedRecords = 0;
  72. for (let round = 1; round <= rounds; round++) {
  73. const reports = (await getReports()).filter(report => report.isParsed !== true);
  74. let pending = 0;
  75. let processedThisRound = 0;
  76. const statusCounts = {};
  77. log('report_round_started', { round, reports: reports.length });
  78. for (const report of reports) {
  79. const shopId = report.shop?.objectId;
  80. const amazonReportId = report.reportId;
  81. if (!shopId || !amazonReportId) {
  82. log('report_skipped', { objectId: report.objectId, reason: 'missing shop pointer or reportId' });
  83. continue;
  84. }
  85. let status = report.processingStatus || 'IN_QUEUE';
  86. let documentId = report.reportDocumentId || '';
  87. if (!finalStates.has(status)) {
  88. try {
  89. const result = await amazonRequest(shopId, '/forward', {
  90. path: `/reports/2021-06-30/reports/${amazonReportId}`,
  91. method: 'GET',
  92. });
  93. status = result.processingStatus || status;
  94. documentId = result.reportDocumentId || documentId;
  95. await updateReport(report.objectId, {
  96. processingStatus: status,
  97. ...(documentId ? { reportDocumentId: documentId } : {}),
  98. ...(result.processingStartTime ? { processingStartTime: { __type: 'Date', iso: result.processingStartTime } } : {}),
  99. ...(result.processingEndTime ? { processingEndTime: { __type: 'Date', iso: result.processingEndTime } } : {}),
  100. });
  101. log('report_status', { objectId: report.objectId, shopId, reportId: amazonReportId, status, hasDocument: Boolean(documentId) });
  102. } catch (error) {
  103. pending++;
  104. log('report_status_failed', { objectId: report.objectId, shopId, reportId: amazonReportId, error: error.message });
  105. continue;
  106. }
  107. }
  108. statusCounts[status] = (statusCounts[status] || 0) + 1;
  109. if (status !== 'DONE') {
  110. if (!finalStates.has(status)) pending++;
  111. continue;
  112. }
  113. if (!documentId) {
  114. pending++;
  115. log('report_waiting_document', { objectId: report.objectId, shopId, reportId: amazonReportId });
  116. continue;
  117. }
  118. if (processedReports.has(report.objectId)) continue;
  119. try {
  120. const result = await amazonRequest(shopId, '/processReport', {
  121. reportDocumentId: documentId,
  122. reportType: report.reportType,
  123. });
  124. const processResult = result.processResult || result.data?.processResult || {};
  125. if (processResult.success === false) throw new Error(processResult.message || 'report parser returned failure');
  126. const parsed = Number(processResult.processed || 0);
  127. await updateReport(report.objectId, {
  128. isParsed: true,
  129. parsedAt: { __type: 'Date', iso: new Date().toISOString() },
  130. parsedRecords: parsed,
  131. });
  132. processedReports.add(report.objectId);
  133. totalParsedRecords += parsed;
  134. processedThisRound++;
  135. log('report_parsed', { objectId: report.objectId, shopId, reportId: amazonReportId, reportType: report.reportType, parsed });
  136. } catch (error) {
  137. pending++;
  138. log('report_parse_failed', { objectId: report.objectId, shopId, reportId: amazonReportId, error: error.message });
  139. }
  140. await sleep(1000);
  141. }
  142. log('report_round_finished', { round, pending, processedThisRound, statusCounts, totalParsedRecords });
  143. if (pending === 0) break;
  144. if (round < rounds) await sleep(roundDelayMs);
  145. }
  146. const remaining = (await getReports()).filter(report => report.isParsed !== true && !['CANCELLED', 'FATAL'].includes(report.processingStatus));
  147. log('report_processing_finished', {
  148. processedReports: processedReports.size,
  149. totalParsedRecords,
  150. remaining: remaining.length,
  151. remainingStatuses: remaining.reduce((counts, report) => {
  152. const status = report.processingStatus || 'UNKNOWN';
  153. counts[status] = (counts[status] || 0) + 1;
  154. return counts;
  155. }, {}),
  156. });