From ae457b00e0fc2cb940e7cff085239778c79630eb Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Fri, 20 Dec 2024 22:39:44 +0000 Subject: [PATCH 1/5] initial commit, functonality added, tests added --- migrations/app/migrations_manifest.txt | 3 +- ...4_add_destination_gbloc_db_function.up.sql | 85 ++++++++++++++++ pkg/gen/ghcapi/embedded_spec.go | 10 ++ pkg/gen/ghcmessages/address.go | 20 ++++ pkg/gen/internalapi/embedded_spec.go | 10 ++ pkg/gen/internalmessages/address.go | 20 ++++ pkg/gen/pptasapi/embedded_spec.go | 10 ++ pkg/gen/pptasmessages/address.go | 20 ++++ pkg/gen/primeapi/embedded_spec.go | 10 ++ pkg/gen/primemessages/address.go | 20 ++++ pkg/gen/primev2api/embedded_spec.go | 10 ++ pkg/gen/primev2messages/address.go | 20 ++++ pkg/gen/primev3api/embedded_spec.go | 10 ++ pkg/gen/primev3messages/address.go | 20 ++++ .../primeapiv3/payloads/model_to_payload.go | 21 ++-- .../payloads/model_to_payload_test.go | 53 +++++----- pkg/models/address.go | 1 + pkg/models/mto_shipments.go | 21 ++++ pkg/models/mto_shipments_test.go | 97 +++++++++++++++++++ .../move_task_order_fetcher.go | 12 +++ .../move_task_order_fetcher_test.go | 57 +++++++++++ scripts/db-truncate | 2 +- swagger-def/definitions/Address.yaml | 4 + swagger/ghc.yaml | 4 + swagger/internal.yaml | 4 + swagger/pptas.yaml | 4 + swagger/prime.yaml | 4 + swagger/prime_v2.yaml | 4 + swagger/prime_v3.yaml | 4 + 29 files changed, 524 insertions(+), 36 deletions(-) create mode 100644 migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index 1a397d886a9..d347591d86f 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1054,6 +1054,7 @@ 20241204155919_update_ordering_proc.up.sql 20241204210208_retroactive_update_of_ppm_max_and_estimated_incentives_prd.up.sql 20241210143143_redefine_mto_shipment_audit_table.up.sql -20241217163231_update_duty_locations_bad_zips.up.sql 20241216170325_update_nts_enum_name.up.sql +20241217163231_update_duty_locations_bad_zips.up.sql 20241217180136_add_AK_zips_to_zip3_distances.up.sql +20241220213134_add_destination_gbloc_db_function.up.sql diff --git a/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql b/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql new file mode 100644 index 00000000000..bd7d9b1db5d --- /dev/null +++ b/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql @@ -0,0 +1,85 @@ +-- this function will handle getting the destination GBLOC associated with a shipment's destination address +-- this only applies to OCONUS destination addresses on a shipment, but this also checks domestic shipments +CREATE OR REPLACE FUNCTION get_destination_gbloc_for_shipment(shipment_id UUID) +RETURNS TEXT AS $$ +DECLARE + service_member_affiliation TEXT; + zip TEXT; + gbloc_result TEXT; + alaska_zone_ii BOOLEAN; + market_code TEXT; +BEGIN + -- get the shipment's market_code + SELECT ms.market_code + INTO market_code + FROM mto_shipments ms + WHERE ms.id = shipment_id; + + -- if it's a domestic shipment, use postal_code_to_gblocs + IF market_code = 'd' THEN + SELECT upc.uspr_zip_id + INTO zip + FROM addresses a + JOIN us_post_region_cities upc ON a.us_post_region_cities_id = upc.id + WHERE a.id = (SELECT destination_address_id FROM mto_shipments WHERE id = shipment_id); + + SELECT gbloc + INTO gbloc_result + FROM postal_code_to_gblocs + WHERE postal_code = zip + LIMIT 1; + + IF gbloc_result IS NULL THEN + RETURN NULL; + END IF; + + RETURN gbloc_result; + + ELSEIF market_code = 'i' THEN + -- if it's 'i' then we need to check for some exceptions + SELECT sm.affiliation + INTO service_member_affiliation + FROM service_members sm + JOIN orders o ON o.service_member_id = sm.id + JOIN moves m ON m.orders_id = o.id + JOIN mto_shipments ms ON ms.move_id = m.id + WHERE ms.id = shipment_id; + + SELECT upc.uspr_zip_id + INTO zip + FROM addresses a + JOIN us_post_region_cities upc ON a.us_post_region_cities_id = upc.id + WHERE a.id = (SELECT destination_address_id FROM mto_shipments WHERE id = shipment_id); + + -- check if the postal code (uspr_zip_id) is in Alaska Zone II + SELECT EXISTS ( + SELECT 1 + FROM re_oconus_rate_areas ro + JOIN re_rate_areas ra ON ro.rate_area_id = ra.id + JOIN us_post_region_cities upc ON upc.id = ro.us_post_region_cities_id + WHERE upc.uspr_zip_id = zip + AND ra.code = 'US8190100' -- Alaska Zone II Code + ) + INTO alaska_zone_ii; + + -- if the service member is USAF or USSF and the address is in Alaska Zone II, return 'MBFL' + IF (service_member_affiliation = 'AIR_FORCE' OR service_member_affiliation = 'SPACE_FORCE') AND alaska_zone_ii THEN + RETURN 'MBFL'; + END IF; + + -- for all other branches except USMC, return the gbloc from the postal_code_to_gbloc table based on the zip + SELECT gbloc + INTO gbloc_result + FROM postal_code_to_gblocs + WHERE postal_code = zip + LIMIT 1; + + IF gbloc_result IS NULL THEN + RETURN NULL; + END IF; + + RETURN gbloc_result; + END IF; + +END; +$$ LANGUAGE plpgsql; diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index fd5e1860d98..51113c85e01 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -6667,6 +6667,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true @@ -23749,6 +23754,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true diff --git a/pkg/gen/ghcmessages/address.go b/pkg/gen/ghcmessages/address.go index 47148e32cf7..42bd1d8d69e 100644 --- a/pkg/gen/ghcmessages/address.go +++ b/pkg/gen/ghcmessages/address.go @@ -36,6 +36,10 @@ type Address struct { // Example: LOS ANGELES County *string `json:"county,omitempty"` + // destination gbloc + // Pattern: ^[A-Z]{4}$ + DestinationGbloc *string `json:"destinationGbloc,omitempty"` + // e tag // Read Only: true ETag string `json:"eTag,omitempty"` @@ -91,6 +95,10 @@ func (m *Address) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateDestinationGbloc(formats); err != nil { + res = append(res, err) + } + if err := m.validateID(formats); err != nil { res = append(res, err) } @@ -138,6 +146,18 @@ func (m *Address) validateCountry(formats strfmt.Registry) error { return nil } +func (m *Address) validateDestinationGbloc(formats strfmt.Registry) error { + if swag.IsZero(m.DestinationGbloc) { // not required + return nil + } + + if err := validate.Pattern("destinationGbloc", "body", *m.DestinationGbloc, `^[A-Z]{4}$`); err != nil { + return err + } + + return nil +} + func (m *Address) validateID(formats strfmt.Registry) error { if swag.IsZero(m.ID) { // not required return nil diff --git a/pkg/gen/internalapi/embedded_spec.go b/pkg/gen/internalapi/embedded_spec.go index 9706d6c936b..0e198c6c4e6 100644 --- a/pkg/gen/internalapi/embedded_spec.go +++ b/pkg/gen/internalapi/embedded_spec.go @@ -3363,6 +3363,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true @@ -12482,6 +12487,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true diff --git a/pkg/gen/internalmessages/address.go b/pkg/gen/internalmessages/address.go index 529cc0d7110..733df1c0680 100644 --- a/pkg/gen/internalmessages/address.go +++ b/pkg/gen/internalmessages/address.go @@ -36,6 +36,10 @@ type Address struct { // Example: LOS ANGELES County *string `json:"county,omitempty"` + // destination gbloc + // Pattern: ^[A-Z]{4}$ + DestinationGbloc *string `json:"destinationGbloc,omitempty"` + // e tag // Read Only: true ETag string `json:"eTag,omitempty"` @@ -91,6 +95,10 @@ func (m *Address) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateDestinationGbloc(formats); err != nil { + res = append(res, err) + } + if err := m.validateID(formats); err != nil { res = append(res, err) } @@ -138,6 +146,18 @@ func (m *Address) validateCountry(formats strfmt.Registry) error { return nil } +func (m *Address) validateDestinationGbloc(formats strfmt.Registry) error { + if swag.IsZero(m.DestinationGbloc) { // not required + return nil + } + + if err := validate.Pattern("destinationGbloc", "body", *m.DestinationGbloc, `^[A-Z]{4}$`); err != nil { + return err + } + + return nil +} + func (m *Address) validateID(formats strfmt.Registry) error { if swag.IsZero(m.ID) { // not required return nil diff --git a/pkg/gen/pptasapi/embedded_spec.go b/pkg/gen/pptasapi/embedded_spec.go index 1757ac556cc..fc54f37df09 100644 --- a/pkg/gen/pptasapi/embedded_spec.go +++ b/pkg/gen/pptasapi/embedded_spec.go @@ -114,6 +114,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true @@ -1008,6 +1013,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true diff --git a/pkg/gen/pptasmessages/address.go b/pkg/gen/pptasmessages/address.go index 1e53ba6d230..0e5a9af985a 100644 --- a/pkg/gen/pptasmessages/address.go +++ b/pkg/gen/pptasmessages/address.go @@ -36,6 +36,10 @@ type Address struct { // Example: LOS ANGELES County *string `json:"county,omitempty"` + // destination gbloc + // Pattern: ^[A-Z]{4}$ + DestinationGbloc *string `json:"destinationGbloc,omitempty"` + // e tag // Read Only: true ETag string `json:"eTag,omitempty"` @@ -91,6 +95,10 @@ func (m *Address) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateDestinationGbloc(formats); err != nil { + res = append(res, err) + } + if err := m.validateID(formats); err != nil { res = append(res, err) } @@ -138,6 +146,18 @@ func (m *Address) validateCountry(formats strfmt.Registry) error { return nil } +func (m *Address) validateDestinationGbloc(formats strfmt.Registry) error { + if swag.IsZero(m.DestinationGbloc) { // not required + return nil + } + + if err := validate.Pattern("destinationGbloc", "body", *m.DestinationGbloc, `^[A-Z]{4}$`); err != nil { + return err + } + + return nil +} + func (m *Address) validateID(formats strfmt.Registry) error { if swag.IsZero(m.ID) { // not required return nil diff --git a/pkg/gen/primeapi/embedded_spec.go b/pkg/gen/primeapi/embedded_spec.go index 44582f2f3f1..5e8ac967ea9 100644 --- a/pkg/gen/primeapi/embedded_spec.go +++ b/pkg/gen/primeapi/embedded_spec.go @@ -1214,6 +1214,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true @@ -6158,6 +6163,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true diff --git a/pkg/gen/primemessages/address.go b/pkg/gen/primemessages/address.go index 2fe5ba87adb..4ff5b6f7932 100644 --- a/pkg/gen/primemessages/address.go +++ b/pkg/gen/primemessages/address.go @@ -36,6 +36,10 @@ type Address struct { // Example: LOS ANGELES County *string `json:"county,omitempty"` + // destination gbloc + // Pattern: ^[A-Z]{4}$ + DestinationGbloc *string `json:"destinationGbloc,omitempty"` + // e tag // Read Only: true ETag string `json:"eTag,omitempty"` @@ -91,6 +95,10 @@ func (m *Address) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateDestinationGbloc(formats); err != nil { + res = append(res, err) + } + if err := m.validateID(formats); err != nil { res = append(res, err) } @@ -138,6 +146,18 @@ func (m *Address) validateCountry(formats strfmt.Registry) error { return nil } +func (m *Address) validateDestinationGbloc(formats strfmt.Registry) error { + if swag.IsZero(m.DestinationGbloc) { // not required + return nil + } + + if err := validate.Pattern("destinationGbloc", "body", *m.DestinationGbloc, `^[A-Z]{4}$`); err != nil { + return err + } + + return nil +} + func (m *Address) validateID(formats strfmt.Registry) error { if swag.IsZero(m.ID) { // not required return nil diff --git a/pkg/gen/primev2api/embedded_spec.go b/pkg/gen/primev2api/embedded_spec.go index c1913d29667..74ee52d88c1 100644 --- a/pkg/gen/primev2api/embedded_spec.go +++ b/pkg/gen/primev2api/embedded_spec.go @@ -399,6 +399,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true @@ -4030,6 +4035,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true diff --git a/pkg/gen/primev2messages/address.go b/pkg/gen/primev2messages/address.go index 631419ea719..2f1631a297c 100644 --- a/pkg/gen/primev2messages/address.go +++ b/pkg/gen/primev2messages/address.go @@ -36,6 +36,10 @@ type Address struct { // Example: LOS ANGELES County *string `json:"county,omitempty"` + // destination gbloc + // Pattern: ^[A-Z]{4}$ + DestinationGbloc *string `json:"destinationGbloc,omitempty"` + // e tag // Read Only: true ETag string `json:"eTag,omitempty"` @@ -91,6 +95,10 @@ func (m *Address) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateDestinationGbloc(formats); err != nil { + res = append(res, err) + } + if err := m.validateID(formats); err != nil { res = append(res, err) } @@ -138,6 +146,18 @@ func (m *Address) validateCountry(formats strfmt.Registry) error { return nil } +func (m *Address) validateDestinationGbloc(formats strfmt.Registry) error { + if swag.IsZero(m.DestinationGbloc) { // not required + return nil + } + + if err := validate.Pattern("destinationGbloc", "body", *m.DestinationGbloc, `^[A-Z]{4}$`); err != nil { + return err + } + + return nil +} + func (m *Address) validateID(formats strfmt.Registry) error { if swag.IsZero(m.ID) { // not required return nil diff --git a/pkg/gen/primev3api/embedded_spec.go b/pkg/gen/primev3api/embedded_spec.go index a8b76579b0d..ec655b5ca41 100644 --- a/pkg/gen/primev3api/embedded_spec.go +++ b/pkg/gen/primev3api/embedded_spec.go @@ -405,6 +405,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true @@ -4721,6 +4726,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true diff --git a/pkg/gen/primev3messages/address.go b/pkg/gen/primev3messages/address.go index edffd06b01c..43fbf3bc550 100644 --- a/pkg/gen/primev3messages/address.go +++ b/pkg/gen/primev3messages/address.go @@ -36,6 +36,10 @@ type Address struct { // Example: LOS ANGELES County *string `json:"county,omitempty"` + // destination gbloc + // Pattern: ^[A-Z]{4}$ + DestinationGbloc *string `json:"destinationGbloc,omitempty"` + // e tag // Read Only: true ETag string `json:"eTag,omitempty"` @@ -91,6 +95,10 @@ func (m *Address) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateDestinationGbloc(formats); err != nil { + res = append(res, err) + } + if err := m.validateID(formats); err != nil { res = append(res, err) } @@ -138,6 +146,18 @@ func (m *Address) validateCountry(formats strfmt.Registry) error { return nil } +func (m *Address) validateDestinationGbloc(formats strfmt.Registry) error { + if swag.IsZero(m.DestinationGbloc) { // not required + return nil + } + + if err := validate.Pattern("destinationGbloc", "body", *m.DestinationGbloc, `^[A-Z]{4}$`); err != nil { + return err + } + + return nil +} + func (m *Address) validateID(formats strfmt.Registry) error { if swag.IsZero(m.ID) { // not required return nil diff --git a/pkg/handlers/primeapiv3/payloads/model_to_payload.go b/pkg/handlers/primeapiv3/payloads/model_to_payload.go index aeddd72ef96..b4bfaf6faa8 100644 --- a/pkg/handlers/primeapiv3/payloads/model_to_payload.go +++ b/pkg/handlers/primeapiv3/payloads/model_to_payload.go @@ -248,16 +248,17 @@ func Address(address *models.Address) *primev3messages.Address { return nil } return &primev3messages.Address{ - ID: strfmt.UUID(address.ID.String()), - StreetAddress1: &address.StreetAddress1, - StreetAddress2: address.StreetAddress2, - StreetAddress3: address.StreetAddress3, - City: &address.City, - State: &address.State, - PostalCode: &address.PostalCode, - Country: Country(address.Country), - ETag: etag.GenerateEtag(address.UpdatedAt), - County: address.County, + ID: strfmt.UUID(address.ID.String()), + StreetAddress1: &address.StreetAddress1, + StreetAddress2: address.StreetAddress2, + StreetAddress3: address.StreetAddress3, + City: &address.City, + State: &address.State, + PostalCode: &address.PostalCode, + Country: Country(address.Country), + ETag: etag.GenerateEtag(address.UpdatedAt), + County: address.County, + DestinationGbloc: address.DestinationGbloc, } } diff --git a/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go b/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go index 54892d9d515..a9c46fe2abb 100644 --- a/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go +++ b/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go @@ -150,12 +150,13 @@ func (suite *PayloadsSuite) TestMoveTaskOrder() { PostalCode: fairbanksAlaskaPostalCode, }, DestinationAddress: &models.Address{ - StreetAddress1: "123 Main St", - StreetAddress2: &streetAddress2, - StreetAddress3: &streetAddress3, - City: "Anchorage", - State: "AK", - PostalCode: anchorageAlaskaPostalCode, + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Anchorage", + State: "AK", + PostalCode: anchorageAlaskaPostalCode, + DestinationGbloc: models.StringPointer("JEAT"), }, }) newMove.MTOShipments = append(newMove.MTOShipments, models.MTOShipment{ @@ -168,12 +169,13 @@ func (suite *PayloadsSuite) TestMoveTaskOrder() { PostalCode: wasillaAlaskaPostalCode, }, DestinationAddress: &models.Address{ - StreetAddress1: "123 Main St", - StreetAddress2: &streetAddress2, - StreetAddress3: &streetAddress3, - City: "Wasilla", - State: "AK", - PostalCode: wasillaAlaskaPostalCode, + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Wasilla", + State: "AK", + PostalCode: wasillaAlaskaPostalCode, + DestinationGbloc: models.StringPointer("JEAT"), }, }) newMove.MTOShipments = append(newMove.MTOShipments, models.MTOShipment{ @@ -237,20 +239,22 @@ func (suite *PayloadsSuite) TestMoveTaskOrder() { }) newMove.MTOShipments = append(newMove.MTOShipments, models.MTOShipment{ PickupAddress: &models.Address{ - StreetAddress1: "123 Main St", - StreetAddress2: &streetAddress2, - StreetAddress3: &streetAddress3, - City: "Beverly Hills", - State: "CA", - PostalCode: "90210", + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Beverly Hills", + State: "CA", + PostalCode: "90210", + DestinationGbloc: models.StringPointer("JEAT"), }, DestinationAddress: &models.Address{ - StreetAddress1: "123 Main St", - StreetAddress2: &streetAddress2, - StreetAddress3: &streetAddress3, - City: "Beverly Hills", - State: "CA", - PostalCode: "90210", + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Beverly Hills", + State: "CA", + PostalCode: "90210", + DestinationGbloc: models.StringPointer("JEAT"), }, }) @@ -357,6 +361,7 @@ func (suite *PayloadsSuite) TestMoveTaskOrder() { } else { suite.NotNil(shipment.PickupAddress) suite.NotNil(shipment.DestinationAddress) + suite.NotNil(shipment.DestinationAddress.DestinationGbloc) if slices.Contains(expectedAlaskaPostalCodes, *shipment.PickupAddress.PostalCode) { ra, contains := shipmentPostalCodeRateAreaLookupMap[*shipment.PickupAddress.PostalCode] suite.True(contains) diff --git a/pkg/models/address.go b/pkg/models/address.go index e683f7771ab..6042ae53960 100644 --- a/pkg/models/address.go +++ b/pkg/models/address.go @@ -34,6 +34,7 @@ type Address struct { IsOconus *bool `json:"is_oconus" db:"is_oconus"` UsPostRegionCityID *uuid.UUID `json:"us_post_region_cities_id" db:"us_post_region_cities_id"` UsPostRegionCity *UsPostRegionCity `belongs_to:"us_post_region_cities" fk_id:"us_post_region_cities_id"` + DestinationGbloc *string `db:"-"` } // TableName overrides the table name used by Pop. diff --git a/pkg/models/mto_shipments.go b/pkg/models/mto_shipments.go index a4dbf53bdb7..edcded1a70c 100644 --- a/pkg/models/mto_shipments.go +++ b/pkg/models/mto_shipments.go @@ -1,6 +1,7 @@ package models import ( + "database/sql" "fmt" "time" @@ -443,6 +444,26 @@ func UpdateEstimatedPricingForShipmentBasicServiceItems(db *pop.Connection, ship return nil } +// GetDestinationGblocForShipment gets the GBLOC associated with the shipment's destination address +// there are certain exceptions for OCONUS addresses in Alaska Zone II based on affiliation +func GetDestinationGblocForShipment(db *pop.Connection, shipmentID uuid.UUID) (*string, error) { + var gbloc *string + + err := db.RawQuery("SELECT * FROM get_destination_gbloc_for_shipment($1)", shipmentID). + First(&gbloc) + + if err != nil && err != sql.ErrNoRows { + return nil, fmt.Errorf("error fetching destination gbloc for shipment ID: %s with error %w", shipmentID, err) + } + + // return the ZIP code and port type, or nil if not found + if gbloc != nil { + return gbloc, nil + } + + return nil, nil +} + // Returns a Shipment for a given id func FetchShipmentByID(db *pop.Connection, shipmentID uuid.UUID) (*MTOShipment, error) { var mtoShipment MTOShipment diff --git a/pkg/models/mto_shipments_test.go b/pkg/models/mto_shipments_test.go index b82c05dd217..f9589e6239b 100644 --- a/pkg/models/mto_shipments_test.go +++ b/pkg/models/mto_shipments_test.go @@ -339,3 +339,100 @@ func (suite *ModelSuite) TestFindShipmentByID() { suite.Equal(models.ErrFetchNotFound, err) }) } + +func (suite *ModelSuite) TestGetDestinationGblocForShipment() { + suite.Run("success - get GBLOC for USAF in AK Zone II", func() { + // Create a USAF move in Alaska Zone II + // this is a hard coded uuid that is a us_post_region_cities_id within AK Zone II + // this should always return MBFL + zone2UUID, err := uuid.FromString("66768964-e0de-41f3-b9be-7ef32e4ae2b4") + suite.FatalNoError(err) + airForce := models.AffiliationAIRFORCE + postalCode := "99501" + + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + PostalCode: postalCode, + UsPostRegionCityID: &zone2UUID, + }, + }, + }, nil) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), []factory.Customization{ + { + Model: models.ServiceMember{ + Affiliation: &airForce, + }, + }, + }, nil) + + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: destinationAddress, + LinkOnly: true, + }, + }, nil) + + gbloc, err := models.GetDestinationGblocForShipment(suite.DB(), shipment.ID) + suite.NoError(err) + suite.NotNil(gbloc) + suite.Equal(*gbloc, "MBFL") + }) + suite.Run("success - get GBLOC for Army in AK Zone II", func() { + // Create an ARMY move in Alaska Zone II + zone2UUID, err := uuid.FromString("66768964-e0de-41f3-b9be-7ef32e4ae2b4") + suite.FatalNoError(err) + army := models.AffiliationARMY + postalCode := "99501" + // since we truncate the test db, we need to add the postal_code_to_gbloc value + factory.FetchOrBuildPostalCodeToGBLOC(suite.DB(), "99744", "JEAT") + + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + PostalCode: postalCode, + UsPostRegionCityID: &zone2UUID, + }, + }, + }, nil) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), []factory.Customization{ + { + Model: models.ServiceMember{ + Affiliation: &army, + }, + }, + }, nil) + + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: destinationAddress, + LinkOnly: true, + }, + }, nil) + + gbloc, err := models.GetDestinationGblocForShipment(suite.DB(), shipment.ID) + suite.NoError(err) + suite.NotNil(gbloc) + suite.Equal(*gbloc, "JEAT") + }) +} diff --git a/pkg/services/move_task_order/move_task_order_fetcher.go b/pkg/services/move_task_order/move_task_order_fetcher.go index b63f5960290..b41d6bc9176 100644 --- a/pkg/services/move_task_order/move_task_order_fetcher.go +++ b/pkg/services/move_task_order/move_task_order_fetcher.go @@ -278,6 +278,18 @@ func (f moveTaskOrderFetcher) FetchMoveTaskOrder(appCtx appcontext.AppContext, s return &models.Move{}, apperror.NewQueryError("MobileHomeShipment", loadErrMH, "") } } + // we need to get the destination GBLOC associated with a shipment's destination address + // USMC always goes to the USMC GBLOC + if mto.MTOShipments[i].DestinationAddress != nil { + if *mto.Orders.ServiceMember.Affiliation == models.AffiliationMARINES { + *mto.MTOShipments[i].DestinationAddress.DestinationGbloc = "USMC" + } else { + mto.MTOShipments[i].DestinationAddress.DestinationGbloc, err = models.GetDestinationGblocForShipment(appCtx.DB(), mto.MTOShipments[i].ID) + if err != nil { + return &models.Move{}, apperror.NewQueryError("Error getting shipment GBLOC", err, "") + } + } + } filteredShipments = append(filteredShipments, mto.MTOShipments[i]) } mto.MTOShipments = filteredShipments diff --git a/pkg/services/move_task_order/move_task_order_fetcher_test.go b/pkg/services/move_task_order/move_task_order_fetcher_test.go index 9ba9f7a8ba2..fb59f8209f3 100644 --- a/pkg/services/move_task_order/move_task_order_fetcher_test.go +++ b/pkg/services/move_task_order/move_task_order_fetcher_test.go @@ -344,6 +344,63 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderFetcher() { } }) + suite.Run("Success with Prime available move, returns destination GBLOC in shipment dest address", func() { + zone2UUID, err := uuid.FromString("66768964-e0de-41f3-b9be-7ef32e4ae2b4") + suite.FatalNoError(err) + army := models.AffiliationARMY + postalCode := "99501" + // since we truncate the test db, we need to add the postal_code_to_gbloc value + factory.FetchOrBuildPostalCodeToGBLOC(suite.DB(), "99744", "JEAT") + + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + PostalCode: postalCode, + UsPostRegionCityID: &zone2UUID, + }, + }, + }, nil) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), []factory.Customization{ + { + Model: models.ServiceMember{ + Affiliation: &army, + }, + }, + }, nil) + + factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: destinationAddress, + LinkOnly: true, + }, + }, nil) + searchParams := services.MoveTaskOrderFetcherParams{ + IncludeHidden: false, + Locator: move.Locator, + ExcludeExternalShipments: true, + } + + actualMTO, err := mtoFetcher.FetchMoveTaskOrder(suite.AppContextForTest(), &searchParams) + suite.NoError(err) + suite.NotNil(actualMTO) + + if suite.Len(actualMTO.MTOShipments, 1) { + suite.Equal(move.ID.String(), actualMTO.ID.String()) + // the shipment should have a destination GBLOC value + suite.NotNil(actualMTO.MTOShipments[0].DestinationAddress.DestinationGbloc) + } + }) + suite.Run("Success with move that has only deleted shipments", func() { mtoWithAllShipmentsDeleted := factory.BuildMove(suite.DB(), nil, nil) factory.BuildMTOShipmentMinimal(suite.DB(), []factory.Customization{ diff --git a/scripts/db-truncate b/scripts/db-truncate index 29c54a8f616..4e65cccd36f 100755 --- a/scripts/db-truncate +++ b/scripts/db-truncate @@ -9,7 +9,7 @@ DO \$\$ DECLARE r RECORD; BEGIN FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema() - AND tablename NOT IN ('us_post_region_cities', 're_countries', 're_states', 're_cities', 're_us_post_regions', 're_oconus_rate_areas', 're_rate_areas', 're_intl_transit_times', 'ub_allowances','re_services','re_service_items', 'ports', 'port_locations', 're_fsc_multipliers')) LOOP + AND tablename NOT IN ('us_post_region_cities', 're_countries', 're_states', 're_cities', 're_us_post_regions', 're_oconus_rate_areas', 're_rate_areas', 're_intl_transit_times', 'ub_allowances','re_services','re_service_items', 'ports', 'port_locations', 're_fsc_multipliers', 're_contracts')) LOOP EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE'; END LOOP; END \$\$; diff --git a/swagger-def/definitions/Address.yaml b/swagger-def/definitions/Address.yaml index baa869f59b3..0c018795ee2 100644 --- a/swagger-def/definitions/Address.yaml +++ b/swagger-def/definitions/Address.yaml @@ -161,6 +161,10 @@ properties: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 + destinationGbloc: + type: string + pattern: '^[A-Z]{4}$' + x-nullable: true required: - streetAddress1 - city diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index f1d89bde632..d8247650b54 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -8539,6 +8539,10 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 + destinationGbloc: + type: string + pattern: ^[A-Z]{4}$ + x-nullable: true required: - streetAddress1 - city diff --git a/swagger/internal.yaml b/swagger/internal.yaml index 91bf98ecf28..99da9030213 100644 --- a/swagger/internal.yaml +++ b/swagger/internal.yaml @@ -2758,6 +2758,10 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 + destinationGbloc: + type: string + pattern: ^[A-Z]{4}$ + x-nullable: true required: - streetAddress1 - city diff --git a/swagger/pptas.yaml b/swagger/pptas.yaml index e2aa9b3c525..6b4223b52f7 100644 --- a/swagger/pptas.yaml +++ b/swagger/pptas.yaml @@ -245,6 +245,10 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 + destinationGbloc: + type: string + pattern: ^[A-Z]{4}$ + x-nullable: true required: - streetAddress1 - city diff --git a/swagger/prime.yaml b/swagger/prime.yaml index 1bd53595301..551a4661fe7 100644 --- a/swagger/prime.yaml +++ b/swagger/prime.yaml @@ -2983,6 +2983,10 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 + destinationGbloc: + type: string + pattern: ^[A-Z]{4}$ + x-nullable: true required: - streetAddress1 - city diff --git a/swagger/prime_v2.yaml b/swagger/prime_v2.yaml index 8e58e9c4258..f8f79ca6ce4 100644 --- a/swagger/prime_v2.yaml +++ b/swagger/prime_v2.yaml @@ -1566,6 +1566,10 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 + destinationGbloc: + type: string + pattern: ^[A-Z]{4}$ + x-nullable: true required: - streetAddress1 - city diff --git a/swagger/prime_v3.yaml b/swagger/prime_v3.yaml index d6a2e4aaeba..a87ccba5cb8 100644 --- a/swagger/prime_v3.yaml +++ b/swagger/prime_v3.yaml @@ -1654,6 +1654,10 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 + destinationGbloc: + type: string + pattern: ^[A-Z]{4}$ + x-nullable: true required: - streetAddress1 - city From 67bfcc31c4f3cd5946562e3e561e0f2986da24a5 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Fri, 20 Dec 2024 22:55:00 +0000 Subject: [PATCH 2/5] comments --- .../20241220213134_add_destination_gbloc_db_function.up.sql | 4 ++-- pkg/models/address.go | 2 +- pkg/models/mto_shipments.go | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql b/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql index bd7d9b1db5d..4b679eb8d48 100644 --- a/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql +++ b/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql @@ -1,5 +1,5 @@ -- this function will handle getting the destination GBLOC associated with a shipment's destination address --- this only applies to OCONUS destination addresses on a shipment, but this also checks domestic shipments +-- this only applies to OCONUS destination addresses on a shipment, but this can also checks domestic shipments CREATE OR REPLACE FUNCTION get_destination_gbloc_for_shipment(shipment_id UUID) RETURNS TEXT AS $$ DECLARE @@ -9,7 +9,7 @@ DECLARE alaska_zone_ii BOOLEAN; market_code TEXT; BEGIN - -- get the shipment's market_code + -- get the shipment's market code to determine conditionals SELECT ms.market_code INTO market_code FROM mto_shipments ms diff --git a/pkg/models/address.go b/pkg/models/address.go index 6042ae53960..d89a163c9aa 100644 --- a/pkg/models/address.go +++ b/pkg/models/address.go @@ -34,7 +34,7 @@ type Address struct { IsOconus *bool `json:"is_oconus" db:"is_oconus"` UsPostRegionCityID *uuid.UUID `json:"us_post_region_cities_id" db:"us_post_region_cities_id"` UsPostRegionCity *UsPostRegionCity `belongs_to:"us_post_region_cities" fk_id:"us_post_region_cities_id"` - DestinationGbloc *string `db:"-"` + DestinationGbloc *string `db:"-"` // this tells Pop not to look in the db for this value } // TableName overrides the table name used by Pop. diff --git a/pkg/models/mto_shipments.go b/pkg/models/mto_shipments.go index edcded1a70c..ec9a79b1fd1 100644 --- a/pkg/models/mto_shipments.go +++ b/pkg/models/mto_shipments.go @@ -456,7 +456,6 @@ func GetDestinationGblocForShipment(db *pop.Connection, shipmentID uuid.UUID) (* return nil, fmt.Errorf("error fetching destination gbloc for shipment ID: %s with error %w", shipmentID, err) } - // return the ZIP code and port type, or nil if not found if gbloc != nil { return gbloc, nil } From 78760e6b3b947437d6be3cda127d06dfd82a8224 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Fri, 20 Dec 2024 23:05:01 +0000 Subject: [PATCH 3/5] added usmc test --- .../move_task_order_fetcher.go | 2 +- .../move_task_order_fetcher_test.go | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/pkg/services/move_task_order/move_task_order_fetcher.go b/pkg/services/move_task_order/move_task_order_fetcher.go index b41d6bc9176..1d30aef5ceb 100644 --- a/pkg/services/move_task_order/move_task_order_fetcher.go +++ b/pkg/services/move_task_order/move_task_order_fetcher.go @@ -282,7 +282,7 @@ func (f moveTaskOrderFetcher) FetchMoveTaskOrder(appCtx appcontext.AppContext, s // USMC always goes to the USMC GBLOC if mto.MTOShipments[i].DestinationAddress != nil { if *mto.Orders.ServiceMember.Affiliation == models.AffiliationMARINES { - *mto.MTOShipments[i].DestinationAddress.DestinationGbloc = "USMC" + mto.MTOShipments[i].DestinationAddress.DestinationGbloc = models.StringPointer("USMC") } else { mto.MTOShipments[i].DestinationAddress.DestinationGbloc, err = models.GetDestinationGblocForShipment(appCtx.DB(), mto.MTOShipments[i].ID) if err != nil { diff --git a/pkg/services/move_task_order/move_task_order_fetcher_test.go b/pkg/services/move_task_order/move_task_order_fetcher_test.go index fb59f8209f3..a80be4aa524 100644 --- a/pkg/services/move_task_order/move_task_order_fetcher_test.go +++ b/pkg/services/move_task_order/move_task_order_fetcher_test.go @@ -401,6 +401,45 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderFetcher() { } }) + suite.Run("Success with Prime available move, returns USMC destination GBLOC for USMC move", func() { + usmc := models.AffiliationMARINES + + destinationAddress := factory.BuildAddress(suite.DB(), nil, nil) + move := factory.BuildAvailableToPrimeMove(suite.DB(), []factory.Customization{ + { + Model: models.ServiceMember{ + Affiliation: &usmc, + }, + }, + }, nil) + + factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: destinationAddress, + LinkOnly: true, + }, + }, nil) + searchParams := services.MoveTaskOrderFetcherParams{ + IncludeHidden: false, + Locator: move.Locator, + ExcludeExternalShipments: true, + } + + actualMTO, err := mtoFetcher.FetchMoveTaskOrder(suite.AppContextForTest(), &searchParams) + suite.NoError(err) + suite.NotNil(actualMTO) + + if suite.Len(actualMTO.MTOShipments, 1) { + suite.Equal(move.ID.String(), actualMTO.ID.String()) + suite.NotNil(actualMTO.MTOShipments[0].DestinationAddress.DestinationGbloc) + suite.Equal(*actualMTO.MTOShipments[0].DestinationAddress.DestinationGbloc, "USMC") + } + }) + suite.Run("Success with move that has only deleted shipments", func() { mtoWithAllShipmentsDeleted := factory.BuildMove(suite.DB(), nil, nil) factory.BuildMTOShipmentMinimal(suite.DB(), []factory.Customization{ From eaac8633f5abe18a206b16bde195e59c1256eb0e Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Fri, 20 Dec 2024 23:10:03 +0000 Subject: [PATCH 4/5] if it broke, ya gotta fix --- pkg/services/postal_codes/postal_code_validator_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/postal_codes/postal_code_validator_test.go b/pkg/services/postal_codes/postal_code_validator_test.go index 80da05fce2d..7038027fca8 100644 --- a/pkg/services/postal_codes/postal_code_validator_test.go +++ b/pkg/services/postal_codes/postal_code_validator_test.go @@ -211,8 +211,8 @@ func (suite *ValidatePostalCodeTestSuite) TestValidatePostalCode() { } func (suite *ValidatePostalCodeTestSuite) buildContractYear(testYear int) models.ReContractYear { - reContract := testdatagen.MakeDefaultReContract(suite.DB()) - reContractYear := testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + reContract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + reContractYear := testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ Contract: reContract, StartDate: time.Date(testYear, time.January, 1, 0, 0, 0, 0, time.UTC), From b0abbcee42e833128c71eaf56b5b4dc9b8910450 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Thu, 26 Dec 2024 13:47:23 +0000 Subject: [PATCH 5/5] updated db func to include USMC check, added test --- ...4_add_destination_gbloc_db_function.up.sql | 5 ++ pkg/models/mto_shipments_test.go | 49 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql b/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql index 4b679eb8d48..0375f64d044 100644 --- a/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql +++ b/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql @@ -45,6 +45,11 @@ BEGIN JOIN mto_shipments ms ON ms.move_id = m.id WHERE ms.id = shipment_id; + -- if the service member is USMC, return 'USMC' + IF service_member_affiliation = 'MARINES' THEN + RETURN 'USMC'; + END IF; + SELECT upc.uspr_zip_id INTO zip FROM addresses a diff --git a/pkg/models/mto_shipments_test.go b/pkg/models/mto_shipments_test.go index f9589e6239b..506c69e4d5e 100644 --- a/pkg/models/mto_shipments_test.go +++ b/pkg/models/mto_shipments_test.go @@ -435,4 +435,53 @@ func (suite *ModelSuite) TestGetDestinationGblocForShipment() { suite.NotNil(gbloc) suite.Equal(*gbloc, "JEAT") }) + suite.Run("success - get GBLOC for USMC in AK Zone II", func() { + // Create a USMC move in Alaska Zone II + // this should always return USMC + zone2UUID, err := uuid.FromString("66768964-e0de-41f3-b9be-7ef32e4ae2b4") + suite.FatalNoError(err) + usmc := models.AffiliationMARINES + postalCode := "99501" + // since we truncate the test db, we need to add the postal_code_to_gbloc value + // this doesn't matter to the db function because it will check for USMC but we are just verifying it won't be JEAT despite the zip matching + factory.FetchOrBuildPostalCodeToGBLOC(suite.DB(), "99744", "JEAT") + + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + PostalCode: postalCode, + UsPostRegionCityID: &zone2UUID, + }, + }, + }, nil) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), []factory.Customization{ + { + Model: models.ServiceMember{ + Affiliation: &usmc, + }, + }, + }, nil) + + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: destinationAddress, + LinkOnly: true, + }, + }, nil) + + gbloc, err := models.GetDestinationGblocForShipment(suite.DB(), shipment.ID) + suite.NoError(err) + suite.NotNil(gbloc) + suite.Equal(*gbloc, "USMC") + }) }