From b395f38b311f19365ca3965d577cd664e7ad26e2 Mon Sep 17 00:00:00 2001 From: Ben Stokes Date: Tue, 8 Sep 2026 19:09:17 +0100 Subject: [PATCH] feat: search users by verification session id in dashboard --- apps/api/src/__tests__/Account.spec.ts | 101 +++++++++++++++++- apps/api/src/modules/Account.ts | 31 +++++- .../src/app/data/services/account.service.ts | 3 +- 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/apps/api/src/__tests__/Account.spec.ts b/apps/api/src/__tests__/Account.spec.ts index a1cede1..13efb49 100644 --- a/apps/api/src/__tests__/Account.spec.ts +++ b/apps/api/src/__tests__/Account.spec.ts @@ -435,7 +435,8 @@ describe('AccountModule', () => { mockDb.Aggregate = jest .fn() .mockResolvedValueOnce([{ id: 'acct_z_connected' }]) - .mockResolvedValueOnce([{ account: 'acct_z_connected' }]); + .mockResolvedValueOnce([{ account: 'acct_z_connected' }]) + .mockResolvedValueOnce([]); mockDb.Get = jest.fn().mockResolvedValue(matched); const result = await accountModule.SearchAccounts( @@ -443,7 +444,7 @@ describe('AccountModule', () => { 'kurniawan.anto7@gmail.com' ); - expect(mockDb.Aggregate).toHaveBeenCalledTimes(2); + expect(mockDb.Aggregate).toHaveBeenCalledTimes(3); expect(mockDb.Aggregate).toHaveBeenNthCalledWith( 1, 'Accounts', @@ -454,6 +455,11 @@ describe('AccountModule', () => { 'Persons', expect.any(Array) ); + expect(mockDb.Aggregate).toHaveBeenNthCalledWith( + 3, + 'VerificationSessions', + expect.any(Array) + ); expect(result.data).toEqual([matched]); expect(result.has_more).toBe(false); expect(result.url).toBe('/v1/accounts/search'); @@ -484,5 +490,96 @@ describe('AccountModule', () => { expect(result.data).toHaveLength(1); expect(result.has_more).toBe(true); }); + + it('should return the related account for a verification session id', async () => { + const matched = { + id: 'acct_z_connected', + created: 1700000000, + platform_account: 'acct_z_platform', + } as Account; + + mockDb.Aggregate = jest + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ related_account: 'acct_z_connected' }]); + mockDb.Get = jest.fn().mockResolvedValue(matched); + + const result = await accountModule.SearchAccounts( + 'acct_z_platform', + '21d342ef-6572-47ee-b305-39b233c0d2a2' + ); + + expect(mockDb.Aggregate).toHaveBeenNthCalledWith( + 3, + 'VerificationSessions', + [ + { + $match: { + platform_account: 'acct_z_platform', + $or: [ + { + id: { + $regex: '21d342ef-6572-47ee-b305-39b233c0d2a2', + $options: 'i', + }, + }, + { + provider_session_id: { + $regex: '21d342ef-6572-47ee-b305-39b233c0d2a2', + $options: 'i', + }, + }, + ], + }, + }, + { $project: { related_account: 1 } }, + { $limit: 11 }, + ] + ); + expect(result.data).toEqual([matched]); + expect(result.has_more).toBe(false); + }); + + it('should return the related account for a Zoneless verification session id', async () => { + const matched = { + id: 'acct_z_connected', + created: 1700000000, + platform_account: 'acct_z_platform', + } as Account; + + mockDb.Aggregate = jest + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ related_account: 'acct_z_connected' }]); + mockDb.Get = jest.fn().mockResolvedValue(matched); + + const result = await accountModule.SearchAccounts( + 'acct_z_platform', + 'vs_z_abc123' + ); + + expect(mockDb.Aggregate).toHaveBeenNthCalledWith( + 3, + 'VerificationSessions', + expect.arrayContaining([ + expect.objectContaining({ + $match: expect.objectContaining({ + $or: [ + { id: { $regex: 'vs_z_abc123', $options: 'i' } }, + { + provider_session_id: { + $regex: 'vs_z_abc123', + $options: 'i', + }, + }, + ], + }), + }), + ]) + ); + expect(result.data).toEqual([matched]); + }); }); }); diff --git a/apps/api/src/modules/Account.ts b/apps/api/src/modules/Account.ts index 5eeef99..dbf64ed 100644 --- a/apps/api/src/modules/Account.ts +++ b/apps/api/src/modules/Account.ts @@ -415,11 +415,12 @@ export class AccountModule { } /** - * Search connected accounts by email, name, or account id. - * Zoneless extension — not part of Stripe's Accounts API. + * Search connected accounts by email, name, account id, or verification + * session id. Zoneless extension — not part of Stripe's Accounts API. * - * Matches account email / business name / display name / id, plus person - * email / first name / last name / full name under the same platform. + * Matches account email / business name / display name / id, person + * email / first name / last name / full name, and verification sessions + * (`vs_z...` or Didit `provider_session_id`) under the same platform. */ async SearchAccounts( platformAccountId: string, @@ -504,6 +505,28 @@ export class AccountModule { } } + if (accountIds.size <= limit) { + const sessionHits = await this.db.Aggregate<{ related_account: string }>( + 'VerificationSessions', + [ + { + $match: { + platform_account: platformAccountId, + $or: [{ id: regex }, { provider_session_id: regex }], + }, + }, + { $project: { related_account: 1 } }, + { $limit: limit + 1 }, + ] + ); + + for (const hit of sessionHits) { + if (hit.related_account && hit.related_account !== platformAccountId) { + accountIds.add(hit.related_account); + } + } + } + const orderedIds = [...accountIds]; const hasMore = orderedIds.length > limit; const pageIds = orderedIds.slice(0, limit); diff --git a/apps/web/src/app/data/services/account.service.ts b/apps/web/src/app/data/services/account.service.ts index c7bc4d8..218382c 100644 --- a/apps/web/src/app/data/services/account.service.ts +++ b/apps/web/src/app/data/services/account.service.ts @@ -163,7 +163,8 @@ export class AccountService { } /** - * Search connected accounts by email, name, or account id. + * Search connected accounts by email, name, account id, or + * verification session id (`vs_z...` or Didit session id). */ async SearchConnectedAccounts( query: string,