import { formatLexofficeDate } from '~/utils/lexofficeExport' // eBay counterpart of app/utils/amazonLexoffice.ts — ONE classifier feeding both // the settlement grid (Kürzel / Klassifikation columns) and the lexoffice CSV. // Codes follow PayJoe's eBay "Sammelkonto" export (verified against a real // PayJoe file, reference/banking-lexoffice/eBay Payments_EUR Sammelkonto_*.csv): // Bestellung (gross sale, matches the invoice) // Erstattung (refund, matches the credit memo) // Bestellgebühr EBA-BG-EUR- (final value fees of an order) // Servicegebühr EBA-SG-EUR- (ADFEE, NSRTNF, … — one row per fee, ref FEE-) // Werbekosten EBA-SG-EUR-ADFEE (PayJoe books ad fees as service fees) // Auszahlung EBA-AUSZ-EUR- (payout, negative) // Versandkosten EBA-SG-EUR-SHPPNGLB (shipping labels — not in PayJoe's sample) // Sonstiges EBA-SG-EUR- // The suffix is the payout CURRENCY (PayJoe: "EUR"), unlike Amazon's marketplace suffix. export type EbayRowBucket = 'sale' | 'refund' | 'orderFee' | 'shipping' | 'serviceFee' | 'advertising' | 'other' | 'payout' export const EBAY_BUCKET_LABELS: Record = { sale: 'Bestellung', refund: 'Erstattung', orderFee: 'Bestellgebühr', shipping: 'Versandkosten', serviceFee: 'Servicegebühr', advertising: 'Werbekosten', other: 'Sonstiges', payout: 'Auszahlung' } export const EBAY_BUCKET_HINTS: Record = { sale: 'Rechnung zuordnen', refund: 'Gutschrift zuordnen', orderFee: 'Gebühr · lt. eBay-Rechnung', shipping: 'Versandkosten', serviceFee: 'Gebühr · lt. eBay-Rechnung', advertising: 'Werbung · lt. eBay-Rechnung', other: 'Prüfen', payout: 'Geldtransit' } // PayJoe's eBay fee abbreviations seen in real exports; everything else falls // back to the same vowel-stripping rule PayJoe uses (INSERTION_FEE → NSRTNF). export const EBAY_FEE_CODES: Record = { 'AD_FEE': 'ADFEE', 'INSERTION_FEE': 'NSRTNF', 'FINAL_VALUE_FEE': 'FNLVLF', 'FINAL_VALUE_FEE_FIXED_PER_ORDER': 'FNLVLFF', 'SHIPPING_LABEL': 'SHPPNGLB', 'ADJUSTMENT': 'DJSTMNT', 'TRANSFER': 'TRNSFR', 'CREDIT': 'CRDT', 'WITHDRAWAL': 'WTHDRWL', 'LOAN_REPAYMENT': 'LNRPYMNT', 'DISPUTE': 'DSPT' } export interface EbaySettlementRowLike { category: string transactionType?: string bookingEntry?: string amountDescription?: string amount: number orderId?: string postedDate?: string reference?: string // eBay transactionId (PayJoe: FEE-) currency?: string internalDocumentNo?: string invoiceDocumentNo?: string bucket?: EbayRowBucket } export const classifyEbayRow = (row: EbaySettlementRowLike): EbayRowBucket => { if (row.bucket === 'payout') return 'payout' const hasOrder = !!row.orderId switch (row.category) { case 'umsaetze': return 'sale' case 'retouren': return hasOrder ? 'refund' : 'other' case 'ebayGebuehren': return hasOrder ? 'orderFee' : 'serviceFee' case 'versandkosten': return 'shipping' case 'werbekosten': return 'advertising' default: return 'other' } } // PayJoe abbreviation: strip everything but letters/digits, drop ALL vowels, // upper-case, max 8 chars; names that get too short keep their letters (AD_FEE → ADFEE). export const ebayFeeCode = (name: string): string => { const raw = String(name || '').trim() if (!raw) return 'OTHER' const upper = raw.toUpperCase() if (EBAY_FEE_CODES[upper]) return EBAY_FEE_CODES[upper] const cleaned = raw.replace(/[^A-Za-z0-9]+/g, '') if (!cleaned) return 'OTHER' const consonants = cleaned.replace(/[aeiouAEIOU]/g, '') const base = consonants.length >= 4 ? consonants : cleaned return base.toUpperCase().substring(0, 8) } const currencyOf = (row: EbaySettlementRowLike, fallback = 'EUR') => String(row.currency || fallback || 'EUR').toUpperCase() export const ebayRowCode = (row: EbaySettlementRowLike, currencyFallback = 'EUR'): string => { const bucket = classifyEbayRow(row) const cur = currencyOf(row, currencyFallback) switch (bucket) { case 'sale': case 'refund': return row.orderId || '' case 'orderFee': return `EBA-BG-${cur}-${row.orderId}` case 'shipping': return `EBA-SG-${cur}-SHPPNGLB` case 'advertising': return `EBA-SG-${cur}-ADFEE` case 'payout': return `EBA-AUSZ-${cur}-${formatLexofficeDate(row.postedDate)}` default: return `EBA-SG-${cur}-${ebayFeeCode(row.amountDescription || row.transactionType || 'Other')}` } } export const ebayBucketLabel = (bucket: EbayRowBucket) => EBAY_BUCKET_LABELS[bucket] || bucket export const ebayBucketHint = (bucket: EbayRowBucket) => EBAY_BUCKET_HINTS[bucket] || 'Prüfen' // PayJoe's VZ2 for fee rows is eBay's own transactionId, which already reads // "FEE-_11" for fees (sales carry a plain numeric id) — used verbatim. export const ebayFeeReference = (row: EbaySettlementRowLike): string => String(row.reference || '') const isoDay = (value?: string) => { if (!value) return '' const d = new Date(value) return isNaN(d.getTime()) ? '' : d.toISOString().split('T')[0] } const earlier = (a?: string, b?: string) => { if (!a) return b || '' if (!b) return a return new Date(a).getTime() <= new Date(b).getTime() ? a : b } export interface EbayLexofficeGroup { bucket: EbayRowBucket code: string orderId: string reference: string feeTypes: Set description: string amount: number postedDate: string internalDocumentNo: string invoiceDocumentNo: string } // Collapses detail rows into export lines the way PayJoe does: one gross line + // one fee line per order, and one line PER FEE TRANSACTION for account-level // fees / ads (keyed by the eBay transaction id, so every FEE- stays // traceable to eBay's own fee invoice). export const groupEbayRowsForLexoffice = (rows: EbaySettlementRowLike[], currencyFallback = 'EUR'): EbayLexofficeGroup[] => { const groups = new Map() for (const r of rows || []) { const bucket = classifyEbayRow(r) if (bucket === 'payout') continue const code = ebayRowCode(r, currencyFallback) const day = isoDay(r.postedDate) let key: string switch (bucket) { case 'sale': case 'refund': case 'orderFee': key = `${bucket}|${r.orderId}` break case 'shipping': key = `${bucket}|${r.orderId || r.reference || day}` break default: key = `${bucket}|${code}|${r.reference || `${r.orderId || ''}|${day}`}` } const g = groups.get(key) || { bucket, code, orderId: r.orderId || '', reference: ebayFeeReference(r), feeTypes: new Set(), description: r.amountDescription || r.transactionType || '', amount: 0, postedDate: '', internalDocumentNo: r.internalDocumentNo || '', invoiceDocumentNo: r.invoiceDocumentNo || '' } g.amount += Number(r.amount) || 0 g.postedDate = earlier(g.postedDate, r.postedDate) if (r.amountDescription) g.feeTypes.add(r.amountDescription) if (!g.internalDocumentNo && r.internalDocumentNo) g.internalDocumentNo = r.internalDocumentNo if (!g.invoiceDocumentNo && r.invoiceDocumentNo) g.invoiceDocumentNo = r.invoiceDocumentNo groups.set(key, g) } return [...groups.values()].filter(g => Math.round(g.amount * 100) !== 0) } export const ebayGroupPurpose = (g: EbayLexofficeGroup): string => { const ref = g.invoiceDocumentNo ? ` · Rechnung ${g.invoiceDocumentNo}` : g.internalDocumentNo ? ` · Auftrag ${g.internalDocumentNo}` : ' · nicht zugeordnet' const traceRef = g.invoiceDocumentNo ? ` · zu Rechnung ${g.invoiceDocumentNo}` : g.internalDocumentNo ? ` · zu Auftrag ${g.internalDocumentNo}` : '' const fees = [...g.feeTypes].filter(Boolean).slice(0, 4).join(', ') const feeRef = g.reference ? ` · ${g.reference}` : '' switch (g.bucket) { case 'sale': return `${g.orderId} · eBay Verkauf${ref}` case 'refund': return `${g.orderId} · eBay Erstattung${ref}` case 'orderFee': return `${g.code} · eBay Bestellgebühr${fees ? ` (${fees})` : ''}${traceRef}` case 'shipping': return `${g.code} · eBay Versandetikett${g.orderId ? ` · ${g.orderId}` : ''}${traceRef}${feeRef}` case 'advertising': return `${g.code} · eBay Werbekosten${g.description && g.description !== 'AD_FEE' ? ` · ${g.description}` : ''}${feeRef}` case 'serviceFee': return `${g.code} · eBay Servicegebühr ${g.description}${feeRef}` default: return `${g.code} · eBay ${g.description || 'Sonstiges'}${g.orderId ? ` · ${g.orderId}` : ''}${feeRef}` } }