Skip to content

Commit c30070f

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(plaid): align integration with staging patterns
1 parent 03d4843 commit c30070f

24 files changed

Lines changed: 1194 additions & 768 deletions

apps/docs/content/docs/en/integrations/plaid.mdx

Lines changed: 228 additions & 1 deletion
Large diffs are not rendered by default.

apps/sim/blocks/blocks/plaid.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,22 @@ import { AuthMode, IntegrationType } from '@/blocks/types'
44
import type { PlaidResponse } from '@/tools/plaid/types'
55
import { toPlaidOptionalBoolean, toPlaidOptionalNumber } from '@/tools/plaid/utils'
66

7-
const ACCOUNT_FILTER_OPERATIONS = ['get_accounts', 'get_balances', 'get_identity', 'get_auth']
7+
type PlaidOperation =
8+
| 'sync_transactions'
9+
| 'get_accounts'
10+
| 'get_balances'
11+
| 'get_identity'
12+
| 'get_auth'
13+
| 'get_item'
14+
| 'search_institutions'
15+
| 'get_institution'
16+
17+
const ACCOUNT_FILTER_OPERATIONS = [
18+
'get_accounts',
19+
'get_balances',
20+
'get_identity',
21+
'get_auth',
22+
] satisfies PlaidOperation[]
823

924
export const PlaidBlock: BlockConfig<PlaidResponse> = {
1025
type: 'plaid',
@@ -107,7 +122,7 @@ export const PlaidBlock: BlockConfig<PlaidResponse> = {
107122
serviceId: 'plaid',
108123
canonicalParamId: 'institutionId',
109124
placeholder: 'Search Plaid institutions',
110-
dependsOn: ['credential'],
125+
dependsOn: ['credential', 'countryCodes'],
111126
mode: 'basic',
112127
condition: {
113128
field: 'operation',
@@ -142,7 +157,10 @@ export const PlaidBlock: BlockConfig<PlaidResponse> = {
142157
type: 'short-input',
143158
placeholder: 'Comma-separated, defaults to US',
144159
mode: 'advanced',
145-
condition: { field: 'operation', value: ['search_institutions', 'get_institution'] },
160+
condition: {
161+
field: 'operation',
162+
value: ['search_institutions', 'get_institution'] satisfies PlaidOperation[],
163+
},
146164
},
147165
{
148166
id: 'products',
@@ -399,7 +417,7 @@ export const PlaidBlockMeta = {
399417
icon: PlaidIcon,
400418
title: 'Plaid ACH payment setup',
401419
prompt:
402-
'Build a workflow that checks account verification status, fetches account and routing numbers for an eligible linked Plaid Item, and passes them directly to the payment step without storing the numbers.',
420+
'Build a workflow that checks account verification status, fetches account and routing numbers only for an eligible linked Plaid Item, passes them directly to an approved non-Plaid-partner payment processor, and never logs or persists the numbers.',
403421
modules: ['workflows'],
404422
category: 'operations',
405423
tags: ['automation'],

apps/sim/hooks/selectors/providers/plaid/selectors.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,56 @@ describe('Plaid selectors', () => {
9191
})
9292
})
9393

94+
it('normalizes selected countries into requests and isolates them in the query key', async () => {
95+
const countryArgs = args({
96+
key: 'plaid.institutions',
97+
search: 'bank',
98+
context: {
99+
workspaceId: 'workspace-1',
100+
plaidCredentialId: 'credential-1',
101+
countryCodes: ' ca, gb ',
102+
},
103+
})
104+
mockRequestJson.mockResolvedValue({ options: [] })
105+
106+
expect(institutions.getQueryKey(countryArgs)).toContain('CA,GB')
107+
await institutions.fetchList?.(countryArgs)
108+
await institutions.fetchById?.({ ...countryArgs, search: undefined, detailId: 'ins-1' })
109+
expect(mockRequestJson).toHaveBeenCalledWith(
110+
expect.anything(),
111+
expect.objectContaining({ body: expect.objectContaining({ country_codes: ['CA', 'GB'] }) })
112+
)
113+
expect(mockRequestJson).toHaveBeenLastCalledWith(
114+
expect.anything(),
115+
expect.objectContaining({
116+
body: expect.objectContaining({
117+
kind: 'institution_detail',
118+
institution_id: 'ins-1',
119+
country_codes: ['CA', 'GB'],
120+
}),
121+
})
122+
)
123+
})
124+
125+
it('defaults countries to US and rejects malformed country codes', async () => {
126+
expect(institutions.getQueryKey(args({ key: 'plaid.institutions', search: 'bank' }))).toContain(
127+
'US'
128+
)
129+
await expect(
130+
institutions.fetchList?.(
131+
args({
132+
key: 'plaid.institutions',
133+
search: 'bank',
134+
context: {
135+
workspaceId: 'workspace-1',
136+
plaidCredentialId: 'credential-1',
137+
countryCodes: 'ZZ',
138+
},
139+
})
140+
)
141+
).rejects.toThrow('countryCodes contains unsupported Plaid country code: ZZ')
142+
})
143+
94144
it('does not issue an unbounded institution search', async () => {
95145
await expect(
96146
institutions.fetchList?.(args({ key: 'plaid.institutions', search: ' ' }))

apps/sim/hooks/selectors/providers/plaid/selectors.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
SelectorOption,
99
SelectorQueryArgs,
1010
} from '@/hooks/selectors/types'
11+
import { parsePlaidCountryCodes } from '@/tools/plaid/utils'
1112

1213
type PlaidSelectorKey = Extract<SelectorKey, 'plaid.accounts' | 'plaid.institutions'>
1314

@@ -29,6 +30,15 @@ async function fetchAccountOptions(args: SelectorQueryArgs): Promise<SelectorOpt
2930
return data.options
3031
}
3132

33+
function plaidCountryQueryKey(value: string | undefined): string {
34+
const normalized = value
35+
?.split(',')
36+
.map((code) => code.trim().toUpperCase())
37+
.filter(Boolean)
38+
.join(',')
39+
return normalized || 'US'
40+
}
41+
3242
export const plaidSelectors = {
3343
'plaid.accounts': {
3444
key: 'plaid.accounts',
@@ -57,6 +67,7 @@ export const plaidSelectors = {
5767
'plaid.institutions',
5868
context.workspaceId ?? 'none',
5969
context.plaidCredentialId ?? 'none',
70+
plaidCountryQueryKey(context.countryCodes),
6071
search ?? 'none',
6172
detailId ?? 'none',
6273
],
@@ -73,7 +84,7 @@ export const plaidSelectors = {
7384
kind: 'institution_search',
7485
...scope,
7586
query,
76-
country_codes: ['US'],
87+
country_codes: parsePlaidCountryCodes(context.countryCodes),
7788
},
7889
signal,
7990
})
@@ -87,7 +98,7 @@ export const plaidSelectors = {
8798
kind: 'institution_detail',
8899
...scope,
89100
institution_id: detailId.trim(),
90-
country_codes: ['US'],
101+
country_codes: parsePlaidCountryCodes(context.countryCodes),
91102
},
92103
signal,
93104
})

apps/sim/hooks/selectors/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export interface SelectorContext {
8989
workflowId?: string
9090
oauthCredential?: string
9191
plaidCredentialId?: string
92+
countryCodes?: string
9293
serviceId?: string
9394
domain?: string
9495
teamId?: string

apps/sim/lib/workflows/subblocks/context.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ describe('buildSelectorContextFromBlock', () => {
123123
expect(ctx.jobId).toBe('job-7')
124124
})
125125

126+
it('exposes Plaid country codes to the institution selector', () => {
127+
const ctx = buildSelectorContextFromBlock('plaid', {
128+
operation: { id: 'operation', type: 'dropdown', value: 'search_institutions' },
129+
countryCodes: { id: 'countryCodes', type: 'short-input', value: 'US,CA' },
130+
})
131+
132+
expect(ctx.countryCodes).toBe('US,CA')
133+
})
134+
126135
it('should ignore subblock keys not in SELECTOR_CONTEXT_FIELDS', () => {
127136
const ctx = buildSelectorContextFromBlock('knowledge', {
128137
operation: { id: 'operation', type: 'dropdown', value: 'search' },

apps/sim/lib/workflows/subblocks/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
export const SELECTOR_CONTEXT_FIELDS = new Set<keyof SelectorContext>([
1616
'oauthCredential',
1717
'plaidCredentialId',
18+
'countryCodes',
1819
'domain',
1920
'teamId',
2021
'projectId',

apps/sim/tools/generated/tool-ids.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/generated/tool-outputs.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)