diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx
index 8449ca81cc..6f5d31751a 100644
--- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx
+++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.test.tsx
@@ -8,6 +8,7 @@ import { DateTime } from 'luxon';
import { SnackbarProvider } from 'notistack';
import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
+import { GetUserQuery } from 'src/components/User/GetUser.generated';
import theme from 'src/theme';
import { StaffSavingFundProvider } from '../../StaffSavingFund/StaffSavingFundContext';
import {
@@ -65,21 +66,29 @@ const transferDefaultData: TransferModalData['transfer'] = {
interface ComponentsProps {
transfer?: TransferModalData['transfer'];
type?: TransferTypeEnum;
+ lastName?: string;
}
const Components = ({
transfer = transferDefaultData,
type,
+ lastName = 'Doe',
}: ComponentsProps) => (
+ mocks={{
+ GetUser: {
+ user: { lastName },
+ },
+ }}
onCall={mutationSpy}
>
@@ -99,9 +108,15 @@ const Components = ({
);
+const renderModal = async (props: ComponentsProps = {}) => {
+ const result = render();
+ await result.findByText(/Fund Transfer/);
+ return result;
+};
+
describe('TransferModal', () => {
- it('should render the modal with correct inputs', () => {
- const { getByRole, getByText } = render();
+ it('should render the modal with correct inputs', async () => {
+ const { getByRole, getByText } = await renderModal();
expect(getByText('New Fund Transfer')).toBeInTheDocument();
expect(
@@ -115,8 +130,8 @@ describe('TransferModal', () => {
expect(getByRole('spinbutton', { name: /amount/i })).toBeInTheDocument();
});
- it('should close modal when cancel button is clicked', () => {
- const { getByRole } = render();
+ it('should close modal when cancel button is clicked', async () => {
+ const { getByRole } = await renderModal();
userEvent.click(getByRole('button', { name: /cancel/i }));
@@ -125,7 +140,7 @@ describe('TransferModal', () => {
describe('Handle submit and validation', () => {
it('should show validation errors for required fields', async () => {
- const { getByRole, findByText } = render();
+ const { getByRole, findByText } = await renderModal();
const toAccount = getByRole('combobox', { name: /to account/i });
const amountField = getByRole('spinbutton', { name: /amount/i });
@@ -144,7 +159,7 @@ describe('TransferModal', () => {
});
it('should validate amount is greater than $0.01', async () => {
- const { getByRole, findByText } = render();
+ const { getByRole, findByText } = await renderModal();
const amountField = getByRole('spinbutton', { name: /amount/i });
@@ -167,10 +182,45 @@ describe('TransferModal', () => {
).toBeInTheDocument();
});
+ it.each([
+ { label: 'empty', input: '' },
+ { label: 'whitespace-only', input: ' ' },
+ ])(
+ 'should reject $label notes on one-time transfers',
+ async ({ input }) => {
+ const { getByRole, findByText } = await renderModal();
+
+ userEvent.click(getByRole('combobox', { name: /from account/i }));
+ userEvent.click(getByRole('option', { name: /staff account/i }));
+ userEvent.click(getByRole('combobox', { name: /to account/i }));
+ userEvent.click(getByRole('option', { name: /staff savings/i }));
+
+ const amountField = getByRole('spinbutton', { name: /amount/i });
+ userEvent.clear(amountField);
+ userEvent.type(amountField, '100');
+
+ const noteField = getByRole('textbox', { name: /note/i });
+ userEvent.clear(noteField);
+ if (input) {
+ userEvent.type(noteField, input);
+ }
+
+ userEvent.click(getByRole('button', { name: /submit/i }));
+
+ expect(await findByText('Note is required')).toBeInTheDocument();
+ expect(mutationSpy).not.toHaveBeenCalledWith(
+ expect.objectContaining({
+ operation: expect.objectContaining({
+ operationName: 'CreateTransfer',
+ }),
+ }),
+ );
+ },
+ );
+
it('should validate end date is after transfer date for recurring transfers', async () => {
- const { getByRole, findByLabelText, getByLabelText, findByText } = render(
- ,
- );
+ const { getByRole, findByLabelText, getByLabelText, findByText } =
+ await renderModal();
userEvent.click(getByRole('radio', { name: /monthly/i }));
expect(getByRole('radio', { name: /monthly/i })).toBeChecked();
@@ -197,11 +247,12 @@ describe('TransferModal', () => {
});
it('should submit form with valid data', async () => {
- const { getByRole } = render();
+ const { getByRole } = await renderModal();
const fromAccount = getByRole('combobox', { name: /from account/i });
const toAccount = getByRole('combobox', { name: /to account/i });
const amountField = getByRole('spinbutton', { name: /amount/i });
+ const noteField = getByRole('textbox', { name: /note/i });
userEvent.click(fromAccount);
userEvent.click(getByRole('option', { name: /staff account/i }));
@@ -212,6 +263,8 @@ describe('TransferModal', () => {
userEvent.clear(amountField);
userEvent.type(amountField, '100');
+ userEvent.type(noteField, 'Test note');
+
userEvent.click(getByRole('button', { name: /submit/i }));
await waitFor(() => {
@@ -225,11 +278,12 @@ describe('TransferModal', () => {
});
it('should handle form submission successfully', async () => {
- const { getByRole } = render();
+ const { getByRole } = await renderModal();
const fromAccount = getByRole('combobox', { name: /from account/i });
const toAccount = getByRole('combobox', { name: /to account/i });
const amountField = getByRole('spinbutton', { name: /amount/i });
+ const noteField = getByRole('textbox', { name: /note/i });
const submitButton = getByRole('button', { name: /submit/i });
userEvent.click(fromAccount);
@@ -241,6 +295,8 @@ describe('TransferModal', () => {
userEvent.clear(amountField);
userEvent.type(amountField, '100');
+ userEvent.type(noteField, 'Test note');
+
userEvent.click(submitButton);
await waitFor(() => {
@@ -255,7 +311,35 @@ describe('TransferModal', () => {
});
describe('Inputs', () => {
- it('should populate initial values from data prop', () => {
+ it('should default the note to " Savings Fund Transfer in MPDX" on new one-time transfers', async () => {
+ const { getByRole } = await renderModal({ lastName: 'Sleight' });
+
+ const noteField = getByRole('textbox', { name: /note/i });
+ expect(noteField).toHaveValue('Sleight Savings Fund Transfer in MPDX');
+ });
+
+ it('should not default the note when lastName is missing', async () => {
+ const { getByRole } = await renderModal({ lastName: '' });
+
+ const noteField = getByRole('textbox', { name: /note/i });
+ expect(noteField).toHaveValue('');
+ });
+
+ it('should trim lastName when building the default note', async () => {
+ const { getByRole } = await renderModal({ lastName: ' Sleight ' });
+
+ const noteField = getByRole('textbox', { name: /note/i });
+ expect(noteField).toHaveValue('Sleight Savings Fund Transfer in MPDX');
+ });
+
+ it('should not default the note when lastName is only whitespace', async () => {
+ const { getByRole } = await renderModal({ lastName: ' ' });
+
+ const noteField = getByRole('textbox', { name: /note/i });
+ expect(noteField).toHaveValue('');
+ });
+
+ it('should populate initial values from data prop', async () => {
const dataWithValues: TransferModalData['transfer'] = {
transferFrom: '70056dcb-1a0f-4279-b710-928bcdff811a',
transferTo: '408caf15-cdfd-41d1-8778-aa42a6561b85',
@@ -266,16 +350,17 @@ describe('TransferModal', () => {
note: 'Test note',
};
- const { getByDisplayValue, getByLabelText } = render(
- ,
- );
+ const { getByDisplayValue, getByLabelText } = await renderModal({
+ transfer: dataWithValues,
+ type: TransferTypeEnum.Edit,
+ });
expect(getByDisplayValue('500')).toBeInTheDocument();
expect(getByLabelText(/end date/i)).toHaveValue('');
});
it('should validate that from and to accounts are different', async () => {
- const { getByRole, queryByRole, getAllByRole } = render();
+ const { getByRole, queryByRole, getAllByRole } = await renderModal();
const [fromAccount, toAccount] = getAllByRole('combobox');
@@ -297,7 +382,7 @@ describe('TransferModal', () => {
});
it('should swap accounts when swap button is clicked', async () => {
- const { getByRole, getByTestId } = render();
+ const { getByRole, getByTestId } = await renderModal();
const icon = getByTestId('SwapHorizIcon');
const swapButton = icon.closest('button');
@@ -326,7 +411,7 @@ describe('TransferModal', () => {
});
it('should show/hide end date based on schedule selection', async () => {
- const { getByRole, queryByRole, getByLabelText } = render();
+ const { getByRole, queryByRole, getByLabelText } = await renderModal();
expect(
queryByRole('textbox', { name: /end date/i }),
@@ -348,7 +433,7 @@ describe('TransferModal', () => {
});
it('should show error message when monthly schedule is selected', async () => {
- const { getByRole, getByLabelText, findByText } = render();
+ const { getByRole, getByLabelText, findByText } = await renderModal();
userEvent.click(getByRole('radio', { name: /monthly/i }));
expect(getByRole('radio', { name: /monthly/i })).toBeChecked();
@@ -374,9 +459,10 @@ describe('TransferModal', () => {
note: 'Test note',
};
- const { getByLabelText, findByText } = render(
- ,
- );
+ const { getByLabelText, findByText } = await renderModal({
+ transfer: dataWithValues,
+ type: TransferTypeEnum.Edit,
+ });
const transferDate = getByLabelText(/transfer date/i);
@@ -391,7 +477,7 @@ describe('TransferModal', () => {
});
it('should show information box when amount exceeds limit', async () => {
- const { getByRole, findByRole } = render();
+ const { getByRole, findByRole } = await renderModal();
const fromAccount = getByRole('combobox', { name: /from account/i });
const toAccount = getByRole('combobox', { name: /to account/i });
@@ -417,14 +503,14 @@ describe('TransferModal', () => {
).toBeInTheDocument();
});
- it('should show proper currency symbol in amount field', () => {
- const { getByText } = render();
+ it('should show proper currency symbol in amount field', async () => {
+ const { getByText } = await renderModal();
expect(getByText('$')).toBeInTheDocument();
});
it('should handle different schedule types', async () => {
- const { getByRole } = render();
+ const { getByRole } = await renderModal();
expect(getByRole('radio', { name: /one time/i })).toBeChecked();
@@ -437,9 +523,10 @@ describe('TransferModal', () => {
describe('Mutations', () => {
it('should create a one-time transfer', async () => {
- const { getByRole } = render();
+ const { getByRole } = await renderModal();
const amountField = getByRole('spinbutton', { name: /amount/i });
+ const noteField = getByRole('textbox', { name: /note/i });
userEvent.click(getByRole('combobox', { name: /from account/i }));
userEvent.click(getByRole('option', { name: /staff account/i }));
@@ -450,6 +537,9 @@ describe('TransferModal', () => {
userEvent.clear(amountField);
userEvent.type(amountField, '100');
+ userEvent.clear(noteField);
+ userEvent.type(noteField, 'Test note');
+
userEvent.click(getByRole('button', { name: /submit/i }));
await waitFor(() => {
@@ -461,7 +551,7 @@ describe('TransferModal', () => {
amount: 100,
sourceFundTypeName: 'Staff Account',
destinationFundTypeName: 'Staff Savings',
- description: '',
+ description: 'Test note',
}),
}),
}),
@@ -470,7 +560,7 @@ describe('TransferModal', () => {
});
it('should create a recurring transfer', async () => {
- const { getByRole, getByLabelText } = render();
+ const { getByRole, getByLabelText } = await renderModal();
const amountField = getByRole('spinbutton', { name: /amount/i });
@@ -532,9 +622,10 @@ describe('TransferModal', () => {
recurringId: 'recurring-id',
};
- const { getByRole, getByLabelText } = render(
- ,
- );
+ const { getByRole, getByLabelText } = await renderModal({
+ transfer: dataWithValues,
+ type: TransferTypeEnum.Edit,
+ });
const amountField = getByRole('spinbutton', { name: /amount/i });
@@ -585,8 +676,8 @@ describe('TransferModal', () => {
});
describe('New Mode', () => {
- it('should display selects', () => {
- const { getByRole } = render();
+ it('should display selects', async () => {
+ const { getByRole } = await renderModal({ type: TransferTypeEnum.New });
const fromAccount = getByRole('combobox', { name: /from account/i });
const toAccount = getByRole('combobox', { name: /to account/i });
diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx
index d74adb7f92..2104ee13cd 100644
--- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx
+++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx
@@ -30,6 +30,7 @@ import {
SubmitButton,
} from 'src/components/Shared/Modal/ActionButtons/ActionButtons';
import Modal from 'src/components/Shared/Modal/Modal';
+import { useGetUserQuery } from 'src/components/User/GetUser.generated';
import { useLocale } from 'src/hooks/useLocale';
import i18n from 'src/lib/i18n';
import { currencyFormat, dateFormat } from 'src/lib/intlFormat';
@@ -135,7 +136,11 @@ const transferSchema = (locale: string) =>
.number()
.required(i18n.t('Amount is required'))
.min(0.01, i18n.t('Amount must be at least $0.01')),
- note: yup.string().nullable(),
+ note: yup.string().when('schedule', {
+ is: ScheduleEnum.OneTime,
+ then: (schema) => schema.trim().required(i18n.t('Note is required')),
+ otherwise: (schema) => schema.nullable(),
+ }),
});
interface TransferModalProps {
data: TransferModalData;
@@ -169,6 +174,19 @@ export const TransferModal: React.FC = ({
const type = data.type || TransferTypeEnum.New;
const isNew = type === TransferTypeEnum.New;
+ const { data: userData, loading: userLoading } = useGetUserQuery();
+
+ if (userLoading) {
+ return null;
+ }
+
+ const trimmedLastName = userData?.user?.lastName?.trim();
+ const defaultNote = trimmedLastName
+ ? t('{{lastName}} Savings Fund Transfer in MPDX', {
+ lastName: trimmedLastName,
+ })
+ : '';
+
const title =
type === TransferTypeEnum.New
? t('New Fund Transfer')
@@ -256,7 +274,7 @@ export const TransferModal: React.FC = ({
status: data.transfer.status ?? '',
transferDate: data.transfer.transferDate ?? getToday(),
endDate: data.transfer.endDate ?? null,
- note: data.transfer.note ?? '',
+ note: data.transfer.note || defaultNote,
isEditing: Boolean(data.transfer.id),
originalStart: data.transfer.transferDate ?? null,
}}
@@ -525,11 +543,17 @@ export const TransferModal: React.FC = ({