Skip to content

Commit 04b4196

Browse files
committed
feat: cancel payroll - WIP
1 parent 79de570 commit 04b4196

File tree

7 files changed

+244
-54
lines changed

7 files changed

+244
-54
lines changed

src/components/Payroll/PayrollHistory/PayrollHistory.test.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,17 @@ describe('PayrollHistory', () => {
190190
})
191191

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

196201
server.use(
197202
http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
198-
return HttpResponse.json(mockUnprocessedPayroll)
203+
return HttpResponse.json(mockProcessedPayroll)
199204
}),
200205
)
201206

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

211-
// Should show cancel option for unprocessed payroll
216+
// Should show cancel option for processed payroll before cutoff
212217
await waitFor(() => {
213218
expect(screen.getByText('Cancel payroll')).toBeInTheDocument()
214219
})
220+
221+
vi.useRealTimers()
215222
})
216223

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

221230
server.use(
222231
http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
223-
return HttpResponse.json(mockUnprocessedPayroll)
232+
return HttpResponse.json(mockProcessedPayroll)
224233
}),
225234
// Mock the cancel API
226235
http.put(`${API_BASE_URL}/v1/companies/:company_id/payrolls/:payroll_id/cancel`, () => {
@@ -260,14 +269,19 @@ describe('PayrollHistory', () => {
260269
}),
261270
)
262271
})
272+
273+
vi.useRealTimers()
263274
})
264275

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

268282
server.use(
269283
http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
270-
return HttpResponse.json(mockUnprocessedPayroll)
284+
return HttpResponse.json(mockProcessedPayroll)
271285
}),
272286
// Mock API error
273287
http.put(`${API_BASE_URL}/v1/companies/:company_id/payrolls/:payroll_id/cancel`, () => {
@@ -301,6 +315,8 @@ describe('PayrollHistory', () => {
301315
await waitFor(() => {
302316
expect(screen.getByText('There was a problem with your submission')).toBeInTheDocument()
303317
})
318+
319+
vi.useRealTimers()
304320
})
305321
})
306322

src/components/Payroll/PayrollHistory/PayrollHistoryPresentation.tsx

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
22
import type { Payroll } from '@gusto/embedded-api/models/components/payroll'
33
import type { WireInRequest } from '@gusto/embedded-api/models/components/wireinrequest'
44
import { PayrollStatusBadges } from '../PayrollStatusBadges'
5-
import { getPayrollType, calculateTotalPayroll } from '../helpers'
5+
import { getPayrollType, calculateTotalPayroll, canCancelPayroll } from '../helpers'
66
import type { TimeFilterOption } from './PayrollHistory'
77
import styles from './PayrollHistoryPresentation.module.scss'
88
import type { MenuItem } from '@/components/Common/UI/Menu/MenuTypes'
@@ -54,48 +54,6 @@ export const PayrollHistoryPresentation = ({
5454
{ value: 'year', label: t('timeFilter.options.year') },
5555
]
5656

57-
const canCancelPayroll = (item: Payroll) => {
58-
if (item.payrollStatusMeta?.cancellable === false) {
59-
return false
60-
}
61-
62-
if (item.processed && item.payrollDeadline) {
63-
const now = new Date()
64-
const deadline = new Date(item.payrollDeadline)
65-
66-
const ptOffset = getPacificTimeOffset(now)
67-
const nowInPT = new Date(now.getTime() + ptOffset * 60 * 60 * 1000)
68-
const deadlineInPT = new Date(
69-
deadline.getTime() + getPacificTimeOffset(deadline) * 60 * 60 * 1000,
70-
)
71-
72-
const isSameDay = nowInPT.toDateString() === deadlineInPT.toDateString()
73-
if (isSameDay) {
74-
const cutoffTime = new Date(deadlineInPT)
75-
cutoffTime.setHours(15, 30, 0, 0)
76-
77-
if (nowInPT > cutoffTime) {
78-
return false
79-
}
80-
}
81-
}
82-
83-
return true
84-
}
85-
86-
const getPacificTimeOffset = (date: Date): number => {
87-
const year = date.getFullYear()
88-
89-
const secondSundayMarch = new Date(year, 2, 1)
90-
secondSundayMarch.setDate(1 + (7 - secondSundayMarch.getDay()) + 7)
91-
92-
const firstSundayNovember = new Date(year, 10, 1)
93-
firstSundayNovember.setDate(1 + ((7 - firstSundayNovember.getDay()) % 7))
94-
95-
const isDST = date >= secondSundayMarch && date < firstSundayNovember
96-
return isDST ? -7 : -8
97-
}
98-
9957
const formatDeadlineForDialog = (item: Payroll): string => {
10058
const deadline = item.payrollDeadline
10159
if (!deadline) return ''

src/components/Payroll/PayrollOverview/PayrollOverview.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ConfirmWireDetails,
1717
type ConfirmWireDetailsComponentType,
1818
} from '../ConfirmWireDetails/ConfirmWireDetails'
19+
import { canCancelPayroll } from '../helpers'
1920
import { PayrollOverviewPresentation } from './PayrollOverviewPresentation'
2021
import {
2122
componentEvents,
@@ -331,6 +332,7 @@ export const Root = ({
331332
isProcessed={
332333
payrollData.processingRequest?.status === PAYROLL_PROCESSING_STATUS.submit_success
333334
}
335+
canCancel={canCancelPayroll(payrollData)}
334336
payrollData={payrollData}
335337
bankAccount={bankAccount}
336338
employeeDetails={employeeData.showEmployees || []}

src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface PayrollOverviewProps {
3434
taxes: Record<string, { employee: number; employer: number }>
3535
isSubmitting?: boolean
3636
isProcessed: boolean
37+
canCancel?: boolean
3738
alerts?: PayrollFlowAlert[]
3839
submissionBlockers?: PayrollSubmissionBlockersType[]
3940
selectedUnblockOptions?: Record<string, string>
@@ -69,6 +70,7 @@ export const PayrollOverviewPresentation = ({
6970
taxes,
7071
isSubmitting = false,
7172
isProcessed,
73+
canCancel = false,
7274
alerts = [],
7375
submissionBlockers = [],
7476
selectedUnblockOptions = {},

src/components/Payroll/helpers.test.ts

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, describe, it } from 'vitest'
1+
import { expect, describe, it, vi } from 'vitest'
22
import {
33
formatEmployeePayRate,
44
getRegularHours,
@@ -10,11 +10,13 @@ import {
1010
getPayrollType,
1111
getAdditionalEarningsCompensations,
1212
getReimbursementCompensation,
13+
canCancelPayroll,
1314
} from './helpers'
1415
import {
1516
type Employee,
1617
EmployeePaymentMethod1,
1718
} from '@gusto/embedded-api/models/components/employee'
19+
import type { Payroll } from '@gusto/embedded-api/models/components/payroll'
1820
import type {
1921
EmployeeCompensations,
2022
PayrollShowPaidTimeOff,
@@ -1235,4 +1237,151 @@ describe('Payroll helpers', () => {
12351237
expect(result).toBeNull()
12361238
})
12371239
})
1240+
1241+
describe('canCancelPayroll', () => {
1242+
const createMockPayroll = (overrides: Partial<Payroll> = {}): Payroll => ({
1243+
payrollUuid: 'test-payroll-id',
1244+
processed: true,
1245+
payrollDeadline: new Date('2024-01-15T23:00:00Z'),
1246+
payrollStatusMeta: {
1247+
cancellable: true,
1248+
expectedCheckDate: '2024-01-20',
1249+
initialCheckDate: '2024-01-20',
1250+
expectedDebitTime: '2024-01-15T23:00:00Z',
1251+
payrollLate: false,
1252+
initialDebitCutoffTime: '2024-01-15T23:00:00Z',
1253+
},
1254+
...overrides,
1255+
})
1256+
1257+
it('returns false when payrollStatusMeta.cancellable is explicitly false', () => {
1258+
const payroll = createMockPayroll({
1259+
payrollStatusMeta: {
1260+
cancellable: false,
1261+
expectedCheckDate: '2024-01-20',
1262+
initialCheckDate: '2024-01-20',
1263+
expectedDebitTime: '2024-01-15T23:00:00Z',
1264+
payrollLate: false,
1265+
initialDebitCutoffTime: '2024-01-15T23:00:00Z',
1266+
},
1267+
})
1268+
1269+
expect(canCancelPayroll(payroll)).toBe(false)
1270+
})
1271+
1272+
it('returns false when payroll is not processed', () => {
1273+
const payroll = createMockPayroll({ processed: false })
1274+
1275+
expect(canCancelPayroll(payroll)).toBe(false)
1276+
})
1277+
1278+
it('returns false when payrollDeadline is missing', () => {
1279+
const payroll = createMockPayroll({ payrollDeadline: undefined })
1280+
1281+
expect(canCancelPayroll(payroll)).toBe(false)
1282+
})
1283+
1284+
it('returns true when processed and before 4 PM PT on deadline day', () => {
1285+
const deadlineDate = new Date('2024-01-15T23:00:00Z')
1286+
const beforeCutoff = new Date('2024-01-15T22:00:00Z')
1287+
1288+
vi.setSystemTime(beforeCutoff)
1289+
1290+
const payroll = createMockPayroll({
1291+
payrollDeadline: deadlineDate,
1292+
})
1293+
1294+
expect(canCancelPayroll(payroll)).toBe(true)
1295+
1296+
vi.useRealTimers()
1297+
})
1298+
1299+
it('returns false when after 4 PM PT on deadline day', () => {
1300+
const deadlineDate = new Date('2024-01-15T23:00:00Z')
1301+
const afterCutoff = new Date('2024-01-16T00:30:00Z')
1302+
1303+
vi.setSystemTime(afterCutoff)
1304+
1305+
const payroll = createMockPayroll({
1306+
payrollDeadline: deadlineDate,
1307+
})
1308+
1309+
expect(canCancelPayroll(payroll)).toBe(false)
1310+
1311+
vi.useRealTimers()
1312+
})
1313+
1314+
it('returns true when before deadline day', () => {
1315+
const deadlineDate = new Date('2024-01-15T23:00:00Z')
1316+
const beforeDeadlineDay = new Date('2024-01-14T20:00:00Z')
1317+
1318+
vi.setSystemTime(beforeDeadlineDay)
1319+
1320+
const payroll = createMockPayroll({
1321+
payrollDeadline: deadlineDate,
1322+
})
1323+
1324+
expect(canCancelPayroll(payroll)).toBe(true)
1325+
1326+
vi.useRealTimers()
1327+
})
1328+
1329+
it('handles DST transitions correctly - PDT (summer)', () => {
1330+
const summerDeadline = new Date('2024-07-15T23:00:00Z')
1331+
const beforeCutoffPDT = new Date('2024-07-15T22:00:00Z')
1332+
1333+
vi.setSystemTime(beforeCutoffPDT)
1334+
1335+
const payroll = createMockPayroll({
1336+
payrollDeadline: summerDeadline,
1337+
})
1338+
1339+
expect(canCancelPayroll(payroll)).toBe(true)
1340+
1341+
vi.useRealTimers()
1342+
})
1343+
1344+
it('handles DST transitions correctly - PST (winter)', () => {
1345+
const winterDeadline = new Date('2024-12-15T00:00:00Z')
1346+
const beforeCutoffPST = new Date('2024-12-14T23:00:00Z')
1347+
1348+
vi.setSystemTime(beforeCutoffPST)
1349+
1350+
const payroll = createMockPayroll({
1351+
payrollDeadline: winterDeadline,
1352+
})
1353+
1354+
expect(canCancelPayroll(payroll)).toBe(true)
1355+
1356+
vi.useRealTimers()
1357+
})
1358+
1359+
it('returns true when cancellable is undefined but other conditions met', () => {
1360+
const payroll = createMockPayroll({
1361+
payrollStatusMeta: undefined,
1362+
})
1363+
1364+
const beforeDeadline = new Date('2024-01-14T20:00:00Z')
1365+
vi.setSystemTime(beforeDeadline)
1366+
1367+
expect(canCancelPayroll(payroll)).toBe(true)
1368+
1369+
vi.useRealTimers()
1370+
})
1371+
1372+
it('returns false exactly at 4:00 PM PT on deadline day', () => {
1373+
const deadlineDate = new Date('2024-01-15T23:00:00Z')
1374+
const exactlyCutoff = new Date('2024-01-16T00:00:00Z')
1375+
1376+
vi.setSystemTime(exactlyCutoff)
1377+
1378+
const payroll = createMockPayroll({
1379+
payrollDeadline: deadlineDate,
1380+
})
1381+
1382+
expect(canCancelPayroll(payroll)).toBe(false)
1383+
1384+
vi.useRealTimers()
1385+
})
1386+
})
12381387
})

0 commit comments

Comments
 (0)