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
@@ -563,3 +563,41 @@ describe('getProviderResourceForbiddenMessage', () => {
expect(getProviderResourceForbiddenMessage(null)).not.toMatch(/återanslut för att fortsätta/i)
})
})
describe('getErrorMessage: INVOICE_SEND_PAYMENT_ACCOUNT_MISSING (#2126)', () => {
const envelope = (currency?: unknown) => ({
error: {
code: 'INVOICE_SEND_PAYMENT_ACCOUNT_MISSING',
message: getErrorEntry('INVOICE_SEND_PAYMENT_ACCOUNT_MISSING')!.message_sv,
message_en: getErrorEntry('INVOICE_SEND_PAYMENT_ACCOUNT_MISSING')!.message_en,
...(currency !== undefined ? { details: { currency } } : {}),
},
})
it('SEK invoice: names bankgiro/plusgiro/Swish, not a currency account', () => {
const msg = getErrorMessage(envelope('SEK'), { statusCode: 400 })
expect(msg).toContain('bankgiro')
expect(msg).toContain('Inställningar → Fakturering')
expect(msg).not.toMatch(/valuta/i)
})
it('EUR invoice: asks for an IBAN account for EUR', () => {
const msg = getErrorMessage(envelope('EUR'), { statusCode: 400 })
expect(msg).toContain('EUR')
expect(msg).toContain('IBAN')
})
it('English locale gets the currency-specific English text, not the registry fallback', () => {
const msg = getErrorMessage(envelope('SEK'), { statusCode: 400, locale: 'en' })
expect(msg).toContain('bankgiro')
expect(msg).toContain('Settings → Invoicing')
expect(msg).not.toMatch(/currency/i)
})
it('without a currency in details, falls back to the registry text', () => {
const msg = getErrorMessage(envelope(), { statusCode: 400 })
expect(msg).toBe(getErrorEntry('INVOICE_SEND_PAYMENT_ACCOUNT_MISSING')!.message_sv)
const unknown = getErrorMessage(envelope('JPY'), { statusCode: 400 })
expect(unknown).toBe(getErrorEntry('INVOICE_SEND_PAYMENT_ACCOUNT_MISSING')!.message_sv)
})
})
+18
View File
@@ -20,6 +20,10 @@
import { formatCurrency } from '@/lib/utils'
// Pure module (no next/server): safe for the client bundles this file lives in.
import { formatDimensionValidationIssues } from '@/lib/bookkeeping/dimension-errors'
import {
describeMissingInvoicePaymentAccount,
isInvoicePaymentAccountCurrency,
} from '@/lib/invoices/payment-accounts'
import { getErrorEntry, hasErrorEntry } from './structured-errors'
type ErrorContext =
@@ -352,6 +356,20 @@ export function getErrorMessage(
details?: unknown
}
// Say what is missing for THIS invoice's currency: on a SEK invoice the
// registry's currency-neutral text read as a foreign-currency account
// when the gap was the company's bankgiro (#2126). Before the English
// registry shortcut on purpose: both locales get the specific text.
if (structured.code === 'INVOICE_SEND_PAYMENT_ACCOUNT_MISSING') {
// Own local name on purpose: the sek-labelled-amount guard keys
// currency reads by owner path, and `details` is also the owner of
// the SEK-only journal totals formatted further down.
const paymentDetails = structured.details as { currency?: unknown } | undefined
if (isInvoicePaymentAccountCurrency(paymentDetails?.currency)) {
return pick(describeMissingInvoicePaymentAccount(paymentDetails.currency), locale)
}
}
// For English UI, return the registry's English message for any known
// code instead of falling through to the Swedish branches below (which
// ignored locale: English users were shown Swedish prose). The Swedish
+6 -3
View File
@@ -1145,10 +1145,13 @@ const INVOICE: Record<string, StructuredErrorEntry> = {
},
INVOICE_SEND_PAYMENT_ACCOUNT_MISSING: {
httpStatus: 400,
message_sv: 'Fakturan saknar ett betalningskonto för vald valuta. Lägg till kontot under Inställningar → Fakturering innan du skapar PDF-filen eller skickar fakturan.',
message_en: 'The invoice has no payment account for its currency. Add the account under Inställningar → Fakturering (Settings → Invoicing) before generating the PDF or sending the invoice.',
// Currency-neutral by necessity (the registry has no details). Surfaces
// that know the invoice currency say exactly what is missing through
// describeMissingInvoicePaymentAccount() (lib/invoices/payment-accounts.ts).
message_sv: 'Fakturan saknar betalningsuppgifter för sin valuta: bankgiro, plusgiro, Swish eller bankkonto för SEK, IBAN för andra valutor. Lägg till dem under Inställningar → Fakturering innan du skapar PDF-filen eller skickar fakturan.',
message_en: 'The invoice has no payment details for its currency: bankgiro, plusgiro, Swish or a bank account for SEK, an IBAN for other currencies. Add them under Inställningar → Fakturering (Settings → Invoicing) before generating the PDF or sending the invoice.',
remediation: {
description: 'Lägg till ett betalningskonto med IBAN för fakturans valuta under Inställningar → Fakturering.',
description: 'Lägg till betalningsuppgifter för fakturans valuta under Inställningar → Fakturering: bankgiro, plusgiro, Swish eller bankkonto för SEK, IBAN för andra valutor.',
},
},
INVOICE_SEND_VAT_NUMBER_MISSING: {