fix(salary): make pain.001 betalfil generatable (company IBAN + BIC) (#950)

The ISO 20022 pain.001 salary payment file could never be generated: the
route required company_settings.iban/bic, but no settings screen wrote
those columns, so every request returned 400. The specific reason was
also swallowed by getErrorMessage (isSwedishUserMessage did not know
"krävs"/"saknar"), surfacing only the generic "Förfrågan innehåller
ogiltiga uppgifter" (issue #945).

- Add IBAN + BIC inputs to Settings > Fakturering > Bankuppgifter. BIC
  auto-derives from the clearing number / bank already entered, so in
  practice only the IBAN is typed. Validated client- and server-side.
- Route requires the company IBAN (canonical debtor form every Swedish
  bank accepts) and derives the BIC, with clear actionable errors.
- Employees are unchanged: domestic clearing + account (BBAN), which is
  what Swedish payroll collects. Only the company (debtor) uses IBAN.
- getErrorMessage recognizes "krävs"/"saknar" so payment-file reasons
  surface instead of the generic 400.

Fixes #945

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-09 12:35:52 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent bacc5914af
commit c1ea0d9bf2
12 changed files with 236 additions and 24 deletions
@@ -3,6 +3,7 @@ import { ensureInitialized } from '@/lib/init'
import { withRouteContext } from '@/lib/api/with-route-context'
import { generatePain001 } from '@/lib/salary/payment/pain001-generator'
import { effectiveNetPayout } from '@/lib/salary/payment/effective-net'
import { normalizeBankNumber, lookupBicByClearing, lookupBicByBankName } from '@/lib/salary/payment/bank-account'
import { getBranding } from '@/lib/branding/service'
import type { Pain001CompanyData, Pain001Employee } from '@/lib/salary/payment/pain001-generator'
@@ -47,7 +48,7 @@ export const GET = withRouteContext<{ params: Promise<{ id: string }> }>(
const { data: settings } = await supabase
.from('company_settings')
.select('company_name, iban, bic')
.select('company_name, iban, bic, clearing_number, bank_name')
.eq('company_id', companyId)
.single()
@@ -55,8 +56,31 @@ export const GET = withRouteContext<{ params: Promise<{ id: string }> }>(
return NextResponse.json({ error: 'Företag hittades inte' }, { status: 404 })
}
if (!settings?.iban || !settings?.bic) {
return NextResponse.json({ error: 'IBAN och BIC krävs i företagsinställningar för betalfil' }, { status: 400 })
if (!settings) {
return NextResponse.json({ error: 'Företagsinställningar saknas' }, { status: 400 })
}
// Debtor (company) account: the company's own IBAN, the canonical payer form
// every Swedish bank accepts. Set under Inställningar → Fakturering.
const senderIban = (settings.iban ?? '').replace(/\s/g, '').toUpperCase()
if (!senderIban) {
return NextResponse.json(
{ error: 'Företagets IBAN saknas i företagsinställningar. Fyll i det under Inställningar → Fakturering för att skapa betalfil (ISO 20022).' },
{ status: 400 },
)
}
// Debtor bank BIC: use the saved BIC, otherwise derive it from the clearing
// number (or bank name) the company already entered, so most users only need
// to fill in the IBAN. Required by the receiving bank.
const senderBic = settings.bic?.trim()
|| lookupBicByClearing(normalizeBankNumber(settings.clearing_number))
|| lookupBicByBankName(settings.bank_name)
if (!senderBic) {
return NextResponse.json(
{ error: 'Företagsbankens BIC saknas och kunde inte härledas. Fyll i BIC under Inställningar → Fakturering för att skapa betalfil.' },
{ status: 400 },
)
}
// Load employees
@@ -89,8 +113,8 @@ export const GET = withRouteContext<{ params: Promise<{ id: string }> }>(
// not the frozen onboarding companies.name.
name: settings.company_name || company.name,
orgNumber: company.org_number || '',
iban: settings.iban,
bic: settings.bic,
iban: senderIban,
bic: senderBic,
}
const employees: Pain001Employee[] = runEmployees