* fix(bank): keep other companies' accounts out of the EB account picker At one-session banks (SEB) a single BankID consent returns every account the signer can see across all their companies, so a reconnect from company A carries company B's accounts. PR #2116 made those arrive unchecked, labelled and unmirrored; they were still listed in company A's picker and in the connection's account list in settings, which read as "the wrong company's data in my books" (user report, Deepgrid group). - New lib/claimed-accounts.ts: partitionByClaim() splits a connection's accounts on claimed_by_company_id; describeClaimedElsewhere() renders the one-line Swedish summary. Unit-tested, including the legacy double-claim (no flag, stays own) and carried-deselection cases. - AccountPickerDialog: main list, "Markera alla" and the "x av y valda" counter cover own accounts only. Claimed accounts sit behind a collapsed "N konton synkas i <bolag>" disclosure (still tickable: a claim is a strong hint, not proof of ownership). Row markup extracted into renderAccountRow so both lists share it. - BankConnectionStatus: foreign rows dropped from the details list and the "x av y konton synkas" count; one muted summary line instead. No data or callback changes; brand-new never-claimed accounts still list unchecked, since Enable Banking's account resource carries no owner org number. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GY5eTAUFZDoCfbdWoERrsi * fix(bank): close the review and skeptic findings on the claimed-accounts picker Review (CodeRabbit): describeClaimedElsewhere decided "same claimant" on the display name; two companies can share a name. Now keyed on claimed_by_company_id as well, with a test. Skeptics (correctness + regression): - The empty-own-list message asserted "synkas redan i andra bolag" even for a consent with no accounts at all (failed connect, nothing ticked at the bank). Now only when claimed accounts exist; otherwise a plain "inga konton" message. - "Markera alla" stayed enabled but inert with zero own accounts: allSelected is now vacuously true there, so the button disables. - A claimed account ticked inside the disclosure kept counting after the disclosure was collapsed: the disclosure line now names the ticked count so the "x av y valda" counter never exceeds what is visible. - The pending_selection row in settings still counted foreign accounts ("3 konton tillgängliga" beside a picker saying none): now own accounts, with a dedicated line when everything is claimed elsewhere. - Claim flags did not survive an in-place renewal (accountsMetadata is rebuilt without them and the guard skipped seen-on-row accounts), so the sibling's accounts returned to the main list unlabeled on the next reconnect. The callback now re-derives the label from a fresh lookup for accounts that stay disabled here; released claims clear themselves. Two callback tests. Skeptic (compliance) hardening: - partitionByClaim requires enabled === false alongside the flag, so a flagged-but-enabled row (any future writer) can never hide a syncing account. - The sibling company's name is data-ph-masked on the settings summary line and the disclosure line, matching the row label. Declined: CodeRabbit docstring-coverage warning (repo has no docstring requirement; the touched functions carry inline rationale comments). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GY5eTAUFZDoCfbdWoERrsi * fix(bank): keep a re-stamped sibling claim out of the cash-account mirror CodeRabbit round 2: the renewal branch that re-derives the claim label left the account out of guardDisabledUids, so the mirror below still ran upsertFromPsd2 for it. The first connect never mirrored that account (#2116), so a renewal would have planted the sibling's IBAN in this company's cash_accounts and burned a 19xx slot for an account that stays off. Now excluded like a fresh claim; the renewal test asserts only the own account is mirrored. Declined (recorded for the summary): compliance-swarm advisory that the sibling's account metadata reaches the client. Both companies belong to the same signed-in user and the data arrives under that user's own PSD2 consent; the ownership decision is already made server-side in the callback, the picker only renders it. Non-blocking, no cross-user data. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GY5eTAUFZDoCfbdWoERrsi --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
63 lines
3.0 KiB
TypeScript
63 lines
3.0 KiB
TypeScript
import type { StoredAccount } from '../types'
|
|
|
|
/**
|
|
* Split a connection's accounts into the ones that belong in THIS company's
|
|
* books and the ones the OAuth callback marked as already booked by another
|
|
* of the user's companies (`claimed_by_company_id`, see lib/session-sharing).
|
|
*
|
|
* At one-session banks (SEB) a single BankID consent returns every account the
|
|
* signer can see across all their companies, so a reconnect from company A
|
|
* carries company B's accounts too. Since PR #2116 those arrive unchecked and
|
|
* unmirrored; this helper is what keeps them out of the main list altogether,
|
|
* so a user working in company A sees company A's accounts. The claimed set is
|
|
* still returned (never dropped): the picker renders it behind a collapsed
|
|
* disclosure, because an account that genuinely belongs here must stay
|
|
* reachable (a claim is a strong hint, not proof of ownership).
|
|
*
|
|
* An account counts as claimed elsewhere only while it is BOTH flagged and
|
|
* disabled here. Every writer keeps those two in step (the callback sets them
|
|
* together, the selection save clears the flag on enable), but the invariant
|
|
* is load-bearing: an account that syncs in this company must never be
|
|
* tucked out of sight, so a flagged-but-enabled row (a future writer, a
|
|
* support SQL fix) stays in the main list. The callback already lets the
|
|
* active company's own standing state outrank a sibling claim, so a legacy
|
|
* account enabled in both companies carries no flag and stays visible too.
|
|
*/
|
|
export function partitionByClaim<T extends Pick<StoredAccount, 'claimed_by_company_id' | 'enabled'>>(
|
|
accounts: readonly T[],
|
|
): { own: T[]; claimedElsewhere: T[] } {
|
|
const own: T[] = []
|
|
const claimedElsewhere: T[] = []
|
|
for (const account of accounts) {
|
|
if (account.claimed_by_company_id && account.enabled === false) claimedElsewhere.push(account)
|
|
else own.push(account)
|
|
}
|
|
return { own, claimedElsewhere }
|
|
}
|
|
|
|
/**
|
|
* One Swedish line summarising the hidden accounts, e.g.
|
|
* "2 konton synkas i Testbrand AB" or "3 konton synkas i andra bolag" when the
|
|
* claimants differ (or a name is missing). "Same claimant" is decided on the
|
|
* company id, not the display name: two companies can share a name. Empty
|
|
* string for an empty list so callers can render conditionally on the text.
|
|
*/
|
|
export function describeClaimedElsewhere(
|
|
accounts: readonly Pick<StoredAccount, 'claimed_by_company_id' | 'claimed_by_company_name'>[],
|
|
): string {
|
|
if (accounts.length === 0) return ''
|
|
const noun = accounts.length === 1 ? 'konto' : 'konton'
|
|
const ids = new Set<string>()
|
|
const names = new Set<string>()
|
|
let anonymous = false
|
|
for (const account of accounts) {
|
|
if (account.claimed_by_company_id) ids.add(account.claimed_by_company_id)
|
|
if (account.claimed_by_company_name) names.add(account.claimed_by_company_name)
|
|
else anonymous = true
|
|
}
|
|
if (ids.size === 1 && names.size === 1 && !anonymous) {
|
|
return `${accounts.length} ${noun} synkas i ${[...names][0]}`
|
|
}
|
|
return `${accounts.length} ${noun} synkas i ${accounts.length === 1 ? 'ett annat bolag' : 'andra bolag'}`
|
|
}
|