import { string } from 'alga-js' import refreshTokenHelper from "../../utils/refreshTokenHelper" import forceLogoutHelper from "../../utils/forceLogoutHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" import fetchHelper from "../../utils/fetchHelper" // Amazon SP-API endpoints const LWA_TOKEN_URL = 'https://api.amazon.com/auth/o2/token' const SP_API_BASE_URL = 'https://sellingpartnerapi-eu.amazon.com' // EU Marketplace IDs const MARKETPLACE_IDS: Record = { 'DE': 'A1PA6795UKMFR9', 'FR': 'A13V1IB3VIYZZH', 'IT': 'APJ6JRA9NG5V4', 'ES': 'A1RKKUPIHCS9HS', 'NL': 'A1805IZSGTT6HS', 'BE': 'AMEN7PMS3EDWL', 'UK': 'A1F83G8C2ARO7P', 'PL': 'A1C3SOZRARQ6R3', 'SE': 'A2NODRKZP88ZB9', 'AT': 'A2CVHYRTWLQO9T' } // Log collector for debugging const createLogger = () => { const logs: string[] = [] return { log: (message: string) => { const timestamp = new Date().toISOString() logs.push(`[${timestamp}] ${message}`) }, getLogs: () => logs } } // Get LWA access token using refresh token const getAmazonAccessToken = async (clientId: string, clientSecret: string, refreshToken: string, logger?: ReturnType): Promise => { logger?.log('Requesting LWA access token...') const response = await fetch(LWA_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: clientId, client_secret: clientSecret }) }) if (!response.ok) { const errorText = await response.text() logger?.log(`LWA token error (${response.status}): ${errorText}`) throw new Error(`Failed to get Amazon access token (${response.status}): ${errorText}`) } const data: any = await response.json() logger?.log('LWA access token obtained successfully') return data.access_token } // Create feed document (get upload URL) const createFeedDocument = async (accessToken: string, logger?: ReturnType): Promise<{ feedDocumentId: string, url: string }> => { logger?.log('Creating feed document...') const response = await fetch(`${SP_API_BASE_URL}/feeds/2021-06-30/documents`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-amz-access-token': accessToken }, body: JSON.stringify({ contentType: 'application/pdf' }) }) if (!response.ok) { const errorText = await response.text() logger?.log(`createFeedDocument error (${response.status}): ${errorText}`) throw new Error(`Failed to create feed document (${response.status}): ${errorText}`) } const data: any = await response.json() logger?.log(`Feed document created: ${data.feedDocumentId}`) return { feedDocumentId: data.feedDocumentId, url: data.url } } // Upload PDF to the pre-signed URL const uploadPdfToAmazon = async (url: string, pdfBuffer: Buffer, logger?: ReturnType): Promise => { logger?.log(`Uploading PDF (${pdfBuffer.length} bytes) to pre-signed URL...`) const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/pdf' }, body: pdfBuffer }) if (!response.ok) { const errorText = await response.text() logger?.log(`PDF upload error (${response.status}): ${errorText}`) throw new Error(`Failed to upload PDF to Amazon (${response.status}): ${errorText}`) } logger?.log('PDF uploaded successfully') } // Create the feed with UPLOAD_VAT_INVOICE type const createFeed = async ( accessToken: string, feedDocumentId: string, amazonOrderId: string, invoiceNumber: string, documentType: string, marketplaceId: string, logger?: ReturnType ): Promise => { logger?.log(`Creating feed: orderID=${amazonOrderId}, invoice=${invoiceNumber}, type=${documentType}, marketplace=${marketplaceId}`) const feedOptions: Record = { 'metadata:orderid': amazonOrderId, 'metadata:invoicenumber': invoiceNumber, 'metadata:documenttype': documentType } const response = await fetch(`${SP_API_BASE_URL}/feeds/2021-06-30/feeds`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-amz-access-token': accessToken }, body: JSON.stringify({ feedType: 'UPLOAD_VAT_INVOICE', marketplaceIds: [marketplaceId], inputFeedDocumentId: feedDocumentId, feedOptions }) }) if (!response.ok) { const errorText = await response.text() logger?.log(`createFeed error (${response.status}): ${errorText}`) throw new Error(`Failed to create feed (${response.status}): ${errorText}`) } const data: any = await response.json() logger?.log(`Feed created: ${data.feedId}`) return data } const handleFunc = async (event: any, authToken: any = null) => { const logger = createLogger() let data: any = {} const token = authToken ?? await getTokenHelper(event) const body = await readBody(event) const invoiceIds: number[] = body.ids || [] const marketplaceCountry: string = body.marketplaceCountry || 'DE' logger.log(`Starting Amazon invoice upload for IDs: ${JSON.stringify(invoiceIds)}`) if (!invoiceIds.length) { return { status: 400, message: 'No invoice IDs provided', logs: logger.getLogs() } } const marketplaceId = MARKETPLACE_IDS[marketplaceCountry.toUpperCase()] || MARKETPLACE_IDS['DE'] logger.log(`Using marketplace: ${marketplaceCountry} (${marketplaceId})`) const errors: string[] = [] const skipped: string[] = [] let uploadedCount = 0 // Cache for order source credentials (to avoid re-fetching for same order source) const orderSourceCache: Record = {} // Cache for LWA access tokens per order source const accessTokenCache: Record = {} for (const invoiceId of invoiceIds) { try { logger.log(`--- Processing invoice ID: ${invoiceId} ---`) // Fetch invoice with order details const invoice: any = await event.context.fetch( `models/c_invoice/${invoiceId}?$expand=C_Order_ID($select=C_Order_ID,amazon_order_id,C_OrderSource_ID,DocumentNo)`, 'GET', token, null ) if (!invoice) { const msg = `Invoice ${invoiceId} not found` logger.log(msg) errors.push(msg) continue } logger.log(`Invoice found: ${invoice.DocumentNo} (DocBaseType: ${invoice.DocBaseType?.id || invoice.DocBaseType})`) // Check if already uploaded if (invoice.isUploadToAmazon === true || invoice.isUploadToAmazon === 'Y') { const msg = `Invoice ${invoice.DocumentNo} already uploaded to Amazon - skipping` logger.log(msg) skipped.push(msg) continue } // Check if invoice has a linked order with an Amazon order ID const amazonOrderId = invoice.C_Order_ID?.amazon_order_id if (!amazonOrderId) { const msg = `Invoice ${invoice.DocumentNo}: No Amazon order ID found on linked order` logger.log(msg) errors.push(msg) continue } logger.log(`Amazon Order ID: ${amazonOrderId}`) // Fetch order source credentials const orderSourceId = invoice.C_Order_ID?.C_OrderSource_ID?.id if (!orderSourceId) { const msg = `Invoice ${invoice.DocumentNo}: No order source found on linked order` logger.log(msg) errors.push(msg) continue } let orderSource = orderSourceCache[orderSourceId] if (!orderSource) { logger.log(`Fetching order source: ${orderSourceId}`) orderSource = await event.context.fetch( `models/c_ordersource/${orderSourceId}`, 'GET', token, null ) orderSourceCache[orderSourceId] = orderSource } if (!orderSource?.marketplace_key || !orderSource?.marketplace_secret || !orderSource?.marketplace_token) { const msg = `Invoice ${invoice.DocumentNo}: Order source missing Amazon SP-API credentials (marketplace_key, marketplace_secret, marketplace_token)` logger.log(msg) errors.push(msg) continue } // Get or cache LWA access token let accessToken = accessTokenCache[orderSourceId] if (!accessToken) { accessToken = await getAmazonAccessToken( orderSource.marketplace_key, orderSource.marketplace_secret, orderSource.marketplace_token, logger ) accessTokenCache[orderSourceId] = accessToken } // Fetch invoice PDF from iDempiere logger.log(`Fetching PDF for invoice ${invoiceId}`) const pdfResponse: any = await event.context.fetch( `models/c_invoice/${invoiceId}/print?$report_type=PDF`, 'GET', token, null ) if (!pdfResponse?.reportFile) { const msg = `Invoice ${invoice.DocumentNo}: Could not generate PDF (reportFile missing)` logger.log(msg) errors.push(msg) continue } const pdfBuffer = Buffer.from(pdfResponse.reportFile, 'base64') logger.log(`PDF generated: ${pdfBuffer.length} bytes`) // Determine document type const docBaseType = invoice.DocBaseType?.id || invoice.DocBaseType?.identifier || invoice.DocBaseType || '' const documentType = (docBaseType === 'ARC') ? 'CreditNote' : 'Invoice' logger.log(`Document type: ${documentType} (DocBaseType: ${docBaseType})`) // Step 1: Create feed document const { feedDocumentId, url } = await createFeedDocument(accessToken, logger) // Step 2: Upload PDF await uploadPdfToAmazon(url, pdfBuffer, logger) // Step 3: Create feed const feedResult = await createFeed( accessToken, feedDocumentId, amazonOrderId, invoice.DocumentNo, documentType, marketplaceId, logger ) // Step 4: Mark invoice as uploaded in iDempiere try { const uploadDate = new Date().toISOString() logger.log(`Marking invoice ${invoiceId} as uploaded to Amazon`) await event.context.fetch( `models/c_invoice/${invoiceId}`, 'PUT', token, { isUploadToAmazon: true, UploadDateAmazon: uploadDate } ) logger.log(`Successfully marked invoice as uploaded`) } catch (markErr: any) { logger.log(`Could not mark invoice as uploaded: ${markErr.message}`) } logger.log(`SUCCESS: Invoice ${invoice.DocumentNo} uploaded to Amazon (Feed ID: ${feedResult.feedId})`) uploadedCount++ // Rate limiting: Amazon allows 1 invoice per 3 seconds await new Promise(resolve => setTimeout(resolve, 3000)) } catch (err: any) { const msg = `Error processing invoice ${invoiceId}: ${err.message || 'Unknown error'}` logger.log(msg) errors.push(msg) } } logger.log(`=== SUMMARY: Uploaded ${uploadedCount} invoice(s), ${skipped.length} skipped, ${errors.length} error(s) ===`) // Output logs for debugging console.log('\n========== AMAZON INVOICE UPLOAD LOGS ==========') logger.getLogs().forEach(log => console.log(log)) console.log('=================================================\n') if (uploadedCount > 0) { data = { status: 200, message: `Successfully uploaded ${uploadedCount} invoice(s) to Amazon${skipped.length > 0 ? `, ${skipped.length} skipped (already uploaded)` : ''}`, uploadedCount, skippedCount: skipped.length, uploadDate: new Date().toISOString(), errors: errors.length > 0 ? errors : undefined, skipped: skipped.length > 0 ? skipped : undefined, logs: logger.getLogs() } } else if (skipped.length > 0 && errors.length === 0) { data = { status: 200, message: `All ${skipped.length} invoice(s) were already uploaded to Amazon`, uploadedCount: 0, skippedCount: skipped.length, skipped, logs: logger.getLogs() } } else { data = { status: 400, message: errors.length > 0 ? errors.join('; ') : 'No invoices were uploaded to Amazon', errors, skipped: skipped.length > 0 ? skipped : undefined, logs: logger.getLogs() } } return data } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch (err: any) { try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch (error) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })