| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- 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);
|