const parseUrl = process.env.PARSE_URL || 'http://127.0.0.1:3000/parse'; const appId = process.env.PARSE_APP_ID; const masterKey = process.env.PARSE_MASTER_KEY; const fallbackShopId = process.env.FALLBACK_SHOP_ID; if (!appId || !masterKey || !fallbackShopId) { throw new Error('Missing PARSE_APP_ID, PARSE_MASTER_KEY, or FALLBACK_SHOP_ID'); } const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': appId, 'X-Parse-Master-Key': masterKey, }; const shopPointer = objectId => ({ __type: 'Pointer', className: 'Shop', objectId }); async function request(path, method = 'GET', body) { const response = await fetch(`${parseUrl}${path}`, { method, headers, 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 allRows(className, keys = '') { const rows = []; let skip = 0; while (true) { const page = await request(`/classes/${className}?limit=1000&skip=${skip}${keys ? `&keys=${keys}` : ''}`); rows.push(...(page.results || [])); const length = page.results?.length || 0; if (length < 1000) return rows; skip += length; } } async function batch(requests) { for (let start = 0; start < requests.length; start += 25) { const result = await request('/batch', 'POST', { requests: requests.slice(start, start + 25) }); const error = result.find?.(item => item.error)?.error; if (error) throw new Error(error.error || error.code || 'Parse batch failed'); } } function returnKey(row) { const date = row.return_date?.iso || row.returnDate?.iso || row.return_date || row.returnDate || ''; const orderId = row.order_id || row.orderId || row.amazon_order_id || ''; const sku = row.sku || row.seller_sku || ''; return [date, orderId, sku, row.asin || '', row.license_plate_number || ''].join('|'); } const [returns, orders] = await Promise.all([ allRows('ReturnRecord'), allRows('Order', 'platformOrderId,orderId,shop'), ]); const orderShop = new Map(); for (const order of orders) { for (const id of [order.platformOrderId, order.orderId]) { if (id && order.shop?.objectId) orderShop.set(id, order.shop.objectId); } } const groups = new Map(); for (const row of returns) { const key = returnKey(row); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(row); } const updates = []; const deletes = []; let matchedOrders = 0; for (const rows of groups.values()) { const orderId = rows[0].order_id || rows[0].orderId || rows[0].amazon_order_id || ''; const targetShopId = orderShop.get(orderId) || fallbackShopId; if (orderShop.has(orderId)) matchedOrders++; const keep = rows.find(row => row.shop?.objectId === targetShopId) || rows[0]; updates.push({ method: 'PUT', path: `/parse/classes/ReturnRecord/${keep.objectId}`, body: { shop: shopPointer(targetShopId), orderId, returnDate: keep.returnDate || keep.return_date, asin: keep.asin || '', sku: keep.sku || keep.seller_sku || '', reason: keep.reason || '', quantity: Number(keep.quantity || 0), comment: keep.customer_comments || keep.comment || '', disposition: keep.detailed_disposition || keep.disposition || '', fulfillmentCenterId: keep.fulfillment_center_id || keep.fulfillmentCenterId || '', licensePlateNumber: keep.license_plate_number || keep.licensePlateNumber || '', }, }); for (const row of rows) { if (row.objectId === keep.objectId) continue; deletes.push({ method: 'DELETE', path: `/parse/classes/ReturnRecord/${row.objectId}` }); } } await batch(updates); await batch(deletes); const after = await allRows('ReturnRecord', 'objectId,shop,orderId'); console.log(JSON.stringify({ before: returns.length, uniqueBusinessKeys: groups.size, matchedOrders, fallbackAssignments: groups.size - matchedOrders, updated: updates.length, deleted: deletes.length, after: after.length, }, null, 2));