/** * POST /api/accounting/amazon-ads/upload (multipart, field "files", PDFs) * Fallback source while the Ads API access is pending — and for invoices that * were downloaded manually anyway. Each PDF is parsed (parseInvoicePdf.ts) and * cached in the store together with the file, so the import can attach the * ORIGINAL Amazon document to the created iDempiere invoice. * * Two document families are accepted in the same drop: Sponsored-Ads invoices and * SELLER FEE invoices (Seller Central → Tax Document Library → Seller Invoices — * no Amazon API exists for those). The fee parser runs first: it gates on the * supplier/recipient block, which an Ads invoice does not have. */ import { parseAmazonAdsInvoicePdf } from '../../../utils/amazonAds/parseInvoicePdf' import { parseAmazonSellerFeeInvoicePdf } from '../../../utils/amazonAds/parseSellerFeeInvoicePdf' import { upsertInvoice, getInvoice, inboxBackendInfo } from '../../../utils/amazonAds/invoiceStore' import { orgIdOf } from '../../../utils/amazonAds/routeShared' import getTokenHelper from '../../../utils/getTokenHelper' import refreshTokenHelper from '../../../utils/refreshTokenHelper' import errorHandlingHelper from '../../../utils/errorHandlingHelper' // Stored in the iDempiere table CUST_AmazonInvoiceInbox (PDF → Strapi) — see invoiceStore.ts; // the local SQLite file is only the fallback when that table is not reachable. const handle = async (event: any, files: any[], token: string) => { const organizationId = orgIdOf(event) const results: any[] = [] for (const f of files) { const name = f.filename || 'invoice.pdf' try { if (!/\.pdf$/i.test(name) && !/pdf/i.test(f.type || '')) { results.push({ fileName: name, status: 415, message: 'Keine PDF-Datei' }); continue } const buf = Buffer.from(f.data) const inv = (await parseAmazonSellerFeeInvoicePdf(buf, name)) || (await parseAmazonAdsInvoicePdf(buf, name)) if (!inv) { results.push({ fileName: name, status: 422, message: 'Keine Amazon-Ads- oder Amazon-Gebührenrechnung erkannt (Rechnungsnummer/Beträge nicht gefunden)' }); continue } const existing = await getInvoice(event, token, inv.invoiceNo) // keep the richer API payload if we already have it, but always store the PDF const toStore = existing?.invoice?.source === 'api' ? { ...existing.invoice, documentAvailable: true } : inv await upsertInvoice(event, token, organizationId, toStore, { pdf: buf, pdfName: name }) results.push({ fileName: name, status: 200, kind: inv.kind || 'ads', isCreditNote: !!inv.isCreditNote, invoiceNo: inv.invoiceNo, invoiceDate: inv.invoiceDate, gross: inv.gross, currency: inv.currency, lines: inv.lines.length, alreadyKnown: !!existing }) } catch (e: any) { const st = Number(e?.status || e?.statusCode || 0) if (st === 401 || st === 403) throw e // expired token → refresh + retry the whole batch (upserts are idempotent) results.push({ fileName: name, status: 500, message: e?.data?.detail || e?.message || String(e) }) } } return { status: 200, results, backend: inboxBackendInfo().backend } } export default defineEventHandler(async (event) => { // multipart is read ONCE — the token-refresh retry below must not read the body again const parts = await readMultipartFormData(event) const files = (parts || []).filter(p => p.filename && p.data?.length) if (!files.length) return { status: 400, message: 'Keine PDF-Dateien empfangen' } if (!orgIdOf(event)) return { status: 422, message: 'Keine Organisation im Login-Kontext — bitte erneut anmelden.' } try { return await handle(event, files, await getTokenHelper(event)) } catch (err: any) { try { return await handle(event, files, await refreshTokenHelper(event)) } catch (error: any) { return errorHandlingHelper(err?.data ?? err, error?.data ?? error) } } })