diff --git a/src/components/Payroll/PayrollEditEmployee/PayrollEditEmployeePresentation.test.tsx b/src/components/Payroll/PayrollEditEmployee/PayrollEditEmployeePresentation.test.tsx index c42b06f68..d384eb784 100644 --- a/src/components/Payroll/PayrollEditEmployee/PayrollEditEmployeePresentation.test.tsx +++ b/src/components/Payroll/PayrollEditEmployee/PayrollEditEmployeePresentation.test.tsx @@ -1115,6 +1115,58 @@ describe('PayrollEditEmployeePresentation', () => { }), ) }) + + // Clearing the field used to omit the entry entirely, so the API kept the previously + // saved amount: the box looked empty while the old value survived the save (SDK-1284). + // Clearing now behaves exactly like typing 0, as in the test above. + it('zeroes an existing compensation when its field is cleared', async () => { + const user = userEvent.setup() + const onSave = vi.fn() + + renderWithProviders( + , + ) + + // findBy, not getBy: the labels are translated, so a getBy here only passes when an + // earlier test in the file has already warmed the i18n namespace. + const bonusInput = await screen.findByLabelText('Bonus') + await user.clear(bonusInput) + + const saveButton = screen.getByRole('button', { name: /save/i }) + await user.click(saveButton) + + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + fixedCompensations: expect.arrayContaining([ + expect.objectContaining({ name: 'Bonus', amount: '0', jobUuid: 'job-1' }), + expect.objectContaining({ name: 'Commission', amount: '50.00', jobUuid: 'job-1' }), + ]), + }), + ) + }) + + it('still omits an earning type that never had a saved amount', async () => { + const user = userEvent.setup() + const onSave = vi.fn() + + renderWithProviders( + , + ) + + // 'Paycheck tips' is an available type with no saved compensation, so there is + // nothing to zero out and it must stay out of the payload. + const tipsInput = await screen.findByLabelText('Paycheck tips') + await user.type(tipsInput, '10') + await user.clear(tipsInput) + + const saveButton = screen.getByRole('button', { name: /save/i }) + await user.click(saveButton) + + const saved = onSave.mock.calls[0]![0] as PayrollEmployeeCompensationsType + expect(saved.fixedCompensations?.map(compensation => compensation.name)).not.toContain( + 'Paycheck Tips', + ) + }) }) describe('Payment Method', () => { diff --git a/src/components/Payroll/PayrollEditEmployee/PayrollEditEmployeePresentation.tsx b/src/components/Payroll/PayrollEditEmployee/PayrollEditEmployeePresentation.tsx index 064101b4d..913fa8f83 100644 --- a/src/components/Payroll/PayrollEditEmployee/PayrollEditEmployeePresentation.tsx +++ b/src/components/Payroll/PayrollEditEmployee/PayrollEditEmployeePresentation.tsx @@ -162,18 +162,25 @@ const buildCompensationFromFormData = ( fixedCompensation.name?.toLowerCase() === fixedCompensationName.toLowerCase(), ) - if (formAmount !== undefined && formAmount !== '') { + // Clearing the field means "no additional earning". For an entry that already has a + // saved amount we have to send an explicit 0: omitting it leaves the previously saved + // value in place server-side, so the box looked empty while the old amount survived + // the save (SDK-1284). This makes clearing behave exactly like typing 0. An entry with + // no saved compensation stays omitted -- there is nothing to zero out. + const resolvedAmount = formAmount === '' && existingFixedCompensation ? '0' : formAmount + + if (resolvedAmount !== undefined && resolvedAmount !== '') { if (existingFixedCompensation) { updatedFixedCompensations.push({ name: existingFixedCompensation.name, jobUuid: existingFixedCompensation.jobUuid, - amount: formAmount, + amount: resolvedAmount, }) - } else if (parseFloat(formAmount) !== 0) { + } else if (parseFloat(resolvedAmount) !== 0) { updatedFixedCompensations.push({ name: fixedCompensationName, jobUuid: primaryJobUuid, - amount: formAmount, + amount: resolvedAmount, }) } }