import refreshTokenHelper from "../../../utils/refreshTokenHelper" import getTokenHelper from "../../../utils/getTokenHelper" import forceLogoutHelper from "../../../utils/forceLogoutHelper" import errorHandlingHelper from "../../../utils/errorHandlingHelper" import { parseCamtUploads } from "../../../utils/banking/camt052Parser" import { listBankAccounts, planAccount, presentPlan, saveUpload } from "../../../utils/banking/bankFileImport" // POST /api/accounting/bank-file-import/preview (multipart, field `files`) // Dry run of the manual CAMT.052/053 file import: parses the online-banking // export (ZIP of XML pages or plain XML), splits it into one statement per // booking day and compares every booking with what iDempiere already holds // (duplicate key = AcctSvcrRef ↔ C_BankStatementLine.EftTrxID). Writes NOTHING // to iDempiere — it only parks the parsed upload on disk and returns a `token` // the import route works from. const MAX_FILES = 40 const MAX_BYTES = 40 * 1024 * 1024 const handleFunc = async (event: any, parsed: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const accounts = [] for (const account of parsed.accounts) { accounts.push(presentPlan(await planAccount(event, token, account))) } // Every active C_BankAccount — the preview offers them as manual target when // the file IBAN matches none (bank merger → new IBAN, old one in iDempiere). const bankAccounts = await listBankAccounts(event, token) return { accounts, bankAccounts } } export default defineEventHandler(async (event) => { // Read the multipart body ONCE (a refresh-retry can't re-read it). const form = await readMultipartFormData(event) const uploads: { name: string; data: Uint8Array }[] = [] let bytes = 0 for (const part of (form || [])) { if (!['file', 'files'].includes(part.name || '') || !part.data?.length) continue const name = part.filename || 'upload' if (!/\.(zip|xml)$/i.test(name)) return { status: 415, message: `Nicht unterstützter Dateityp: ${name} (nur CAMT als .zip oder .xml)` } bytes += part.data.length uploads.push({ name, data: new Uint8Array(part.data) }) } if (!uploads.length) return { status: 400, message: 'Keine Datei übermittelt' } if (uploads.length > MAX_FILES || bytes > MAX_BYTES) return { status: 413, message: 'Zu viele / zu große Dateien — bitte in mehreren Schritten hochladen' } const parsed = parseCamtUploads(uploads) if (!parsed.accounts.length) { return { status: 422, message: 'Keine Umsätze gefunden — ist das ein CAMT.052-Export?', errors: parsed.errors, files: parsed.files } } let planned: any = { accounts: [], bankAccounts: [] } try { planned = await handleFunc(event, parsed) } catch (err: any) { try { let authToken: any = await refreshTokenHelper(event) planned = await handleFunc(event, parsed, authToken) } catch (error: any) { const data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) return data } } const token = await saveUpload(parsed, uploads) return { status: 200, token, files: parsed.files, errors: parsed.errors, accounts: planned.accounts, bankAccounts: planned.bankAccounts } })