import type { Invoice, Customer, CompanySettings } from '@/types' import { formatCurrency, formatDate, getCompanyDisplayName, getCompanyPrimaryName } from '@/lib/utils' export interface ReminderEmailData { invoice: Invoice customer: Customer company: CompanySettings reminderLevel: 1 | 2 | 3 daysOverdue: number actionUrl: string // URL for customer to mark as paid or dispute // Dröjsmålsränta + lagstadgad påminnelseavgift (Räntelagen §6, Lag 1981:739). // When the reminder is generated by the cron, these are computed and persisted // on the invoice_reminders row and then passed in here. // // interestAmount is a share of the invoice total, so it carries the INVOICE // currency. reminderFee is a fixed statutory krona amount and always carries // SEK (see REMINDER_FEE_CURRENCY). The two are deliberately not summed here: // the amount to pay is derived per currency by calculateReminderAmounts(). interestAmount: number interestRate: number // annual rate as decimal (0.105 = 10.5%) interestFromDate: string interestDays: number reminderFee: number // always SEK, never the invoice currency } /** * The lagstadgad påminnelseavgift is a Swedish-krona statute: Lag (1981:739) om * ersättning för inkassokostnader m.m. 2 §, with the amount fixed at 60 kr by * förordning (1981:1057) 1 §. There is no conversion rule for a foreign-currency * claim, and Accounted books the fee 1510/3990 in SEK regardless of the invoice * currency (lib/bookkeeping/reminder-fee-entries.ts). * * So the fee is always quoted in SEK. Rendering "60" with the invoice's currency * code would demand roughly ten times the statutory amount from a EUR customer * and would not match the receivable recorded in the books. */ export const REMINDER_FEE_CURRENCY = 'SEK' export interface ReminderAmounts { /** Invoice currency (uppercased ISO code). Carries the invoice total + interest. */ currency: string /** invoice.total + dröjsmålsränta, plus the fee only when the invoice is itself in SEK. */ totalDue: number /** * The påminnelseavgift in SEK when the invoice is in another currency, so it * has to be demanded alongside rather than inside `totalDue`. 0 when the fee * is already folded into `totalDue`. */ feeDueSeparately: number } /** * Split the amount to pay per currency. Never returns a single scalar that mixes * a foreign invoice currency with the SEK statutory fee. */ export function calculateReminderAmounts(input: { invoiceTotal: number interestAmount: number reminderFee: number currency?: string | null }): ReminderAmounts { const currency = (input.currency || REMINDER_FEE_CURRENCY).toUpperCase() const feeSharesInvoiceCurrency = currency === REMINDER_FEE_CURRENCY const fee = input.reminderFee > 0 ? Math.round(input.reminderFee * 100) / 100 : 0 const totalDue = Math.round( (input.invoiceTotal + input.interestAmount + (feeSharesInvoiceCurrency ? fee : 0)) * 100, ) / 100 return { currency, totalDue, feeDueSeparately: feeSharesInvoiceCurrency ? 0 : fee, } } /** * Render the amount to pay. When the statutory fee is in a different currency * than the invoice it is appended as its own amount ("1 010,00 € + 60 kr") * instead of being folded into a misleading single figure. */ export function formatReminderTotalDue(amounts: ReminderAmounts): string { const base = formatCurrency(amounts.totalDue, amounts.currency) if (amounts.feeDueSeparately <= 0) { return base } return `${base} + ${formatCurrency(amounts.feeDueSeparately, REMINDER_FEE_CURRENCY)}` } /** Shown whenever a SEK fee rides along with a foreign-currency invoice. */ export const REMINDER_FEE_CURRENCY_NOTE = 'Påminnelseavgiften är lagstadgad (Lag 1981:739) och anges i svenska kronor. ' + 'Den räknas inte om till fakturans valuta utan betalas som ett separat belopp i SEK.' // Reminder level configurations const REMINDER_CONFIG = { 1: { title: 'Vänlig påminnelse', tone: 'friendly', daysAfterDue: 15 }, 2: { title: 'Andra påminnelsen', tone: 'firm', daysAfterDue: 30 }, 3: { title: 'Slutlig påminnelse', tone: 'urgent', daysAfterDue: 45 } } as const /** * Generate HTML email for payment reminder */ export function generateReminderEmailHtml(data: ReminderEmailData): string { const { invoice, customer, company, reminderLevel, daysOverdue, actionUrl, interestAmount, interestRate, interestDays, reminderFee, } = data const config = REMINDER_CONFIG[reminderLevel] const interestRatePercent = (interestRate * 100).toLocaleString('sv-SE', { minimumFractionDigits: 0, maximumFractionDigits: 2, }) const hasInterest = interestAmount > 0 const hasFee = reminderFee > 0 const hasSurcharges = hasInterest || hasFee const amounts = calculateReminderAmounts({ invoiceTotal: invoice.total, interestAmount, reminderFee, currency: invoice.currency, }) const feeInOtherCurrency = amounts.feeDueSeparately > 0 // Different styling based on urgency const headerColor = reminderLevel === 3 ? '#dc2626' : reminderLevel === 2 ? '#ea580c' : '#2563eb' const buttonColor = reminderLevel === 3 ? '#dc2626' : reminderLevel === 2 ? '#ea580c' : '#2563eb' return `
Hej${customer.name ? ` ${customer.name.split(' ')[0]}` : ''},
${reminderLevel === 1 ? `Vi vill påminna dig om att faktura ${invoice.invoice_number} förföll till betalning den ${formatDate(invoice.due_date)}. Om du redan har betalat kan du bortse från denna påminnelse.
` : reminderLevel === 2 ? `Trots vår tidigare påminnelse har vi ännu inte mottagit betalning för faktura ${invoice.invoice_number} som förföll den ${formatDate(invoice.due_date)}.
Vi ber dig vänligen att omgående reglera detta belopp för att undvika ytterligare åtgärder.
` : `Detta är vår slutliga påminnelse gällande faktura ${invoice.invoice_number}.
Fakturan förföll till betalning den ${formatDate(invoice.due_date)} och vi har ännu inte mottagit betalning trots tidigare påminnelser.
Om betalning inte inkommer inom 7 dagar kommer ärendet att överlämnas för vidare hantering.
`}| Fakturanummer: | ${invoice.invoice_number} |
| Fakturadatum: | ${formatDate(invoice.invoice_date)} |
| Förfallodatum: | ${formatDate(invoice.due_date)} |
| Ursprungligt belopp: | ${formatCurrency(invoice.total, invoice.currency)} |
| Dröjsmålsränta (${interestRatePercent}% per år, ${interestDays} dagar): | ${formatCurrency(interestAmount, invoice.currency)} |
| Påminnelseavgift: | ${formatCurrency(reminderFee, REMINDER_FEE_CURRENCY)} |
| Att betala: | ${formatReminderTotalDue(amounts)} |
| ${REMINDER_FEE_CURRENCY_NOTE} | |
| Bank: | ${company.bank_name} |
| Kontonummer: | ${company.clearing_number}-${company.account_number} |
| IBAN: | ${company.iban} |
| BIC/SWIFT: | ${company.bic} |
| Meddelande: | ${invoice.invoice_number} |
Har du redan betalat eller har frågor om fakturan?
Hantera fakturaHar du frågor? Svara direkt på detta mejl så hjälper vi dig.
Med vänliga hälsningar,
${getCompanyPrimaryName(company)}
Org.nr: ${company.org_number} ${company.vat_number ? ` | VAT: ${company.vat_number}` : ''}
` : ''}Detta är ett automatiskt meddelande angående en obetald faktura.