import { downloadLexofficeCsv, type LexofficeCsvRow } from '~/utils/lexofficeExport' // Shared export orchestration for the marketplace settlement pages (Amazon // settlement periods / eBay payouts) → lexoffice bank-statement CSV. // // Three variants, all driven by the SAME list of ticked periods: // - combined : positions + the closing payout ("Geldtransit") row per period // in ONE file — PayJoe's "Sammelkonto" layout, every period nets // to exactly 0,00 on the clearing account. Recommended default. // - payouts : only the payout rows (one per period). Pure list data. // - positions : only the position rows (per order / fee / ad invoice). // The page supplies the marketplace-specific pieces (how to fetch a period's // detail rows and how to turn them into CSV rows); everything else — progress, // warnings, foreign-currency skips, netting check, sorting, file naming — lives // here once instead of twice. export interface LexofficeRowsResult { rows: LexofficeCsvRow[] warning?: string } export interface LexofficeSettlementExportOptions { // Loads the detail rows + summary of one period. Return { error } to skip it. fetchDetail: (item: TItem) => Promise<{ rows?: any[]; summary?: any; error?: string }> buildPositionRows: (item: TItem, detailRows: any[], summary: any) => LexofficeRowsResult buildPayoutRow: (item: TItem) => LexofficeRowsResult // Short human label for warnings ("Zeitraum 01.06.–15.06.2026"). itemLabel: (item: TItem) => string // ISO date used for the file name range (payout date). itemDate: (item: TItem) => string | null | undefined filePrefix: string // e.g. 'lexoffice-amazon' onAlert: (alert: { status: number; message: string }) => void emptySelectionMessage: () => string // Optional: called AFTER the CSV file was handed to the browser, with the // detail rows of every period that went into it (only for variants that // contain positions — payouts-only exports carry no order data). Used by // the Amazon page to mark the exported orders as "CSV-exportiert". onExported?: (variant: LexofficeExportVariant, exported: { item: TItem; rows: any[] }[]) => void | Promise } export type LexofficeExportVariant = 'combined' | 'payouts' | 'positions' const isoDay = (value: string | null | undefined) => { if (!value) return '' const d = new Date(value) return isNaN(d.getTime()) ? '' : d.toISOString().split('T')[0] } const rowTime = (row: LexofficeCsvRow) => { const d = row.bookingDate instanceof Date ? row.bookingDate : new Date(row.bookingDate || '') return isNaN(d.getTime()) ? Number.MAX_SAFE_INTEGER : d.getTime() } const round2 = (n: number) => Math.round((Number(n) || 0) * 100) / 100 export const useLexofficeSettlementExport = (options: LexofficeSettlementExportOptions) => { const exporting = ref(false) const progress = ref({ done: 0, total: 0 }) const buildFileName = (variant: LexofficeExportVariant, items: TItem[]) => { const days = items.map(i => isoDay(options.itemDate(i))).filter(Boolean).sort() const range = days.length ? `${days[0]}_${days[days.length - 1]}` : new Date().toISOString().split('T')[0] const part = variant === 'combined' ? 'sammelkonto' : variant === 'payouts' ? 'auszahlungen' : 'positionen' return `${options.filePrefix}-${part}-${range}.csv` } const finish = (variant: LexofficeExportVariant, items: TItem[], allRows: LexofficeCsvRow[], warnings: string[]): boolean => { if (!allRows.length) { options.onAlert({ status: 400, message: 'Keine exportierbaren Zeilen gefunden.' + (warnings.length ? ' — ' + warnings.join(' | ') : '') }) return false } // Stable chronological order (Buchungstag), payout row of a period last // among equal dates because it is pushed after the positions. const sorted = allRows .map((row, idx) => ({ row, idx, t: rowTime(row) })) .sort((a, b) => a.t - b.t || a.idx - b.idx) .map(x => x.row) const result = downloadLexofficeCsv(sorted, buildFileName(variant, items)) const base = variant === 'combined' ? `Lexoffice-CSV erstellt: ${result.rowCount} Zeilen für ${items.length} Zeitraum/Zeiträume (Positionen + Auszahlung, je Zeitraum Saldo 0,00). In lexoffice auf das Verrechnungskonto importieren; die Auszahlungszeile als Geldtransit mit dem Bankeingang verknüpfen.` : variant === 'payouts' ? `Auszahlungs-CSV erstellt: ${result.rowCount} Zeilen. Jede Zeile in lexoffice als Geldtransit kategorisieren und mit dem passenden Eingang auf dem Bankkonto verknüpfen.` : `Positions-CSV erstellt: ${result.rowCount} Zeilen.` options.onAlert({ status: 200, message: warnings.length ? `${base} Warnungen: ${warnings.join(' | ')}` : base }) return true } const run = async (variant: LexofficeExportVariant, items: TItem[]) => { if (exporting.value) return if (!items.length) { options.onAlert({ status: 400, message: options.emptySelectionMessage() }) return } const needsDetail = variant !== 'payouts' exporting.value = true progress.value = { done: 0, total: needsDetail ? items.length : 0 } const allRows: LexofficeCsvRow[] = [] const warnings: string[] = [] // Periods whose position rows actually made it into the file (for onExported). const exportedDetails: { item: TItem; rows: any[] }[] = [] try { for (const item of items) { const label = options.itemLabel(item) const periodRows: LexofficeCsvRow[] = [] try { if (needsDetail) { const detail = await options.fetchDetail(item) if (detail?.error) { warnings.push(`${label}: ${detail.error}`) continue } const pos = options.buildPositionRows(item, detail?.rows || [], detail?.summary) if (pos.warning) warnings.push(pos.warning) periodRows.push(...pos.rows) if (pos.rows.length) exportedDetails.push({ item, rows: detail?.rows || [] }) } if (variant !== 'positions') { const pay = options.buildPayoutRow(item) if (pay.warning && !needsDetail) warnings.push(pay.warning) periodRows.push(...pay.rows) } if (variant === 'combined' && periodRows.length) { // Every period must net to 0,00 on the clearing account. The position // builder already surfaces any reconciliation residue as its own row, // so a non-zero here means a genuine data problem worth flagging. const net = round2(periodRows.reduce((s, r) => s + round2(r.amount), 0)) if (Math.abs(net) >= 0.01) { warnings.push(`${label}: Saldo ${net.toFixed(2).replace('.', ',')} ≠ 0,00 — bitte prüfen`) } } allRows.push(...periodRows) } catch (error: any) { warnings.push(`${label}: ${error?.data?.message || error?.message || 'Fehler beim Laden'}`) } finally { if (needsDetail) progress.value.done++ } } } finally { exporting.value = false } const downloaded = finish(variant, items, allRows, warnings) if (downloaded && needsDetail && exportedDetails.length && options.onExported) { await options.onExported(variant, exportedDetails) } } return { exporting, progress, exportCombined: (items: TItem[]) => run('combined', items), exportPayoutsOnly: (items: TItem[]) => run('payouts', items), exportPositionsOnly: (items: TItem[]) => run('positions', items) } }