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
@@ -42,7 +42,9 @@ import {
processOverdueReminders,
determineReminderLevel,
calculateDaysOverdue,
sendReminder,
} from '../reminder-processor'
import { makeCompanySettings, makeCustomer, makeInvoice } from '@/tests/helpers'
import { getReminderDaysConfig } from '@/lib/email/reminder-templates'
describe('determineReminderLevel', () => {
@@ -173,3 +175,32 @@ describe('processOverdueReminders: credit-note filter', () => {
expect(inStatus?.args[1]).toContain('overdue')
})
})
describe('sendReminder payment-account gate', () => {
const customer = makeCustomer({ email: 'kund@example.se' })
const surcharges = { interestAmount: 0, interestRate: 0.1, interestDays: 0, reminderFee: 60 }
it('skips a EUR reminder when the company has no EUR payment account (no SEK fallback)', async () => {
const sekOnly = makeCompanySettings({ bankgiro: '123-4567', iban: 'SE4550000000058398257466' })
const invoice = { ...makeInvoice({ currency: 'EUR', total: 500 }), customer }
const result = await sendReminder(invoice, sekOnly, 1, 'tok', surcharges)
expect(result.success).toBe(false)
expect(result.error).toBe('INVOICE_PAYMENT_ACCOUNT_MISSING:EUR')
})
it('sends a SEK reminder on legacy SEK details', async () => {
const sekOnly = makeCompanySettings({ bankgiro: '123-4567' })
const invoice = { ...makeInvoice({ currency: 'SEK', total: 500 }), customer }
const result = await sendReminder(invoice, sekOnly, 1, 'tok', surcharges)
expect(result.success).toBe(true)
})
it('sends a EUR reminder once a EUR account is configured', async () => {
const company = makeCompanySettings({
invoice_payment_accounts: { EUR: { iban: 'DE89370400440532013000', bic: 'DEUTDEFF' } } as never,
})
const invoice = { ...makeInvoice({ currency: 'EUR', total: 500 }), customer }
const result = await sendReminder(invoice, company, 1, 'tok', surcharges)
expect(result.success).toBe(true)
})
})