From c71e7b4c4260482d6333666cf940beb809be8e2d Mon Sep 17 00:00:00 2001 From: Kristine White Date: Wed, 26 Aug 2026 14:33:22 -0700 Subject: [PATCH] fix(SDK-1284): persist 0 when an additional-earning field is cleared buildCompensationFromFormData skipped any fixed compensation whose form value was an empty string, so clearing a field dropped the entry from the payload entirely. The payroll update API leaves anything it is not sent untouched, so the previously saved amount survived: the box looked empty, the save appeared to succeed, and the old value came back on refetch. Typing an explicit 0 worked because that took the existing-compensation branch. An entry that already has a saved amount now resolves a cleared field to '0', so clearing behaves exactly like typing 0. Entries with no saved compensation stay omitted -- there is nothing to zero out. This also fixes the live gross-pay preview, which runs through the same builder, so the total now drops the moment the field is cleared instead of showing the stale amount. Note for reviewers: the ticket attributed this to the gws-flows NumberInput adapter forwarding NaN. That component is not on this path -- additional earnings render as TextInputField type="number", so the values are strings and the cause is entirely in this builder. --- .../PayrollEditEmployeePresentation.test.tsx | 52 +++++++++++++++++++ .../PayrollEditEmployeePresentation.tsx | 15 ++++-- 2 files changed, 63 insertions(+), 4 deletions(-) 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, }) } }