Skip to content

Commit 3e69b4c

Browse files
authored
fix: align party detail and account transaction UI with proto definitions (#1277)
* fix: align party detail components and account transactions with proto definitions Party detail tabs were calling non-existent RPC methods (getParticipant, updateParticipant, getReferences, etc.) and using wrong field names. Updated all 7 tabs to use correct proto-generated methods (retrieveParty, retrieveDemographics, retrieveReference, etc.) and field mappings. Account detail transactions tab was rendering raw enum integers for direction and status, and accessing nested amount fields incorrectly. Added enum-to-label helpers and fixed field access paths. * fix: remove unused PaymentMethod interface * fix: scope E2E references tab assertion to tabpanel The getByText('References') was matching both the tab label and the heading inside the empty state, causing a strict mode violation. * fix: address review feedback from CodeRabbit - Reset demographics form to loaded values on cancel (not empty) - Rename getPostingStatusName to getTransactionStatusName for accuracy * fix: address CodeRabbit review feedback - Add .first() to E2E references tab or-locator to prevent strict mode violation - Add htmlFor/id to demographics form labels for accessibility --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent f1c7e37 commit 3e69b4c

17 files changed

Lines changed: 260 additions & 570 deletions

frontend/e2e/specs/parties.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,11 @@ test.describe('Tab switching', () => {
195195
await switchToTab(page, 'References')
196196
await expect(page.getByRole('tab', { name: 'References', selected: true })).toBeVisible()
197197
// ReferencesTab always renders EmptyState when not loading (references-tab.tsx:30)
198+
// Scope to tabpanel to avoid matching the tab label itself
198199
await expect(
199-
page.getByText('References').or(page.getByText('No references information available'))
200+
page.getByRole('tabpanel').getByRole('heading', { name: 'References' })
201+
.or(page.getByRole('tabpanel').getByText('No references information available'))
202+
.first()
200203
).toBeVisible()
201204
})
202205

frontend/src/pages/accounts/[accountId].tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,28 @@ function DetailField({ label, children }: { label: string; children: React.React
204204
)
205205
}
206206

207+
// ---------------------------------------------------------------------------
208+
// Enum helpers
209+
// ---------------------------------------------------------------------------
210+
211+
function getDirectionName(direction: unknown): string {
212+
if (typeof direction === 'string') return direction
213+
if (typeof direction === 'number') {
214+
const dirMap: Record<number, string> = { 0: 'UNSPECIFIED', 1: 'DEBIT', 2: 'CREDIT' }
215+
return dirMap[direction] ?? String(direction)
216+
}
217+
return String(direction ?? '')
218+
}
219+
220+
function getTransactionStatusName(status: unknown): string {
221+
if (typeof status === 'string') return status
222+
if (typeof status === 'number') {
223+
const statusMap: Record<number, string> = { 0: 'UNSPECIFIED', 1: 'PENDING', 2: 'POSTED', 3: 'FAILED', 4: 'CANCELLED', 5: 'REVERSED' }
224+
return statusMap[status] ?? String(status)
225+
}
226+
return String(status ?? '')
227+
}
228+
207229
// ---------------------------------------------------------------------------
208230
// Transactions (ledger postings for this account)
209231
// ---------------------------------------------------------------------------
@@ -258,16 +280,16 @@ function AccountTransactions({ accountId, instrumentCode }: { accountId: string;
258280
{postings.map((p) => (
259281
<tr key={p.id} className="border-b last:border-0">
260282
<td className="py-2 pr-4">
261-
<StatusBadge status={String(p.postingDirection ?? '')} />
283+
<StatusBadge status={getDirectionName(p.postingDirection)} />
262284
</td>
263285
<td className="py-2 pr-4 tabular-nums">
264286
<MoneyDisplay
265-
amount={p.postingAmount?.amount?.units}
266-
currency={p.postingAmount?.amount?.currencyCode ?? instrumentCode}
287+
amount={p.postingAmount?.units}
288+
currency={p.postingAmount?.currencyCode ?? instrumentCode}
267289
/>
268290
</td>
269291
<td className="py-2 pr-4">
270-
<StatusBadge status={String(p.status ?? '')} />
292+
<StatusBadge status={getTransactionStatusName(p.status)} />
271293
</td>
272294
<td className="py-2">
273295
<TimeDisplay timestamp={p.createdAt} format="relative" />

frontend/src/pages/parties/[partyId].test.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'
77
vi.mock('@/api/context', () => ({
88
useClients: vi.fn(() => ({
99
party: {
10-
getParticipant: vi.fn().mockResolvedValue({
11-
partyId: 'test-party-1',
12-
name: 'Test Party',
13-
partyType: 'ORGANIZATION',
14-
status: 'ACTIVE',
10+
retrieveParty: vi.fn().mockResolvedValue({
11+
party: {
12+
partyId: 'test-party-1',
13+
legalName: 'Test Party',
14+
partyType: 'PARTY_TYPE_ORGANIZATION',
15+
status: 'PARTY_STATUS_ACTIVE',
16+
},
1517
}),
16-
getPaymentMethods: vi.fn().mockResolvedValue({ paymentMethods: [] }),
17-
getReferences: vi.fn().mockResolvedValue({}),
18-
getAssociations: vi.fn().mockResolvedValue({}),
19-
getBankRelations: vi.fn().mockResolvedValue({}),
18+
listPaymentMethods: vi.fn().mockResolvedValue({ paymentMethods: [] }),
19+
retrieveReference: vi.fn().mockResolvedValue({}),
20+
retrieveAssociations: vi.fn().mockResolvedValue({}),
21+
retrieveBankRelations: vi.fn().mockResolvedValue({}),
22+
retrieveDemographics: vi.fn().mockResolvedValue(null),
2023
},
2124
})),
2225
}))

frontend/src/pages/parties/components/party-header.test.tsx

Lines changed: 44 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { render, screen, waitFor } from '@testing-library/react'
33
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
44
import { PartyHeader } from './party-header'
55

6-
const mockGetParticipant = vi.fn()
6+
const mockRetrieveParty = vi.fn()
77

88
vi.mock('@/api/context', () => ({
99
useClients: vi.fn(() => ({
1010
party: {
11-
getParticipant: mockGetParticipant,
11+
retrieveParty: mockRetrieveParty,
1212
},
1313
})),
1414
}))
@@ -33,12 +33,11 @@ function renderPartyHeader(partyId = 'test-party-1') {
3333
describe('PartyHeader - loading state', () => {
3434
beforeEach(() => {
3535
vi.clearAllMocks()
36-
mockGetParticipant.mockReturnValue(new Promise(() => {})) // never resolves
36+
mockRetrieveParty.mockReturnValue(new Promise(() => {})) // never resolves
3737
})
3838

3939
it('renders skeleton placeholders while loading', () => {
4040
renderPartyHeader()
41-
// Skeletons are rendered during loading - check for loading container
4241
const container = document.querySelector('.space-y-4')
4342
expect(container).toBeInTheDocument()
4443
})
@@ -52,183 +51,77 @@ describe('PartyHeader - loading state', () => {
5251
describe('PartyHeader - error/not found state', () => {
5352
beforeEach(() => {
5453
vi.clearAllMocks()
55-
mockGetParticipant.mockResolvedValue(undefined)
54+
mockRetrieveParty.mockResolvedValue({ party: undefined })
5655
})
5756

5857
it('shows "Party not found" when party data is undefined', async () => {
5958
renderPartyHeader()
60-
6159
await waitFor(() => {
6260
expect(screen.getByText('Party not found')).toBeInTheDocument()
6361
})
6462
})
6563
})
6664

67-
describe('PartyHeader - INDIVIDUAL party type', () => {
65+
describe('PartyHeader - PARTY_TYPE_PERSON', () => {
6866
beforeEach(() => {
6967
vi.clearAllMocks()
70-
mockGetParticipant.mockResolvedValue({
71-
partyId: 'indv-001',
72-
name: 'Jane Smith',
73-
partyType: 'INDIVIDUAL',
74-
status: 'ACTIVE',
68+
mockRetrieveParty.mockResolvedValue({
69+
party: {
70+
partyId: 'indv-001',
71+
legalName: 'Jane Smith',
72+
partyType: 'PARTY_TYPE_PERSON',
73+
status: 'PARTY_STATUS_ACTIVE',
74+
},
7575
})
7676
})
7777

78-
it('renders party name', async () => {
78+
it('renders party legal name', async () => {
7979
renderPartyHeader('indv-001')
8080
await waitFor(() => {
8181
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
8282
})
8383
})
8484

85-
it('renders INDIVIDUAL party type label', async () => {
85+
it('renders party type label', async () => {
8686
renderPartyHeader('indv-001')
8787
await waitFor(() => {
88-
expect(screen.getByText('INDIVIDUAL')).toBeInTheDocument()
88+
expect(screen.getByText('PARTY_TYPE_PERSON')).toBeInTheDocument()
8989
})
9090
})
9191

92-
it('renders ACTIVE status badge', async () => {
92+
it('renders status badge', async () => {
9393
renderPartyHeader('indv-001')
9494
await waitFor(() => {
95-
expect(screen.getByText('ACTIVE')).toBeInTheDocument()
95+
expect(screen.getByText(/ACTIVE/)).toBeInTheDocument()
9696
})
9797
})
9898
})
9999

100-
describe('PartyHeader - ORGANIZATION party type', () => {
100+
describe('PartyHeader - PARTY_TYPE_ORGANIZATION', () => {
101101
beforeEach(() => {
102102
vi.clearAllMocks()
103-
mockGetParticipant.mockResolvedValue({
104-
partyId: 'org-001',
105-
name: 'Acme Corp',
106-
partyType: 'ORGANIZATION',
107-
status: 'INACTIVE',
103+
mockRetrieveParty.mockResolvedValue({
104+
party: {
105+
partyId: 'org-001',
106+
legalName: 'Acme Corp',
107+
partyType: 'PARTY_TYPE_ORGANIZATION',
108+
status: 'PARTY_STATUS_RESTRICTED',
109+
},
108110
})
109111
})
110112

111-
it('renders party name', async () => {
113+
it('renders party legal name', async () => {
112114
renderPartyHeader('org-001')
113115
await waitFor(() => {
114116
expect(screen.getByText('Acme Corp')).toBeInTheDocument()
115117
})
116118
})
117119

118-
it('renders ORGANIZATION party type label', async () => {
120+
it('renders party type label', async () => {
119121
renderPartyHeader('org-001')
120122
await waitFor(() => {
121-
expect(screen.getByText('ORGANIZATION')).toBeInTheDocument()
122-
})
123-
})
124-
125-
it('renders INACTIVE status badge', async () => {
126-
renderPartyHeader('org-001')
127-
await waitFor(() => {
128-
expect(screen.getByText('INACTIVE')).toBeInTheDocument()
129-
})
130-
})
131-
})
132-
133-
describe('PartyHeader - GOVERNMENT party type', () => {
134-
beforeEach(() => {
135-
vi.clearAllMocks()
136-
mockGetParticipant.mockResolvedValue({
137-
partyId: 'gov-001',
138-
name: 'HM Treasury',
139-
partyType: 'GOVERNMENT',
140-
status: 'SUSPENDED',
141-
})
142-
})
143-
144-
it('renders party name', async () => {
145-
renderPartyHeader('gov-001')
146-
await waitFor(() => {
147-
expect(screen.getByText('HM Treasury')).toBeInTheDocument()
148-
})
149-
})
150-
151-
it('renders GOVERNMENT party type label', async () => {
152-
renderPartyHeader('gov-001')
153-
await waitFor(() => {
154-
expect(screen.getByText('GOVERNMENT')).toBeInTheDocument()
155-
})
156-
})
157-
158-
it('renders SUSPENDED status badge', async () => {
159-
renderPartyHeader('gov-001')
160-
await waitFor(() => {
161-
expect(screen.getByText('SUSPENDED')).toBeInTheDocument()
162-
})
163-
})
164-
})
165-
166-
describe('PartyHeader - status badge variants', () => {
167-
beforeEach(() => {
168-
vi.clearAllMocks()
169-
})
170-
171-
it('renders PENDING_VERIFICATION status', async () => {
172-
mockGetParticipant.mockResolvedValue({
173-
partyId: 'party-pv',
174-
name: 'Pending Party',
175-
partyType: 'INDIVIDUAL',
176-
status: 'PENDING_VERIFICATION',
177-
})
178-
renderPartyHeader('party-pv')
179-
await waitFor(() => {
180-
// StatusBadge renders the status, which may format underscores as spaces
181-
expect(screen.getByText(/pending.?verification/i)).toBeInTheDocument()
182-
})
183-
})
184-
})
185-
186-
describe('PartyHeader - verification status', () => {
187-
beforeEach(() => {
188-
vi.clearAllMocks()
189-
})
190-
191-
it('shows verification status when provided', async () => {
192-
mockGetParticipant.mockResolvedValue({
193-
partyId: 'party-v',
194-
name: 'Verified Party',
195-
partyType: 'ORGANIZATION',
196-
status: 'ACTIVE',
197-
verificationStatus: 'KYC_COMPLETE',
198-
})
199-
renderPartyHeader('party-v')
200-
await waitFor(() => {
201-
expect(screen.getByText('Verification: KYC_COMPLETE')).toBeInTheDocument()
202-
})
203-
})
204-
205-
it('does not show verification section when verificationStatus is absent', async () => {
206-
mockGetParticipant.mockResolvedValue({
207-
partyId: 'party-nv',
208-
name: 'Unverified Party',
209-
partyType: 'INDIVIDUAL',
210-
status: 'ACTIVE',
211-
})
212-
renderPartyHeader('party-nv')
213-
await waitFor(() => {
214-
expect(screen.getByText('Unverified Party')).toBeInTheDocument()
215-
})
216-
expect(screen.queryByText(/verification:/i)).not.toBeInTheDocument()
217-
})
218-
219-
it('does not show verification section when verificationStatus is empty string', async () => {
220-
mockGetParticipant.mockResolvedValue({
221-
partyId: 'party-ev',
222-
name: 'Empty Verification',
223-
partyType: 'INDIVIDUAL',
224-
status: 'ACTIVE',
225-
verificationStatus: '',
226-
})
227-
renderPartyHeader('party-ev')
228-
await waitFor(() => {
229-
expect(screen.getByText('Empty Verification')).toBeInTheDocument()
123+
expect(screen.getByText('PARTY_TYPE_ORGANIZATION')).toBeInTheDocument()
230124
})
231-
expect(screen.queryByText(/verification:/i)).not.toBeInTheDocument()
232125
})
233126
})
234127

@@ -238,11 +131,13 @@ describe('PartyHeader - party name display', () => {
238131
})
239132

240133
it('renders party name as heading level 2', async () => {
241-
mockGetParticipant.mockResolvedValue({
242-
partyId: 'party-h2',
243-
name: 'Test Corporation',
244-
partyType: 'ORGANIZATION',
245-
status: 'ACTIVE',
134+
mockRetrieveParty.mockResolvedValue({
135+
party: {
136+
partyId: 'party-h2',
137+
legalName: 'Test Corporation',
138+
partyType: 'PARTY_TYPE_ORGANIZATION',
139+
status: 'PARTY_STATUS_ACTIVE',
140+
},
246141
})
247142
renderPartyHeader('party-h2')
248143
await waitFor(() => {
@@ -251,16 +146,18 @@ describe('PartyHeader - party name display', () => {
251146
})
252147
})
253148

254-
it('calls getParticipant with the correct partyId', async () => {
255-
mockGetParticipant.mockResolvedValue({
256-
partyId: 'specific-id',
257-
name: 'Specific Party',
258-
partyType: 'INDIVIDUAL',
259-
status: 'ACTIVE',
149+
it('calls retrieveParty with the correct partyId', async () => {
150+
mockRetrieveParty.mockResolvedValue({
151+
party: {
152+
partyId: 'specific-id',
153+
legalName: 'Specific Party',
154+
partyType: 'PARTY_TYPE_PERSON',
155+
status: 'PARTY_STATUS_ACTIVE',
156+
},
260157
})
261158
renderPartyHeader('specific-id')
262159
await waitFor(() => {
263-
expect(mockGetParticipant).toHaveBeenCalledWith({ partyId: 'specific-id' })
160+
expect(mockRetrieveParty).toHaveBeenCalledWith({ partyId: 'specific-id' })
264161
})
265162
})
266163
})

0 commit comments

Comments
 (0)