fix(settings): clarify bankgiro source on Foretag tab, offer IBAN prefill from bank connection (#1695)
* fix(settings): stop registry bank data masquerading as a setting, offer IBAN from bank connection User report: the Foretag tab shows a bankgiro from the Bolagsverket snapshot, which reads as a configured setting while the field payment files and invoices actually use (Fakturering) was empty. - Note on the Foretag Bankuppgifter row: data is from Bolagsverket; the editable fields live under Installningar -> Fakturering. - One-click IBAN prefill on the SEK payment account, sourced from the connected bank accounts (cash_accounts.iban). Deterministic: only offered when every connected account agrees on a single IBAN. - Delete dead BankDetailsForm.tsx (unmounted since the settings restructure); its bank fields are edited via InvoicePaymentAccountsSettings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(settings): only suggest IBAN from enabled, still-connected SEK accounts Skeptic refutation on the initial PR state: cash_accounts keeps rows after disconnect (bank_connection_id nulled) and the connect picker mirrors deselected accounts with enabled=false, so an unfiltered read could offer a closed or third-party IBAN as the invoice payee / pain.001 sender. Filter on enabled=true, currency=SEK and a non-null bank_connection_id, matching the enable-banking session-sharing invariant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1af5846adf
commit
d0640e0968
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formatIbanGroups, uniqueConnectionIban } from '../connection-iban'
|
||||
|
||||
const IBAN_A = 'SE3550000000054910000003'
|
||||
const IBAN_B = 'SE4550000000058398257466'
|
||||
|
||||
describe('uniqueConnectionIban', () => {
|
||||
it('returns null for non-array input', () => {
|
||||
expect(uniqueConnectionIban(null)).toBeNull()
|
||||
expect(uniqueConnectionIban(undefined)).toBeNull()
|
||||
expect(uniqueConnectionIban('rows')).toBeNull()
|
||||
expect(uniqueConnectionIban({ iban: IBAN_A })).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for an empty result', () => {
|
||||
expect(uniqueConnectionIban([])).toBeNull()
|
||||
})
|
||||
|
||||
it('returns the IBAN when exactly one account carries one', () => {
|
||||
expect(uniqueConnectionIban([{ iban: IBAN_A }])).toBe(IBAN_A)
|
||||
})
|
||||
|
||||
it('normalises spacing and case', () => {
|
||||
expect(uniqueConnectionIban([{ iban: 'se35 5000 0000 0549 1000 0003' }])).toBe(IBAN_A)
|
||||
})
|
||||
|
||||
it('treats the same IBAN across several accounts as one', () => {
|
||||
expect(
|
||||
uniqueConnectionIban([
|
||||
{ iban: IBAN_A },
|
||||
{ iban: 'SE35 5000 0000 0549 1000 0003' },
|
||||
]),
|
||||
).toBe(IBAN_A)
|
||||
})
|
||||
|
||||
it('returns null when accounts disagree: no guessing between banks', () => {
|
||||
expect(uniqueConnectionIban([{ iban: IBAN_A }, { iban: IBAN_B }])).toBeNull()
|
||||
})
|
||||
|
||||
it('skips rows without a usable IBAN', () => {
|
||||
expect(
|
||||
uniqueConnectionIban([
|
||||
null,
|
||||
'not-a-row',
|
||||
{ iban: null },
|
||||
{ iban: 42 },
|
||||
{ iban: 'NOT-AN-IBAN' },
|
||||
{ iban: IBAN_A },
|
||||
]),
|
||||
).toBe(IBAN_A)
|
||||
})
|
||||
|
||||
it('returns null when only invalid IBANs exist', () => {
|
||||
expect(uniqueConnectionIban([{ iban: 'SE12' }, { iban: '' }])).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatIbanGroups', () => {
|
||||
it('groups the IBAN in blocks of four', () => {
|
||||
expect(formatIbanGroups(IBAN_A)).toBe('SE35 5000 0000 0549 1000 0003')
|
||||
})
|
||||
|
||||
it('leaves a trailing partial group intact', () => {
|
||||
expect(formatIbanGroups('SE355000')).toBe('SE35 5000')
|
||||
expect(formatIbanGroups('SE3550001')).toBe('SE35 5000 1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Pick the IBAN to suggest for the SEK invoice payment account from the
|
||||
* company's connected bank accounts (cash_accounts.iban). Like the
|
||||
* Bolagsverket bankgiro suggestion, this is a suggestion only: the user
|
||||
* still confirms and saves the value.
|
||||
*
|
||||
* The caller must pre-filter rows to accounts a live connection vouches for
|
||||
* (enabled, non-null bank_connection_id, SEK): cash_accounts also holds rows
|
||||
* for disconnected and picker-deselected accounts whose IBANs must never be
|
||||
* offered as the company's own. This helper only enforces determinism: the
|
||||
* IBAN is returned when every remaining row agrees on a single one. A
|
||||
* company with accounts at two banks gets no suggestion rather than a guess
|
||||
* at which one receives customer payments.
|
||||
*
|
||||
* Rows come from an unvalidated query result, so every level is checked
|
||||
* defensively. Returns the compact uppercase IBAN, or null.
|
||||
*/
|
||||
export function uniqueConnectionIban(rows: unknown): string | null {
|
||||
if (!Array.isArray(rows)) return null
|
||||
const seen = new Set<string>()
|
||||
for (const row of rows) {
|
||||
if (!row || typeof row !== 'object') continue
|
||||
const iban = (row as { iban?: unknown }).iban
|
||||
if (typeof iban !== 'string') continue
|
||||
const compact = iban.replace(/\s/g, '').toUpperCase()
|
||||
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(compact)) continue
|
||||
seen.add(compact)
|
||||
if (seen.size > 1) return null
|
||||
}
|
||||
return seen.size === 1 ? [...seen][0] : null
|
||||
}
|
||||
|
||||
/** Display form: groups of four, "SE12 3456 ...". Storage stays compact. */
|
||||
export function formatIbanGroups(iban: string): string {
|
||||
return iban.replace(/(.{4})/g, '$1 ').trim()
|
||||
}
|
||||
Reference in New Issue
Block a user