dedupe-return-records.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. const parseUrl = process.env.PARSE_URL || 'http://127.0.0.1:3000/parse';
  2. const appId = process.env.PARSE_APP_ID;
  3. const masterKey = process.env.PARSE_MASTER_KEY;
  4. const fallbackShopId = process.env.FALLBACK_SHOP_ID;
  5. if (!appId || !masterKey || !fallbackShopId) {
  6. throw new Error('Missing PARSE_APP_ID, PARSE_MASTER_KEY, or FALLBACK_SHOP_ID');
  7. }
  8. const headers = {
  9. 'Content-Type': 'application/json',
  10. 'X-Parse-Application-Id': appId,
  11. 'X-Parse-Master-Key': masterKey,
  12. };
  13. const shopPointer = objectId => ({ __type: 'Pointer', className: 'Shop', objectId });
  14. async function request(path, method = 'GET', body) {
  15. const response = await fetch(`${parseUrl}${path}`, {
  16. method,
  17. headers,
  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 allRows(className, keys = '') {
  29. const rows = [];
  30. let skip = 0;
  31. while (true) {
  32. const page = await request(`/classes/${className}?limit=1000&skip=${skip}${keys ? `&keys=${keys}` : ''}`);
  33. rows.push(...(page.results || []));
  34. const length = page.results?.length || 0;
  35. if (length < 1000) return rows;
  36. skip += length;
  37. }
  38. }
  39. async function batch(requests) {
  40. for (let start = 0; start < requests.length; start += 25) {
  41. const result = await request('/batch', 'POST', { requests: requests.slice(start, start + 25) });
  42. const error = result.find?.(item => item.error)?.error;
  43. if (error) throw new Error(error.error || error.code || 'Parse batch failed');
  44. }
  45. }
  46. function returnKey(row) {
  47. const date = row.return_date?.iso || row.returnDate?.iso || row.return_date || row.returnDate || '';
  48. const orderId = row.order_id || row.orderId || row.amazon_order_id || '';
  49. const sku = row.sku || row.seller_sku || '';
  50. return [date, orderId, sku, row.asin || '', row.license_plate_number || ''].join('|');
  51. }
  52. const [returns, orders] = await Promise.all([
  53. allRows('ReturnRecord'),
  54. allRows('Order', 'platformOrderId,orderId,shop'),
  55. ]);
  56. const orderShop = new Map();
  57. for (const order of orders) {
  58. for (const id of [order.platformOrderId, order.orderId]) {
  59. if (id && order.shop?.objectId) orderShop.set(id, order.shop.objectId);
  60. }
  61. }
  62. const groups = new Map();
  63. for (const row of returns) {
  64. const key = returnKey(row);
  65. if (!groups.has(key)) groups.set(key, []);
  66. groups.get(key).push(row);
  67. }
  68. const updates = [];
  69. const deletes = [];
  70. let matchedOrders = 0;
  71. for (const rows of groups.values()) {
  72. const orderId = rows[0].order_id || rows[0].orderId || rows[0].amazon_order_id || '';
  73. const targetShopId = orderShop.get(orderId) || fallbackShopId;
  74. if (orderShop.has(orderId)) matchedOrders++;
  75. const keep = rows.find(row => row.shop?.objectId === targetShopId) || rows[0];
  76. updates.push({
  77. method: 'PUT',
  78. path: `/parse/classes/ReturnRecord/${keep.objectId}`,
  79. body: {
  80. shop: shopPointer(targetShopId),
  81. orderId,
  82. returnDate: keep.returnDate || keep.return_date,
  83. asin: keep.asin || '',
  84. sku: keep.sku || keep.seller_sku || '',
  85. reason: keep.reason || '',
  86. quantity: Number(keep.quantity || 0),
  87. comment: keep.customer_comments || keep.comment || '',
  88. disposition: keep.detailed_disposition || keep.disposition || '',
  89. fulfillmentCenterId: keep.fulfillment_center_id || keep.fulfillmentCenterId || '',
  90. licensePlateNumber: keep.license_plate_number || keep.licensePlateNumber || '',
  91. },
  92. });
  93. for (const row of rows) {
  94. if (row.objectId === keep.objectId) continue;
  95. deletes.push({ method: 'DELETE', path: `/parse/classes/ReturnRecord/${row.objectId}` });
  96. }
  97. }
  98. await batch(updates);
  99. await batch(deletes);
  100. const after = await allRows('ReturnRecord', 'objectId,shop,orderId');
  101. console.log(JSON.stringify({
  102. before: returns.length,
  103. uniqueBusinessKeys: groups.size,
  104. matchedOrders,
  105. fallbackAssignments: groups.size - matchedOrders,
  106. updated: updates.length,
  107. deleted: deletes.length,
  108. after: after.length,
  109. }, null, 2));