diff --git a/resources/js/components/open-banking/connect-account-dialog.tsx b/resources/js/components/open-banking/connect-account-dialog.tsx index d1734a3a..ed505170 100644 --- a/resources/js/components/open-banking/connect-account-dialog.tsx +++ b/resources/js/components/open-banking/connect-account-dialog.tsx @@ -16,7 +16,10 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import type { EnableBankingInstitution } from '@/types/banking'; +import type { + BankingConnection, + EnableBankingInstitution, +} from '@/types/banking'; import { __ } from '@/utils/i18n'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -65,6 +68,7 @@ const BITPANDA_INSTITUTION: EnableBankingInstitution = { interface ConnectAccountDialogProps { open: boolean; onOpenChange: (open: boolean) => void; + connections?: BankingConnection[]; } type Step = 'country' | 'bank' | 'confirm'; @@ -81,6 +85,7 @@ function getCsrfToken(): string { export function ConnectAccountDialog({ open, onOpenChange, + connections = [], }: ConnectAccountDialogProps) { const [step, setStep] = useState('country'); const [country, setCountry] = useState(''); @@ -171,6 +176,14 @@ export function ConnectAccountDialog({ const data = await response.json(); + const connectedEnableBankingNames = new Set( + connections + .filter((c) => c.provider === 'enablebanking') + .map((c) => c.aspsp_name), + ); + const hasProvider = (provider: string) => + connections.some((c) => c.provider === provider); + const extraInstitutions = [ BINANCE_INSTITUTION, BITPANDA_INSTITUTION, @@ -179,9 +192,20 @@ export function ConnectAccountDialog({ extraInstitutions.push(INDEXA_CAPITAL_INSTITUTION); } - const allInstitutions = [...extraInstitutions, ...data].sort( - (a, b) => a.name.localeCompare(b.name), - ); + const allInstitutions = [...extraInstitutions, ...data] + .filter((institution) => { + if (institution.name === 'Binance') { + return !hasProvider('binance'); + } + if (institution.name === 'Bitpanda') { + return !hasProvider('bitpanda'); + } + if (institution.name === 'Indexa Capital') { + return !hasProvider('indexacapital'); + } + return !connectedEnableBankingNames.has(institution.name); + }) + .sort((a, b) => a.name.localeCompare(b.name)); setInstitutions(allInstitutions); setFilteredInstitutions(allInstitutions); diff --git a/resources/js/components/open-banking/connect-account-inline.tsx b/resources/js/components/open-banking/connect-account-inline.tsx index 97960e5d..8ec5d5a7 100644 --- a/resources/js/components/open-banking/connect-account-inline.tsx +++ b/resources/js/components/open-banking/connect-account-inline.tsx @@ -9,7 +9,10 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import type { EnableBankingInstitution } from '@/types/banking'; +import type { + BankingConnection, + EnableBankingInstitution, +} from '@/types/banking'; import { __ } from '@/utils/i18n'; import { ArrowLeft } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -70,9 +73,13 @@ function getCsrfToken(): string { interface ConnectAccountInlineProps { onBack: () => void; + connections?: BankingConnection[]; } -export function ConnectAccountInline({ onBack }: ConnectAccountInlineProps) { +export function ConnectAccountInline({ + onBack, + connections = [], +}: ConnectAccountInlineProps) { const [step, setStep] = useState('country'); const { trigger } = useWebHaptics(); const [country, setCountry] = useState(''); @@ -153,6 +160,14 @@ export function ConnectAccountInline({ onBack }: ConnectAccountInlineProps) { const data = await response.json(); + const connectedEnableBankingNames = new Set( + connections + .filter((c) => c.provider === 'enablebanking') + .map((c) => c.aspsp_name), + ); + const hasProvider = (provider: string) => + connections.some((c) => c.provider === provider); + const extraInstitutions = [ BINANCE_INSTITUTION, BITPANDA_INSTITUTION, @@ -161,9 +176,20 @@ export function ConnectAccountInline({ onBack }: ConnectAccountInlineProps) { extraInstitutions.push(INDEXA_CAPITAL_INSTITUTION); } - const allInstitutions = [...extraInstitutions, ...data].sort( - (a, b) => a.name.localeCompare(b.name), - ); + const allInstitutions = [...extraInstitutions, ...data] + .filter((institution) => { + if (institution.name === 'Binance') { + return !hasProvider('binance'); + } + if (institution.name === 'Bitpanda') { + return !hasProvider('bitpanda'); + } + if (institution.name === 'Indexa Capital') { + return !hasProvider('indexacapital'); + } + return !connectedEnableBankingNames.has(institution.name); + }) + .sort((a, b) => a.name.localeCompare(b.name)); setInstitutions(allInstitutions); setFilteredInstitutions(allInstitutions); diff --git a/resources/js/pages/settings/connections.tsx b/resources/js/pages/settings/connections.tsx index 50aeee01..1cb17d91 100644 --- a/resources/js/pages/settings/connections.tsx +++ b/resources/js/pages/settings/connections.tsx @@ -351,6 +351,7 @@ export default function ConnectionsPage({ connections }: Props) { assertSessionHasErrors('api_secret'); }); + +test('connections page includes provider and aspsp_name fields needed for frontend duplicate filtering', function () { + $user = User::factory()->onboarded()->create(); + Feature::for($user)->activate('open-banking'); + + BankingConnection::factory()->create([ + 'user_id' => $user->id, + 'provider' => 'enablebanking', + 'aspsp_name' => 'CaixaBank', + ]); + + $response = $this->actingAs($user)->get('/settings/connections'); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->has('connections', 1) + ->where('connections.0.provider', 'enablebanking') + ->where('connections.0.aspsp_name', 'CaixaBank') + ); +}); + +test('connections page includes connections from all provider types for frontend filtering', function () { + $user = User::factory()->onboarded()->create(); + Feature::for($user)->activate('open-banking'); + + BankingConnection::factory()->create(['user_id' => $user->id, 'provider' => 'enablebanking', 'aspsp_name' => 'CaixaBank']); + BankingConnection::factory()->binance()->create(['user_id' => $user->id]); + BankingConnection::factory()->bitpanda()->create(['user_id' => $user->id]); + BankingConnection::factory()->indexaCapital()->create(['user_id' => $user->id]); + + $response = $this->actingAs($user)->get('/settings/connections'); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->has('connections', 4) + ->has('connections.0.provider') + ->has('connections.0.aspsp_name') + ); +});