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
34 changes: 25 additions & 9 deletions src/components/Payroll/PayrollHistory/PayrollHistory.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,17 @@ describe('PayrollHistory', () => {
})

it('shows cancel option only for cancellable payrolls', async () => {
// Use fixture with unprocessed payroll data
const mockUnprocessedPayroll = await getFixture('payroll-history-unprocessed-test-data')
// Mock current time to be before 4 PM PT cutoff on deadline day
// Deadline is 2024-12-14T23:30:00Z (3:30 PM PST Dec 14)
// Cutoff is 4:00 PM PST Dec 14 (2024-12-15T00:00:00Z)
// Set current time to 2:00 PM PST Dec 14 (2024-12-14T22:00:00Z)
vi.setSystemTime(new Date('2024-12-14T22:00:00Z'))

const mockProcessedPayroll = await getFixture('payroll-history-unprocessed-test-data')

server.use(
http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
return HttpResponse.json(mockUnprocessedPayroll)
return HttpResponse.json(mockProcessedPayroll)
}),
)

Expand All @@ -208,19 +213,23 @@ describe('PayrollHistory', () => {
const menuButtons = screen.getAllByRole('button', { name: /open menu/i })
await user.click(menuButtons[0]!)

// Should show cancel option for unprocessed payroll
// Should show cancel option for processed payroll before cutoff
await waitFor(() => {
expect(screen.getByText('Cancel payroll')).toBeInTheDocument()
})

vi.useRealTimers()
})

it('handles payroll cancellation', async () => {
// Use fixture with unprocessed payroll data
const mockUnprocessedPayroll = await getFixture('payroll-history-unprocessed-test-data')
// Mock current time to be before 4 PM PT cutoff on deadline day
vi.setSystemTime(new Date('2024-12-14T22:00:00Z'))

const mockProcessedPayroll = await getFixture('payroll-history-unprocessed-test-data')

server.use(
http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
return HttpResponse.json(mockUnprocessedPayroll)
return HttpResponse.json(mockProcessedPayroll)
}),
// Mock the cancel API
http.put(`${API_BASE_URL}/v1/companies/:company_id/payrolls/:payroll_id/cancel`, () => {
Expand Down Expand Up @@ -260,14 +269,19 @@ describe('PayrollHistory', () => {
}),
)
})

vi.useRealTimers()
})

it('handles cancellation errors gracefully', async () => {
const mockUnprocessedPayroll = await getFixture('payroll-history-unprocessed-test-data')
// Mock current time to be before 4 PM PT cutoff on deadline day
vi.setSystemTime(new Date('2024-12-14T22:00:00Z'))

const mockProcessedPayroll = await getFixture('payroll-history-unprocessed-test-data')

server.use(
http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
return HttpResponse.json(mockUnprocessedPayroll)
return HttpResponse.json(mockProcessedPayroll)
}),
// Mock API error
http.put(`${API_BASE_URL}/v1/companies/:company_id/payrolls/:payroll_id/cancel`, () => {
Expand Down Expand Up @@ -301,6 +315,8 @@ describe('PayrollHistory', () => {
await waitFor(() => {
expect(screen.getByText('There was a problem with your submission')).toBeInTheDocument()
})

vi.useRealTimers()
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
import type { Payroll } from '@gusto/embedded-api/models/components/payroll'
import type { WireInRequest } from '@gusto/embedded-api/models/components/wireinrequest'
import { PayrollStatusBadges } from '../PayrollStatusBadges'
import { getPayrollType, calculateTotalPayroll } from '../helpers'
import { getPayrollType, calculateTotalPayroll, canCancelPayroll } from '../helpers'
import type { TimeFilterOption } from './PayrollHistory'
import styles from './PayrollHistoryPresentation.module.scss'
import type { MenuItem } from '@/components/Common/UI/Menu/MenuTypes'
Expand Down Expand Up @@ -54,48 +54,6 @@ export const PayrollHistoryPresentation = ({
{ value: 'year', label: t('timeFilter.options.year') },
]

const canCancelPayroll = (item: Payroll) => {
if (item.payrollStatusMeta?.cancellable === false) {
return false
}

if (item.processed && item.payrollDeadline) {
const now = new Date()
const deadline = new Date(item.payrollDeadline)

const ptOffset = getPacificTimeOffset(now)
const nowInPT = new Date(now.getTime() + ptOffset * 60 * 60 * 1000)
const deadlineInPT = new Date(
deadline.getTime() + getPacificTimeOffset(deadline) * 60 * 60 * 1000,
)

const isSameDay = nowInPT.toDateString() === deadlineInPT.toDateString()
if (isSameDay) {
const cutoffTime = new Date(deadlineInPT)
cutoffTime.setHours(15, 30, 0, 0)

if (nowInPT > cutoffTime) {
return false
}
}
}

return true
}

const getPacificTimeOffset = (date: Date): number => {
const year = date.getFullYear()

const secondSundayMarch = new Date(year, 2, 1)
secondSundayMarch.setDate(1 + (7 - secondSundayMarch.getDay()) + 7)

const firstSundayNovember = new Date(year, 10, 1)
firstSundayNovember.setDate(1 + ((7 - firstSundayNovember.getDay()) % 7))

const isDST = date >= secondSundayMarch && date < firstSundayNovember
return isDST ? -7 : -8
}

const formatDeadlineForDialog = (item: Payroll): string => {
const deadline = item.payrollDeadline
if (!deadline) return ''
Expand Down
2 changes: 2 additions & 0 deletions src/components/Payroll/PayrollOverview/PayrollOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ConfirmWireDetails,
type ConfirmWireDetailsComponentType,
} from '../ConfirmWireDetails/ConfirmWireDetails'
import { canCancelPayroll } from '../helpers'
import { PayrollOverviewPresentation } from './PayrollOverviewPresentation'
import {
componentEvents,
Expand Down Expand Up @@ -331,6 +332,7 @@ export const Root = ({
isProcessed={
payrollData.processingRequest?.status === PAYROLL_PROCESSING_STATUS.submit_success
}
canCancel={canCancelPayroll(payrollData)}
payrollData={payrollData}
bankAccount={bankAccount}
employeeDetails={employeeData.showEmployees || []}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface PayrollOverviewProps {
taxes: Record<string, { employee: number; employer: number }>
isSubmitting?: boolean
isProcessed: boolean
canCancel?: boolean
alerts?: PayrollFlowAlert[]
submissionBlockers?: PayrollSubmissionBlockersType[]
selectedUnblockOptions?: Record<string, string>
Expand Down Expand Up @@ -69,6 +70,7 @@ export const PayrollOverviewPresentation = ({
taxes,
isSubmitting = false,
isProcessed,
canCancel = false,
alerts = [],
submissionBlockers = [],
selectedUnblockOptions = {},
Expand Down
151 changes: 150 additions & 1 deletion src/components/Payroll/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest'
import { expect, describe, it, vi } from 'vitest'
import {
formatEmployeePayRate,
getRegularHours,
Expand All @@ -10,11 +10,13 @@ import {
getPayrollType,
getAdditionalEarningsCompensations,
getReimbursementCompensation,
canCancelPayroll,
} from './helpers'
import {
type Employee,
EmployeePaymentMethod1,
} from '@gusto/embedded-api/models/components/employee'
import type { Payroll } from '@gusto/embedded-api/models/components/payroll'
import type {
EmployeeCompensations,
PayrollShowPaidTimeOff,
Expand Down Expand Up @@ -1235,4 +1237,151 @@ describe('Payroll helpers', () => {
expect(result).toBeNull()
})
})

describe('canCancelPayroll', () => {
const createMockPayroll = (overrides: Partial<Payroll> = {}): Payroll => ({
payrollUuid: 'test-payroll-id',
processed: true,
payrollDeadline: new Date('2024-01-15T23:00:00Z'),
payrollStatusMeta: {
cancellable: true,
expectedCheckDate: '2024-01-20',
initialCheckDate: '2024-01-20',
expectedDebitTime: '2024-01-15T23:00:00Z',
payrollLate: false,
initialDebitCutoffTime: '2024-01-15T23:00:00Z',
},
...overrides,
})

it('returns false when payrollStatusMeta.cancellable is explicitly false', () => {
const payroll = createMockPayroll({
payrollStatusMeta: {
cancellable: false,
expectedCheckDate: '2024-01-20',
initialCheckDate: '2024-01-20',
expectedDebitTime: '2024-01-15T23:00:00Z',
payrollLate: false,
initialDebitCutoffTime: '2024-01-15T23:00:00Z',
},
})

expect(canCancelPayroll(payroll)).toBe(false)
})

it('returns false when payroll is not processed', () => {
const payroll = createMockPayroll({ processed: false })

expect(canCancelPayroll(payroll)).toBe(false)
})

it('returns false when payrollDeadline is missing', () => {
const payroll = createMockPayroll({ payrollDeadline: undefined })

expect(canCancelPayroll(payroll)).toBe(false)
})

it('returns true when processed and before 4 PM PT on deadline day', () => {
const deadlineDate = new Date('2024-01-15T23:00:00Z')
const beforeCutoff = new Date('2024-01-15T22:00:00Z')

vi.setSystemTime(beforeCutoff)

const payroll = createMockPayroll({
payrollDeadline: deadlineDate,
})

expect(canCancelPayroll(payroll)).toBe(true)

vi.useRealTimers()
})

it('returns false when after 4 PM PT on deadline day', () => {
const deadlineDate = new Date('2024-01-15T23:00:00Z')
const afterCutoff = new Date('2024-01-16T00:30:00Z')

vi.setSystemTime(afterCutoff)

const payroll = createMockPayroll({
payrollDeadline: deadlineDate,
})

expect(canCancelPayroll(payroll)).toBe(false)

vi.useRealTimers()
})

it('returns true when before deadline day', () => {
const deadlineDate = new Date('2024-01-15T23:00:00Z')
const beforeDeadlineDay = new Date('2024-01-14T20:00:00Z')

vi.setSystemTime(beforeDeadlineDay)

const payroll = createMockPayroll({
payrollDeadline: deadlineDate,
})

expect(canCancelPayroll(payroll)).toBe(true)

vi.useRealTimers()
})

it('handles DST transitions correctly - PDT (summer)', () => {
const summerDeadline = new Date('2024-07-15T23:00:00Z')
const beforeCutoffPDT = new Date('2024-07-15T22:00:00Z')

vi.setSystemTime(beforeCutoffPDT)

const payroll = createMockPayroll({
payrollDeadline: summerDeadline,
})

expect(canCancelPayroll(payroll)).toBe(true)

vi.useRealTimers()
})

it('handles DST transitions correctly - PST (winter)', () => {
const winterDeadline = new Date('2024-12-15T00:00:00Z')
const beforeCutoffPST = new Date('2024-12-14T23:00:00Z')

vi.setSystemTime(beforeCutoffPST)

const payroll = createMockPayroll({
payrollDeadline: winterDeadline,
})

expect(canCancelPayroll(payroll)).toBe(true)

vi.useRealTimers()
})

it('returns true when cancellable is undefined but other conditions met', () => {
const payroll = createMockPayroll({
payrollStatusMeta: undefined,
})

const beforeDeadline = new Date('2024-01-14T20:00:00Z')
vi.setSystemTime(beforeDeadline)

expect(canCancelPayroll(payroll)).toBe(true)

vi.useRealTimers()
})

it('returns false exactly at 4:00 PM PT on deadline day', () => {
const deadlineDate = new Date('2024-01-15T23:00:00Z')
const exactlyCutoff = new Date('2024-01-16T00:00:00Z')

vi.setSystemTime(exactlyCutoff)

const payroll = createMockPayroll({
payrollDeadline: deadlineDate,
})

expect(canCancelPayroll(payroll)).toBe(false)

vi.useRealTimers()
})
})
})
Loading