From 1f0d1a254a0f15a2e2879af95c8d33d5d6ba4067 Mon Sep 17 00:00:00 2001 From: joeydoyecaci Date: Tue, 4 Feb 2025 21:06:56 +0000 Subject: [PATCH 01/36] fixed error modal mailto Link --- src/shared/ErrorModal/ErrorModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/ErrorModal/ErrorModal.jsx b/src/shared/ErrorModal/ErrorModal.jsx index cf6f28794aa..b5533a20c58 100644 --- a/src/shared/ErrorModal/ErrorModal.jsx +++ b/src/shared/ErrorModal/ErrorModal.jsx @@ -12,7 +12,7 @@ export const ErrorModal = ({ closeModal, errorMessage, displayHelpDeskLink = tru {errorMessage} {displayHelpDeskLink && ( - Technical Help Desk + Technical Help Desk )} From f3eee7b0b1a8b314b9214f22d65b8c11a32e5a34 Mon Sep 17 00:00:00 2001 From: Jon Spight Date: Tue, 4 Feb 2025 22:50:23 +0000 Subject: [PATCH 02/36] third addres on --- .envrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.envrc b/.envrc index c5e72d09ad2..a33401c69de 100644 --- a/.envrc +++ b/.envrc @@ -137,7 +137,7 @@ export FEATURE_FLAG_SAFETY_MOVE=true export FEATURE_FLAG_MANAGE_SUPPORTING_DOCS=true # Feature flags to enable third address -export FEATURE_FLAG_THIRD_ADDRESS_AVAILABLE=false +export FEATURE_FLAG_THIRD_ADDRESS_AVAILABLE=true # Feature flag to disable/enable headquarters role export FEATURE_FLAG_HEADQUARTERS_ROLE=true From 26923c6cf0ab2de69060da2ec166a0dc8b550377 Mon Sep 17 00:00:00 2001 From: Jon Spight Date: Tue, 4 Feb 2025 23:57:57 +0000 Subject: [PATCH 03/36] Initial checks for empty second address --- .../Office/ShipmentForm/ShipmentForm.jsx | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/Office/ShipmentForm/ShipmentForm.jsx b/src/components/Office/ShipmentForm/ShipmentForm.jsx index 076212d6953..08e9e4fdc53 100644 --- a/src/components/Office/ShipmentForm/ShipmentForm.jsx +++ b/src/components/Office/ShipmentForm/ShipmentForm.jsx @@ -357,6 +357,29 @@ const ShipmentForm = (props) => { : generatePath(servicesCounselingRoutes.BASE_ORDERS_EDIT_PATH, { moveCode }); const submitMTOShipment = (formValues, actions) => { + if (formValues.hasTertiaryDestination === 'true' && formValues.secondaryDestination.address.streetAddress1 === '') { + actions.setFieldError('secondaryDestination.address.streetAddress1', 'destination address required'); + actions.setSubmitting(false); + return; + } + if (formValues.hasTertiaryPickup === 'true' && formValues.secondaryPickup.address.streetAddress1 === '') { + actions.setFieldError('secondaryPickup.address.streetAddress1', 'Pickup address required'); + actions.setSubmitting(false); + return; + } + + if (formValues.hasTertiaryDelivery === 'yes' && formValues.secondaryDelivery.address.streetAddress1 === '') { + actions.setFieldError('secondaryDelivery.address.streetAddress1', 'destination address required'); + actions.setSubmitting(false); + return; + } + + if (formValues.hasTertiaryPickup === 'yes' && formValues.secondaryPickup.address.streetAddress1 === '') { + actions.setFieldError('secondaryPickup.address.streetAddress1', 'Pickup address required'); + actions.setSubmitting(false); + return; + } + //* PPM Shipment *// if (isPPM) { const ppmShipmentBody = formatPpmShipmentForAPI(formValues); @@ -1496,7 +1519,7 @@ const ShipmentForm = (props) => { name="hasTertiaryPickup" value="false" title="No, there is not a third pickup address" - checked={hasTertiaryPickup !== 'true'} + checked={hasTertiaryPickup !== 'yes'} /> From df43551390c0d7e5ab3ed54539e684c1c32d7826 Mon Sep 17 00:00:00 2001 From: Jon Spight Date: Wed, 5 Feb 2025 15:31:23 +0000 Subject: [PATCH 04/36] Finished checks --- .../Office/ShipmentForm/ShipmentForm.jsx | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/components/Office/ShipmentForm/ShipmentForm.jsx b/src/components/Office/ShipmentForm/ShipmentForm.jsx index 08e9e4fdc53..56718759557 100644 --- a/src/components/Office/ShipmentForm/ShipmentForm.jsx +++ b/src/components/Office/ShipmentForm/ShipmentForm.jsx @@ -357,11 +357,12 @@ const ShipmentForm = (props) => { : generatePath(servicesCounselingRoutes.BASE_ORDERS_EDIT_PATH, { moveCode }); const submitMTOShipment = (formValues, actions) => { - if (formValues.hasTertiaryDestination === 'true' && formValues.secondaryDestination.address.streetAddress1 === '') { - actions.setFieldError('secondaryDestination.address.streetAddress1', 'destination address required'); + if (formValues.hasSecondaryDelivery === 'yes' && formValues.delivery.address.streetAddress1 === '') { + actions.setFieldError('delivery.address.streetAddress1', 'Delivery address required'); actions.setSubmitting(false); return; } + if (formValues.hasTertiaryPickup === 'true' && formValues.secondaryPickup.address.streetAddress1 === '') { actions.setFieldError('secondaryPickup.address.streetAddress1', 'Pickup address required'); actions.setSubmitting(false); @@ -369,19 +370,22 @@ const ShipmentForm = (props) => { } if (formValues.hasTertiaryDelivery === 'yes' && formValues.secondaryDelivery.address.streetAddress1 === '') { - actions.setFieldError('secondaryDelivery.address.streetAddress1', 'destination address required'); - actions.setSubmitting(false); - return; - } - - if (formValues.hasTertiaryPickup === 'yes' && formValues.secondaryPickup.address.streetAddress1 === '') { - actions.setFieldError('secondaryPickup.address.streetAddress1', 'Pickup address required'); + actions.setFieldError('secondaryDelivery.address.streetAddress1', 'Delivery address required'); actions.setSubmitting(false); return; } //* PPM Shipment *// if (isPPM) { + if ( + formValues.hasTertiaryDestination === 'true' && + formValues.secondaryDestination.address.streetAddress1 === '' + ) { + actions.setFieldError('secondaryDestination.address.streetAddress1', 'Destination address required'); + actions.setSubmitting(false); + return; + } + const ppmShipmentBody = formatPpmShipmentForAPI(formValues); // Allow blank values to be entered into Pro Gear input fields @@ -587,8 +591,8 @@ const ShipmentForm = (props) => { secondaryPickup: hasSecondaryPickup === 'yes' ? secondaryPickup : {}, hasSecondaryDelivery: hasSecondaryDelivery === 'yes', secondaryDelivery: hasSecondaryDelivery === 'yes' ? secondaryDelivery : {}, - hasTertiaryPickup: hasTertiaryPickup === 'yes', - tertiaryPickup: hasTertiaryPickup === 'yes' ? tertiaryPickup : {}, + hasTertiaryPickup: hasTertiaryPickup === 'true', + tertiaryPickup: hasTertiaryPickup === 'true' ? tertiaryPickup : {}, hasTertiaryDelivery: hasTertiaryDelivery === 'yes', tertiaryDelivery: hasTertiaryDelivery === 'yes' ? tertiaryDelivery : {}, }); @@ -680,7 +684,6 @@ const ShipmentForm = (props) => { hasTertiaryDelivery, isActualExpenseReimbursement, } = values; - const lengthHasError = !!( (formikProps.touched.lengthFeet && formikProps.errors.lengthFeet === 'Required') || (formikProps.touched.lengthInches && formikProps.errors.lengthFeet === 'Required') @@ -1020,9 +1023,9 @@ const ShipmentForm = (props) => { data-testid="has-tertiary-pickup" label="Yes" name="hasTertiaryPickup" - value="yes" + value="true" title="Yes, I have a third pickup address" - checked={hasTertiaryPickup === 'yes'} + checked={hasTertiaryPickup === 'true'} /> { data-testid="no-tertiary-pickup" label="No" name="hasTertiaryPickup" - value="no" + value="false" title="No, I do not have a third pickup address" - checked={hasTertiaryPickup !== 'yes'} + checked={hasTertiaryPickup !== 'true'} /> - {hasTertiaryPickup === 'yes' && ( + {hasTertiaryPickup === 'true' && ( { name="hasTertiaryPickup" value="false" title="No, there is not a third pickup address" - checked={hasTertiaryPickup !== 'yes'} + checked={hasTertiaryPickup !== 'true'} /> From a93a18d24541ec49f19dd2887c4fadd2052f1302 Mon Sep 17 00:00:00 2001 From: Jon Spight Date: Wed, 5 Feb 2025 17:02:55 +0000 Subject: [PATCH 05/36] Moved to helper function --- .../Office/ShipmentForm/ShipmentForm.jsx | 27 +++---------------- src/shared/utils.js | 21 +++++++++++++++ 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/components/Office/ShipmentForm/ShipmentForm.jsx b/src/components/Office/ShipmentForm/ShipmentForm.jsx index 56718759557..1290a33c810 100644 --- a/src/components/Office/ShipmentForm/ShipmentForm.jsx +++ b/src/components/Office/ShipmentForm/ShipmentForm.jsx @@ -70,6 +70,7 @@ import { validateDate } from 'utils/validation'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; import { dateSelectionWeekendHolidayCheck } from 'utils/calendar'; import { datePickerFormat, formatDate } from 'shared/dates'; +import { checkPreceedingAddress } from 'shared/utils'; const ShipmentForm = (props) => { const { @@ -357,35 +358,15 @@ const ShipmentForm = (props) => { : generatePath(servicesCounselingRoutes.BASE_ORDERS_EDIT_PATH, { moveCode }); const submitMTOShipment = (formValues, actions) => { - if (formValues.hasSecondaryDelivery === 'yes' && formValues.delivery.address.streetAddress1 === '') { - actions.setFieldError('delivery.address.streetAddress1', 'Delivery address required'); - actions.setSubmitting(false); - return; - } - - if (formValues.hasTertiaryPickup === 'true' && formValues.secondaryPickup.address.streetAddress1 === '') { - actions.setFieldError('secondaryPickup.address.streetAddress1', 'Pickup address required'); - actions.setSubmitting(false); - return; - } - - if (formValues.hasTertiaryDelivery === 'yes' && formValues.secondaryDelivery.address.streetAddress1 === '') { - actions.setFieldError('secondaryDelivery.address.streetAddress1', 'Delivery address required'); + const preceedingAddressError = checkPreceedingAddress(formValues); + if (preceedingAddressError !== '') { + actions.setFieldError(preceedingAddressError, 'Address required'); actions.setSubmitting(false); return; } //* PPM Shipment *// if (isPPM) { - if ( - formValues.hasTertiaryDestination === 'true' && - formValues.secondaryDestination.address.streetAddress1 === '' - ) { - actions.setFieldError('secondaryDestination.address.streetAddress1', 'Destination address required'); - actions.setSubmitting(false); - return; - } - const ppmShipmentBody = formatPpmShipmentForAPI(formValues); // Allow blank values to be entered into Pro Gear input fields diff --git a/src/shared/utils.js b/src/shared/utils.js index 12ccf91c7a8..cd4c84ed2a9 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -209,3 +209,24 @@ export function checkAddressTogglesToClearAddresses(body) { return values; } + +export function checkPreceedingAddress(formValues) { + const values = formValues; + let formError = ''; + + if (values.hasSecondaryDelivery === 'yes' && values.delivery.address.streetAddress1 === '') { + formError = 'delivery.address.streetAddress1'; + } + + if (values.hasTertiaryPickup === 'true' && values.secondaryPickup.address.streetAddress1 === '') { + formError = 'secondaryPickup.address.streetAddress1'; + } + + if (values.hasTertiaryDelivery === 'yes' && values.secondaryDelivery.address.streetAddress1 === '') { + formError = 'secondaryDelivery.address.streetAddress1'; + } + if (values.hasTertiaryDestination === 'true' && values.secondaryDestination.address.streetAddress1 === '') { + formError = 'secondaryDestination.address.streetAddress1'; + } + return formError; +} From 2ca1a1fa5744070de77f3e88665223381c835b3c Mon Sep 17 00:00:00 2001 From: Jon Spight Date: Wed, 5 Feb 2025 17:44:27 +0000 Subject: [PATCH 06/36] changed var --- .envrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.envrc b/.envrc index a33401c69de..c5e72d09ad2 100644 --- a/.envrc +++ b/.envrc @@ -137,7 +137,7 @@ export FEATURE_FLAG_SAFETY_MOVE=true export FEATURE_FLAG_MANAGE_SUPPORTING_DOCS=true # Feature flags to enable third address -export FEATURE_FLAG_THIRD_ADDRESS_AVAILABLE=true +export FEATURE_FLAG_THIRD_ADDRESS_AVAILABLE=false # Feature flag to disable/enable headquarters role export FEATURE_FLAG_HEADQUARTERS_ROLE=true From 3e8d320c843c4fb04fa2d52a963923fcb6cda132 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Wed, 5 Feb 2025 18:29:10 +0000 Subject: [PATCH 07/36] update move history to indicate closeout counselors --- .../MoveHistory/Database/FieldMappings.js | 2 + .../UpdateAssignedOfficeUser.test.jsx | 65 ++++++++++++- src/utils/formatters.js | 11 ++- src/utils/formatters.test.js | 94 ++++++++++++++++++- 4 files changed, 163 insertions(+), 9 deletions(-) diff --git a/src/constants/MoveHistory/Database/FieldMappings.js b/src/constants/MoveHistory/Database/FieldMappings.js index fee085a2b95..bdfa842f934 100644 --- a/src/constants/MoveHistory/Database/FieldMappings.js +++ b/src/constants/MoveHistory/Database/FieldMappings.js @@ -149,9 +149,11 @@ export default { approved_at: 'Approved at', counseling_office_name: 'Counseling office', assigned_sc: 'Counselor assigned', + assigned_sc_ppm: 'Closeout counselor assigned', assigned_too: 'Task ordering officer assigned', assigned_tio: 'Task invoicing officer assigned', re_assigned_sc: 'Counselor reassigned', + re_assigned_sc_ppm: 'Closeout counselor reassigned', re_assigned_too: 'Task ordering officer reassigned', re_assigned_tio: 'Task invoicing officer reassigned', available_to_prime_at: 'Available to Prime at', diff --git a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.test.jsx index bc1e6aa2d73..f1223b4b3a5 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.test.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.test.jsx @@ -2,6 +2,7 @@ import { screen, render } from '@testing-library/react'; import e from 'constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser'; import getTemplate from 'constants/MoveHistory/TemplateManager'; +import { MOVE_STATUSES } from 'shared/constants'; describe('When given a move that has been assigned', () => { const historyRecord = { @@ -13,6 +14,7 @@ describe('When given a move that has been assigned', () => { }, oldValues: { sc_assigned_id: null, + status: MOVE_STATUSES.NEEDS_SERVICE_COUNSELING, }, context: [{ assigned_office_user_last_name: 'Daniels', assigned_office_user_first_name: 'Jayden' }], }; @@ -30,14 +32,47 @@ describe('When given a move that has been assigned', () => { }); describe('displays the proper details for', () => { - it('services counselor', () => { + it('assignment of a services counselor', () => { const template = getTemplate(historyRecord); render(template.getDetails(historyRecord)); expect(screen.getByText('Counselor assigned')).toBeInTheDocument(); expect(screen.getByText(': Daniels, Jayden')).toBeInTheDocument(); }); - it('task ordering officer', () => { + it('reassignment of a services counselor', () => { + const template = getTemplate(historyRecord); + historyRecord.oldValues = { + sc_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64', + status: MOVE_STATUSES.NEEDS_SERVICE_COUNSELING, + }; + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Counselor reassigned')).toBeInTheDocument(); + expect(screen.getByText(': Daniels, Jayden')).toBeInTheDocument(); + }); + it('assignment of a closeout counselor', () => { + const template = getTemplate(historyRecord); + historyRecord.oldValues = { + sc_assigned_id: null, + status: MOVE_STATUSES.SERVICE_COUNSELING_COMPLETED, + }; + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Closeout counselor assigned')).toBeInTheDocument(); + expect(screen.getByText(': Daniels, Jayden')).toBeInTheDocument(); + }); + it('reassignment of a closeout counselor', () => { + const template = getTemplate(historyRecord); + historyRecord.oldValues = { + sc_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64', + status: MOVE_STATUSES.SERVICE_COUNSELING_COMPLETED, + }; + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Closeout counselor reassigned')).toBeInTheDocument(); + expect(screen.getByText(': Daniels, Jayden')).toBeInTheDocument(); + }); + it('assignment of a task ordering officer', () => { historyRecord.changedValues = { too_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; historyRecord.oldValues = { too_assigned_id: null }; historyRecord.context = [ @@ -50,7 +85,20 @@ describe('When given a move that has been assigned', () => { expect(screen.getByText('Task ordering officer assigned')).toBeInTheDocument(); expect(screen.getByText(': Robinson, Brian')).toBeInTheDocument(); }); - it('task invoicing officer', () => { + it('reassignment of a task ordering officer', () => { + historyRecord.changedValues = { too_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; + historyRecord.oldValues = { too_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64' }; + historyRecord.context = [ + { assigned_office_user_last_name: 'Robinson', assigned_office_user_first_name: 'Brian' }, + ]; + + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task ordering officer reassigned')).toBeInTheDocument(); + expect(screen.getByText(': Robinson, Brian')).toBeInTheDocument(); + }); + it('assignment of a task invoicing officer', () => { historyRecord.changedValues = { tio_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; historyRecord.oldValues = { tio_assigned_id: null }; historyRecord.context = [{ assigned_office_user_last_name: 'Luvu', assigned_office_user_first_name: 'Frankie' }]; @@ -61,5 +109,16 @@ describe('When given a move that has been assigned', () => { expect(screen.getByText('Task invoicing officer assigned')).toBeInTheDocument(); expect(screen.getByText(': Luvu, Frankie')).toBeInTheDocument(); }); + it('reassignment of a task invoicing officer', () => { + historyRecord.changedValues = { tio_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; + historyRecord.oldValues = { tio_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64' }; + historyRecord.context = [{ assigned_office_user_last_name: 'Luvu', assigned_office_user_first_name: 'Frankie' }]; + + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task invoicing officer reassigned')).toBeInTheDocument(); + expect(screen.getByText(': Luvu, Frankie')).toBeInTheDocument(); + }); }); }); diff --git a/src/utils/formatters.js b/src/utils/formatters.js index d979dcdc624..3dff740f21d 100644 --- a/src/utils/formatters.js +++ b/src/utils/formatters.js @@ -7,7 +7,7 @@ import { DEPARTMENT_INDICATOR_OPTIONS } from 'constants/departmentIndicators'; import { SERVICE_MEMBER_AGENCY_LABELS } from 'content/serviceMemberAgencies'; import { ORDERS_TYPE_OPTIONS, ORDERS_TYPE_DETAILS_OPTIONS } from 'constants/orders'; import { PAYMENT_REQUEST_STATUS_LABELS } from 'constants/paymentRequestStatus'; -import { DEFAULT_EMPTY_VALUE } from 'shared/constants'; +import { DEFAULT_EMPTY_VALUE, MOVE_STATUSES } from 'shared/constants'; /** * Formats number into a dollar string. Eg. $1,234.12 @@ -609,8 +609,13 @@ export const formatAssignedOfficeUserFromContext = (historyRecord) => { const name = `${context[0].assigned_office_user_last_name}, ${context[0].assigned_office_user_first_name}`; if (changedValues?.sc_assigned_id) { - if (oldValues.sc_assigned_id === null) newValues.assigned_sc = name; - if (oldValues.sc_assigned_id !== null) newValues.re_assigned_sc = name; + if (oldValues.status === MOVE_STATUSES.NEEDS_SERVICE_COUNSELING) { + if (oldValues.sc_assigned_id === null) newValues.assigned_sc = name; + if (oldValues.sc_assigned_id !== null) newValues.re_assigned_sc = name; + } else { + if (oldValues.sc_assigned_id === null) newValues.assigned_sc_ppm = name; + if (oldValues.sc_assigned_id !== null) newValues.re_assigned_sc_ppm = name; + } } if (changedValues?.too_assigned_id) { if (oldValues.too_assigned_id === null) newValues.assigned_too = name; diff --git a/src/utils/formatters.test.js b/src/utils/formatters.test.js index 97b99f470a3..b09ac4b0937 100644 --- a/src/utils/formatters.test.js +++ b/src/utils/formatters.test.js @@ -4,6 +4,7 @@ import * as formatters from './formatters'; import { formatQAReportID } from './formatters'; import PAYMENT_REQUEST_STATUS from 'constants/paymentRequestStatus'; +import { MOVE_STATUSES } from 'shared/constants'; describe('formatters', () => { describe('format date for customer app', () => { @@ -350,13 +351,14 @@ describe('formatters', () => { }); describe('formatAssignedOfficeUserFromContext', () => { - it('properly formats an SCs name', () => { + it(`properly formats a Services Counselor's name for assignment`, () => { const values = { changedValues: { sc_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', }, oldValues: { sc_assigned_id: null, + status: MOVE_STATUSES.NEEDS_SERVICE_COUNSELING, }, context: [{ assigned_office_user_last_name: 'Daniels', assigned_office_user_first_name: 'Jayden' }], }; @@ -367,8 +369,61 @@ describe('formatAssignedOfficeUserFromContext', () => { assigned_sc: 'Daniels, Jayden', }); }); + it(`properly formats a Services Counselor's name for reassignment`, () => { + const values = { + changedValues: { + sc_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', + }, + oldValues: { + sc_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64', + status: MOVE_STATUSES.NEEDS_SERVICE_COUNSELING, + }, + context: [{ assigned_office_user_last_name: 'Daniels', assigned_office_user_first_name: 'Jayden' }], + }; + + const result = formatters.formatAssignedOfficeUserFromContext(values); - it('properly formats a TOOs name', () => { + expect(result).toEqual({ + re_assigned_sc: 'Daniels, Jayden', + }); + }); + it(`properly formats a Closeout Counselor's name for assignment`, () => { + const values = { + changedValues: { + sc_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', + }, + oldValues: { + sc_assigned_id: null, + status: MOVE_STATUSES.SERVICE_COUNSELING_COMPLETED, + }, + context: [{ assigned_office_user_last_name: 'Daniels', assigned_office_user_first_name: 'Jayden' }], + }; + + const result = formatters.formatAssignedOfficeUserFromContext(values); + + expect(result).toEqual({ + assigned_sc_ppm: 'Daniels, Jayden', + }); + }); + it(`properly formats a Closeout Counselor's name for reassignment`, () => { + const values = { + changedValues: { + sc_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', + }, + oldValues: { + sc_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64', + status: MOVE_STATUSES.SERVICE_COUNSELING_COMPLETED, + }, + context: [{ assigned_office_user_last_name: 'Daniels', assigned_office_user_first_name: 'Jayden' }], + }; + + const result = formatters.formatAssignedOfficeUserFromContext(values); + + expect(result).toEqual({ + re_assigned_sc_ppm: 'Daniels, Jayden', + }); + }); + it('properly formats a TOOs name for assignment', () => { const values = { changedValues: { too_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', @@ -385,8 +440,24 @@ describe('formatAssignedOfficeUserFromContext', () => { assigned_too: 'McLaurin, Terry', }); }); + it('properly formats a TOOs name for reassignment', () => { + const values = { + changedValues: { + too_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', + }, + oldValues: { + too_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64', + }, + context: [{ assigned_office_user_last_name: 'McLaurin', assigned_office_user_first_name: 'Terry' }], + }; + + const result = formatters.formatAssignedOfficeUserFromContext(values); - it('properly formats a TIOs name', () => { + expect(result).toEqual({ + re_assigned_too: 'McLaurin, Terry', + }); + }); + it('properly formats a TIOs name for assignment', () => { const values = { changedValues: { tio_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', @@ -403,6 +474,23 @@ describe('formatAssignedOfficeUserFromContext', () => { assigned_tio: 'Robinson, Brian', }); }); + it('properly formats a TIOs name for reassignment', () => { + const values = { + changedValues: { + tio_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', + }, + oldValues: { + tio_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64', + }, + context: [{ assigned_office_user_last_name: 'Robinson', assigned_office_user_first_name: 'Brian' }], + }; + + const result = formatters.formatAssignedOfficeUserFromContext(values); + + expect(result).toEqual({ + re_assigned_tio: 'Robinson, Brian', + }); + }); }); describe('constructSCOrderOconusFields', () => { From 8940a21d85c61de666d9fbe54a9d79d67b11bf73 Mon Sep 17 00:00:00 2001 From: Jon Spight Date: Wed, 5 Feb 2025 18:56:10 +0000 Subject: [PATCH 08/36] Fixed spacing --- src/shared/utils.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/shared/utils.js b/src/shared/utils.js index cd4c84ed2a9..13720d91ef0 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -217,11 +217,9 @@ export function checkPreceedingAddress(formValues) { if (values.hasSecondaryDelivery === 'yes' && values.delivery.address.streetAddress1 === '') { formError = 'delivery.address.streetAddress1'; } - if (values.hasTertiaryPickup === 'true' && values.secondaryPickup.address.streetAddress1 === '') { formError = 'secondaryPickup.address.streetAddress1'; } - if (values.hasTertiaryDelivery === 'yes' && values.secondaryDelivery.address.streetAddress1 === '') { formError = 'secondaryDelivery.address.streetAddress1'; } From 4046d860acc0a93e1ec63e23ecb83674a18b4cfb Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Wed, 5 Feb 2025 20:07:47 +0000 Subject: [PATCH 09/36] update DeleteAssignedOfficeUser.jsx to reflect counselor vs closeout counselor in move history --- .../UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.jsx | 8 ++++++-- .../DeleteAssignedOfficeUser.test.jsx | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.jsx b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.jsx index e721b67abd0..000e6ec56f7 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.jsx @@ -3,14 +3,18 @@ import React from 'react'; import o from 'constants/MoveHistory/UIDisplay/Operations'; import a from 'constants/MoveHistory/Database/Actions'; import t from 'constants/MoveHistory/Database/Tables'; +import { MOVE_STATUSES } from 'shared/constants'; export default { action: a.UPDATE, eventName: o.deleteAssignedOfficeUser, tableName: t.moves, getEventNameDisplay: () => 'Updated move', - getDetails: ({ changedValues }) => { - if (changedValues.sc_assigned_id === null) return <>Counselor unassigned; + getDetails: ({ changedValues, oldValues }) => { + if (changedValues.sc_assigned_id === null && oldValues?.status === MOVE_STATUSES.NEEDS_SERVICE_COUNSELING) + return <>Counselor unassigned; + if (changedValues.sc_assigned_id === null && oldValues?.status !== MOVE_STATUSES.NEEDS_SERVICE_COUNSELING) + return <>Closeout counselor unassigned; if (changedValues.too_assigned_id === null) return <>Task ordering officer unassigned; if (changedValues.tio_assigned_id === null) return <>Task invoicing officer unassigned; return <>Unassigned; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.test.jsx index cf02b1f8fb5..5c9a613271b 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.test.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.test.jsx @@ -2,6 +2,7 @@ import { screen, render } from '@testing-library/react'; import e from 'constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser'; import getTemplate from 'constants/MoveHistory/TemplateManager'; +import { MOVE_STATUSES } from 'shared/constants'; describe('When given a move that has been unassigned', () => { const historyRecord = { @@ -26,8 +27,15 @@ describe('When given a move that has been unassigned', () => { }); describe('displays the proper details for', () => { + it('closeout counselor', () => { + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Closeout counselor unassigned')).toBeInTheDocument(); + }); it('services counselor', () => { const template = getTemplate(historyRecord); + historyRecord.oldValues = { status: MOVE_STATUSES.NEEDS_SERVICE_COUNSELING }; render(template.getDetails(historyRecord)); expect(screen.getByText('Counselor unassigned')).toBeInTheDocument(); From 87be21382d713769403585c1468660bebf85fa7b Mon Sep 17 00:00:00 2001 From: Brooklyn Welsh Date: Tue, 28 Jan 2025 12:18:04 -0500 Subject: [PATCH 10/36] Removed unneeded conditional --- .../ExpandableServiceItemRow/ExpandableServiceItemRow.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.jsx b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.jsx index 41e4295c642..e6cbba78392 100644 --- a/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.jsx +++ b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.jsx @@ -26,7 +26,7 @@ const ExpandableServiceItemRow = ({ }; const canShowExpandableContent = !disableExpansion && - (allowedServiceItemCalculations.includes(serviceItem.mtoServiceItemCode) === true || serviceItem.rejectionReason); + (allowedServiceItemCalculations.includes(serviceItem.mtoServiceItemCode) || serviceItem.rejectionReason); const handleExpandClick = () => { setIsExpanded((prev) => !prev); From d0d77164065adf7dd3a52cc02075bf05073326aa Mon Sep 17 00:00:00 2001 From: Jon Spight Date: Thu, 6 Feb 2025 23:03:29 +0000 Subject: [PATCH 11/36] Rework --- .../Office/ShipmentForm/ShipmentForm.jsx | 82 +++++++++++++++++-- src/shared/utils.js | 39 +++++++++ 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/components/Office/ShipmentForm/ShipmentForm.jsx b/src/components/Office/ShipmentForm/ShipmentForm.jsx index 1290a33c810..6a627eea98a 100644 --- a/src/components/Office/ShipmentForm/ShipmentForm.jsx +++ b/src/components/Office/ShipmentForm/ShipmentForm.jsx @@ -70,7 +70,11 @@ import { validateDate } from 'utils/validation'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; import { dateSelectionWeekendHolidayCheck } from 'utils/calendar'; import { datePickerFormat, formatDate } from 'shared/dates'; -import { checkPreceedingAddress } from 'shared/utils'; +import { + isSecondaryPicukupAddressComplete, + isSecondaryDeliveryAddressComplete, + isDeliveryAddressComplete, +} from 'shared/utils'; const ShipmentForm = (props) => { const { @@ -358,13 +362,6 @@ const ShipmentForm = (props) => { : generatePath(servicesCounselingRoutes.BASE_ORDERS_EDIT_PATH, { moveCode }); const submitMTOShipment = (formValues, actions) => { - const preceedingAddressError = checkPreceedingAddress(formValues); - if (preceedingAddressError !== '') { - actions.setFieldError(preceedingAddressError, 'Address required'); - actions.setSubmitting(false); - return; - } - //* PPM Shipment *// if (isPPM) { const ppmShipmentBody = formatPpmShipmentForAPI(formValues); @@ -777,7 +774,6 @@ const ShipmentForm = (props) => { onErrorHandler, ); }; - return ( <> { value="true" title="Yes, I have a third pickup address" checked={hasTertiaryPickup === 'true'} + disabled={ + !isSecondaryPicukupAddressComplete( + hasSecondaryPickup, + values.secondaryPickup.address, + ) + } /> { value="false" title="No, I do not have a third pickup address" checked={hasTertiaryPickup !== 'true'} + disabled={ + !isSecondaryPicukupAddressComplete( + hasSecondaryPickup, + values.secondaryPickup.address, + ) + } /> @@ -1123,6 +1131,7 @@ const ShipmentForm = (props) => { value="yes" title="Yes, I have a second destination location" checked={hasSecondaryDelivery === 'yes'} + disabled={!isDeliveryAddressComplete('yes', values.delivery.address)} /> { value="no" title="No, I do not have a second destination location" checked={hasSecondaryDelivery !== 'yes'} + disabled={!isDeliveryAddressComplete('yes', values.delivery.address)} /> @@ -1158,6 +1168,12 @@ const ShipmentForm = (props) => { value="yes" title="Yes, I have a third delivery address" checked={hasTertiaryDelivery === 'yes'} + disabled={ + !isSecondaryDeliveryAddressComplete( + hasSecondaryDelivery, + values.secondaryDelivery.address, + ) + } /> { value="no" title="No, I do not have a third delivery address" checked={hasTertiaryDelivery !== 'yes'} + disabled={ + !isSecondaryDeliveryAddressComplete( + hasSecondaryDelivery, + values.secondaryDelivery.address, + ) + } /> @@ -1280,6 +1302,9 @@ const ShipmentForm = (props) => { value="yes" title="Yes, I have a second destination location" checked={hasSecondaryDelivery === 'yes'} + disabled={ + !isDeliveryAddressComplete(hasDeliveryAddress, values.delivery.address) + } /> { value="no" title="No, I do not have a second destination location" checked={hasSecondaryDelivery !== 'yes'} + disabled={ + !isDeliveryAddressComplete(hasDeliveryAddress, values.delivery.address) + } /> @@ -1317,6 +1345,12 @@ const ShipmentForm = (props) => { value="yes" title="Yes, I have a third delivery address" checked={hasTertiaryDelivery === 'yes'} + disabled={ + !isSecondaryDeliveryAddressComplete( + hasSecondaryDelivery, + values.secondaryDelivery.address, + ) + } /> { value="no" title="No, I do not have a third delivery address" checked={hasTertiaryDelivery !== 'yes'} + disabled={ + !isSecondaryDeliveryAddressComplete( + hasSecondaryDelivery, + values.secondaryDelivery.address, + ) + } /> @@ -1494,6 +1534,12 @@ const ShipmentForm = (props) => { value="true" title="Yes, there is a third pickup address" checked={hasTertiaryPickup === 'true'} + disabled={ + !isSecondaryPicukupAddressComplete( + hasSecondaryPickup, + values.secondaryPickup.address, + ) + } /> { value="false" title="No, there is not a third pickup address" checked={hasTertiaryPickup !== 'true'} + disabled={ + !isSecondaryPicukupAddressComplete( + hasSecondaryPickup, + values.secondaryPickup.address, + ) + } /> @@ -1584,6 +1636,12 @@ const ShipmentForm = (props) => { value="true" title="Yes, I have a third delivery address" checked={hasTertiaryDestination === 'true'} + disabled={ + !isSecondaryDeliveryAddressComplete( + hasSecondaryDestination, + values.secondaryDestination.address, + ) + } /> { value="false" title="No, I do not have a third delivery address" checked={hasTertiaryDestination !== 'true'} + disabled={ + !isSecondaryDeliveryAddressComplete( + hasSecondaryDestination, + values.secondaryDestination.address, + ) + } /> diff --git a/src/shared/utils.js b/src/shared/utils.js index 13720d91ef0..6d788474779 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -228,3 +228,42 @@ export function checkPreceedingAddress(formValues) { } return formError; } + +export function isSecondaryPicukupAddressComplete(hasSecondaryPickup, addressValues) { + if ( + (hasSecondaryPickup === 'yes' || hasSecondaryPickup === 'true') && + addressValues.streetAddress1 !== '' && + addressValues.state !== '' && + addressValues.city !== '' && + addressValues.postalCode !== '' + ) { + return true; + } + return false; +} + +export function isSecondaryDeliveryAddressComplete(hasSecondaryDelivery, addressValues) { + if ( + (hasSecondaryDelivery === 'yes' || hasSecondaryDelivery === 'true') && + addressValues.streetAddress1 !== '' && + addressValues.state !== '' && + addressValues.city !== '' && + addressValues.postalCode !== '' + ) { + return true; + } + return false; +} + +export function isDeliveryAddressComplete(hasDeliveryAddress, addressValues) { + if ( + hasDeliveryAddress === 'yes' && + addressValues.streetAddress1 !== '' && + addressValues.state !== '' && + addressValues.city !== '' && + addressValues.postalCode !== '' + ) { + return true; + } + return false; +} From e19e11c090e40bcb48f2ab856553f7d85636b8a7 Mon Sep 17 00:00:00 2001 From: Tae Jung Date: Thu, 6 Feb 2025 23:09:22 +0000 Subject: [PATCH 12/36] added frontend work for intl crating and uncrating payment request --- pkg/testdatagen/testharness/dispatch.go | 3 + pkg/testdatagen/testharness/make_move.go | 371 ++++++++++++++++++ .../office/txo/tioFlowsInternational.spec.js | 92 +++++ playwright/tests/utils/testharness.js | 8 + .../Office/ServiceItemCalculations/helpers.js | 102 ++++- .../ServiceItemCalculations/helpers.test.js | 28 +- .../serviceItemTestParams.js | 54 +++ src/constants/serviceItems.js | 13 +- 8 files changed, 663 insertions(+), 8 deletions(-) diff --git a/pkg/testdatagen/testharness/dispatch.go b/pkg/testdatagen/testharness/dispatch.go index 0ed11caf74a..f7e39c22f2f 100644 --- a/pkg/testdatagen/testharness/dispatch.go +++ b/pkg/testdatagen/testharness/dispatch.go @@ -272,6 +272,9 @@ var actionDispatcher = map[string]actionFunc{ "InternationalHHGMoveWithServiceItemsandPaymentRequestsForTIO": func(appCtx appcontext.AppContext) testHarnessResponse { return MakeBasicInternationalHHGMoveWithServiceItemsandPaymentRequestsForTIO(appCtx) }, + "IntlHHGMoveWithCratingUncratingServiceItemsAndPaymentRequestsForTIO": func(appCtx appcontext.AppContext) testHarnessResponse { + return MakeIntlHHGMoveWithCratingUncratingServiceItemsAndPaymentRequestsForTIO(appCtx) + }, } func Actions() []string { diff --git a/pkg/testdatagen/testharness/make_move.go b/pkg/testdatagen/testharness/make_move.go index 3e2f5fed100..16984751b00 100644 --- a/pkg/testdatagen/testharness/make_move.go +++ b/pkg/testdatagen/testharness/make_move.go @@ -9380,3 +9380,374 @@ func MakeBasicInternationalHHGMoveWithServiceItemsandPaymentRequestsForTIO(appCt return *newmove } + +// MakeIntlHHGMoveWithCratingUncratingServiceItemsAndPaymentRequestsForTIO creates an iHHG move +// that has been approved by TOO & prime has requested payment for intl crating and uncrating service items +func MakeIntlHHGMoveWithCratingUncratingServiceItemsAndPaymentRequestsForTIO(appCtx appcontext.AppContext) models.Move { + userUploader := newUserUploader(appCtx) + + // Create Customer + userInfo := newUserInfo("customer") + customer := factory.BuildExtendedServiceMember(appCtx.DB(), []factory.Customization{ + { + Model: models.ServiceMember{ + PersonalEmail: &userInfo.email, + FirstName: &userInfo.firstName, + LastName: &userInfo.lastName, + CacValidated: true, + }, + }, + }, nil) + + // address setup + addressAK := factory.BuildAddress(appCtx.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "123 Cold St", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + }, + }, + }, nil) + destDutyLocationAK := factory.BuildDutyLocation(appCtx.DB(), []factory.Customization{ + { + Model: addressAK, + LinkOnly: true, + }, + }, nil) + + // orders setup using AK destination duty location + orders := factory.BuildOrder(appCtx.DB(), []factory.Customization{ + { + Model: customer, + LinkOnly: true, + }, + { + Model: models.UserUpload{}, + ExtendedParams: &factory.UserUploadExtendedParams{ + UserUploader: userUploader, + AppContext: appCtx, + }, + }, + { + Model: models.Order{ + NewDutyLocationID: destDutyLocationAK.ID, + }, + }, + }, nil) + + mto := factory.BuildMove(appCtx.DB(), []factory.Customization{ + { + Model: orders, + LinkOnly: true, + }, + { + Model: models.Move{ + AvailableToPrimeAt: models.TimePointer(time.Now()), + }, + }, + }, nil) + + shipmentPickupAddress := factory.BuildAddress(appCtx.DB(), []factory.Customization{ + { + Model: models.Address{ + // This is a postal code that maps to the default office user gbloc KKFA in the PostalCodeToGBLOC table + PostalCode: "85004", + }, + }, + }, nil) + alaskaDestAddress := factory.BuildAddress(appCtx.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "123 Cold St", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + + estimatedWeight := unit.Pound(2000) + actualWeight := unit.Pound(2000) + mtoShipmentHHG := factory.BuildMTOShipment(appCtx.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + PrimeEstimatedWeight: &estimatedWeight, + PrimeActualWeight: &actualWeight, + ShipmentType: models.MTOShipmentTypeHHG, + ApprovedDate: models.TimePointer(time.Now()), + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: shipmentPickupAddress, + LinkOnly: true, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: alaskaDestAddress, + LinkOnly: true, + Type: &factory.Addresses.DeliveryAddress, + }, + { + Model: mto, + LinkOnly: true, + }, + }, nil) + + // Create Releasing Agent + agentUserInfo := newUserInfo("agent") + factory.BuildMTOAgent(appCtx.DB(), []factory.Customization{ + { + Model: mtoShipmentHHG, + LinkOnly: true, + }, + { + Model: models.MTOAgent{ + ID: uuid.Must(uuid.NewV4()), + FirstName: &agentUserInfo.firstName, + LastName: &agentUserInfo.lastName, + Email: &agentUserInfo.email, + MTOAgentType: models.MTOAgentReleasing, + }, + }, + }, nil) + + paymentRequestHHG := factory.BuildPaymentRequest(appCtx.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: mto, + LinkOnly: true, + }, + }, nil) + + // for soft deleted proof of service docs + factory.BuildPrimeUpload(appCtx.DB(), []factory.Customization{ + { + Model: paymentRequestHHG, + LinkOnly: true, + }, + }, []factory.Trait{factory.GetTraitPrimeUploadDeleted}) + + currentTime := time.Now() + + cratingPaymentServiceItemParams := []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: factory.DefaultContractCode, + }, + { + Key: models.ServiceItemParamNameEscalationCompounded, + KeyType: models.ServiceItemParamTypeString, + Value: strconv.FormatFloat(1.125, 'f', 5, 64), + }, + { + Key: models.ServiceItemParamNamePriceRateOrFactor, + KeyType: models.ServiceItemParamTypeString, + Value: "1.71", + }, + { + Key: models.ServiceItemParamNameCubicFeetBilled, + KeyType: models.ServiceItemParamTypeDecimal, + Value: "12", + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: currentTime.Format("2006-01-02"), + }, + { + Key: models.ServiceItemParamNameStandaloneCrate, + KeyType: models.ServiceItemParamTypeBoolean, + Value: strconv.FormatBool(true), + }, + { + Key: models.ServiceItemParamNameStandaloneCrateCap, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.FormatInt(100000, 10), + }, + { + Key: models.ServiceItemParamNameMarketOrigin, + KeyType: models.ServiceItemParamTypeString, + Value: "O", + }, + { + Key: models.ServiceItemParamNameExternalCrate, + KeyType: models.ServiceItemParamTypeBoolean, + Value: strconv.FormatBool(true), + }, + { + Key: models.ServiceItemParamNameDimensionHeight, + KeyType: models.ServiceItemParamTypeString, + Value: "10", + }, + { + Key: models.ServiceItemParamNameDimensionLength, + KeyType: models.ServiceItemParamTypeString, + Value: "12", + }, + { + Key: models.ServiceItemParamNameDimensionWidth, + KeyType: models.ServiceItemParamTypeString, + Value: "3", + }, + } + desc := "description test" + icrt := factory.BuildMTOServiceItem(appCtx.DB(), []factory.Customization{ + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + Description: &desc, + StandaloneCrate: models.BoolPointer(true), + ExternalCrate: models.BoolPointer(true), + }, + }, + { + Model: mto, + LinkOnly: true, + }, + { + Model: mtoShipmentHHG, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeICRT, + }, + }, + }, nil) + + factory.BuildPaymentServiceItemWithParams(appCtx.DB(), models.ReServiceCodeICRT, + cratingPaymentServiceItemParams, []factory.Customization{ + { + Model: mto, + LinkOnly: true, + }, + { + Model: mtoShipmentHHG, + LinkOnly: true, + }, + { + Model: paymentRequestHHG, + LinkOnly: true, + }, + { + Model: icrt, + LinkOnly: true, + }, + }, nil) + + iucrt := factory.BuildMTOServiceItem(appCtx.DB(), []factory.Customization{ + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + Description: &desc, + }, + }, + { + Model: mto, + LinkOnly: true, + }, + { + Model: mtoShipmentHHG, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIUCRT, + }, + }, + }, nil) + + unCratingPaymentServiceItemParams := []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: factory.DefaultContractCode, + }, + { + Key: models.ServiceItemParamNameEscalationCompounded, + KeyType: models.ServiceItemParamTypeString, + Value: strconv.FormatFloat(1.125, 'f', 5, 64), + }, + { + Key: models.ServiceItemParamNamePriceRateOrFactor, + KeyType: models.ServiceItemParamTypeString, + Value: "1.71", + }, + { + Key: models.ServiceItemParamNameCubicFeetBilled, + KeyType: models.ServiceItemParamTypeDecimal, + Value: "12", + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: currentTime.Format("2006-01-02"), + }, + { + Key: models.ServiceItemParamNameMarketDest, + KeyType: models.ServiceItemParamTypeString, + Value: "O", + }, + { + Key: models.ServiceItemParamNameDimensionHeight, + KeyType: models.ServiceItemParamTypeString, + Value: "10", + }, + { + Key: models.ServiceItemParamNameDimensionLength, + KeyType: models.ServiceItemParamTypeString, + Value: "12", + }, + { + Key: models.ServiceItemParamNameDimensionWidth, + KeyType: models.ServiceItemParamTypeString, + Value: "3", + }, + } + + factory.BuildPaymentServiceItemWithParams(appCtx.DB(), models.ReServiceCodeIUCRT, + unCratingPaymentServiceItemParams, []factory.Customization{ + { + Model: mto, + LinkOnly: true, + }, + { + Model: mtoShipmentHHG, + LinkOnly: true, + }, + { + Model: paymentRequestHHG, + LinkOnly: true, + }, + { + Model: iucrt, + LinkOnly: true, + }, + }, nil) + + // re-fetch the move so that we ensure we have exactly what is in + // the db + newmove, err := models.FetchMove(appCtx.DB(), &auth.Session{}, mto.ID) + if err != nil { + log.Panic(fmt.Errorf("failed to fetch move: %w", err)) + } + + // load payment requests so tests can confirm + err = appCtx.DB().Load(newmove, "PaymentRequests") + if err != nil { + log.Panic(fmt.Errorf("failed to fetch move payment requestse: %w", err)) + } + + return *newmove +} diff --git a/playwright/tests/office/txo/tioFlowsInternational.spec.js b/playwright/tests/office/txo/tioFlowsInternational.spec.js index 2b97f19078b..bf565aa85c2 100644 --- a/playwright/tests/office/txo/tioFlowsInternational.spec.js +++ b/playwright/tests/office/txo/tioFlowsInternational.spec.js @@ -187,4 +187,96 @@ test.describe('TIO user', () => { // in the TIO queue - only "Payment requested" moves will appear await expect(paymentSection.locator('td', { hasText: 'Reviewed' })).not.toBeVisible(); }); + + test('can review a payment request for international crating/uncrating service items', async ({ + page, + officePage, + }) => { + test.slow(); + const move = + await officePage.testHarness.buildIntlHHGMoveWithCratingUncratingServiceItemsAndPaymentRequestsForTIO(); + await officePage.signInAsNewTIOUser(); + + tioFlowPage = new TioFlowPage(officePage, move, true); + await tioFlowPage.waitForLoading(); + await officePage.tioNavigateToMove(tioFlowPage.moveLocator); + await officePage.page.getByRole('heading', { name: 'Payment Requests', exact: true }).waitFor(); + expect(page.url()).toContain('/payment-requests'); + await expect(page.getByTestId('MovePaymentRequests')).toBeVisible(); + + const prNumber = tioFlowPage.paymentRequest.payment_request_number; + const prHeading = page.getByRole('heading', { name: `Payment Request ${prNumber}` }); + await expect(prHeading).toBeVisible(); + await tioFlowPage.waitForLoading(); + + await page.getByRole('button', { name: 'Review service items' }).click(); + + await page.waitForURL(`**/payment-requests/${tioFlowPage.paymentRequest.id}`); + await tioFlowPage.waitForLoading(); + + // ICRT + await expect(page.getByTestId('ReviewServiceItems')).toBeVisible(); + await expect(page.getByText('International crating')).toBeVisible(); + await page.getByText('Show calculations').click(); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Calculations'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Crating size (cu ft)'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Description'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Dimensions'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('External crate'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Crating price (per cu ft)'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Market'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Crating date'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('International'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Price escalation factor'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Uncapped request total'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Standalone crate cap'); + // approve + await tioFlowPage.approveServiceItem(); + await page.getByTestId('nextServiceItem').click(); + await tioFlowPage.slowDown(); + + // IUCRT + await expect(page.getByText('International uncrating')).toBeVisible(); + await page.getByText('Show calculations').click(); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Calculations'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Crating size (cu ft)'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Description'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Dimensions'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Uncrating price (per cu ft)'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Market'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Uncrating date'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('International'); + await expect(page.locator('[data-testid="ServiceItemCalculations"]')).toContainText('Price escalation factor'); + // approve + await tioFlowPage.approveServiceItem(); + await page.getByTestId('nextServiceItem').click(); + await tioFlowPage.slowDown(); + + await expect(page.getByText('needs your review')).toHaveCount(0, { timeout: 10000 }); + await page.getByText('Complete request').click(); + + await page.getByText('Authorize payment').click(); + await tioFlowPage.waitForLoading(); + + await tioFlowPage.slowDown(); + expect(page.url()).toContain('/payment-requests'); + + await expect(page.getByTestId('tag')).toBeVisible(); + await expect(page.getByTestId('tag').getByText('Reviewed')).toHaveCount(1); + + // ensure the payment request we approved no longer has the "Review Service Items" button + await expect(page.getByText('Review Service Items')).toHaveCount(0); + + // Go back to queue + await page.locator('a[title="Home"]').click(); + await tioFlowPage.waitForLoading(); + + // search for the moveLocator in case this move doesn't show up on the first page + await page.locator('#locator').fill(tioFlowPage.moveLocator); + await page.locator('#locator').blur(); + const paymentSection = page.locator(`[data-uuid="${tioFlowPage.paymentRequest.id}"]`); + // the payment request that is now in the "Reviewed" status will no longer appear + // in the TIO queue - only "Payment requested" moves will appear + await expect(paymentSection.locator('td', { hasText: 'Reviewed' })).not.toBeVisible(); + }); }); diff --git a/playwright/tests/utils/testharness.js b/playwright/tests/utils/testharness.js index cc84f4c61f9..9a9e6ebb124 100644 --- a/playwright/tests/utils/testharness.js +++ b/playwright/tests/utils/testharness.js @@ -387,6 +387,14 @@ export class TestHarness { return this.buildDefault('InternationalHHGMoveWithServiceItemsandPaymentRequestsForTIO'); } + /** + * Use testharness to build ihhg move for TIO + * @returns {Promise} + */ + async buildIntlHHGMoveWithCratingUncratingServiceItemsAndPaymentRequestsForTIO() { + return this.buildDefault('IntlHHGMoveWithCratingUncratingServiceItemsAndPaymentRequestsForTIO'); + } + /** * Use testharness to build hhg move for QAE * @returns {Promise} diff --git a/src/components/Office/ServiceItemCalculations/helpers.js b/src/components/Office/ServiceItemCalculations/helpers.js index c5d26d73e75..6621896a7ff 100644 --- a/src/components/Office/ServiceItemCalculations/helpers.js +++ b/src/components/Office/ServiceItemCalculations/helpers.js @@ -28,6 +28,22 @@ const peak = (params) => { }`; }; +const market = (params) => { + let marketText = `${SERVICE_ITEM_CALCULATION_LABELS.Market}: `; + + if (getParamValue(SERVICE_ITEM_PARAM_KEYS.MarketOrigin, params)) { + marketText += ` ${ + getParamValue(SERVICE_ITEM_PARAM_KEYS.MarketOrigin, params)?.toLowerCase() === 'o' ? 'OCONUS' : 'CONUS' + }`; + } else { + marketText += ` ${ + getParamValue(SERVICE_ITEM_PARAM_KEYS.MarketDest, params)?.toLowerCase() === 'o' ? 'OCONUS' : 'CONUS' + }`; + } + + return marketText; +}; + const serviceAreaOrigin = (params) => { return `${SERVICE_ITEM_CALCULATION_LABELS[SERVICE_ITEM_PARAM_KEYS.ServiceAreaOrigin]}: ${getParamValue( SERVICE_ITEM_PARAM_KEYS.ServiceAreaOrigin, @@ -647,18 +663,71 @@ const unCratingPrice = (params) => { ); }; +const cratingPriceIntl = (params) => { + const value = getParamValue(SERVICE_ITEM_PARAM_KEYS.PriceRateOrFactor, params); + const label = SERVICE_ITEM_CALCULATION_LABELS.CratingPrice; + + return calculation( + value, + label, + formatDetail(market(params)), + formatDetail(cratingDate(params)), + formatDetail(SERVICE_ITEM_CALCULATION_LABELS.International), + ); +}; + +const unCratingPriceIntl = (params) => { + const value = getParamValue(SERVICE_ITEM_PARAM_KEYS.PriceRateOrFactor, params); + const label = SERVICE_ITEM_CALCULATION_LABELS.UncratingPrice; + + return calculation( + value, + label, + formatDetail(market(params)), + formatDetail(unCratingDate(params)), + formatDetail(SERVICE_ITEM_CALCULATION_LABELS.International), + ); +}; + const cratingSize = (params, mtoParams) => { - const value = getParamValue(SERVICE_ITEM_PARAM_KEYS.CubicFeetBilled, params); + const cubicFeetBilled = getParamValue(SERVICE_ITEM_PARAM_KEYS.CubicFeetBilled, params); const length = getParamValue(SERVICE_ITEM_PARAM_KEYS.DimensionLength, params); const height = getParamValue(SERVICE_ITEM_PARAM_KEYS.DimensionHeight, params); const width = getParamValue(SERVICE_ITEM_PARAM_KEYS.DimensionWidth, params); - const label = SERVICE_ITEM_CALCULATION_LABELS.CubicFeetBilled; + let label = SERVICE_ITEM_CALCULATION_LABELS.CubicFeetBilled; const description = `${SERVICE_ITEM_CALCULATION_LABELS.Description}: ${mtoParams.description}`; const formattedDimensions = `${SERVICE_ITEM_CALCULATION_LABELS.Dimensions}: ${length}x${width}x${height} in`; - return calculation(value, label, formatDetail(description), formatDetail(formattedDimensions)); + const externalCrate = + getParamValue(SERVICE_ITEM_PARAM_KEYS.ExternalCrate, params)?.toLowerCase() === 'true' + ? SERVICE_ITEM_CALCULATION_LABELS.ExternalCrate + : ''; + + // currently external intl crate gets 4 cu ft min applied to pricing + const minimumSizeApplied = externalCrate && cubicFeetBilled.toString() === '4.00'; + + if (minimumSizeApplied) { + label += ' - Minimum'; + } + + // show actual size if minimum was applied + const cubicFeetCrating = minimumSizeApplied + ? `${SERVICE_ITEM_CALCULATION_LABELS.CubicFeetCrating}: ${getParamValue( + SERVICE_ITEM_PARAM_KEYS.CubicFeetCrating, + params, + )} cu ft` + : ''; + + return calculation( + cubicFeetBilled, + label, + formatDetail(description), + formatDetail(formattedDimensions), + formatDetail(cubicFeetCrating), + formatDetail(externalCrate), + ); }; const standaloneCrate = (params) => { @@ -680,7 +749,7 @@ const standaloneCrate = (params) => { const uncappedRequestTotal = (params) => { const uncappedTotal = getParamValue(SERVICE_ITEM_PARAM_KEYS.UncappedRequestTotal, params); const value = toDollarString(uncappedTotal); - const label = `${SERVICE_ITEM_CALCULATION_LABELS.UncappedRequestTotal}:`; + const label = `${SERVICE_ITEM_CALCULATION_LABELS.UncappedRequestTotal}`; return calculation(value, label); }; @@ -950,6 +1019,31 @@ export default function makeCalculations(itemCode, totalAmount, params, mtoParam totalAmountRequested(totalAmount), ]; break; + // International crating + case SERVICE_ITEM_CODES.ICRT: + result = [ + cratingSize(params, mtoParams), + cratingPriceIntl(params), + priceEscalationFactorWithoutContractYear(params), + totalAmountRequested(totalAmount), + ]; + if ( + SERVICE_ITEM_PARAM_KEYS.StandaloneCrate !== null && + getParamValue(SERVICE_ITEM_PARAM_KEYS.StandaloneCrate, params) === 'true' + ) { + result.splice(result.length - 1, 0, uncappedRequestTotal(params)); + result.splice(result.length - 1, 0, standaloneCrate(params)); + } + break; + // International uncrating + case SERVICE_ITEM_CODES.IUCRT: + result = [ + cratingSize(params, mtoParams), + unCratingPriceIntl(params), + priceEscalationFactorWithoutContractYear(params), + totalAmountRequested(totalAmount), + ]; + break; default: break; } diff --git a/src/components/Office/ServiceItemCalculations/helpers.test.js b/src/components/Office/ServiceItemCalculations/helpers.test.js index 8ca38112f65..4ff4fb3e02b 100644 --- a/src/components/Office/ServiceItemCalculations/helpers.test.js +++ b/src/components/Office/ServiceItemCalculations/helpers.test.js @@ -11,12 +11,12 @@ function testData(code) { 'Crating size (cu ft)': '4.00', }; } - if (code === 'DCRT') { + if (code === 'DCRT' || code === 'ICRT') { result = { ...result, 'Crating price (per cu ft)': '1.71', }; - } else if (code === 'DUCRT') { + } else if (code === 'DUCRT' || code === 'IUCRT') { result = { ...result, 'Uncrating price (per cu ft)': '1.71', @@ -363,4 +363,28 @@ describe('DomesticDestinationSITDelivery', () => { const expected = testData('PODFSC'); testAB(result, expected); }); + + it('returns correct data for ICRT', () => { + const result = makeCalculations( + 'ICRT', + 99999, + testParams.InternationalCrating, + testParams.additionalCratingDataDCRT, + ); + const expected = testData('ICRT'); + + testAB(result, expected); + }); + + it('returns correct data for IUCRT', () => { + const result = makeCalculations( + 'IUCRT', + 99999, + testParams.InternationalUncrating, + testParams.additionalCratingDataDCRT, + ); + const expected = testData('IUCRT'); + + testAB(result, expected); + }); }); diff --git a/src/components/Office/ServiceItemCalculations/serviceItemTestParams.js b/src/components/Office/ServiceItemCalculations/serviceItemTestParams.js index e234dc167c9..bf29cdd1807 100644 --- a/src/components/Office/ServiceItemCalculations/serviceItemTestParams.js +++ b/src/components/Office/ServiceItemCalculations/serviceItemTestParams.js @@ -493,6 +493,33 @@ const PortZip = { type: 'STRING', value: '99505', }; +const ExternalCrate = { + eTag: 'MjAyMS0wNy0yOVQyMDoxNTowMS4xNDA1MjZa', + id: 'f5bb063e-38da-4c86-88ce-a6a328e70b92', + key: 'ExternalCrate', + origin: 'PRIME', + paymentServiceItemID: '28039a62-387d-479f-b50f-e0041b7e6e22', + type: 'BOOLEAN', + value: 'FALSE', +}; +const MarketOrigin = { + eTag: 'MjAyMS0wNy0yOVQyMDoxNTowMS4xNDA1MjZa', + id: 'f5bb063e-38da-4c86-88ce-a6a328e70b92', + key: 'MarketOrigin', + origin: 'PRIME', + paymentServiceItemID: '28039a62-387d-479f-b50f-e0041b7e6e22', + type: 'STRING', + value: 'O', +}; +const MarketDest = { + eTag: 'MjAyMS0wNy0yOVQyMDoxNTowMS4xNDA1MjZa', + id: 'f5bb063e-38da-4c86-88ce-a6a328e70b92', + key: 'MarketDest', + origin: 'PRIME', + paymentServiceItemID: '28039a62-387d-479f-b50f-e0041b7e6e22', + type: 'STRING', + value: 'C', +}; const testParams = { DomesticLongHaul: [ ContractCode, @@ -916,6 +943,33 @@ const testParams = { ZipPickupAddress, PortZip, ], + InternationalCrating: [ + ContractYearName, + EscalationCompounded, + PriceRateOrFactor, + ReferenceDate, + CubicFeetBilled, + MarketOrigin, + ServiceAreaOrigin, + ZipPickupAddress, + DimensionWidth, + DimensionHeight, + DimensionLength, + StandaloneCrate, + ExternalCrate, + ], + InternationalUncrating: [ + ReferenceDate, + EscalationCompounded, + CubicFeetBilled, + PriceRateOrFactor, + MarketDest, + ServiceAreaDest, + ZipDestAddress, + DimensionWidth, + DimensionHeight, + DimensionLength, + ], additionalCratingDataDCRT: { reServiceCode: 'DCRT', description: 'Grand piano', diff --git a/src/constants/serviceItems.js b/src/constants/serviceItems.js index 213afb45f38..9c3f3d1db23 100644 --- a/src/constants/serviceItems.js +++ b/src/constants/serviceItems.js @@ -17,10 +17,13 @@ const SERVICE_ITEM_PARAM_KEYS = { DistanceZipSITDest: 'DistanceZipSITDest', DistanceZipSITOrigin: 'DistanceZipSITOrigin', EIAFuelPrice: 'EIAFuelPrice', + ExternalCrate: 'ExternalCrate', FSCPriceDifferenceInCents: 'FSCPriceDifferenceInCents', EscalationCompounded: 'EscalationCompounded', FSCWeightBasedDistanceMultiplier: 'FSCWeightBasedDistanceMultiplier', IsPeak: 'IsPeak', + MarketDest: 'MarketDest', + MarketOrigin: 'MarketOrigin', NTSPackingFactor: 'NTSPackingFactor', NumberDaysSIT: 'NumberDaysSIT', OriginPrice: 'OriginPrice', @@ -59,8 +62,10 @@ const SERVICE_ITEM_PARAM_KEYS = { const SERVICE_ITEM_CALCULATION_LABELS = { [SERVICE_ITEM_PARAM_KEYS.ActualPickupDate]: 'Pickup date', [SERVICE_ITEM_PARAM_KEYS.ContractYearName]: 'Base year', + [SERVICE_ITEM_PARAM_KEYS.CubicFeetCrating]: 'Actual size', [SERVICE_ITEM_PARAM_KEYS.DestinationPrice]: 'Destination price', [SERVICE_ITEM_PARAM_KEYS.EIAFuelPrice]: 'EIA diesel', + [SERVICE_ITEM_PARAM_KEYS.ExternalCrate]: 'External crate', [SERVICE_ITEM_PARAM_KEYS.FSCPriceDifferenceInCents]: 'Baseline rate difference', [SERVICE_ITEM_PARAM_KEYS.FSCWeightBasedDistanceMultiplier]: 'Weight-based distance multiplier', // Domestic non-peak or Domestic peak @@ -101,7 +106,9 @@ const SERVICE_ITEM_CALCULATION_LABELS = { Dimensions: 'Dimensions', Domestic: 'Domestic', FuelSurchargePrice: 'Mileage factor', + International: 'International', InternationalShippingAndLinehaul: 'ISLH price', + Market: 'Market', Mileage: 'Mileage', MileageIntoSIT: 'Mileage into SIT', MileageOutOfSIT: 'Mileage out of SIT', @@ -121,8 +128,8 @@ const SERVICE_ITEM_CALCULATION_LABELS = { UncratingDate: 'Uncrating date', UncratingPrice: 'Uncrating price (per cu ft)', SITFuelSurchargePrice: 'SIT mileage factor', - StandaloneCrate: 'Standalone Crate Cap', - UncappedRequestTotal: 'Uncapped Request Total', + StandaloneCrate: 'Standalone crate cap', + UncappedRequestTotal: 'Uncapped request total', Total: 'Total', }; @@ -234,6 +241,8 @@ const allowedServiceItemCalculations = [ SERVICE_ITEM_CODES.ISLH, SERVICE_ITEM_CODES.POEFSC, SERVICE_ITEM_CODES.PODFSC, + SERVICE_ITEM_CODES.ICRT, + SERVICE_ITEM_CODES.IUCRT, ]; export default SERVICE_ITEM_STATUSES; From 72dd20bd21f855ee896cf1e895beca184feb5c00 Mon Sep 17 00:00:00 2001 From: joeydoyecaci Date: Fri, 7 Feb 2025 20:30:47 +0000 Subject: [PATCH 13/36] Fixed 'mailto:' email link --- src/shared/ErrorModal/ErrorModal.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/shared/ErrorModal/ErrorModal.jsx b/src/shared/ErrorModal/ErrorModal.jsx index b5533a20c58..2706dce1c8c 100644 --- a/src/shared/ErrorModal/ErrorModal.jsx +++ b/src/shared/ErrorModal/ErrorModal.jsx @@ -11,9 +11,7 @@ export const ErrorModal = ({ closeModal, errorMessage, displayHelpDeskLink = tru {errorMessage} - {displayHelpDeskLink && ( - Technical Help Desk - )} + {displayHelpDeskLink && Technical Help Desk}