diff --git a/Dockerfile.e2e b/Dockerfile.e2e index d161f38e33d..c88d243eb48 100644 --- a/Dockerfile.e2e +++ b/Dockerfile.e2e @@ -1,4 +1,4 @@ -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox diff --git a/Dockerfile.migrations b/Dockerfile.migrations index c3852e777b4..b4187431cd2 100644 --- a/Dockerfile.migrations +++ b/Dockerfile.migrations @@ -1,4 +1,4 @@ -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox diff --git a/Dockerfile.migrations_local b/Dockerfile.migrations_local index 3b9ea3c75a5..12aaeabe13b 100644 --- a/Dockerfile.migrations_local +++ b/Dockerfile.migrations_local @@ -18,7 +18,7 @@ RUN rm -f bin/milmove && make bin/milmove # FINAL # ######### -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox --force-missing-repositories diff --git a/Dockerfile.reviewapp b/Dockerfile.reviewapp index 94620459b8a..852803975cd 100644 --- a/Dockerfile.reviewapp +++ b/Dockerfile.reviewapp @@ -45,7 +45,7 @@ RUN set -x \ && make bin/generate-test-data # define migrations before client build since it doesn't need client -FROM alpine:3.20.2 as migrate +FROM alpine:3.20.3 as migrate COPY --from=server_builder /build/bin/rds-ca-2019-root.pem /bin/rds-ca-2019-root.pem COPY --from=server_builder /build/bin/milmove /bin/milmove diff --git a/Dockerfile.tools b/Dockerfile.tools index c3fd78134b6..97fe065b9d3 100644 --- a/Dockerfile.tools +++ b/Dockerfile.tools @@ -1,4 +1,4 @@ -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox diff --git a/Dockerfile.tools_local b/Dockerfile.tools_local index 57edc1744da..8b724400d9e 100644 --- a/Dockerfile.tools_local +++ b/Dockerfile.tools_local @@ -18,7 +18,7 @@ RUN rm -f bin/prime-api-client && make bin/prime-api-client # FINAL # ######### -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox diff --git a/pkg/factory/boat_shipment_factory.go b/pkg/factory/boat_shipment_factory.go index dacbd3f26a8..bf6fae5a275 100644 --- a/pkg/factory/boat_shipment_factory.go +++ b/pkg/factory/boat_shipment_factory.go @@ -14,7 +14,7 @@ type boatBuildType byte const ( boatBuildStandard boatBuildType = iota boatBuildStandardTowAway - boatBuildStandardHualAway + boatBuildStandardHaulAway ) // buildBoatShipmentWithBuildType does the actual work @@ -56,7 +56,7 @@ func buildBoatShipmentWithBuildType(db *pop.Connection, customs []Customization, shipment.ShipmentType = models.MTOShipmentTypeBoatTowAway } - if buildType == boatBuildStandardHualAway { + if buildType == boatBuildStandardHaulAway { shipment.ShipmentType = models.MTOShipmentTypeBoatHaulAway } @@ -80,7 +80,7 @@ func buildBoatShipmentWithBuildType(db *pop.Connection, customs []Customization, boatShipment.IsRoadworthy = models.BoolPointer(true) } - if buildType == boatBuildStandardHualAway { + if buildType == boatBuildStandardHaulAway { boatShipment.Type = models.BoatShipmentTypeHaulAway boatShipment.HasTrailer = models.BoolPointer(false) boatShipment.IsRoadworthy = models.BoolPointer(false) @@ -106,7 +106,7 @@ func BuildBoatShipmentTowAway(db *pop.Connection, customs []Customization, trait return buildBoatShipmentWithBuildType(db, customs, traits, boatBuildStandardTowAway) } func BuildBoatShipmentHaulAway(db *pop.Connection, customs []Customization, traits []Trait) models.BoatShipment { - return buildBoatShipmentWithBuildType(db, customs, traits, boatBuildStandardHualAway) + return buildBoatShipmentWithBuildType(db, customs, traits, boatBuildStandardHaulAway) } // ------------------------ diff --git a/pkg/services/move/move_router_test.go b/pkg/services/move/move_router_test.go index cd6b9f6623e..945a65d6033 100644 --- a/pkg/services/move/move_router_test.go +++ b/pkg/services/move/move_router_test.go @@ -296,6 +296,32 @@ func (suite *MoveServiceSuite) TestMoveSubmission() { }, }, nil) + boatMTOSHipment := factory.BuildMTOShipmentMinimal(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusDraft, + ShipmentType: models.MTOShipmentTypeBoatHaulAway, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + mobileHomeMTOShipment := factory.BuildMTOShipmentMinimal(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusDraft, + ShipmentType: models.MTOShipmentTypeMobileHome, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + ppmShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ { Model: models.PPMShipment{ @@ -303,8 +329,23 @@ func (suite *MoveServiceSuite) TestMoveSubmission() { }, }, }, nil) - move.MTOShipments = models.MTOShipments{shipment} + + boatShipment := factory.BuildBoatShipmentHaulAway(suite.DB(), []factory.Customization{ + { + Model: models.BoatShipment{}, + }, + }, nil) + + mobileHomeShipment := factory.BuildMobileHomeShipment(suite.DB(), []factory.Customization{ + { + Model: models.MobileHome{}, + }, + }, nil) + + move.MTOShipments = models.MTOShipments{shipment, boatMTOSHipment, mobileHomeMTOShipment} move.MTOShipments[0].PPMShipment = &ppmShipment + move.MTOShipments[0].BoatShipment = &boatShipment + move.MTOShipments[0].MobileHome = &mobileHomeShipment newSignedCertification := factory.BuildSignedCertification(nil, []factory.Customization{ { @@ -397,6 +438,7 @@ func (suite *MoveServiceSuite) TestMoveSubmission() { suite.Equal(models.MTOShipmentStatusSubmitted, move.MTOShipments[0].Status, "expected Submitted") suite.Equal(models.PPMShipmentStatusSubmitted, move.MTOShipments[0].PPMShipment.Status, "expected Submitted") }) + } func (suite *MoveServiceSuite) TestMoveCancellation() { @@ -745,6 +787,28 @@ func (suite *MoveServiceSuite) TestCompleteServiceCounseling() { suite.IsType(apperror.ConflictError{}, err) suite.Contains(err.Error(), "NTS-release shipment must include facility info") }) + + suite.Run("Boat Shipment - status changed to 'SERVICE_COUNSELING_COMPLETED'", func() { + move := factory.BuildStubbedMoveWithStatus(models.MoveStatusNeedsServiceCounseling) + boatShipment := factory.BuildBoatShipment(nil, nil, nil) + move.MTOShipments = models.MTOShipments{boatShipment.Shipment} + + err := moveRouter.CompleteServiceCounseling(suite.AppContextForTest(), &move) + + suite.NoError(err) + suite.Equal(models.MoveStatusServiceCounselingCompleted, move.Status) + }) + + suite.Run("Mobile Home Shipment - status changed to 'SERVICE_COUNSELING_COMPLETED'", func() { + move := factory.BuildStubbedMoveWithStatus(models.MoveStatusNeedsServiceCounseling) + mobileHomeShipment := factory.BuildMobileHomeShipment(nil, nil, nil) + move.MTOShipments = models.MTOShipments{mobileHomeShipment.Shipment} + + err := moveRouter.CompleteServiceCounseling(suite.AppContextForTest(), &move) + + suite.NoError(err) + suite.Equal(models.MoveStatusServiceCounselingCompleted, move.Status) + }) } func (suite *MoveServiceSuite) createServiceItem() (models.MTOServiceItem, models.Move) { diff --git a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go index bc861588e2b..d388dc3700d 100644 --- a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go +++ b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go @@ -227,12 +227,13 @@ func (s SSWPPMComputer) FormatValuesShipmentSummaryWorksheetFormPage1(data model page1.WeightAllotmentProgearSpouse = FormatWeights(data.WeightAllotment.SpouseProGear) page1.TotalWeightAllotment = FormatWeights(data.WeightAllotment.TotalWeight) + formattedSIT := WorkSheetSIT{} + formattedShipment := s.FormatShipment(data.PPMShipment, data.WeightAllotment, isPaymentPacket) page1.ShipmentNumberAndTypes = formattedShipment.ShipmentNumberAndTypes page1.ShipmentPickUpDates = formattedShipment.PickUpDates page1.ShipmentCurrentShipmentStatuses = formattedShipment.CurrentShipmentStatuses - formattedSIT := WorkSheetSIT{} // Shipment weights for Payment Packet are actual, for AOA Packet are estimated. if isPaymentPacket { formattedSIT = FormatAllSITSForPaymentPacket(data.MovingExpenses) @@ -1008,12 +1009,13 @@ func (SSWPPMComputer *SSWPPMComputer) FetchDataShipmentSummaryWorksheetFormData( return nil, err } - // Fetches all signed certifications for a move to be filtered in this file by ppmid and type - signedCertifications, err := models.FetchSignedCertifications(appCtx.DB(), session, ppmShipment.Shipment.MoveTaskOrderID) + serviceMember.Edipi, err = formatEmplid(serviceMember) if err != nil { return nil, err } - serviceMember.Edipi, err = formatEmplid(serviceMember) + + // Fetches all signed certifications for a move to be filtered in this file by ppmid and type + signedCertifications, err := models.FetchSignedCertifications(appCtx.DB(), session, ppmShipment.Shipment.MoveTaskOrderID) if err != nil { return nil, err } diff --git a/pkg/testdatagen/scenario/shared.go b/pkg/testdatagen/scenario/shared.go index 6e8a2fcc5ef..f545cfd4370 100644 --- a/pkg/testdatagen/scenario/shared.go +++ b/pkg/testdatagen/scenario/shared.go @@ -1,6 +1,7 @@ package scenario import ( + "errors" "fmt" "log" "sort" @@ -10175,6 +10176,167 @@ func createMoveWithUniqueDestinationAddress(appCtx appcontext.AppContext) { }, nil) } +/* +Generic helper function that lets you create a move with any staus and with any shipment type +*/ +func CreateMoveWithMTOShipment(appCtx appcontext.AppContext, ordersType internalmessages.OrdersType, shipmentType models.MTOShipmentType, destinationType *models.DestinationType, locator string, moveStatus models.MoveStatus) models.Move { + if shipmentType == models.MTOShipmentTypeBoatHaulAway || shipmentType == models.MTOShipmentTypeBoatTowAway { // Add boat specific fields in relevant PR + log.Panic(fmt.Errorf("Unable to generate random integer for submitted move date"), zap.Error(errors.New("Not yet implemented"))) + } + + db := appCtx.DB() + submittedAt := time.Now() + hhgPermitted := internalmessages.OrdersTypeDetailHHGPERMITTED + ordersNumber := "8675309" + departmentIndicator := "ARMY" + tac := "E19A" + newDutyLocation := factory.FetchOrBuildCurrentDutyLocation(db) + newDutyLocation.Address.PostalCode = "52549" + orders := factory.BuildOrderWithoutDefaults(db, []factory.Customization{ + { + Model: models.DutyLocation{ + ProvidesServicesCounseling: true, + }, + Type: &factory.DutyLocations.OriginDutyLocation, + }, + { + Model: newDutyLocation, + LinkOnly: true, + Type: &factory.DutyLocations.NewDutyLocation, + }, + { + Model: models.Order{ + OrdersType: ordersType, + OrdersTypeDetail: &hhgPermitted, + OrdersNumber: &ordersNumber, + DepartmentIndicator: &departmentIndicator, + TAC: &tac, + }, + }, + }, nil) + move := factory.BuildMove(db, []factory.Customization{ + { + Model: orders, + LinkOnly: true, + }, + { + Model: models.Move{ + Locator: locator, + Status: moveStatus, + SubmittedAt: &submittedAt, + }, + }, + }, nil) + requestedPickupDate := submittedAt.Add(60 * 24 * time.Hour) + requestedDeliveryDate := requestedPickupDate.Add(7 * 24 * time.Hour) + destinationAddress := factory.BuildAddress(db, nil, nil) + + if destinationType != nil { // Destination type is only used for retirement moves + retirementMTOShipment := factory.BuildMTOShipment(db, []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + ShipmentType: shipmentType, + Status: models.MTOShipmentStatusSubmitted, + RequestedPickupDate: &requestedPickupDate, + RequestedDeliveryDate: &requestedDeliveryDate, + DestinationType: destinationType, + }, + }, + { + Model: destinationAddress, + LinkOnly: true, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + if shipmentType == models.MTOShipmentTypeMobileHome { + factory.BuildMobileHomeShipment(appCtx.DB(), []factory.Customization{ + { + Model: models.MobileHome{ + Year: models.IntPointer(2000), + Make: models.StringPointer("Boat Make"), + Model: models.StringPointer("Boat Model"), + LengthInInches: models.IntPointer(300), + WidthInInches: models.IntPointer(108), + HeightInInches: models.IntPointer(72), + }, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: retirementMTOShipment, + LinkOnly: true, + }, + }, nil) + } + } + + requestedPickupDate = submittedAt.Add(30 * 24 * time.Hour) + requestedDeliveryDate = requestedPickupDate.Add(7 * 24 * time.Hour) + regularMTOShipment := factory.BuildMTOShipment(db, []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + ShipmentType: shipmentType, + Status: models.MTOShipmentStatusSubmitted, + RequestedPickupDate: &requestedPickupDate, + RequestedDeliveryDate: &requestedDeliveryDate, + }, + }, + }, nil) + + if shipmentType == models.MTOShipmentTypeMobileHome { + factory.BuildMobileHomeShipment(appCtx.DB(), []factory.Customization{ + { + Model: models.MobileHome{ + Year: models.IntPointer(2000), + Make: models.StringPointer("Boat Make"), + Model: models.StringPointer("Boat Model"), + LengthInInches: models.IntPointer(300), + WidthInInches: models.IntPointer(108), + HeightInInches: models.IntPointer(72), + }, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: regularMTOShipment, + LinkOnly: true, + }, + }, nil) + } + + officeUser := factory.BuildOfficeUserWithRoles(db, nil, []roles.RoleType{roles.RoleTypeTOO}) + factory.BuildCustomerSupportRemark(db, []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: officeUser, + LinkOnly: true, + }, + { + Model: models.CustomerSupportRemark{ + Content: "The customer mentioned that they need to provide some more complex instructions for pickup and drop off.", + }, + }, + }, nil) + + return move +} + /* Create Needs Service Counseling - pass in orders with all required information, shipment type, destination type, locator */ @@ -10225,7 +10387,7 @@ func CreateNeedsServicesCounseling(appCtx appcontext.AppContext, ordersType inte requestedPickupDate := submittedAt.Add(60 * 24 * time.Hour) requestedDeliveryDate := requestedPickupDate.Add(7 * 24 * time.Hour) destinationAddress := factory.BuildAddress(db, nil, nil) - factory.BuildMTOShipment(db, []factory.Customization{ + retirementMTOShipment := factory.BuildMTOShipment(db, []factory.Customization{ { Model: move, LinkOnly: true, @@ -10246,9 +10408,32 @@ func CreateNeedsServicesCounseling(appCtx appcontext.AppContext, ordersType inte }, }, nil) + if shipmentType == models.MTOShipmentTypeMobileHome { + factory.BuildMobileHomeShipment(appCtx.DB(), []factory.Customization{ + { + Model: models.MobileHome{ + Year: models.IntPointer(2000), + Make: models.StringPointer("Boat Make"), + Model: models.StringPointer("Boat Model"), + LengthInInches: models.IntPointer(300), + WidthInInches: models.IntPointer(108), + HeightInInches: models.IntPointer(72), + }, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: retirementMTOShipment, + LinkOnly: true, + }, + }, nil) + } + requestedPickupDate = submittedAt.Add(30 * 24 * time.Hour) requestedDeliveryDate = requestedPickupDate.Add(7 * 24 * time.Hour) - factory.BuildMTOShipment(db, []factory.Customization{ + regularMTOShipment := factory.BuildMTOShipment(db, []factory.Customization{ { Model: move, LinkOnly: true, @@ -10262,6 +10447,30 @@ func CreateNeedsServicesCounseling(appCtx appcontext.AppContext, ordersType inte }, }, }, nil) + + if shipmentType == models.MTOShipmentTypeMobileHome { + factory.BuildMobileHomeShipment(appCtx.DB(), []factory.Customization{ + { + Model: models.MobileHome{ + Year: models.IntPointer(2000), + Make: models.StringPointer("Boat Make"), + Model: models.StringPointer("Boat Model"), + LengthInInches: models.IntPointer(300), + WidthInInches: models.IntPointer(108), + HeightInInches: models.IntPointer(72), + }, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: regularMTOShipment, + LinkOnly: true, + }, + }, nil) + } + officeUser := factory.BuildOfficeUserWithRoles(db, nil, []roles.RoleType{roles.RoleTypeTOO}) factory.BuildCustomerSupportRemark(db, []factory.Customization{ { diff --git a/pkg/testdatagen/testharness/dispatch.go b/pkg/testdatagen/testharness/dispatch.go index c4638e5dc1b..746923daaa2 100644 --- a/pkg/testdatagen/testharness/dispatch.go +++ b/pkg/testdatagen/testharness/dispatch.go @@ -35,6 +35,9 @@ var actionDispatcher = map[string]actionFunc{ "HHGMoveWithNTSAndNeedsSC": func(appCtx appcontext.AppContext) testHarnessResponse { return MakeHHGMoveWithNTSAndNeedsSC(appCtx) }, + "MobileHomeMoveNeedsSC": func(appCtx appcontext.AppContext) testHarnessResponse { + return MakeMobileHomeMoveNeedsSC(appCtx) + }, "GoodTACAndLoaCombination": func(appCtx appcontext.AppContext) testHarnessResponse { return MakeGoodTACAndLoaCombination(appCtx) }, diff --git a/pkg/testdatagen/testharness/make_move.go b/pkg/testdatagen/testharness/make_move.go index f9a51021485..2f704378f6b 100644 --- a/pkg/testdatagen/testharness/make_move.go +++ b/pkg/testdatagen/testharness/make_move.go @@ -3850,6 +3850,49 @@ func MakeBoatHaulAwayMoveNeedsTOOApproval(appCtx appcontext.AppContext) models.M return *newmove } +// MakeHHGMoveNeedsSC creates an fully ready move needing SC approval +func MakeMobileHomeMoveNeedsSC(appCtx appcontext.AppContext) models.Move { + locator := models.GenerateLocator() + move := scenario.CreateMoveWithMTOShipment(appCtx, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION, models.MTOShipmentTypeMobileHome, nil, locator, models.MoveStatusNeedsServiceCounseling) + + // re-fetch the move so that we ensure we have exactly what is in + // the db + newmove, err := models.FetchMove(appCtx.DB(), &auth.Session{}, move.ID) + if err != nil { + log.Panic(fmt.Errorf("failed to fetch move: %w", err)) + } + return *newmove +} + +func MakeMobileHomeMoveForTOO(appCtx appcontext.AppContext) models.Move { + hhg := models.MTOShipmentTypeHHG + hor := models.DestinationTypeHomeOfRecord + originDutyLocation := factory.FetchOrBuildCurrentDutyLocation(appCtx.DB()) + move := scenario.CreateMoveWithOptions(appCtx, testdatagen.Assertions{ + Order: models.Order{ + OriginDutyLocation: &originDutyLocation, + }, + MTOShipment: models.MTOShipment{ + ShipmentType: hhg, + DestinationType: &hor, + }, + Move: models.Move{ + Status: models.MoveStatusSUBMITTED, + }, + DutyLocation: models.DutyLocation{ + ProvidesServicesCounseling: false, + }, + }) + + // re-fetch the move so that we ensure we have exactly what is in + // the db + newmove, err := models.FetchMove(appCtx.DB(), &auth.Session{}, move.ID) + if err != nil { + log.Panic(fmt.Errorf("failed to fetch move: %w", err)) + } + return *newmove +} + // MakeHHGMoveNeedsServicesCounselingUSMC creates an fully ready move as USMC needing SC approval func MakeHHGMoveNeedsServicesCounselingUSMC(appCtx appcontext.AppContext) models.Move { userUploader := newUserUploader(appCtx) diff --git a/playwright/tests/office/servicescounseling/servicesCounselingMobileHome.spec.js b/playwright/tests/office/servicescounseling/servicesCounselingMobileHome.spec.js index 0588fe6cd0d..be161ca0594 100644 --- a/playwright/tests/office/servicescounseling/servicesCounselingMobileHome.spec.js +++ b/playwright/tests/office/servicescounseling/servicesCounselingMobileHome.spec.js @@ -1,16 +1,90 @@ // @ts-check import { test, expect } from './servicesCounselingTestFixture'; +const today = new Date(); +const pickupDate = today; +const pickupDateString = pickupDate.toLocaleDateString('en-US'); +const deliveryDate = new Date(new Date().setDate(today.getDate() + 14)); +const deliveryDateString = deliveryDate.toLocaleDateString('en-US'); + +const pickupAddress = { + Address1: '7 Q St', + City: 'Atco', + State: 'NJ', + ZIP: '08004', +}; + +const secondaryPickupAddress = { + ...pickupAddress, +}; +secondaryPickupAddress.Address1 = '8 Q St'; + +const addressToString = (address) => { + return `${address.Address1}, ${address.Address2 ? `${address.Address2}, ` : ''}${ + address.Address3 ? `${address.Address3}, ` : '' + }${address.City}, ${address.State} ${address.ZIP}`; +}; + +const deliveryAddress = { + Address1: '9 W 2nd Ave', + Address2: 'P.O. Box 456', + City: 'Hollywood', + State: 'MD', + ZIP: '20636', +}; + +const secondaryDeliveryAddress = { + Address1: '9 Q St', + City: 'Atco', + State: 'NJ', + ZIP: '08004', +}; + +const releasingAgent = { + firstName: 'Grace', + lastName: 'Griffin', + phone: '2025551234', + email: 'grace.griffin@example.com', +}; + +const receivingAgent = { + firstName: 'Leo', + lastName: 'Spacemen', + phone: '2025552345', + email: 'leo.spaceman@example.com', +}; + +const formatPhone = (phone) => { + return `${phone.slice(0, 3)}-${phone.slice(3, 6)}-${phone.slice(6)}`; +}; + +const agentToString = (agent) => { + return `${agent.firstName} ${agent.lastName}${formatPhone(agent.phone)}${agent.email}`; +}; + +const formatDate = (date) => { + const formattedDay = date.toLocaleDateString(undefined, { day: '2-digit' }); + const formattedMonth = date.toLocaleDateString(undefined, { + month: 'short', + }); + const formattedYear = date.toLocaleDateString(undefined, { + year: 'numeric', + }); + + return `${formattedDay} ${formattedMonth} ${formattedYear}`; +}; + test.describe('Services counselor user', () => { test.beforeEach(async ({ scPage }) => { - const move = await scPage.testHarness.buildHHGMoveWithNTSAndNeedsSC(); + const move = await scPage.testHarness.buildMobileHomeMoveNeedsSC(); await scPage.navigateToMove(move.locator); }); test('Services Counselor can create a mobile home shipment and view shipment card info', async ({ page, scPage }) => { - const deliveryDate = new Date().toLocaleDateString('en-US'); await page.getByTestId('dropdown').selectOption({ label: 'Mobile Home' }); + await expect(page.getByText('Mobile Home Information')).toBeVisible(); + await expect(page.getByRole('heading', { level: 1 })).toHaveText('Add shipment details'); await expect(page.getByTestId('tag')).toHaveText('Mobile Home'); @@ -18,30 +92,165 @@ test.describe('Services counselor user', () => { await page.getByLabel('Make').fill('make'); await page.getByLabel('Model').fill('model'); await page.getByTestId('lengthFeet').fill('22'); + await page.getByTestId('lengthInches').fill('0'); await page.getByTestId('widthFeet').fill('22'); + await page.getByTestId('widthInches').fill('0'); await page.getByTestId('heightFeet').fill('22'); + await page.getByTestId('heightInches').fill('0'); - await page.locator('#requestedPickupDate').fill(deliveryDate); + await page.locator('#requestedPickupDate').fill(pickupDateString); await page.locator('#requestedPickupDate').blur(); await page.getByText('Use current address').click(); - await page.locator('#requestedDeliveryDate').fill('16 Mar 2022'); - await page.locator('#requestedDeliveryDate').blur(); await page.getByLabel('Counselor remarks').fill('Sample counselor remarks'); + await page.locator('#requestedDeliveryDate').fill(deliveryDateString); + await page.locator('#requestedDeliveryDate').blur(); + // Save the shipment await page.getByRole('button', { name: 'Save' }).click(); await scPage.waitForPage.moveDetails(); await expect(page.getByTestId('ShipmentContainer')).toHaveCount(2); - await expect(page.getByText('Mobile home year')).toBeVisible(); - await expect(page.getByTestId('year')).toHaveText('2022'); - await expect(page.getByText('Mobile home make')).toBeVisible(); - await expect(page.getByTestId('make')).toHaveText('make'); - await expect(page.getByText('Mobile home model')).toBeVisible(); - await expect(page.getByTestId('model')).toHaveText('model'); - await expect(page.getByText('Dimensions')).toBeVisible(); - await expect(page.getByTestId('dimensions')).toHaveText("22' L x 22' W x 22' H"); + await expect(page.getByText('Mobile home year').last()).toBeVisible(); + await expect(page.getByTestId('year').last()).toHaveText('2022'); + await expect(page.getByText('Mobile home make').last()).toBeVisible(); + await expect(page.getByTestId('make').last()).toHaveText('make'); + await expect(page.getByText('Mobile home model').last()).toBeVisible(); + await expect(page.getByTestId('model').last()).toHaveText('model'); + await expect(page.getByText('Dimensions').last()).toBeVisible(); + await expect(page.getByTestId('dimensions').last()).toHaveText("22' L x 22' W x 22' H"); + }); + + test('Services Counselor can delete an existing Mobile Home shipment', async ({ page, scPage }) => { + await expect(page.getByText('Edit Shipment')).toHaveCount(1); + // Choose a shipment and store it's shipment ID + const editShipmentButton = await page.getByRole('button', { name: 'Edit Shipment' }); + process.stdout.write(await editShipmentButton.evaluate((el) => el.outerHTML)); + + await editShipmentButton.click(); + await scPage.waitForLoading(); + await scPage.waitForPage.editMobileHomeShipment(); + + // Delete that shipment + await page.getByRole('button', { name: 'Delete shipment' }).click(); + await expect(page.getByTestId('modalCloseButton')).toBeVisible(); + await page.getByTestId('modal').getByRole('button', { name: 'Delete shipment' }).click(); + await scPage.waitForPage.moveDetails(); + + // Verify that the shipment has been deleted + await expect(page.getByTestId('ShipmentContainer')).toHaveCount(0); + }); + + test('Services Counselor can edit an existing Mobile Home shipment', async ({ page, scPage }) => { + await expect(page.getByText('Edit Shipment')).toHaveCount(1); + + // Choose a shipment, store it's container, and click the edit button + const shipmentContainer = await page.getByTestId('ShipmentContainer'); + await shipmentContainer.getByRole('button').click(); + await scPage.waitForLoading(); + await scPage.waitForPage.editMobileHomeShipment(); + + // Fill in all of the form fields with new data + await page.getByLabel('Year').fill('2024'); + await page.getByLabel('Make').fill('Test Make'); + await page.getByLabel('Model').fill('Test Model'); + + await page.getByTestId('lengthFeet').fill('20'); + await page.getByTestId('lengthInches').fill('6'); + + await page.getByTestId('widthFeet').fill('15'); + await page.getByTestId('widthInches').fill('1'); + + await page.getByTestId('heightFeet').fill('10'); + await page.getByTestId('heightInches').fill('0'); + + await page.locator('#requestedPickupDate').fill(pickupDateString); + await page.locator('#requestedPickupDate').blur(); + await page.locator('#requestedDeliveryDate').fill(deliveryDateString); + await page.locator('#requestedDeliveryDate').blur(); + + // Update form (adding pickup and delivery address) + const pickupAddressGroup = await page.getByRole('group', { name: 'Pickup location' }); + await pickupAddressGroup.getByText('Yes').click(); + await pickupAddressGroup.getByLabel('Address 1').nth(0).fill(pickupAddress.Address1); + await pickupAddressGroup.getByLabel('Address 2').nth(0).clear(); + await pickupAddressGroup.getByLabel('Address 3').nth(0).clear(); + await pickupAddressGroup.getByLabel('City').nth(0).fill(pickupAddress.City); + await pickupAddressGroup.getByLabel('State').nth(0).selectOption({ label: pickupAddress.State }); + await pickupAddressGroup.getByLabel('ZIP').nth(0).fill(pickupAddress.ZIP); + + // Secondary pickup address + await pickupAddressGroup.getByText('Yes').click(); + await pickupAddressGroup.getByLabel('Address 1').nth(1).fill(secondaryPickupAddress.Address1); + await pickupAddressGroup.getByLabel('Address 2').nth(1).clear(); + await pickupAddressGroup.getByLabel('Address 3').nth(1).clear(); + await pickupAddressGroup.getByLabel('City').nth(1).fill(secondaryPickupAddress.City); + await pickupAddressGroup.getByLabel('State').nth(1).selectOption({ label: secondaryPickupAddress.State }); + await pickupAddressGroup.getByLabel('ZIP').nth(1).fill(secondaryPickupAddress.ZIP); + + // Releasing agent + await page.locator(`[name='pickup.agent.firstName']`).fill(releasingAgent.firstName); + await page.locator(`[name='pickup.agent.lastName']`).fill(releasingAgent.lastName); + await page.locator(`[name='pickup.agent.phone']`).fill(releasingAgent.phone); + await page.locator(`[name='pickup.agent.email']`).fill(releasingAgent.email); + + const deliveryAddressGroup = await page.getByRole('group', { name: 'Delivery location' }); + await deliveryAddressGroup.getByText('Yes').nth(0).click(); + await deliveryAddressGroup.getByLabel('Address 1').nth(0).fill(deliveryAddress.Address1); + await deliveryAddressGroup.getByLabel('Address 2').nth(0).fill(deliveryAddress.Address2); + await deliveryAddressGroup.getByLabel('Address 3').nth(0).clear(); + await deliveryAddressGroup.getByLabel('City').nth(0).fill(deliveryAddress.City); + await deliveryAddressGroup.getByLabel('State').nth(0).selectOption({ label: deliveryAddress.State }); + await deliveryAddressGroup.getByLabel('ZIP').nth(0).fill(deliveryAddress.ZIP); + + // Secondary delivery address + await deliveryAddressGroup.getByText('Yes').nth(1).click(); + await deliveryAddressGroup.getByLabel('Address 1').nth(1).fill(secondaryDeliveryAddress.Address1); + await deliveryAddressGroup.getByLabel('Address 2').nth(1).clear(); + await deliveryAddressGroup.getByLabel('Address 3').nth(1).clear(); + await deliveryAddressGroup.getByLabel('City').nth(1).fill(secondaryDeliveryAddress.City); + await deliveryAddressGroup.getByLabel('State').nth(1).selectOption({ label: secondaryDeliveryAddress.State }); + await deliveryAddressGroup.getByLabel('ZIP').nth(1).fill(secondaryDeliveryAddress.ZIP); + + // Receiving agent + await page.locator(`[name='delivery.agent.firstName']`).fill(receivingAgent.firstName); + await page.locator(`[name='delivery.agent.lastName']`).fill(receivingAgent.lastName); + await page.locator(`[name='delivery.agent.phone']`).fill(receivingAgent.phone); + await page.locator(`[name='delivery.agent.email']`).fill(receivingAgent.email); + + await page.getByLabel('Counselor remarks').fill('Sample counselor remarks'); + + // Submit edits + await page.getByTestId('submitForm').click(); + await scPage.waitForLoading(); + await expect(page.locator('.usa-alert__text')).toContainText('Your changes were saved.'); + + // Check that the data in the shipment card now matches what we just submitted + await shipmentContainer.locator('[data-prefix="fas"][data-icon="chevron-down"]').click(); + await expect(shipmentContainer.getByTestId('requestedPickupDate')).toHaveText(formatDate(pickupDate)); + await expect(shipmentContainer.getByTestId('pickupAddress')).toHaveText(addressToString(pickupAddress)); + await expect(shipmentContainer.getByTestId('secondaryPickupAddress')).toHaveText( + addressToString(secondaryPickupAddress), + ); + + await expect(shipmentContainer.getByTestId('RELEASING_AGENT')).toHaveText(agentToString(releasingAgent)); + + await expect(shipmentContainer.getByTestId('requestedDeliveryDate')).toHaveText(formatDate(deliveryDate)); + await expect(shipmentContainer.getByTestId('destinationAddress')).toHaveText(addressToString(deliveryAddress)); + await expect(shipmentContainer.getByTestId('secondaryDeliveryAddress')).toHaveText( + addressToString(secondaryDeliveryAddress), + ); + + await expect(shipmentContainer.getByTestId('RECEIVING_AGENT')).toHaveText(agentToString(receivingAgent)); + + await expect(shipmentContainer.getByTestId('year')).toHaveText('2024'); + await expect(shipmentContainer.getByTestId('make')).toHaveText('Test Make'); + await expect(shipmentContainer.getByTestId('model')).toHaveText('Test Model'); + + await expect(shipmentContainer.getByTestId('dimensions')).toHaveText(`20' 6" L x 15' 1" W x 10' H`); + + await expect(shipmentContainer.getByTestId('counselorRemarks')).toHaveText('Sample counselor remarks'); }); }); diff --git a/playwright/tests/utils/office/waitForOfficePage.js b/playwright/tests/utils/office/waitForOfficePage.js index e3bf4b82f80..7a785949cdf 100644 --- a/playwright/tests/utils/office/waitForOfficePage.js +++ b/playwright/tests/utils/office/waitForOfficePage.js @@ -68,6 +68,14 @@ export class WaitForOfficePage extends WaitForPage { await base.expect(this.page.getByTestId('tag')).toHaveText('NTS-release'); } + /** + * @returns {Promise} + */ + async editMobileHomeShipment() { + await base.expect(this.page.getByRole('heading', { level: 1 })).toHaveText('Edit shipment details'); + await base.expect(this.page.getByTestId('tag')).toHaveText('Mobile Home'); + } + /** * @returns {Promise} */ diff --git a/playwright/tests/utils/testharness.js b/playwright/tests/utils/testharness.js index 458941598b6..edd1518ac17 100644 --- a/playwright/tests/utils/testharness.js +++ b/playwright/tests/utils/testharness.js @@ -411,6 +411,14 @@ export class TestHarness { return this.buildDefault('HHGMoveWithNTSAndNeedsSC'); } + /** + * Use testharness to build Mobile move + * @returns {Promise} + */ + async buildMobileHomeMoveNeedsSC() { + return this.buildDefault('MobileHomeMoveNeedsSC'); + } + /** * Use testharness to build a good TAC and LOA combination, return the TAC * so that office users can input the TAC, and preview the LOA (If the diff --git a/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.jsx b/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.jsx index 8854c983d75..b8630805eeb 100644 --- a/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.jsx +++ b/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.jsx @@ -315,7 +315,6 @@ const ShipmentInfoList = ({ {secondaryPickupAddressElement} {isTertiaryAddressEnabled ? tertiaryPickupAddressElement : null} {showElement(agentsElementFlags) && releasingAgentElement} - {showElement(requestedDeliveryDateElementFlags) && requestedDeliveryDateElement} {requestedDeliveryDateElement} {destinationAddressElement} {showElement(destinationTypeFlags) && displayDestinationType && destinationTypeElement} diff --git a/src/constants/userRoles.js b/src/constants/userRoles.js index 487a424aad8..45739f19da0 100644 --- a/src/constants/userRoles.js +++ b/src/constants/userRoles.js @@ -7,9 +7,9 @@ export const roleTypes = { SERVICES_COUNSELOR: 'services_counselor', PRIME_SIMULATOR: 'prime_simulator', QAE: 'qae', - HQ: 'headquarters', CUSTOMER_SERVICE_REPRESENTATIVE: 'customer_service_representative', GSR: 'gsr', + HQ: 'headquarters', }; export const adminOfficeRoles = [ @@ -20,9 +20,9 @@ export const adminOfficeRoles = [ { roleType: 'services_counselor', name: 'Services Counselor' }, { roleType: 'prime_simulator', name: 'Prime Simulator' }, { roleType: 'qae', name: 'Quality Assurance Evaluator' }, - { roleType: 'headquarters', name: 'Headquarters' }, { roleType: 'customer_service_representative', name: 'Customer Service Representative' }, { roleType: 'gsr', name: 'Government Surveillance Representative' }, + { roleType: 'headquarters', name: 'Headquarters' }, ]; export const officeRoles = [ diff --git a/src/pages/Office/MoveDetails/MoveDetails.jsx b/src/pages/Office/MoveDetails/MoveDetails.jsx index e0f06a4d01c..d57d16a8638 100644 --- a/src/pages/Office/MoveDetails/MoveDetails.jsx +++ b/src/pages/Office/MoveDetails/MoveDetails.jsx @@ -29,11 +29,12 @@ import LeftNavTag from 'components/LeftNavTag/LeftNavTag'; import Restricted from 'components/Restricted/Restricted'; import LoadingPlaceholder from 'shared/LoadingPlaceholder'; import SomethingWentWrong from 'shared/SomethingWentWrong'; -import { SHIPMENT_OPTIONS_URL } from 'shared/constants'; +import { SHIPMENT_OPTIONS_URL, FEATURE_FLAG_KEYS } from 'shared/constants'; import { SIT_EXTENSION_STATUS } from 'constants/sitExtensions'; import { ORDERS_TYPE } from 'constants/orders'; import { permissionTypes } from 'constants/permissions'; import { objectIsMissingFieldWithCondition } from 'utils/displayFlags'; +import { isBooleanFlagEnabled } from 'utils/featureFlags'; import formattedCustomerName from 'utils/formattedCustomerName'; import { calculateEstimatedWeight } from 'hooks/custom'; import { ADVANCE_STATUSES } from 'constants/ppms'; @@ -76,6 +77,8 @@ const MoveDetails = ({ const [isFinancialModalVisible, setIsFinancialModalVisible] = useState(false); const [alertMessage, setAlertMessage] = useState(null); const [alertType, setAlertType] = useState('success'); + const [enableBoat, setEnableBoat] = useState(false); + const [enableMobileHome, setEnableMobileHome] = useState(false); const navigate = useNavigate(); @@ -180,6 +183,14 @@ const MoveDetails = ({ navigate(addShipmentPath); }; + useEffect(() => { + const fetchData = async () => { + setEnableBoat(await isBooleanFlagEnabled(FEATURE_FLAG_KEYS.BOAT)); + setEnableMobileHome(await isBooleanFlagEnabled(FEATURE_FLAG_KEYS.MOBILE_HOME)); + }; + fetchData(); + }, []); + useEffect(() => { const shipmentCount = shipmentWithDestinationAddressChangeRequest?.length || 0; if (setShipmentsWithDeliveryAddressUpdateRequestedCount) @@ -333,6 +344,21 @@ const MoveDetails = ({ const hasDestinationAddressUpdate = shipmentWithDestinationAddressChangeRequest && shipmentWithDestinationAddressChangeRequest.length > 0; + const allowedShipmentOptions = () => { + return ( + <> + + + + + {enableBoat && } + {enableMobileHome && } + + ); + }; + return (
@@ -416,14 +442,7 @@ const MoveDetails = ({ - - - - - - + {allowedShipmentOptions()} )} diff --git a/src/pages/Office/RequestAccount/RequestAccount.module.scss b/src/pages/Office/RequestAccount/RequestAccount.module.scss index f0c9aed241b..11e528b5a71 100644 --- a/src/pages/Office/RequestAccount/RequestAccount.module.scss +++ b/src/pages/Office/RequestAccount/RequestAccount.module.scss @@ -4,4 +4,4 @@ .error { width: 100%; -} \ No newline at end of file +} diff --git a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx index 0ea67d88212..65b5780fd3c 100644 --- a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx +++ b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx @@ -23,7 +23,7 @@ import ShipmentDisplay from 'components/Office/ShipmentDisplay/ShipmentDisplay'; import { SubmitMoveConfirmationModal } from 'components/Office/SubmitMoveConfirmationModal/SubmitMoveConfirmationModal'; import { useMoveDetailsQueries, useOrdersDocumentQueries } from 'hooks/queries'; import { updateMoveStatusServiceCounselingCompleted, updateFinancialFlag } from 'services/ghcApi'; -import { MOVE_STATUSES, SHIPMENT_OPTIONS_URL, SHIPMENT_OPTIONS } from 'shared/constants'; +import { MOVE_STATUSES, SHIPMENT_OPTIONS_URL, SHIPMENT_OPTIONS, FEATURE_FLAG_KEYS } from 'shared/constants'; import { ppmShipmentStatuses, shipmentStatuses } from 'constants/shipments'; import shipmentCardsStyles from 'styles/shipmentCards.module.scss'; import LeftNav from 'components/LeftNav/LeftNav'; @@ -40,6 +40,7 @@ import NotificationScrollToTop from 'components/NotificationScrollToTop'; import { objectIsMissingFieldWithCondition } from 'utils/displayFlags'; import { ReviewButton } from 'components/form/IconButtons'; import { calculateWeightRequested } from 'hooks/custom'; +import { isBooleanFlagEnabled } from 'utils/featureFlags'; import { ADVANCE_STATUSES } from 'constants/ppms'; const ServicesCounselingMoveDetails = ({ @@ -60,6 +61,8 @@ const ServicesCounselingMoveDetails = ({ const [moveHasExcessWeight, setMoveHasExcessWeight] = useState(false); const [isSubmitModalVisible, setIsSubmitModalVisible] = useState(false); const [isFinancialModalVisible, setIsFinancialModalVisible] = useState(false); + const [enableBoat, setEnableBoat] = useState(false); + const [enableMobileHome, setEnableMobileHome] = useState(false); const { upload, amendedUpload } = useOrdersDocumentQueries(moveCode); const documentsForViewer = Object.values(upload || {}) .concat(Object.values(amendedUpload || {})) @@ -143,6 +146,14 @@ const ServicesCounselingMoveDetails = ({ checkProGearAllowances(); }); + useEffect(() => { + const fetchData = async () => { + setEnableBoat(await isBooleanFlagEnabled(FEATURE_FLAG_KEYS.BOAT)); + setEnableMobileHome(await isBooleanFlagEnabled(FEATURE_FLAG_KEYS.MOBILE_HOME)); + }; + fetchData(); + }, []); + // for now we are only showing dest type on retiree and separatee orders const isRetirementOrSeparation = order.order_type === ORDERS_TYPE.RETIREMENT || order.order_type === ORDERS_TYPE.SEPARATION; @@ -496,6 +507,21 @@ const ServicesCounselingMoveDetails = ({ const hasMissingOrdersRequiredInfo = Object.values(requiredOrdersInfo).some((value) => !value || value === ''); const hasAmendedOrders = ordersInfo.uploadedAmendedOrderID && !ordersInfo.amendedOrdersAcknowledgedAt; + const allowedShipmentOptions = () => { + return ( + <> + + + + + {enableBoat && } + {enableMobileHome && } + + ); + }; + return (
@@ -613,18 +639,7 @@ const ServicesCounselingMoveDetails = ({ - - - - - - + {allowedShipmentOptions()} ) }