fix(invoices): say what payment detail is missing, per currency (#2126) (#2139)

"Fakturan saknar ett betalningskonto för vald valuta" read as a
foreign-currency account when the invoice was in SEK and the gap was
simply the company's bankgiro; the remediation line also asked for an
IBAN, which SEK does not need. A Visma-migrated user marking invoices
as sent hit this and went looking for a valutakonto.

- describeMissingInvoicePaymentAccount(currency) in
  lib/invoices/payment-accounts.ts: SEK names bankgiro, plusgiro, Swish
  or bank account; other currencies ask for an IBAN account in that
  currency (USD/GBP also offer routing number / sort code + BIC). Both
  point at Inställningar → Fakturering.
- getErrorMessage branches on INVOICE_SEND_PAYMENT_ACCOUNT_MISSING +
  details.currency (every dashboard route already sends it), before the
  English registry shortcut so both locales get the specific text.
- Registry entry rewritten currency-neutral for consumers without
  details (API, MCP): bankgiro/plusgiro/Swish/bankkonto for SEK, IBAN
  otherwise; remediation no longer says IBAN for everything.
- Staged-operation commit path uses the helper directly.

Tests: helper per currency, client mapping sv/en and the no-details
fallback.

Closes #2126


Claude-Session: https://claude.ai/code/session_01WFhSQWzu5SyXB6kG5ActZc

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-01 22:38:01 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent 4e1f739913
commit ea45e9dc2f
7 changed files with 140 additions and 9 deletions
@@ -3,6 +3,8 @@ import {
InvoicePaymentAccountMissingError,
assertInvoicePaymentAccountForRender,
companyWithInvoicePaymentAccount,
describeMissingInvoicePaymentAccount,
isInvoicePaymentAccountCurrency,
hasRequiredInvoicePaymentAccount,
hasUsableInvoicePaymentAccount,
invoiceRequiresPaymentAccount,
@@ -212,3 +214,42 @@ describe('invoice payment accounts', () => {
expect(hasRequiredInvoicePaymentAccount(emptySettings, makeInvoice())).toBe(false)
})
})
describe('describeMissingInvoicePaymentAccount (#2126)', () => {
it('SEK: names the domestic options and never talks about currency accounts', () => {
const { sv, en } = describeMissingInvoicePaymentAccount('SEK')
expect(sv).toContain('bankgiro')
expect(sv).toContain('plusgiro')
expect(sv).toContain('Swish')
expect(sv).toContain('Inställningar → Fakturering')
expect(sv).not.toMatch(/valuta/i)
expect(sv).not.toMatch(/IBAN/)
expect(en).toContain('bankgiro')
expect(en).toContain('Settings → Invoicing')
expect(en).not.toMatch(/currency/i)
})
it('EUR: asks for an IBAN account in that currency', () => {
const { sv, en } = describeMissingInvoicePaymentAccount('EUR')
expect(sv).toContain('EUR')
expect(sv).toContain('IBAN')
expect(sv).not.toContain('routing number')
expect(sv).toContain('Inställningar → Fakturering')
expect(en).toContain('EUR')
expect(en).toContain('IBAN')
})
it('USD/GBP: also offers the non-IBAN routing path', () => {
expect(describeMissingInvoicePaymentAccount('USD').sv).toContain('routing number')
expect(describeMissingInvoicePaymentAccount('GBP').sv).toContain('sort code')
expect(describeMissingInvoicePaymentAccount('GBP').en).toContain('sort code')
})
it('isInvoicePaymentAccountCurrency guards the details field', () => {
expect(isInvoicePaymentAccountCurrency('SEK')).toBe(true)
expect(isInvoicePaymentAccountCurrency('NOK')).toBe(true)
expect(isInvoicePaymentAccountCurrency('JPY')).toBe(false)
expect(isInvoicePaymentAccountCurrency(undefined)).toBe(false)
expect(isInvoicePaymentAccountCurrency(42)).toBe(false)
})
})
+33
View File
@@ -141,6 +141,39 @@ export function hasRequiredInvoicePaymentAccount(
)
}
/**
* What "payment account missing" means for THIS currency, in the user's
* words. The registry entry for INVOICE_SEND_PAYMENT_ACCOUNT_MISSING has to
* stay currency-neutral; on a SEK invoice its "betalningskonto för vald
* valuta" read as a foreign-currency account when the gap was simply the
* company's bankgiro (#2126). Pure: shared by the server envelopes, the
* staged-operation commit path and the client-side error mapper.
*/
export function describeMissingInvoicePaymentAccount(
currency: Currency,
): { sv: string; en: string } {
if (currency === 'SEK') {
return {
sv: 'Fakturan saknar betalningsuppgifter: företaget har inget bankgiro, plusgiro, Swish-nummer eller bankkonto att skriva på fakturan. Lägg till ett under Inställningar → Fakturering och försök igen.',
en: 'The invoice has no payment details: the company has no bankgiro, plusgiro, Swish number or bank account to print on the invoice. Add one under Inställningar → Fakturering (Settings → Invoicing) and try again.',
}
}
const routingSv = isNonIbanCurrency(currency)
? `, eller med ${currency === 'USD' ? 'routing number' : 'sort code'}, kontonummer och BIC,`
: ''
const routingEn = isNonIbanCurrency(currency)
? `, or with ${currency === 'USD' ? 'routing number' : 'sort code'}, account number and BIC,`
: ''
return {
sv: `Fakturan är i ${currency}, men företaget saknar ett betalningskonto för ${currency}. Lägg till ett konto med IBAN${routingSv} för ${currency} under Inställningar → Fakturering och försök igen.`,
en: `The invoice is in ${currency}, but the company has no ${currency} payment account. Add an account with an IBAN${routingEn} for ${currency} under Inställningar → Fakturering (Settings → Invoicing) and try again.`,
}
}
export function isInvoicePaymentAccountCurrency(value: unknown): value is Currency {
return typeof value === 'string' && (INVOICE_PAYMENT_ACCOUNT_CURRENCIES as readonly string[]).includes(value)
}
export class InvoicePaymentAccountMissingError extends Error {
readonly code = 'INVOICE_SEND_PAYMENT_ACCOUNT_MISSING'
readonly currency: Currency