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:
co-authored by
Claude Fable 5
parent
bf3104dd21
commit
1f6dd778e5
@@ -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')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user