"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:
co-authored by
Jakob Wennberg
Claude Fable 5.1
parent
4e1f739913
commit
ea45e9dc2f
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -125,6 +125,7 @@ import { renderToBuffer } from '@react-pdf/renderer'
|
||||
import { InvoicePDF } from '@/lib/invoices/pdf-template'
|
||||
import { prepareInvoicePdfRender, buildSwishQrDataUrl } from '@/lib/invoices/pdf-render-helpers'
|
||||
import {
|
||||
describeMissingInvoicePaymentAccount,
|
||||
hasRequiredInvoicePaymentAccount,
|
||||
invoiceRequiresPaymentAccount,
|
||||
} from '@/lib/invoices/payment-accounts'
|
||||
@@ -2472,9 +2473,7 @@ async function commitSendInvoice(
|
||||
const paymentAccountRequired = invoiceRequiresPaymentAccount(invoice as Invoice)
|
||||
if (!hasRequiredInvoicePaymentAccount(company as CompanySettings, invoice as Invoice)) {
|
||||
return {
|
||||
error:
|
||||
getErrorEntry('INVOICE_SEND_PAYMENT_ACCOUNT_MISSING')?.message_sv
|
||||
?? 'Betalningskonto saknas för fakturans valuta.',
|
||||
error: describeMissingInvoicePaymentAccount((invoice as Invoice).currency).sv,
|
||||
status: 400,
|
||||
}
|
||||
}
|
||||
@@ -2729,9 +2728,7 @@ async function commitMarkInvoiceSent(
|
||||
|
||||
if (!hasRequiredInvoicePaymentAccount(settings as CompanySettings, invoice as Invoice)) {
|
||||
return {
|
||||
error:
|
||||
getErrorEntry('INVOICE_SEND_PAYMENT_ACCOUNT_MISSING')?.message_sv
|
||||
?? 'Betalningskonto saknas för fakturans valuta.',
|
||||
error: describeMissingInvoicePaymentAccount((invoice as Invoice).currency).sv,
|
||||
status: 400,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user