Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PaymentsList } from '../PaymentsList/PaymentsList'
import { CreatePayment } from '../CreatePayment/CreatePayment'
import { PaymentHistory } from '../PaymentHistory/PaymentHistory'
import { PaymentStatement } from '../PaymentStatement/PaymentStatement'
import type { InternalAlert } from '../types'
import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow'
import type { BaseComponentInterface } from '@/components/Base'
Expand All @@ -15,6 +16,7 @@ export interface PaymentFlowContextInterface extends FlowContextInterface {
companyId: string
breadcrumbs?: BreadcrumbTrail
currentPaymentId?: string
currentContractorUuid?: string
alerts?: InternalAlert[]
}

Expand All @@ -32,3 +34,15 @@ export function PaymentHistoryContextual() {
const { currentPaymentId, onEvent } = useFlow<PaymentFlowContextInterface>()
return <PaymentHistory onEvent={onEvent} paymentId={ensureRequired(currentPaymentId)} />
}

export function PaymentStatementContextual() {
const { currentPaymentId, currentContractorUuid, onEvent } =
useFlow<PaymentFlowContextInterface>()
return (
<PaymentStatement
onEvent={onEvent}
paymentGroupId={ensureRequired(currentPaymentId)}
contractorUuid={ensureRequired(currentContractorUuid)}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type PaymentFlowContextInterface,
PaymentHistoryContextual,
PaymentListContextual,
PaymentStatementContextual,
} from './PaymentFlowComponents'
import { componentEvents } from '@/shared/constants'
import type { MachineEventType, MachineTransition } from '@/types/Helpers'
Expand All @@ -20,6 +21,10 @@ type EventPayloads = {
[componentEvents.CONTRACTOR_PAYMENT_SUBMIT]: undefined
[componentEvents.CONTRACTOR_PAYMENT_CREATED]: ContractorPaymentGroup
[componentEvents.CONTRACTOR_PAYMENT_VIEW]: { paymentId: string }
[componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS]: {
contractorUuid: string
paymentGroupId: string
}
[componentEvents.BREADCRUMB_NAVIGATE]: {
key: string
onNavigate: (ctx: PaymentFlowContextInterface) => PaymentFlowContextInterface
Expand Down Expand Up @@ -78,9 +83,21 @@ export const paymentFlowBreadcrumbsNodes: BreadcrumbNodes = {
history: {
parent: 'landing',
item: {
id: 'receipts',
id: 'history',
label: 'breadcrumbLabel',
namespace: 'Contractor.Payments.PaymentHistory',
onNavigate: ((ctx: PaymentFlowContextInterface) => ({
...updateBreadcrumbs('history', ctx),
component: PaymentHistoryContextual,
})) as (context: unknown) => unknown,
},
},
statement: {
parent: 'history',
item: {
id: 'statement',
label: 'breadcrumbLabel',
namespace: 'Contractor.Payments.PaymentStatement',
},
},
} as const
Expand Down Expand Up @@ -163,5 +180,33 @@ export const paymentMachine = {
),
breadcrumbNavigateTransition('landing'),
),
history: state(),
history: state<MachineTransition>(
transition(
componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS,
'statement',
reduce(
(
ctx: PaymentFlowContextInterface,
ev: MachineEventType<
EventPayloads,
typeof componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS
>,
): PaymentFlowContextInterface => {
return {
...updateBreadcrumbs('statement', ctx),
component: PaymentStatementContextual,
currentContractorUuid: ev.payload.contractorUuid,
currentPaymentId: ev.payload.paymentGroupId,
progressBarType: 'breadcrumbs',
alerts: undefined,
}
},
),
),
breadcrumbNavigateTransition('landing'),
),
statement: state<MachineTransition>(
breadcrumbNavigateTransition('landing'),
breadcrumbNavigateTransition('history'),
),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { http, HttpResponse } from 'msw'
import { PaymentHistory } from './PaymentHistory'
import { server } from '@/test/mocks/server'
import { componentEvents } from '@/shared/constants'
import { setupApiTestMocks } from '@/test/mocks/apiServer'
import { renderWithProviders } from '@/test-utils/renderWithProviders'
import { API_BASE_URL } from '@/test/constants'

const mockPaymentGroup = {
uuid: 'payment-group-123',
company_uuid: 'company-123',
check_date: '2024-12-15',
debit_date: '2024-12-13',
status: 'Funded',
contractor_payments: [
{
uuid: 'payment-1',
contractor_uuid: 'contractor-123',
bonus: '350.00',
hours: '16.0',
payment_method: 'Direct Deposit',
reimbursement: '0.00',
hourly_rate: '18.00',
wage: '0.00',
wage_type: 'Hourly',
wage_total: '638.00',
may_cancel: false,
},
{
uuid: 'payment-2',
contractor_uuid: 'contractor-456',
bonus: '100.00',
hours: '0',
payment_method: 'Check',
reimbursement: '50.00',
wage: '1000.00',
wage_type: 'Fixed',
wage_total: '1000.00',
may_cancel: true,
},
],
}

const mockContractorsList = {
contractor_list: [
{
uuid: 'contractor-123',
first_name: 'Ella',
last_name: 'Fitzgerald',
type: 'Individual',
is_active: true,
onboarding_status: 'onboarding_completed',
},
{
uuid: 'contractor-456',
business_name: 'Acme Consulting LLC',
type: 'Business',
is_active: true,
onboarding_status: 'onboarding_completed',
},
],
}

describe('PaymentHistory', () => {
const onEvent = vi.fn()
const user = userEvent.setup()
const defaultProps = {
paymentId: 'payment-group-123',
onEvent,
}

beforeEach(() => {
setupApiTestMocks()
onEvent.mockClear()

server.use(
http.get(
`${API_BASE_URL}/v1/contractor_payment_groups/:contractor_payment_group_uuid`,
() => {
return HttpResponse.json({ contractor_payment_group: mockPaymentGroup })
},
),
http.get(`${API_BASE_URL}/v1/companies/:company_uuid/contractors`, () => {
return HttpResponse.json(mockContractorsList)
}),
)
})

describe('rendering', () => {
it('renders payment history with contractor payments', async () => {
renderWithProviders(<PaymentHistory {...defaultProps} />)

await waitFor(() => {

Check failure on line 96 in src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx

View workflow job for this annotation

GitHub Actions / build

src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx > PaymentHistory > rendering > renders payment history with contractor payments

TestingLibraryElementError: Unable to find an element with the text: Contractor payment history. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <article class="GSDK" data-testid="GSDK" > <div lang="en-US" > <div class="_internalErrorCard_c4fd83" data-testid="internal-error-card" role="alert" > <div> <h1 class="_internalErrorCardTitle_c4fd83 _root_6f170c _h3_6f170c _textAlign-undefined_6f170c" > Error </h1> <p class="_errorMessage_c4fd83 _root_173295 _md_173295" > Error while rendering SDK component: Input validation failed: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "companyUuid" ], "message": "Required" } ] </p> </div> <div> <button class="_root_fb3795 react-aria-Button" data-rac="" data-react-aria-pressable="true" data-variant="secondary" id="react-aria-_r_2_" tabindex="0" type="button" > Try again </button> </div> </div> </div> </article> </div> </body> Ignored nodes: comments, script, style <html> <head /> <body> <div> <article class="GSDK" data-testid="GSDK" > <div lang="en-US" > <div class="_internalErrorCard_c4fd83" data-testid="internal-error-card" role="alert" > <div> <h1 class="_internalErrorCardTitle_c4fd83 _root_6f170c _h3_6f170c _textAlign-undefined_6f170c" > Error </h1> <p class="_errorMessage_c4fd83 _root_173295 _md_173295" > Error while rendering SDK component: Input validation failed: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "companyUuid" ], "message": "Required" } ] </p> </div> <div> <button class="_root_fb3795 react-aria-Button" data-rac="" data-react-aria-pressable="true" data-variant="secondary" id="react-aria-_r_2_" tabindex="0" type="button" > Try again </button> </div> </div> </div> </article> </div> </body> </html> ❯ waitFor node_modules/@testing-library/dom/dist/wait-for.js:163:27 ❯ src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx:96:13
expect(screen.getByText('Contractor payment history')).toBeInTheDocument()
})

expect(screen.getByText('Ella Fitzgerald')).toBeInTheDocument()
expect(screen.getByText('Acme Consulting LLC')).toBeInTheDocument()
expect(screen.getByText('$638.00')).toBeInTheDocument()
expect(screen.getByText('$1,150.00')).toBeInTheDocument()
})

it('renders empty state when no payments', async () => {
server.use(
http.get(
`${API_BASE_URL}/v1/contractor_payment_groups/:contractor_payment_group_uuid`,
() => {
return HttpResponse.json({
contractor_payment_group: { ...mockPaymentGroup, contractor_payments: [] },
})
},
),
)

renderWithProviders(<PaymentHistory {...defaultProps} />)

await waitFor(() => {

Check failure on line 120 in src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx

View workflow job for this annotation

GitHub Actions / build

src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx > PaymentHistory > rendering > renders empty state when no payments

TestingLibraryElementError: Unable to find an element with the text: No payments found. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <article class="GSDK" data-testid="GSDK" > <div lang="en-US" > <div class="_internalErrorCard_c4fd83" data-testid="internal-error-card" role="alert" > <div> <h1 class="_internalErrorCardTitle_c4fd83 _root_6f170c _h3_6f170c _textAlign-undefined_6f170c" > Error </h1> <p class="_errorMessage_c4fd83 _root_173295 _md_173295" > Error while rendering SDK component: Input validation failed: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "companyUuid" ], "message": "Required" } ] </p> </div> <div> <button class="_root_fb3795 react-aria-Button" data-rac="" data-react-aria-pressable="true" data-variant="secondary" id="react-aria-_r_6_" tabindex="0" type="button" > Try again </button> </div> </div> </div> </article> </div> </body> Ignored nodes: comments, script, style <html> <head /> <body> <div> <article class="GSDK" data-testid="GSDK" > <div lang="en-US" > <div class="_internalErrorCard_c4fd83" data-testid="internal-error-card" role="alert" > <div> <h1 class="_internalErrorCardTitle_c4fd83 _root_6f170c _h3_6f170c _textAlign-undefined_6f170c" > Error </h1> <p class="_errorMessage_c4fd83 _root_173295 _md_173295" > Error while rendering SDK component: Input validation failed: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "companyUuid" ], "message": "Required" } ] </p> </div> <div> <button class="_root_fb3795 react-aria-Button" data-rac="" data-react-aria-pressable="true" data-variant="secondary" id="react-aria-_r_6_" tabindex="0" type="button" > Try again </button> </div> </div> </div> </article> </div> </body> </html> ❯ waitFor node_modules/@testing-library/dom/dist/wait-for.js:163:27 ❯ src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx:120:13
expect(screen.getByText('No payments found')).toBeInTheDocument()
})
})
})

describe('payment actions', () => {
it('emits view details event when payment is clicked', async () => {
renderWithProviders(<PaymentHistory {...defaultProps} />)

await waitFor(() => {

Check failure on line 130 in src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx

View workflow job for this annotation

GitHub Actions / build

src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx > PaymentHistory > payment actions > emits view details event when payment is clicked

TestingLibraryElementError: Unable to find an element with the text: Ella Fitzgerald. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <article class="GSDK" data-testid="GSDK" > <div lang="en-US" > <div class="_internalErrorCard_c4fd83" data-testid="internal-error-card" role="alert" > <div> <h1 class="_internalErrorCardTitle_c4fd83 _root_6f170c _h3_6f170c _textAlign-undefined_6f170c" > Error </h1> <p class="_errorMessage_c4fd83 _root_173295 _md_173295" > Error while rendering SDK component: Input validation failed: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "companyUuid" ], "message": "Required" } ] </p> </div> <div> <button class="_root_fb3795 react-aria-Button" data-rac="" data-react-aria-pressable="true" data-variant="secondary" id="react-aria-_r_a_" tabindex="0" type="button" > Try again </button> </div> </div> </div> </article> </div> </body> Ignored nodes: comments, script, style <html> <head /> <body> <div> <article class="GSDK" data-testid="GSDK" > <div lang="en-US" > <div class="_internalErrorCard_c4fd83" data-testid="internal-error-card" role="alert" > <div> <h1 class="_internalErrorCardTitle_c4fd83 _root_6f170c _h3_6f170c _textAlign-undefined_6f170c" > Error </h1> <p class="_errorMessage_c4fd83 _root_173295 _md_173295" > Error while rendering SDK component: Input validation failed: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "companyUuid" ], "message": "Required" } ] </p> </div> <div> <button class="_root_fb3795 react-aria-Button" data-rac="" data-react-aria-pressable="true" data-variant="secondary" id="react-aria-_r_a_" tabindex="0" type="button" > Try again </button> </div> </div> </div> </article> </div> </body> </html> ❯ waitFor node_modules/@testing-library/dom/dist/wait-for.js:163:27 ❯ src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx:130:13
expect(screen.getByText('Ella Fitzgerald')).toBeInTheDocument()
})

await user.click(screen.getByText('Ella Fitzgerald'))

expect(onEvent).toHaveBeenCalledWith(componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS, {
contractorUuid: 'contractor-123',
paymentGroupId: 'payment-group-123',
})
})

it('shows cancel option for cancellable payments', async () => {
renderWithProviders(<PaymentHistory {...defaultProps} />)

await waitFor(() => {

Check failure on line 145 in src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx

View workflow job for this annotation

GitHub Actions / build

src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx > PaymentHistory > payment actions > shows cancel option for cancellable payments

TestingLibraryElementError: Unable to find an element with the text: Acme Consulting LLC. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <article class="GSDK" data-testid="GSDK" > <div lang="en-US" > <div class="_internalErrorCard_c4fd83" data-testid="internal-error-card" role="alert" > <div> <h1 class="_internalErrorCardTitle_c4fd83 _root_6f170c _h3_6f170c _textAlign-undefined_6f170c" > Error </h1> <p class="_errorMessage_c4fd83 _root_173295 _md_173295" > Error while rendering SDK component: Input validation failed: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "companyUuid" ], "message": "Required" } ] </p> </div> <div> <button class="_root_fb3795 react-aria-Button" data-rac="" data-react-aria-pressable="true" data-variant="secondary" id="react-aria-_r_e_" tabindex="0" type="button" > Try again </button> </div> </div> </div> </article> </div> </body> Ignored nodes: comments, script, style <html> <head /> <body> <div> <article class="GSDK" data-testid="GSDK" > <div lang="en-US" > <div class="_internalErrorCard_c4fd83" data-testid="internal-error-card" role="alert" > <div> <h1 class="_internalErrorCardTitle_c4fd83 _root_6f170c _h3_6f170c _textAlign-undefined_6f170c" > Error </h1> <p class="_errorMessage_c4fd83 _root_173295 _md_173295" > Error while rendering SDK component: Input validation failed: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "companyUuid" ], "message": "Required" } ] </p> </div> <div> <button class="_root_fb3795 react-aria-Button" data-rac="" data-react-aria-pressable="true" data-variant="secondary" id="react-aria-_r_e_" tabindex="0" type="button" > Try again </button> </div> </div> </div> </article> </div> </body> </html>... ❯ waitFor node_modules/@testing-library/dom/dist/wait-for.js:163:27 ❯ src/components/Contractor/Payments/PaymentHistory/PaymentHistory.test.tsx:145:13
expect(screen.getByText('Acme Consulting LLC')).toBeInTheDocument()
})

const menuButtons = screen.getAllByRole('button', { name: /action/i })
await user.click(menuButtons[1]!)

await waitFor(() => {
expect(screen.getByText('Cancel payment')).toBeInTheDocument()
})
})
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test expects "Cancel payment" to be visible in the menu, but the cancel payment functionality has been commented out in PaymentHistoryPresentation.tsx (lines 134-147). The test will fail because the cancel option is not rendered. Either remove this test or uncomment the cancel payment functionality if it should be available.

Suggested change
it('shows cancel option for cancellable payments', async () => {
renderWithProviders(<PaymentHistory {...defaultProps} />)
await waitFor(() => {
expect(screen.getByText('Acme Consulting LLC')).toBeInTheDocument()
})
const menuButtons = screen.getAllByRole('button', { name: /action/i })
await user.click(menuButtons[1]!)
await waitFor(() => {
expect(screen.getByText('Cancel payment')).toBeInTheDocument()
})
})

Copilot uses AI. Check for mistakes.
})

describe('error handling', () => {
it('handles missing payment group', async () => {
server.use(
http.get(
`${API_BASE_URL}/v1/contractor_payment_groups/:contractor_payment_group_uuid`,
() => {
return HttpResponse.json({})
},
),
)

renderWithProviders(<PaymentHistory {...defaultProps} />)

await waitFor(() => {
expect(screen.queryByText('Contractor payment history')).not.toBeInTheDocument()
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { PaymentHistoryPresentation } from './PaymentHistoryPresentation'
import { useComponentDictionary } from '@/i18n'
import { BaseComponent, type BaseComponentInterface } from '@/components/Base'
import { componentEvents, ContractorOnboardingStatus } from '@/shared/constants'
import { componentEvents } from '@/shared/constants'

interface PaymentHistoryProps extends BaseComponentInterface<'Contractor.Payments.PaymentHistory'> {
paymentId: string
Expand Down Expand Up @@ -32,14 +32,13 @@ export const Root = ({ paymentId, dictionary, onEvent }: PaymentHistoryProps) =>
const companyId = paymentGroupResponse.contractorPaymentGroup.companyUuid!

const { data: contractorList } = useContractorsListSuspense({ companyUuid: companyId })
const contractors = (contractorList.contractorList || []).filter(
contractor =>
contractor.isActive &&
contractor.onboardingStatus === ContractorOnboardingStatus.ONBOARDING_COMPLETED,
)
const contractors = contractorList.contractorList || []

const handleViewPayment = (paymentId: string) => {
onEvent(componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS, { paymentId })
const handleViewPayment = (contractorUuid: string) => {
onEvent(componentEvents.CONTRACTOR_PAYMENT_VIEW_DETAILS, {
contractorUuid,
paymentGroupId: paymentId,
})
}

const handleCancelPayment = (paymentId: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { formatHoursDisplay } from '@/components/Payroll/helpers'
import useNumberFormatter from '@/hooks/useNumberFormatter'
import { useDateFormatter } from '@/hooks/useDateFormatter'
import EyeIcon from '@/assets/icons/eye.svg?react'
import CancelIcon from '@/assets/icons/slash-circle.svg?react'
// import CancelIcon from '@/assets/icons/slash-circle.svg?react'

interface PaymentHistoryPresentationProps {
paymentGroup: ContractorPaymentGroup
Expand All @@ -37,7 +37,7 @@ export const PaymentHistoryPresentation = ({
return (
<Flex flexDirection="column" gap={32}>
<Flex flexDirection="column" gap={8}>
<Heading as="h1">{t('title')}</Heading>
<Heading as="h2">{t('title')}</Heading>
<Text>
<Trans
i18nKey={'subtitle'}
Expand Down Expand Up @@ -131,20 +131,20 @@ export const PaymentHistoryPresentation = ({
),
},
]

if (mayCancel) {
items.push({
label: t('actions.cancel'),
onClick: () => {
onCancelPayment(contractorUuid!)
},
icon: (
<span className={styles.icon}>
<CancelIcon aria-hidden />
</span>
),
})
}
// TODO: Waiting for new UX for cancelling payments
// if (mayCancel) {
// items.push({
// label: t('actions.cancel'),
// onClick: () => {
// onCancelPayment(contractorUuid!)
// },
// icon: (
// <span className={styles.icon}>
// <CancelIcon aria-hidden />
// </span>
// ),
// })
// }
return <HamburgerMenu items={items} triggerLabel={t('tableHeaders.action')} />
}}
data={payments}
Expand Down
Loading
Loading