fix(salary): surface missing sender bankgiro/IBAN before betalfil download (#1640)

* fix(salary): surface missing sender bankgiro/IBAN before betalfil download

Users see a bankgiro under BANKUPPGIFTER in settings (Bolagsverket
snapshot, display only) while the payment-file routes read
company_settings.bankgiro, so the LB download failed with an error
that pointed at a page that looked correct. 153 companies have a
registry bankgiro but an empty settings field.

- PaymentFilePanel warns up front when the sender bankgiro (bg_lb)
  or IBAN (pain001) is missing, linking to Installningar -> Fakturering
- betalkonton form offers a one-click prefill of the bankgiro from
  companies.tic_snapshot (Luhn-validated, user still saves)
- bg-lb and skattekonto payment-file error copy now names the exact
  place to fix instead of 'foretagsinstallningar'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(salary): harden bankgiro prefill and warning per skeptic review

- bankgiroFromTicSnapshot now requires the snapshot's orgNumber to match
  companies.org_number before suggesting anything: stale fuzzy-matched
  snapshots can hold another entity's profile, and this field becomes the
  payee account on invoices and Peppol e-invoices
- salary run page refetches settings when the URL returns from the
  intercepting settings modal, so a bankgiro/IBAN saved there clears the
  missing-sender warning instead of leaving it stale

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-17 11:15:37 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 25524e1df4
commit 1bb423b2b3
11 changed files with 310 additions and 16 deletions
+31
View File
@@ -22,6 +22,15 @@ interface PaymentFilePanelProps {
defaultFormat: PaymentFormat
/** company_settings.salary_default_bank: sorts and auto-expands the matching bank's instructions. */
defaultBank?: string | null
/**
* company_settings.bankgiro / iban: the sender account each format requires.
* null means missing as of the latest settings fetch (warn up front, the
* download would 400); undefined means unknown (settings not loaded), so no
* warning is shown. The caller must refetch after detours that can fix the
* setting (the warning links into the settings modal over this page).
*/
senderBankgiro?: string | null
senderIban?: string | null
readOnly?: boolean
onDownloaded?: () => void
}
@@ -49,6 +58,8 @@ export function PaymentFilePanel({
paymentFileGeneratedAt,
defaultFormat,
defaultBank,
senderBankgiro,
senderIban,
readOnly,
onDownloaded,
}: PaymentFilePanelProps) {
@@ -175,6 +186,26 @@ export function PaymentFilePanel({
</div>
)}
{/* The sender account lives in company_settings, not in the
Bolagsverket snapshot shown on the settings overview: users see
a bankgiro there and reasonably believe it is configured. Say
the precondition here, before the download 400s on it. */}
{((format === 'bg_lb' && senderBankgiro === null) ||
(format === 'pain001' && senderIban === null)) && (
<div className="flex items-start gap-2 rounded-lg border border-border p-3 text-xs">
<AlertTriangle className="h-3.5 w-3.5 mt-0.5 shrink-0" />
<span className="text-muted-foreground">
{format === 'bg_lb' ? t('missing_bankgiro_warning') : t('missing_iban_warning')}{' '}
<Link
href="/settings/invoicing"
className="underline underline-offset-2 hover:text-foreground"
>
{t('missing_sender_link')}
</Link>
</span>
</div>
)}
<div className="flex justify-end">
<Button onClick={handleDownload} disabled={downloading}>
{downloading ? (
@@ -15,7 +15,9 @@ import {
} from '@/components/settings/SettingsRows'
import { useToast } from '@/components/ui/use-toast'
import { useCompany } from '@/contexts/CompanyContext'
import { validateBankgiroNumber, validatePlusgiroNumber } from '@/lib/bankgiro/luhn'
import { createClient } from '@/lib/supabase/client'
import { bankgiroFromTicSnapshot } from '@/lib/company/snapshot-bank'
import { formatBankgiroNumber, validateBankgiroNumber, validatePlusgiroNumber } from '@/lib/bankgiro/luhn'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
import {
INVOICE_PAYMENT_ACCOUNT_CURRENCIES,
@@ -78,7 +80,13 @@ export function InvoicePaymentAccountsSettings({
}: InvoicePaymentAccountsSettingsProps) {
const t = useTranslations('settings_invoice_payment_accounts')
const { toast } = useToast()
const { role } = useCompany()
const { role, company } = useCompany()
// Bolagsverket knows most companies' bankgiro (companies.tic_snapshot), but
// the payment files read this form's field. Offer the registry number as a
// one-click prefill when the SEK field is empty; the user still saves. The
// helper only suggests when the snapshot's orgNumber matches the company's
// org_number: stale fuzzy-matched snapshots can describe another entity.
const [snapshotBankgiro, setSnapshotBankgiro] = useState<string | null>(null)
const legacySekAccount = useMemo(
() => legacySekInvoicePaymentAccount({
bank_name: settings.bank_name,
@@ -133,6 +141,24 @@ export function InvoicePaymentAccountsSettings({
previousServerAccountsKey.current = serverAccountsKey
}, [serverAccounts, serverAccountsKey])
useEffect(() => {
if (!company?.id) return
const supabase = createClient()
let cancelled = false
supabase
.from('companies')
.select('tic_snapshot, org_number')
.eq('id', company.id)
.maybeSingle()
.then(({ data }) => {
if (cancelled) return
setSnapshotBankgiro(bankgiroFromTicSnapshot(data?.tic_snapshot, data?.org_number))
})
return () => {
cancelled = true
}
}, [company?.id])
const configuredCurrencies = useMemo(
() => INVOICE_PAYMENT_ACCOUNT_CURRENCIES.filter((currency) => !!accounts[currency]),
[accounts],
@@ -397,6 +423,15 @@ export function InvoicePaymentAccountsSettings({
onChange={(event) => updateField('bankgiro', event.target.value)}
className="max-w-40 flex-none tabular-nums"
/>
{activeCurrency === 'SEK' && !value(activeAccount, 'bankgiro') && snapshotBankgiro && (
<button
type="button"
onClick={() => updateField('bankgiro', formatBankgiroNumber(snapshotBankgiro))}
className="text-xs text-muted-foreground underline underline-offset-2 transition-colors duration-150 hover:text-foreground"
>
{t('bankgiro_prefill', { value: formatBankgiroNumber(snapshotBankgiro) })}
</button>
)}
</SettingsRow>
<SettingsRow
label={t('plusgiro_label')}