Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -21,6 +21,12 @@ const mockEmployees = [
{ label: 'John Doe', value: 'uuid-3' },
]

// Fixed rather than derived from today so the stories render deterministically.
const mockDateBounds = {
minCheckDate: new Date(2026, 8, 4),
minCheckOnlyDate: new Date(2026, 8, 2),
}

const defaultFormValues = {
reason: 'bonus',
isCheckOnly: false,
Expand Down Expand Up @@ -72,12 +78,24 @@ export default {

export const Default = () => {
const taxWithholding = useTaxWithholdingState()
return <OffCycleCreationPresentation employees={mockEmployees} {...taxWithholding} />
return (
<OffCycleCreationPresentation
employees={mockEmployees}
{...mockDateBounds}
{...taxWithholding}
/>
)
}

export const CorrectionSelected = () => {
const taxWithholding = useTaxWithholdingState('regular')
return <OffCycleCreationPresentation employees={mockEmployees} {...taxWithholding} />
return (
<OffCycleCreationPresentation
employees={mockEmployees}
{...mockDateBounds}
{...taxWithholding}
/>
)
}
CorrectionSelected.decorators = [
(Story: React.ComponentType) => (
Expand All @@ -91,7 +109,13 @@ CorrectionSelected.decorators = [

export const CheckOnlyMode = () => {
const taxWithholding = useTaxWithholdingState()
return <OffCycleCreationPresentation employees={mockEmployees} {...taxWithholding} />
return (
<OffCycleCreationPresentation
employees={mockEmployees}
{...mockDateBounds}
{...taxWithholding}
/>
)
}
CheckOnlyMode.decorators = [
(Story: React.ComponentType) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createOffCyclePayPeriodDateFormSchema } from '../OffCyclePayPeriodDateForm/OffCyclePayPeriodDateFormTypes'
Expand Down Expand Up @@ -132,6 +132,61 @@ describe('OffCycleCreation', () => {
})
})

// The payment date carried no minDate, so the ACH lead-time rule existed only as a
// submit-time error while legacy gws-flows disables invalid dates in the picker
// (SDK-1274). System time is pinned because the bound is derived from today.
describe('payment date minimum', () => {
// Wed Sep 2 2026 + 2 business days (the mocked paymentSpeedDays) = Fri Sep 4 2026.
const TODAY = new Date(2026, 8, 2)

beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true })
vi.setSystemTime(TODAY)
})

afterEach(() => {
vi.useRealTimers()
})

const openPaymentDateCalendar = async (user: ReturnType<typeof userEvent.setup>) => {
const group = await waitFor(() => screen.getByRole('group', { name: 'Payment date' }))
await user.click(within(group).getByRole('button'))
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
return screen.getAllByRole('gridcell')
}

const cellFor = (cells: HTMLElement[], day: string) =>
cells.find(cell => cell.textContent.trim() === day)

it('disables dates before the ACH lead time', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
renderComponent()

const cells = await openPaymentDateCalendar(user)

expect(cellFor(cells, '3')).toHaveAttribute('aria-disabled', 'true')
expect(cellFor(cells, '4')).not.toHaveAttribute('aria-disabled')
})

it('relaxes the minimum to today when check-only is selected', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
renderComponent()

const checkOnly = await waitFor(() =>
screen.getByRole('checkbox', { name: /check-only payroll/i }),
)
await user.click(checkOnly)

const cells = await openPaymentDateCalendar(user)

// Today becomes selectable; the day before it stays out of range.
expect(cellFor(cells, '2')).not.toHaveAttribute('aria-disabled')
expect(cellFor(cells, '1')).toHaveAttribute('aria-disabled', 'true')
})
})

describe('payrollType initialization', () => {
it('defaults to bonus reason when no payrollType is provided', async () => {
renderComponent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ function Root({ dictionary, companyId, payrollType = 'bonus' }: OffCycleCreation
<OffCycleCreationPresentation
employees={employees}
isPending={isPending}
minCheckDate={minCheckDate}
minCheckOnlyDate={today}
taxWithholdingConfig={taxWithholdingConfig}
isTaxWithholdingModalOpen={isTaxWithholdingModalOpen}
onTaxWithholdingEditClick={handleTaxWithholdingEditClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { Flex, RadioGroupField, SwitchField, MultiSelectComboBoxField } from '@/
export function OffCycleCreationPresentation({
employees,
isPending,
minCheckDate,
minCheckOnlyDate,
taxWithholdingConfig,
isTaxWithholdingModalOpen,
onTaxWithholdingEditClick,
Expand Down Expand Up @@ -89,7 +91,10 @@ export function OffCycleCreationPresentation({
<Heading as="h3">{t('payPeriodSectionTitle')}</Heading>
<Text variant="supporting">{t('payPeriodSectionDescription')}</Text>
</Flex>
<OffCyclePayPeriodDateFormPresentation />
<OffCyclePayPeriodDateFormPresentation
minCheckDate={minCheckDate}
minCheckOnlyDate={minCheckOnlyDate}
/>
</Flex>

<hr className={styles.divider} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export interface OffCycleCreationPresentationProps {
employees: MultiSelectComboBoxOption[]
/** Whether the off-cycle create mutation is in flight. */
isPending?: boolean
/** Earliest selectable payment date for direct deposit (today plus the ACH lead time). */
minCheckDate: Date
/** Earliest selectable payment date when the payroll is check-only (today). */
minCheckOnlyDate: Date
/** Current tax withholding configuration shown in the table. */
taxWithholdingConfig: OffCycleTaxWithholdingConfig
/** Whether the tax withholding edit modal is open. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
import { useFormContext, useWatch } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import type { OffCyclePayPeriodDateFormData } from './OffCyclePayPeriodDateFormTypes'
import styles from './OffCyclePayPeriodDateFormPresentation.module.scss'
import { useI18n } from '@/i18n'
import { CheckboxField, DatePickerField } from '@/components/Common'

/** @internal */
export function OffCyclePayPeriodDateFormPresentation() {
export interface OffCyclePayPeriodDateFormPresentationProps {
/** Earliest selectable payment date for direct deposit (today plus the ACH lead time). */
minCheckDate: Date
/** Earliest selectable payment date when the payroll is check-only (today). */
minCheckOnlyDate: Date
}

/** @internal */
export function OffCyclePayPeriodDateFormPresentation({
minCheckDate,
minCheckOnlyDate,
}: OffCyclePayPeriodDateFormPresentationProps) {
useI18n('Payroll.OffCyclePayPeriodDateForm')
const { t } = useTranslation('Payroll.OffCyclePayPeriodDateForm')

const { control } = useFormContext<OffCyclePayPeriodDateFormData>()
const isCheckOnly = useWatch({ control, name: 'isCheckOnly' })

// Mirrors the resolver's own `isCheckOnly ? today : minCheckDate` so the picker and the
// validation agree. Without a minDate the rule existed only as a submit-time error, and
// legacy gws-flows disables invalid dates in the picker (SDK-1274). Checking "check-only"
// has to widen the bound back to today, live.
const checkDateMinimum = isCheckOnly ? minCheckOnlyDate : minCheckDate

return (
<div className={styles.root}>
<div className={styles.dateFields}>
Expand All @@ -16,7 +38,12 @@ export function OffCyclePayPeriodDateFormPresentation() {
</div>

<div className={styles.checkDateField}>
<DatePickerField name="checkDate" label={t('checkDateLabel')} isRequired />
<DatePickerField
name="checkDate"
label={t('checkDateLabel')}
isRequired
minDate={checkDateMinimum}
/>
</div>

<CheckboxField
Expand Down
Loading