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,
})
}
}