import refreshTokenHelper from "../../../utils/refreshTokenHelper" import getTokenHelper from "../../../utils/getTokenHelper" import forceLogoutHelper from "../../../utils/forceLogoutHelper" import errorHandlingHelper from "../../../utils/errorHandlingHelper" import { archiveUpload, isValidToken, loadUpload, planAccount, rememberBankAccountAlias, writeDay } from "../../../utils/banking/bankFileImport" // POST /api/accounting/bank-file-import/import // Body: { token, iban, days: ['2026-06-23', …], bankAccountId? } — the client // sends the days in small chunks (sequentially) so one request never outlives // the proxy timeout. `bankAccountId` is the target the user picked in the // preview when the file IBAN matches no iDempiere account; it is remembered // per IBAN once a line was written. // The plan is RECOMPUTED against live iDempiere data right before writing, so a // booking that arrived in the meantime (EBICS fetch, second browser tab) is // still recognised as a duplicate. Statements are created Drafted. const MAX_DAYS_PER_REQUEST = 10 const handleFunc = async (event: any, account: any, days: string[], sourceLabel: string, bankAccountId: number | null, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const plan = await planAccount(event, token, account, days, bankAccountId) if (!plan.bankAccount) { return { status: 409, message: bankAccountId ? `Bankkonto ${bankAccountId} ist nicht (mehr) aktiv` : `Kein aktives iDempiere-Bankkonto mit IBAN ${account.iban} gefunden` } } const results = [] // Oldest first — keeps DocumentNo order aligned with the balance chain. for (const day of plan.days) { results.push(await writeDay(event, token, plan, day, sourceLabel)) } return { status: 200, bankAccount: plan.bankAccount, results } } export default defineEventHandler(async (event) => { const body = await readBody(event) const uploadToken = String(body?.token || '') if (!isValidToken(uploadToken)) return { status: 400, message: 'Ungültiges Upload-Token' } const parsed = await loadUpload(uploadToken) if (!parsed) return { status: 404, message: 'Upload nicht mehr vorhanden — bitte die Datei erneut hochladen' } const iban = String(body?.iban || '').replace(/\s+/g, '').toUpperCase() const account = parsed.accounts.find((acc: any) => acc.iban === iban) || (parsed.accounts.length === 1 && !iban ? parsed.accounts[0] : null) if (!account) return { status: 404, message: 'Konto nicht im Upload enthalten' } const days: string[] = Array.from(new Set( (Array.isArray(body?.days) ? body.days : []).map((d: any) => String(d)).filter((d: string) => /^\d{4}-\d{2}-\d{2}$/.test(d)) )).sort() as string[] if (!days.length) return { status: 400, message: 'Keine Tage ausgewählt' } if (days.length > MAX_DAYS_PER_REQUEST) return { status: 400, message: `Maximal ${MAX_DAYS_PER_REQUEST} Tage pro Anfrage` } const sourceLabel = parsed.files.length === 1 ? (parsed.files[0] || 'upload') : `${parsed.files[0] || 'upload'} +${parsed.files.length - 1}` const bankAccountId = Number(body?.bankAccountId || 0) || null let data: any = {} try { data = await handleFunc(event, account, days, sourceLabel, bankAccountId) } catch (err: any) { try { // Re-planning after the refresh is safe: days whose header already exists // come back as `resume`, already written lines as duplicates. let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, account, days, sourceLabel, bankAccountId, authToken) } catch (error: any) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } if (Number(data?.status) === 200 && (data.results || []).some((r: any) => r.linesCreated > 0)) { await archiveUpload(uploadToken, account.iban) if (bankAccountId) await rememberBankAccountAlias(account.iban, bankAccountId) } return data })