import type { SupabaseClient } from '@supabase/supabase-js' import { fetchMultipleRates } from '@/lib/currency/riksbanken' import { createJournalEntry } from '@/lib/bookkeeping/engine' import type { Currency, Invoice, SupplierInvoice, RevaluationItem, CurrencyRevaluationPreview, CurrencyRevaluationResult, CreateJournalEntryLineInput, } from '@/types' /** * Fetch open foreign-currency receivables (invoices). * Returns invoices with status 'sent' or 'overdue', non-SEK currency, * and a known exchange rate. */ export async function getOpenForeignCurrencyReceivables( supabase: SupabaseClient, userId: string ): Promise { const { data, error } = await supabase .from('invoices') .select('*') .eq('user_id', userId) .in('status', ['sent', 'overdue']) .neq('currency', 'SEK') .not('exchange_rate', 'is', null) if (error) { throw new Error(`Failed to fetch foreign currency receivables: ${error.message}`) } return (data || []) as Invoice[] } /** * Fetch open foreign-currency payables (supplier invoices). * Returns supplier invoices with open statuses, non-SEK currency, * and a known exchange rate. Uses remaining_amount for partial payments. */ export async function getOpenForeignCurrencyPayables( supabase: SupabaseClient, userId: string ): Promise { const { data, error } = await supabase .from('supplier_invoices') .select('*') .eq('user_id', userId) .in('status', ['registered', 'approved', 'overdue', 'partially_paid']) .neq('currency', 'SEK') .not('exchange_rate', 'is', null) if (error) { throw new Error(`Failed to fetch foreign currency payables: ${error.message}`) } return (data || []) as SupplierInvoice[] } /** * Preview currency revaluation without persisting. * Computes per-item differences and aggregated journal lines. * * Receivables (1510): * closing > original → gain: Debit 1510, Credit 3960 * closing < original → loss: Credit 1510, Debit 7960 * * Payables (2440): * closing > original → loss (liability grew): Debit 7960, Credit 2440 * closing < original → gain (liability shrank): Debit 2440, Credit 3960 */ export async function previewCurrencyRevaluation( supabase: SupabaseClient, userId: string, closingDate: string ): Promise { const [receivables, payables] = await Promise.all([ getOpenForeignCurrencyReceivables(supabase, userId), getOpenForeignCurrencyPayables(supabase, userId), ]) // Collect distinct currencies const currencies = new Set() for (const inv of receivables) { currencies.add(inv.currency) } for (const si of payables) { currencies.add(si.currency as Currency) } if (currencies.size === 0) { return { items: [], lines: [], closingRates: {}, totalGain: 0, totalLoss: 0, netEffect: 0, } } // Fetch closing rates const rateMap = await fetchMultipleRates( Array.from(currencies), new Date(closingDate) ) const closingRates: Record = {} for (const [currency, rate] of rateMap) { closingRates[currency] = rate.rate } const items: RevaluationItem[] = [] // Process receivables for (const inv of receivables) { const closingRate = rateMap.get(inv.currency)?.rate if (!closingRate || !inv.exchange_rate) continue const amountInCurrency = inv.total const originalSek = Math.round(amountInCurrency * inv.exchange_rate * 100) / 100 const closingSek = Math.round(amountInCurrency * closingRate * 100) / 100 const difference = Math.round((closingSek - originalSek) * 100) / 100 if (Math.abs(difference) < 0.01) continue items.push({ type: 'receivable', source_id: inv.id, reference: inv.invoice_number, currency: inv.currency, amount_in_currency: amountInCurrency, original_rate: inv.exchange_rate, closing_rate: closingRate, original_sek: originalSek, closing_sek: closingSek, difference_sek: difference, }) } // Process payables (use remaining_amount for partial payments) for (const si of payables) { const closingRate = rateMap.get(si.currency as Currency)?.rate if (!closingRate || !si.exchange_rate) continue const amountInCurrency = si.remaining_amount if (amountInCurrency <= 0) continue const originalSek = Math.round(amountInCurrency * si.exchange_rate * 100) / 100 const closingSek = Math.round(amountInCurrency * closingRate * 100) / 100 const difference = Math.round((closingSek - originalSek) * 100) / 100 if (Math.abs(difference) < 0.01) continue items.push({ type: 'payable', source_id: si.id, reference: si.supplier_invoice_number, currency: si.currency as Currency, amount_in_currency: amountInCurrency, original_rate: si.exchange_rate, closing_rate: closingRate, original_sek: originalSek, closing_sek: closingSek, difference_sek: difference, }) } // Build aggregated journal lines let debit1510 = 0 // Receivable gain (revalue up) let credit1510 = 0 // Receivable loss (revalue down) let debit2440 = 0 // Payable gain (liability shrank) let credit2440 = 0 // Payable loss (liability grew) let debit3960 = 0 // Placeholder — we won't debit 3960 let credit3960 = 0 // Gains let debit7960 = 0 // Losses let credit7960 = 0 // Placeholder — we won't credit 7960 for (const item of items) { if (item.type === 'receivable') { if (item.difference_sek > 0) { // Closing > original → gain: Debit 1510, Credit 3960 debit1510 += item.difference_sek credit3960 += item.difference_sek } else { // Closing < original → loss: Credit 1510, Debit 7960 credit1510 += Math.abs(item.difference_sek) debit7960 += Math.abs(item.difference_sek) } } else { // Payable if (item.difference_sek > 0) { // Closing > original → loss (liability grew): Debit 7960, Credit 2440 debit7960 += item.difference_sek credit2440 += item.difference_sek } else { // Closing < original → gain (liability shrank): Debit 2440, Credit 3960 debit2440 += Math.abs(item.difference_sek) credit3960 += Math.abs(item.difference_sek) } } } const lines: CreateJournalEntryLineInput[] = [] if (debit1510 > 0) { lines.push({ account_number: '1510', debit_amount: Math.round(debit1510 * 100) / 100, credit_amount: 0, line_description: 'Omvärdering kundfordringar — orealiserad kursvinst', }) } if (credit1510 > 0) { lines.push({ account_number: '1510', debit_amount: 0, credit_amount: Math.round(credit1510 * 100) / 100, line_description: 'Omvärdering kundfordringar — orealiserad kursförlust', }) } if (debit2440 > 0) { lines.push({ account_number: '2440', debit_amount: Math.round(debit2440 * 100) / 100, credit_amount: 0, line_description: 'Omvärdering leverantörsskulder — orealiserad kursvinst', }) } if (credit2440 > 0) { lines.push({ account_number: '2440', debit_amount: 0, credit_amount: Math.round(credit2440 * 100) / 100, line_description: 'Omvärdering leverantörsskulder — orealiserad kursförlust', }) } if (credit3960 > 0) { lines.push({ account_number: '3960', debit_amount: 0, credit_amount: Math.round(credit3960 * 100) / 100, line_description: 'Orealiserade valutakursvinster', }) } if (debit7960 > 0) { lines.push({ account_number: '7960', debit_amount: Math.round(debit7960 * 100) / 100, credit_amount: 0, line_description: 'Orealiserade valutakursförluster', }) } const totalGain = Math.round(credit3960 * 100) / 100 const totalLoss = Math.round(debit7960 * 100) / 100 const netEffect = Math.round((totalGain - totalLoss) * 100) / 100 return { items, lines, closingRates, totalGain, totalLoss, netEffect, } } /** * Execute currency revaluation for a fiscal period. * Creates a journal entry with source_type 'currency_revaluation'. * * Returns null if no foreign-currency items exist. * Throws if a revaluation entry already exists for this period (idempotency). */ export async function executeCurrencyRevaluation( supabase: SupabaseClient, userId: string, closingDate: string, fiscalPeriodId: string ): Promise { // Idempotency check: prevent double revaluation const { count, error: checkError } = await supabase .from('journal_entries') .select('id', { count: 'exact', head: true }) .eq('user_id', userId) .eq('fiscal_period_id', fiscalPeriodId) .eq('source_type', 'currency_revaluation') .eq('status', 'posted') if (checkError) { throw new Error(`Failed to check existing revaluation: ${checkError.message}`) } if ((count ?? 0) > 0) { throw new Error('Currency revaluation already exists for this period') } const preview = await previewCurrencyRevaluation(supabase, userId, closingDate) if (preview.items.length === 0 || preview.lines.length === 0) { return null } const entry = await createJournalEntry(supabase, userId, { fiscal_period_id: fiscalPeriodId, entry_date: closingDate, description: `Omvärdering utländsk valuta ${closingDate}`, source_type: 'currency_revaluation', voucher_series: 'A', lines: preview.lines, }) return { entry, preview } }