diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 4e4077fd3ae..fb1f6643917 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -6675,8 +6675,9 @@ func init() { }, "edipi": { "type": "string", - "x-nullable": true, - "example": "John" + "maxLength": 10, + "x-nullable": false, + "example": "1234567890" }, "emailIsPreferred": { "type": "boolean" @@ -22175,8 +22176,9 @@ func init() { }, "edipi": { "type": "string", - "x-nullable": true, - "example": "John" + "maxLength": 10, + "x-nullable": false, + "example": "1234567890" }, "emailIsPreferred": { "type": "boolean" diff --git a/pkg/gen/ghcmessages/create_customer_payload.go b/pkg/gen/ghcmessages/create_customer_payload.go index d9d28d6c570..68ece54fb88 100644 --- a/pkg/gen/ghcmessages/create_customer_payload.go +++ b/pkg/gen/ghcmessages/create_customer_payload.go @@ -37,8 +37,9 @@ type CreateCustomerPayload struct { CreateOktaAccount bool `json:"createOktaAccount,omitempty"` // edipi - // Example: John - Edipi *string `json:"edipi,omitempty"` + // Example: 1234567890 + // Max Length: 10 + Edipi string `json:"edipi,omitempty"` // email is preferred EmailIsPreferred bool `json:"emailIsPreferred,omitempty"` @@ -102,6 +103,10 @@ func (m *CreateCustomerPayload) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateEdipi(formats); err != nil { + res = append(res, err) + } + if err := m.validateEmplid(formats); err != nil { res = append(res, err) } @@ -174,6 +179,18 @@ func (m *CreateCustomerPayload) validateBackupMailingAddress(formats strfmt.Regi return nil } +func (m *CreateCustomerPayload) validateEdipi(formats strfmt.Registry) error { + if swag.IsZero(m.Edipi) { // not required + return nil + } + + if err := validate.MaxLength("edipi", "body", m.Edipi, 10); err != nil { + return err + } + + return nil +} + func (m *CreateCustomerPayload) validateEmplid(formats strfmt.Registry) error { if swag.IsZero(m.Emplid) { // not required return nil diff --git a/pkg/handlers/ghcapi/customer.go b/pkg/handlers/ghcapi/customer.go index 51e57284945..7452fa9589c 100644 --- a/pkg/handlers/ghcapi/customer.go +++ b/pkg/handlers/ghcapi/customer.go @@ -163,7 +163,6 @@ func (h CreateCustomerWithOktaOptionHandler) Handle(params customercodeop.Create payload := params.Body var err error var serviceMembers []models.ServiceMember - var edipi *string var dodidUniqueFeatureFlag bool // evaluating feature flag to see if we need to check if the DODID exists already @@ -177,30 +176,31 @@ func (h CreateCustomerWithOktaOptionHandler) Handle(params customercodeop.Create } if dodidUniqueFeatureFlag { - if payload.Edipi == nil || *payload.Edipi == "" { - edipi = nil - } else { - query := `SELECT service_members.edipi + query := `SELECT service_members.edipi FROM service_members WHERE service_members.edipi = $1` - err := appCtx.DB().RawQuery(query, payload.Edipi).All(&serviceMembers) - if err != nil { - errorMsg := apperror.NewBadDataError("error when checking for existing service member") - payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) - return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg - } else if len(serviceMembers) > 0 { - errorMsg := apperror.NewConflictError(h.GetTraceIDFromRequest(params.HTTPRequest), "Service member with this DODID already exists. Please use a different DODID number.") - payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) - return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg - } + err := appCtx.DB().RawQuery(query, payload.Edipi).All(&serviceMembers) + if err != nil { + errorMsg := apperror.NewBadDataError("error when checking for existing service member") + payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) + return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg + } else if len(serviceMembers) > 0 { + errorMsg := apperror.NewConflictError(h.GetTraceIDFromRequest(params.HTTPRequest), "Service member with this DODID already exists. Please use a different DODID number.") + payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) + return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg } + } - if len(serviceMembers) == 0 { - edipi = params.Body.Edipi + // Endpoint specific EDIPI and EMPLID check + // The following validation currently is only intended for the customer creation + // conducted by an office user such as the Service Counselor + if payload.Affiliation != nil && *payload.Affiliation == ghcmessages.AffiliationCOASTGUARD { + // EMPLID cannot be null + if payload.Emplid == nil { + errorMsg := apperror.NewConflictError(h.GetTraceIDFromRequest(params.HTTPRequest), "Service members from the Coast Guard require an EMPLID for creation.") + payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) + return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg } - } else { - // If the feature flag is not enabled, we will just set the dodid and continue - edipi = params.Body.Edipi } var newServiceMember models.ServiceMember @@ -250,18 +250,11 @@ func (h CreateCustomerWithOktaOptionHandler) Handle(params customercodeop.Create residentialAddress := addressModelFromPayload(&payload.ResidentialAddress.Address) backupMailingAddress := addressModelFromPayload(&payload.BackupMailingAddress.Address) - var emplid *string - if *payload.Emplid == "" { - emplid = nil - } else { - emplid = payload.Emplid - } - // Create a new serviceMember using the userID newServiceMember = models.ServiceMember{ UserID: userID, - Edipi: edipi, - Emplid: emplid, + Edipi: &payload.Edipi, + Emplid: payload.Emplid, Affiliation: (*models.ServiceMemberAffiliation)(payload.Affiliation), FirstName: &payload.FirstName, MiddleName: payload.MiddleName, diff --git a/pkg/handlers/ghcapi/customer_test.go b/pkg/handlers/ghcapi/customer_test.go index 4e8564af27c..3a6a3305172 100644 --- a/pkg/handlers/ghcapi/customer_test.go +++ b/pkg/handlers/ghcapi/customer_test.go @@ -162,7 +162,7 @@ func (suite *HandlerSuite) TestCreateCustomerWithOktaOptionHandler() { FirstName: "First", Telephone: handlers.FmtString("223-455-3399"), Affiliation: &affiliation, - Edipi: handlers.FmtString(""), + Edipi: "", Emplid: handlers.FmtString(""), PersonalEmail: *handlers.FmtString("email@email.com"), BackupContact: &ghcmessages.BackupContact{ @@ -260,7 +260,7 @@ func (suite *HandlerSuite) TestCreateCustomerWithOktaOptionHandler() { FirstName: "First", Telephone: handlers.FmtString("223-455-3399"), Affiliation: &affiliation, - Edipi: customer.Edipi, + Edipi: *customer.Edipi, PersonalEmail: *handlers.FmtString("email@email.com"), BackupContact: &ghcmessages.BackupContact{ Name: handlers.FmtString("New Backup Contact"), @@ -298,6 +298,81 @@ func (suite *HandlerSuite) TestCreateCustomerWithOktaOptionHandler() { response := handler.Handle(params) suite.Assertions.IsType(&customerops.CreateCustomerWithOktaOptionUnprocessableEntity{}, response) }) + + suite.Run("Unable to create customer of affiliation Coast Guard with no EMPLID", func() { + // in order to call the endpoint, we need to be an authenticated office user that's a SC + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), nil, []roles.RoleType{roles.RoleTypeTOO}) + officeUser.User.Roles = append(officeUser.User.Roles, roles.Role{ + RoleType: roles.RoleTypeServicesCounselor, + }) + + // Build provider + provider, err := factory.BuildOktaProvider(officeProviderName) + suite.NoError(err) + + mockAndActivateOktaEndpoints(provider) + + residentialAddress := ghcmessages.Address{ + StreetAddress1: handlers.FmtString("123 New Street"), + City: handlers.FmtString("Newcity"), + State: handlers.FmtString("MA"), + PostalCode: handlers.FmtString("02110"), + } + + backupAddress := ghcmessages.Address{ + StreetAddress1: handlers.FmtString("123 Backup Street"), + City: handlers.FmtString("Backupcity"), + State: handlers.FmtString("MA"), + PostalCode: handlers.FmtString("02115"), + } + + affiliation := ghcmessages.AffiliationCOASTGUARD + + body := &ghcmessages.CreateCustomerPayload{ + LastName: "Last", + FirstName: "First", + Telephone: handlers.FmtString("223-455-3399"), + Affiliation: &affiliation, + Edipi: "1234567890", + PersonalEmail: *handlers.FmtString("email@email.com"), + BackupContact: &ghcmessages.BackupContact{ + Name: handlers.FmtString("New Backup Contact"), + Phone: handlers.FmtString("445-345-1212"), + Email: handlers.FmtString("newbackup@mail.com"), + }, + ResidentialAddress: struct { + ghcmessages.Address + }{ + Address: residentialAddress, + }, + BackupMailingAddress: struct { + ghcmessages.Address + }{ + Address: backupAddress, + }, + CreateOktaAccount: true, + // when CacUser is false, this indicates a non-CAC user so CacValidated is set to true + CacUser: false, + } + + defer goth.ClearProviders() + goth.UseProviders(provider) + + request := httptest.NewRequest("POST", "/customer", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := customerops.CreateCustomerWithOktaOptionParams{ + HTTPRequest: request, + Body: body, + } + handlerConfig := suite.HandlerConfig() + handler := CreateCustomerWithOktaOptionHandler{ + handlerConfig, + } + response := handler.Handle(params) + suite.Assertions.IsType(&customerops.CreateCustomerWithOktaOptionUnprocessableEntity{}, response) + failedToCreateCustomerPayload := response.(*customerops.CreateCustomerWithOktaOptionUnprocessableEntity).Payload.ClientError.Detail + suite.Equal("ID: 00000000-0000-0000-0000-000000000000 is in a conflicting state Service members from the Coast Guard require an EMPLID for creation.", *failedToCreateCustomerPayload) + }) } func (suite *HandlerSuite) TestSearchCustomersHandler() { diff --git a/src/pages/Office/CustomerOnboarding/CreateCustomerForm.jsx b/src/pages/Office/CustomerOnboarding/CreateCustomerForm.jsx index 72f27a84ca5..69354c506e6 100644 --- a/src/pages/Office/CustomerOnboarding/CreateCustomerForm.jsx +++ b/src/pages/Office/CustomerOnboarding/CreateCustomerForm.jsx @@ -151,8 +151,26 @@ export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { const validationSchema = Yup.object().shape({ affiliation: Yup.mixed().oneOf(Object.keys(SERVICE_MEMBER_AGENCY_LABELS)).required('Required'), - edipi: Yup.string().matches(/^(SM[0-9]{8}|[0-9]{10})$/, 'Enter a 10-digit DOD ID number'), - emplid: Yup.string().matches(/^(SM[0-9]{5}|[0-9]{7})$/, 'Enter a 7-digit EMPLID number'), + // All branches require an EDIPI unless it is a safety move + // where a fake DoD ID may be used + edipi: + !isSafetyMove && + Yup.string() + .matches(/^(SM[0-9]{8}|[0-9]{10})$/, 'Enter a 10-digit DoD ID number') + .required('Required'), + // Only the coast guard requires both EDIPI and EMPLID + // unless it is a safety move + emplid: + !isSafetyMove && + showEmplid && + Yup.string().when('affiliation', { + is: (affiliationValue) => affiliationValue === departmentIndicators.COAST_GUARD, + then: () => + Yup.string() + .matches(/^(SM[0-9]{5}|[0-9]{7})$/, 'Enter a 7-digit EMPLID number') + .required(`EMPLID is required for the Coast Guard`), + otherwise: Yup.string().notRequired(), + }), first_name: Yup.string().required('Required'), middle_name: Yup.string(), last_name: Yup.string().required('Required'), diff --git a/src/pages/Office/CustomerOnboarding/CreateCustomerForm.test.jsx b/src/pages/Office/CustomerOnboarding/CreateCustomerForm.test.jsx index 088b20f415e..841f7eb7c5e 100644 --- a/src/pages/Office/CustomerOnboarding/CreateCustomerForm.test.jsx +++ b/src/pages/Office/CustomerOnboarding/CreateCustomerForm.test.jsx @@ -236,6 +236,7 @@ describe('CreateCustomerForm', () => { await user.type(getByLabelText('Best contact phone'), fakePayload.telephone); await user.type(getByLabelText('Personal email'), fakePayload.personal_email); + await userEvent.type(getByTestId('edipiInput'), fakePayload.edipi); await user.type(getByTestId('res-add-street1'), fakePayload.residential_address.streetAddress1); await user.type(getByTestId('res-add-city'), fakePayload.residential_address.city); @@ -309,6 +310,8 @@ describe('CreateCustomerForm', () => { await user.type(getByLabelText('Best contact phone'), fakePayload.telephone); await user.type(getByLabelText('Personal email'), fakePayload.personal_email); + await userEvent.type(getByTestId('edipiInput'), fakePayload.edipi); + await userEvent.type(getByTestId('res-add-street1'), fakePayload.residential_address.streetAddress1); await userEvent.type(getByTestId('res-add-city'), fakePayload.residential_address.city); await userEvent.selectOptions(getByTestId('res-add-state'), [fakePayload.residential_address.state]); @@ -342,6 +345,57 @@ describe('CreateCustomerForm', () => { }); }, 10000); + it('validates emplid against a coast guard member', async () => { + createCustomerWithOktaOption.mockImplementation(() => Promise.resolve(fakeResponse)); + + const { getByLabelText, getByTestId, getByRole } = render( + + + , + ); + + const user = userEvent.setup(); + + const saveBtn = await screen.findByRole('button', { name: 'Save' }); + expect(saveBtn).toBeInTheDocument(); + + await user.selectOptions(getByLabelText('Branch of service'), 'COAST_GUARD'); + + await user.type(getByLabelText('First name'), fakePayload.first_name); + await user.type(getByLabelText('Last name'), fakePayload.last_name); + + await user.type(getByLabelText('Best contact phone'), fakePayload.telephone); + await user.type(getByLabelText('Personal email'), fakePayload.personal_email); + + await userEvent.type(getByTestId('edipiInput'), fakePayload.edipi); + + await userEvent.type(getByTestId('res-add-street1'), fakePayload.residential_address.streetAddress1); + await userEvent.type(getByTestId('res-add-city'), fakePayload.residential_address.city); + await userEvent.selectOptions(getByTestId('res-add-state'), [fakePayload.residential_address.state]); + await userEvent.type(getByTestId('res-add-zip'), fakePayload.residential_address.postalCode); + + await userEvent.type(getByTestId('backup-add-street1'), fakePayload.backup_mailing_address.streetAddress1); + await userEvent.type(getByTestId('backup-add-city'), fakePayload.backup_mailing_address.city); + await userEvent.selectOptions(getByTestId('backup-add-state'), [fakePayload.backup_mailing_address.state]); + await userEvent.type(getByTestId('backup-add-zip'), fakePayload.backup_mailing_address.postalCode); + + await userEvent.type(getByLabelText('Name'), fakePayload.backup_contact.name); + await userEvent.type(getByRole('textbox', { name: 'Email' }), fakePayload.backup_contact.email); + await userEvent.type(getByRole('textbox', { name: 'Phone' }), fakePayload.backup_contact.telephone); + + await userEvent.type(getByTestId('create-okta-account-yes'), fakePayload.create_okta_account); + + await userEvent.type(getByTestId('cac-user-no'), fakePayload.cac_user); + + await waitFor(() => { + expect(saveBtn).toBeDisabled(); // EMPLID not set yet + }); + await userEvent.type(getByTestId('emplidInput'), '1234567'); + await waitFor(() => { + expect(saveBtn).toBeEnabled(); // EMPLID is set now, all validations true + }); + }, 10000); + it('allows safety privileged users to pass safety move status to orders screen', async () => { createCustomerWithOktaOption.mockImplementation(() => Promise.resolve(fakeResponse)); isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); @@ -475,6 +529,7 @@ describe('CreateCustomerForm', () => { expect(saveBtn).toBeInTheDocument(); await user.selectOptions(getByLabelText('Branch of service'), [fakePayload.affiliation]); + await userEvent.type(getByTestId('edipiInput'), fakePayload.edipi); await user.type(getByLabelText('First name'), fakePayload.first_name); await user.type(getByLabelText('Last name'), fakePayload.last_name); diff --git a/swagger-def/ghc.yaml b/swagger-def/ghc.yaml index 4b60dc576c2..d5874081453 100644 --- a/swagger-def/ghc.yaml +++ b/swagger-def/ghc.yaml @@ -4546,8 +4546,9 @@ definitions: $ref: 'definitions/Affiliation.yaml' edipi: type: string - example: John - x-nullable: true + example: '1234567890' + maxLength: 10 + x-nullable: false emplid: type: string example: '9485155' diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index ce67ccb909d..ba89fd29162 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -4738,8 +4738,9 @@ definitions: $ref: '#/definitions/Affiliation' edipi: type: string - example: John - x-nullable: true + example: '1234567890' + maxLength: 10 + x-nullable: false emplid: type: string example: '9485155'