Files
accounted/lib/company/snapshot-bank.ts
T
MattssonandClaude Fable 5 1bb423b2b3 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>
2026-08-17 11:15:37 +02:00

58 lines
2.4 KiB
TypeScript

import { validateBankgiroNumber } from '@/lib/bankgiro/luhn'
/**
* Extract the company's bankgiro number from the cached TIC company snapshot
* (companies.tic_snapshot). The snapshot's BANKUPPGIFTER rows are registry
* display data from Bolagsverket and are never read by the payment-file
* generators; those read company_settings.bankgiro. This helper bridges the
* two as a suggestion only: the user still confirms and saves the value.
*
* The snapshot must prove it describes THIS company: older snapshots were
* fetched via fuzzy search and can hold a different entity's whole profile
* (see lib/company/tic-refresh.ts), and this field ends up as the payee
* account on invoices. A suggestion is only returned when the snapshot's
* orgNumber matches the company's org_number; no match, no suggestion.
*
* Returns the raw digits (no hyphen) of the first bankgiro-typed account that
* passes the Luhn check, or null. The snapshot is unvalidated registry JSON,
* so every level is checked defensively.
*/
export function bankgiroFromTicSnapshot(
snapshot: unknown,
companyOrgNumber: string | null | undefined,
): string | null {
if (!snapshot || typeof snapshot !== 'object') return null
const snapshotOrg = (snapshot as { orgNumber?: unknown }).orgNumber
if (typeof snapshotOrg !== 'string' || !orgNumbersMatch(snapshotOrg, companyOrgNumber)) {
return null
}
const accounts = (snapshot as { bankAccounts?: unknown }).bankAccounts
if (!Array.isArray(accounts)) return null
for (const entry of accounts) {
if (!entry || typeof entry !== 'object') continue
const { type, accountNumber } = entry as { type?: unknown; accountNumber?: unknown }
if (type !== 'bankgiro' || typeof accountNumber !== 'string') continue
const digits = accountNumber.replace(/[-\s]/g, '')
if (validateBankgiroNumber(digits)) return digits
}
return null
}
/**
* Digits-only identity compare. Org numbers appear both with and without the
* hyphen, and enskild firma personnummer both as 10 and century-prefixed 12
* digits; a 12-vs-10 pair matches on the trailing 10 digits.
*/
function orgNumbersMatch(a: string, b: string | null | undefined): boolean {
if (!b) return false
const da = a.replace(/\D/g, '')
const db = b.replace(/\D/g, '')
if (!da || !db) return false
if (da === db) return true
if (da.length === 12 && db.length === 10) return da.slice(2) === db
if (da.length === 10 && db.length === 12) return da === db.slice(2)
return false
}