import refreshTokenHelper from "../../../utils/refreshTokenHelper" import forceLogoutHelper from "../../../utils/forceLogoutHelper" import errorHandlingHelper from "../../../utils/errorHandlingHelper" import { resolveOrderSourceAmazonConnection, getListingsItem, patchListingsItemQuantity, updateCachedListingQuantity, withListingsPushQueue } from "../../../utils/amazonSpApi" const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) // POST /api/amazon/stocks/sync // Pushes ONE merchant-fulfilled quantity to Amazon for the Amazon stock page // (/fulfillment/amazon-stocks). Body: { orderSourceId, sku, quantity }. // // Quantity only — no price, no attributes. Mirrors sales/sis/price-update: // Listings Items PATCH of `fulfillment_availability`, which Amazon answers // with an async "ACCEPTED" submission; we poll the read briefly for a // confirmed value and otherwise trust the accepted submission. FBA listings // (fulfillment channel != DEFAULT) are refused — their stock is Amazon's. const handleFunc = async (event: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const body = await readBody(event) const orderSourceId = Number(body?.orderSourceId) const sku = String(body?.sku || '').trim() const quantity = Number(body?.quantity) if (!orderSourceId) return { status: 400, success: false, message: 'orderSourceId is required' } if (!sku) return { status: 400, success: false, message: 'sku is required' } if (!Number.isFinite(quantity) || quantity < 0) return { status: 400, success: false, message: 'quantity must be a number >= 0' } const targetQty = Math.trunc(quantity) const { sellerId, marketplaceId, accessToken } = await resolveOrderSourceAmazonConnection(event, token, orderSourceId) let userLabel = 'unknown' try { const raw = getCookie(event, 'logship_user') const user = raw ? JSON.parse(raw) : null userLabel = user?.Name || user?.email || getCookie(event, 'logship_user_id') || 'unknown' } catch { userLabel = getCookie(event, 'logship_user_id') || 'unknown' } return withListingsPushQueue(orderSourceId, async () => { let before: any = null try { before = await getListingsItem(accessToken, sellerId, sku, marketplaceId) } catch (e: any) { if (Number(e?.status) === 404) { return { status: 404, success: false, sku, message: `SKU "${sku}" is not listed in this marketplace` } } throw e } const channel = String(before?.fulfillmentChannel || 'DEFAULT').toUpperCase() if (channel !== 'DEFAULT') { return { status: 400, success: false, sku, message: `SKU "${sku}" is fulfilled by Amazon (${channel}) — its quantity cannot be set from here` } } let patchResult: any = null try { patchResult = await patchListingsItemQuantity(accessToken, sellerId, sku, marketplaceId, targetQty, before) } catch (e: any) { console.error('[amazon-stocks] qty push FAILED', { user: userLabel, orderSourceId, sku, oldQty: before?.quantity ?? null, targetQty, error: e?.message || String(e) }) throw e } const blockingIssues = (patchResult?.issues || []).filter((i: any) => (i?.severity || '').toUpperCase() !== 'WARNING') if (patchResult?.status !== 'ACCEPTED' || blockingIssues.length > 0) { const msg = blockingIssues.map((i: any) => i.message).join('; ') || `Amazon rejected the submission (status: ${patchResult?.status})` console.error('[amazon-stocks] qty push REJECTED', { user: userLabel, orderSourceId, sku, targetQty, issues: patchResult?.issues }) return { status: 502, success: false, sku, message: `Amazon rejected the quantity update: ${msg}` } } // Poll briefly for the read to catch up — Amazon applies ACCEPTED // submissions asynchronously. Not required for success. let after: any = null for (let attempt = 0; attempt < 4; attempt++) { await sleep(1500) after = await getListingsItem(accessToken, sellerId, sku, marketplaceId).catch(() => null) if (after?.quantity != null && Number(after.quantity) === targetQty) break } const confirmed = after?.quantity != null && Number(after.quantity) === targetQty const pending = !confirmed updateCachedListingQuantity(orderSourceId, sku, targetQty) console.log('[amazon-stocks] qty push', { user: userLabel, orderSourceId, sku, oldQty: before?.quantity ?? null, targetQty, confirmed, pending }) return { status: 200, success: true, sku, oldQty: before?.quantity ?? null, requestedQty: targetQty, updatedQty: targetQty, pending, message: pending ? `Amazon accepted ${targetQty} for ${sku} (still applying)` : `Amazon stock set to ${targetQty} for ${sku}` } }) } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch (err: any) { // An Amazon-side failure is not an auth problem — never re-run the whole // push through the token-refresh path (see sales/sis/price-update). if (err?.isAmazonError) { data = errorHandlingHelper(err?.data ?? err, err?.data ?? err) return { ...data, success: false } } try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch (error: any) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) data = { ...data, success: false } } } return data })