fix(invoices): reminder emails use the per-currency payment account, never the SEK IBAN (#1806)

* fix(invoices): reminder emails use the per-currency payment account, never the SEK IBAN

Invoice PDF and invoice email resolve payment details by invoice currency
(invoice_payment_accounts, #1116), but the overdue-reminder templates still
read the legacy company_settings fields, so a EUR reminder printed the SEK
account's IBAN. A customer has paid to the wrong account from one.

Both reminder generators now go through companyWithInvoicePaymentAccount,
and sendReminder applies the same gate as invoice send: no usable account
for the invoice currency means the reminder is skipped with
INVOICE_PAYMENT_ACCOUNT_MISSING:<currency> in the result, instead of going
out with no (or the wrong) payment details.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(invoices): run the reminder payment-account gate before the fee entry and reminder row

Skeptic refutation (3/3): the gate lived in sendReminder, after
processOverdueReminders had already posted the 60 kr påminnelseavgift
verifikat and inserted the invoice_reminders row, so a skipped EUR reminder
booked a fee for an email never sent and burned the level for good. The
gate now runs in the loop before any write; sendReminder keeps it only as
a backstop for direct callers. New processor test asserts no fee entry, no
row and no email for the skipped case, and all three once an account exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-23 01:17:27 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent bf3104dd21
commit 1f6dd778e5
5 changed files with 278 additions and 2 deletions
@@ -10,6 +10,7 @@ import {
} from '../reminder-templates'
import { formatCurrency } from '@/lib/utils'
import { makeCustomer, makeInvoice, makeCompanySettings } from '@/tests/helpers'
import type { CompanySettings } from '@/types'
const company = makeCompanySettings({ company_name: 'Acme AB' })
const customer = makeCustomer({ name: 'Erik Andersson', email: 'erik@example.se' })
@@ -294,3 +295,53 @@ describe('reminder email templates: ROT/RUT-avdrag (fakturamodellen)', () => {
expect(amounts.totalDue).toBe(8_822.5)
})
})
describe('payment details follow the invoice currency', () => {
const multiCurrencyCompany = makeCompanySettings({
company_name: 'Acme AB',
bank_name: 'Svenska Banken',
iban: 'SE4550000000058398257466',
bic: 'ESSESESS',
invoice_payment_accounts: {
SEK: { bank_name: 'Svenska Banken', iban: 'SE4550000000058398257466', bic: 'ESSESESS' },
EUR: { bank_name: 'Deutsche Bank', iban: 'DE89370400440532013000', bic: 'DEUTDEFF' },
} as CompanySettings['invoice_payment_accounts'],
})
it('prints the EUR account on a EUR reminder, never the SEK IBAN', () => {
const data = { ...baseData, company: multiCurrencyCompany, invoice: eurInvoice }
const html = generateReminderEmailHtml(data)
const text = generateReminderEmailText(data)
for (const out of [html, text]) {
expect(out).toContain('DE89370400440532013000')
expect(out).toContain('DEUTDEFF')
expect(out).toContain('Deutsche Bank')
expect(out).not.toContain('SE4550000000058398257466')
expect(out).not.toContain('ESSESESS')
}
})
it('keeps the SEK account on a SEK reminder', () => {
const data = { ...baseData, company: multiCurrencyCompany, invoice }
const html = generateReminderEmailHtml(data)
const text = generateReminderEmailText(data)
for (const out of [html, text]) {
expect(out).toContain('SE4550000000058398257466')
expect(out).not.toContain('DE89370400440532013000')
}
})
it('never falls back to the legacy SEK fields for a EUR reminder when no EUR account exists', () => {
const sekOnly = makeCompanySettings({
company_name: 'Acme AB',
bank_name: 'Svenska Banken',
iban: 'SE4550000000058398257466',
bic: 'ESSESESS',
})
const data = { ...baseData, company: sekOnly, invoice: eurInvoice }
for (const out of [generateReminderEmailHtml(data), generateReminderEmailText(data)]) {
expect(out).not.toContain('SE4550000000058398257466')
expect(out).not.toContain('Svenska Banken')
}
})
})
+7 -2
View File
@@ -1,6 +1,7 @@
import type { Invoice, Customer, CompanySettings } from '@/types'
import { formatCurrency, formatDate, getCompanyDisplayName, getCompanyPrimaryName } from '@/lib/utils'
import { getAmountToPay } from '@/lib/invoices/rounding'
import { companyWithInvoicePaymentAccount } from '@/lib/invoices/payment-accounts'
/**
* What the customer was asked to pay on the original invoice: the öre-rounded
@@ -131,7 +132,6 @@ export function generateReminderEmailHtml(data: ReminderEmailData): string {
const {
invoice,
customer,
company,
reminderLevel,
daysOverdue,
actionUrl,
@@ -140,6 +140,9 @@ export function generateReminderEmailHtml(data: ReminderEmailData): string {
interestDays,
reminderFee,
} = data
// Payment details follow the invoice currency, same as the invoice email
// and PDF: a EUR reminder must never print the SEK account's IBAN.
const company = companyWithInvoicePaymentAccount(data.company, invoice.currency)
const config = REMINDER_CONFIG[reminderLevel]
const interestRatePercent = (interestRate * 100).toLocaleString('sv-SE', {
minimumFractionDigits: 0,
@@ -357,7 +360,6 @@ export function generateReminderEmailText(data: ReminderEmailData): string {
const {
invoice,
customer,
company,
reminderLevel,
daysOverdue,
actionUrl,
@@ -366,6 +368,9 @@ export function generateReminderEmailText(data: ReminderEmailData): string {
interestDays,
reminderFee,
} = data
// Payment details follow the invoice currency, same as the invoice email
// and PDF: a EUR reminder must never print the SEK account's IBAN.
const company = companyWithInvoicePaymentAccount(data.company, invoice.currency)
const config = REMINDER_CONFIG[reminderLevel]
const interestRatePercent = (interestRate * 100).toLocaleString('sv-SE', {
minimumFractionDigits: 0,