diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index a5330171a14..a0db58ee090 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1052,10 +1052,16 @@ 20241203024453_add_ppm_max_incentive_column.up.sql 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 +20241216190428_update_get_zip_code_function_and_update_pricing_proc.up.sql 20241217163231_update_duty_locations_bad_zips.up.sql 20241217180136_add_AK_zips_to_zip3_distances.up.sql 20241218201833_add_PPPO_BASE_ELIZABETH.up.sql 20241220171035_add_additional_AK_zips_to_zip3_distances.up.sql +20241220213134_add_destination_gbloc_db_function.up.sql +20241226173330_add_intl_param_values_to_service_params_table.up.sql 20241227153723_remove_empty_string_emplid_values.up.sql +20241227202424_insert_transportation_offices_camp_pendelton.up.sql 20241230190638_remove_AK_zips_from_zip3.up.sql 20241230190647_add_missing_AK_zips_to_zip3_distances.up.sql +20250103180420_update_pricing_proc_to_use_local_price_variable.up.sql diff --git a/migrations/app/schema/20241210143143_redefine_mto_shipment_audit_table.up.sql b/migrations/app/schema/20241210143143_redefine_mto_shipment_audit_table.up.sql new file mode 100644 index 00000000000..8321b45908c --- /dev/null +++ b/migrations/app/schema/20241210143143_redefine_mto_shipment_audit_table.up.sql @@ -0,0 +1,13 @@ +DROP TRIGGER IF EXISTS audit_trigger_row ON "mto_shipments"; + +SELECT add_audit_history_table( + target_table := 'mto_shipments', + audit_rows := BOOLEAN 't', + audit_query_text := BOOLEAN 't', + ignored_cols := ARRAY[ + 'created_at', + 'updated_at', + 'move_id', + 'storage_facility_id' + ] +); \ No newline at end of file diff --git a/migrations/app/schema/20241216190428_update_get_zip_code_function_and_update_pricing_proc.up.sql b/migrations/app/schema/20241216190428_update_get_zip_code_function_and_update_pricing_proc.up.sql new file mode 100644 index 00000000000..de355bc8146 --- /dev/null +++ b/migrations/app/schema/20241216190428_update_get_zip_code_function_and_update_pricing_proc.up.sql @@ -0,0 +1,234 @@ +-- removing the exception that was previously being returned +-- this is to avoid the db update failing for the entire proc it is used in +-- we won't always have the POE/POD locations and want to ignore any errors here +CREATE OR REPLACE FUNCTION get_zip_code(shipment_id uuid, address_type VARCHAR) +RETURNS VARCHAR AS $$ + DECLARE zip_code VARCHAR; + BEGIN + + IF address_type = 'pickup' THEN + SELECT vl.uspr_zip_id + INTO zip_code + FROM mto_shipments ms + JOIN addresses a ON a.id = ms.pickup_address_id + JOIN v_locations vl ON vl.uprc_id = a.us_post_region_cities_id + WHERE ms.id = shipment_id; + ELSIF address_type = 'destination' THEN + SELECT vl.uspr_zip_id + INTO zip_code + FROM mto_shipments ms + JOIN addresses a ON a.id = ms.destination_address_id + JOIN v_locations vl ON vl.uprc_id = a.us_post_region_cities_id + WHERE ms.id = shipment_id; + ELSIF address_type = 'poe' THEN + SELECT vl.uspr_zip_id + INTO zip_code + FROM mto_service_items si + JOIN port_locations pl ON pl.id = si.poe_location_id + JOIN v_locations vl ON vl.uprc_id = pl.us_post_region_cities_id + WHERE si.mto_shipment_id = shipment_id; + ELSIF address_type = 'pod' THEN + SELECT vl.uspr_zip_id + INTO zip_code + FROM mto_service_items si + JOIN port_locations pl ON pl.id = si.pod_location_id + JOIN v_locations vl ON vl.uprc_id = pl.us_post_region_cities_id + WHERE si.mto_shipment_id = shipment_id; + END IF; + + RETURN zip_code; +END; +$$ LANGUAGE plpgsql; + + +-- updating the get rate area function to include the contract id +CREATE OR REPLACE FUNCTION get_rate_area_id( + address_id UUID, + service_item_id UUID, + c_id uuid, + OUT o_rate_area_id UUID +) +RETURNS UUID AS $$ +DECLARE + is_oconus BOOLEAN; + zip3_value TEXT; +BEGIN + is_oconus := get_is_oconus(address_id); + + IF is_oconus THEN + -- re_oconus_rate_areas if is_oconus is TRUE + SELECT ro.rate_area_id + INTO o_rate_area_id + FROM addresses a + JOIN re_oconus_rate_areas ro + ON a.us_post_region_cities_id = ro.us_post_region_cities_id + JOIN re_rate_areas ra ON ro.rate_area_id = ra.id + WHERE a.id = address_id + AND ra.contract_id = c_id; + ELSE + -- re_zip3s if is_oconus is FALSE + SELECT rupr.zip3 + INTO zip3_value + FROM addresses a + JOIN us_post_region_cities uprc + ON a.us_post_region_cities_id = uprc.id + JOIN re_us_post_regions rupr + ON uprc.us_post_regions_id = rupr.id + WHERE a.id = address_id; + + -- use the zip3 value to find the rate_area_id in re_zip3s + SELECT rz.rate_area_id + INTO o_rate_area_id + FROM re_zip3s rz + JOIN re_rate_areas ra + ON rz.rate_area_id = ra.id + WHERE rz.zip3 = zip3_value + AND ra.contract_id = c_id; + END IF; + + -- Raise an exception if no rate area is found + IF o_rate_area_id IS NULL THEN + RAISE EXCEPTION 'Rate area not found for address % for service item ID %', address_id, service_item_id; + END IF; +END; +$$ LANGUAGE plpgsql; + +-- this function will help us get the ZIP code & port type for a port to calculate mileage +CREATE OR REPLACE FUNCTION get_port_location_info_for_shipment(shipment_id UUID) +RETURNS TABLE(uspr_zip_id TEXT, port_type TEXT) AS $$ +BEGIN + -- select the ZIP code and port type (POEFSC or PODFSC) + RETURN QUERY + SELECT + COALESCE(poe_usprc.uspr_zip_id::TEXT, pod_usprc.uspr_zip_id::TEXT) AS uspr_zip_id, + CASE + WHEN msi.poe_location_id IS NOT NULL THEN 'POEFSC' + WHEN msi.pod_location_id IS NOT NULL THEN 'PODFSC' + ELSE NULL + END AS port_type + FROM mto_shipments ms + JOIN mto_service_items msi ON ms.id = msi.mto_shipment_id + LEFT JOIN port_locations poe_pl ON msi.poe_location_id = poe_pl.id + LEFT JOIN port_locations pod_pl ON msi.pod_location_id = pod_pl.id + LEFT JOIN us_post_region_cities poe_usprc ON poe_pl.us_post_region_cities_id = poe_usprc.id + LEFT JOIN us_post_region_cities pod_usprc ON pod_pl.us_post_region_cities_id = pod_usprc.id + WHERE ms.id = shipment_id + AND (msi.poe_location_id IS NOT NULL OR msi.pod_location_id IS NOT NULL) + LIMIT 1; +END; +$$ LANGUAGE plpgsql; + +-- updating the pricing proc to now consume the mileage we get from DTOD instead of calculate it using Rand McNally +-- this is a requirement for E-06210 +-- also updating the get_rate_area parameters and passing in the contract_id +CREATE OR REPLACE PROCEDURE update_service_item_pricing( + shipment_id UUID, + mileage INT +) AS +' +DECLARE + shipment RECORD; + service_item RECORD; + escalated_price NUMERIC; + estimated_price NUMERIC; + o_rate_area_id UUID; + d_rate_area_id UUID; + contract_id UUID; + service_code TEXT; + o_zip_code TEXT; + d_zip_code TEXT; + distance NUMERIC; -- This will be replaced by mileage + estimated_fsc_multiplier NUMERIC; + fuel_price NUMERIC; + cents_above_baseline NUMERIC; + price_difference NUMERIC; +BEGIN + SELECT ms.id, ms.pickup_address_id, ms.destination_address_id, ms.requested_pickup_date, ms.prime_estimated_weight + INTO shipment + FROM mto_shipments ms + WHERE ms.id = shipment_id; + + IF shipment IS NULL THEN + RAISE EXCEPTION ''Shipment with ID % not found'', shipment_id; + END IF; + + -- exit the proc if prime_estimated_weight is NULL + IF shipment.prime_estimated_weight IS NULL THEN + RETURN; + END IF; + + -- loop through service items in the shipment + FOR service_item IN + SELECT si.id, si.re_service_id + FROM mto_service_items si + WHERE si.mto_shipment_id = shipment_id + LOOP + -- get the service code for the current service item to determine calculation + SELECT code + INTO service_code + FROM re_services + WHERE id = service_item.re_service_id; + + CASE + WHEN service_code IN (''ISLH'', ''UBP'') THEN + contract_id := get_contract_id(shipment.requested_pickup_date); + o_rate_area_id := get_rate_area_id(shipment.pickup_address_id, service_item.re_service_id, contract_id); + d_rate_area_id := get_rate_area_id(shipment.destination_address_id, service_item.re_service_id, contract_id); + escalated_price := calculate_escalated_price(o_rate_area_id, d_rate_area_id, service_item.re_service_id, contract_id, service_code); + + IF shipment.prime_estimated_weight IS NOT NULL THEN + estimated_price := ROUND((escalated_price * (shipment.prime_estimated_weight / 100)::NUMERIC) * 100, 0); + RAISE NOTICE ''%: Received estimated price of % (% * (% / 100)) cents'', service_code, estimated_price, escalated_price, shipment.prime_estimated_weight; + END IF; + + WHEN service_code IN (''IHPK'', ''IUBPK'') THEN + -- perform IHPK/IUBPK-specific logic (no destination rate area) + contract_id := get_contract_id(shipment.requested_pickup_date); + o_rate_area_id := get_rate_area_id(shipment.pickup_address_id, service_item.re_service_id, contract_id); + escalated_price := calculate_escalated_price(o_rate_area_id, NULL, service_item.re_service_id, contract_id, service_code); + + IF shipment.prime_estimated_weight IS NOT NULL THEN + estimated_price := ROUND((escalated_price * (shipment.prime_estimated_weight / 100)::NUMERIC) * 100, 0); + RAISE NOTICE ''%: Received estimated price of % (% * (% / 100)) cents'', service_code, estimated_price, escalated_price, shipment.prime_estimated_weight; + END IF; + + WHEN service_code IN (''IHUPK'', ''IUBUPK'') THEN + -- perform IHUPK/IUBUPK-specific logic (no origin rate area) + contract_id := get_contract_id(shipment.requested_pickup_date); + d_rate_area_id := get_rate_area_id(shipment.destination_address_id, service_item.re_service_id, contract_id); + escalated_price := calculate_escalated_price(NULL, d_rate_area_id, service_item.re_service_id, contract_id, service_code); + + IF shipment.prime_estimated_weight IS NOT NULL THEN + estimated_price := ROUND((escalated_price * (shipment.prime_estimated_weight / 100)::NUMERIC) * 100, 0); + RAISE NOTICE ''%: Received estimated price of % (% * (% / 100)) cents'', service_code, estimated_price, escalated_price, shipment.prime_estimated_weight; + END IF; + + WHEN service_code IN (''POEFSC'', ''PODFSC'') THEN + -- use the passed mileage parameter + distance := mileage; + + -- getting FSC multiplier from re_fsc_multipliers + estimated_fsc_multiplier := get_fsc_multiplier(shipment.prime_estimated_weight); + + fuel_price := get_fuel_price(shipment.requested_pickup_date); + + price_difference := calculate_price_difference(fuel_price); + + -- calculate estimated price, return as cents + IF estimated_fsc_multiplier IS NOT NULL AND distance IS NOT NULL THEN + cents_above_baseline := distance * estimated_fsc_multiplier; + RAISE NOTICE ''Distance: % * FSC Multipler: % = $% cents above baseline of $2.50'', distance, estimated_fsc_multiplier, cents_above_baseline; + RAISE NOTICE ''The fuel price is % cents above the baseline ($% - $2.50 baseline)'', price_difference, fuel_price; + estimated_price := ROUND((cents_above_baseline * price_difference) * 100); + RAISE NOTICE ''Received estimated price of % cents for service_code: %.'', estimated_price, service_code; + END IF; + END CASE; + + -- update the pricing_estimate value in mto_service_items + UPDATE mto_service_items + SET pricing_estimate = estimated_price + WHERE id = service_item.id; + END LOOP; +END; +' +LANGUAGE plpgsql; 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..0375f64d044 --- /dev/null +++ b/migrations/app/schema/20241220213134_add_destination_gbloc_db_function.up.sql @@ -0,0 +1,90 @@ +-- 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 can 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 to determine conditionals + 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; + + -- 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 + 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/migrations/app/schema/20241226173330_add_intl_param_values_to_service_params_table.up.sql b/migrations/app/schema/20241226173330_add_intl_param_values_to_service_params_table.up.sql new file mode 100644 index 00000000000..1186b75a7b2 --- /dev/null +++ b/migrations/app/schema/20241226173330_add_intl_param_values_to_service_params_table.up.sql @@ -0,0 +1,115 @@ +-- dropping function that is not needed anymore +DROP FUNCTION IF EXISTS get_rate_area_id(UUID, UUID); + +-- need to add in param keys for international shipments, this will be used to show breakdowns to the TIO +INSERT INTO service_item_param_keys (id, key,description,type,origin,created_at,updated_at) VALUES + ('d9ad3878-4b94-4722-bbaf-d4b8080f339d','PortZip','ZIP of the port for an international shipment pickup or destination port','STRING','SYSTEM','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957'), + ('597bb77e-0ce7-4ba2-9624-24300962625f','PerUnitCents','Per unit cents for a service item','INTEGER','SYSTEM','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957'); + +-- inserting params for PODFSC +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('9848562b-50c1-4e6e-aef0-f9539bf243fa'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','adeb57e5-6b1c-4c0f-b5c9-9e57e600303f','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('9c244768-07ce-4368-936b-0ac14a8078a4'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','14a93209-370d-42f3-8ca2-479c953be839','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('5668accf-afac-46a3-b097-177b74076fc9'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','54c9cc4e-0d46-4956-b92e-be9847f894de','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('0e8bd8d5-40fd-46fb-8228-5b66088681a2'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','b9739817-6408-4829-8719-1e26f8a9ceb3','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('a22090b4-3ce6-448d-82b0-36592655d822'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','117da2f5-fff0-41e0-bba1-837124373098','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('d59c674a-eaf9-4158-8303-dbcb50a7230b'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','6ba0aeca-19f8-4247-a317-fffa81c5d5c1','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('450dad51-dcc9-4258-ba56-db39de6a8637'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','0c95581d-67de-48ae-a54b-a3748851d613','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('bf006fc0-8f33-4553-b567-a529af04eafe'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','45fce5ce-6a4c-4a6c-ab37-16ee0133628c','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('ad005c4b-dd71-4d42-99d8-95de7b1ed571'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','e6096350-9ac4-40aa-90c4-bbdff6e0b194','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('274356bc-8139-4e34-9332-ce7396f42c79'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','a1d31d35-c87d-4a7d-b0b8-8b2646b96e43','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('aa68a318-fe17-445c-ab53-0505fe48d0bb'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','b79978a7-21b7-4656-af83-25585acffb20','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('882e7978-9754-4c8e-bb71-8fe4f4059503'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','d87d82da-3ac2-44e8-bce0-cb4de40f9a72','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('8fc4571d-235b-4d4f-90e4-77e7ad9250d5'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','1e6257e9-757d-4d59-8846-727dd8a055e7','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('836606ce-894e-4765-bba5-b696cb5fe8cc'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','2cbc2251-eb7d-4c69-a120-9a83785c994b','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('08701fa6-6352-4808-88b6-1fe103068f29'::uuid,'388115e8-abe9-441d-96cf-a39f24baa0a3','d9ad3878-4b94-4722-bbaf-d4b8080f339d','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false); + +-- inserting params for POEFSC +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('a57c01b1-cb1c-40f7-87e0-99d1dfd69902'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','adeb57e5-6b1c-4c0f-b5c9-9e57e600303f','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('8f49b289-c1d0-438d-b9fc-3cb234167987'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','14a93209-370d-42f3-8ca2-479c953be839','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('46742d5d-dde9-4e3c-9e59-f2cf87ff016a'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','54c9cc4e-0d46-4956-b92e-be9847f894de','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('f6e178a9-5de3-4312-87c5-81d88ae0b45b'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','b9739817-6408-4829-8719-1e26f8a9ceb3','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('34d4c1a3-b218-4083-93b5-cbbc57688594'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','117da2f5-fff0-41e0-bba1-837124373098','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('6b07db8d-9f7a-4d33-9a5f-7d98fc7038f1'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','6ba0aeca-19f8-4247-a317-fffa81c5d5c1','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('9d8cc94b-4d5f-4c62-b9db-b87bb0213b8d'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','0c95581d-67de-48ae-a54b-a3748851d613','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('d63a9079-c99b-4d92-864f-46cc9bb18388'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','45fce5ce-6a4c-4a6c-ab37-16ee0133628c','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('625970f9-7c93-4c3d-97fe-62f5a9d598f1'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','e6096350-9ac4-40aa-90c4-bbdff6e0b194','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('34fa9839-8289-473a-9095-c2b1159ef5d3'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','a1d31d35-c87d-4a7d-b0b8-8b2646b96e43','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('f747f231-66f5-4a52-bb71-8d7b5f618d23'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','b79978a7-21b7-4656-af83-25585acffb20','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('0177f93a-15f6-41e5-a3ca-dc8f5bb727ab'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','d87d82da-3ac2-44e8-bce0-cb4de40f9a72','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('cbf5b41f-2d89-4284-858f-d2cda7b060f7'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','1e6257e9-757d-4d59-8846-727dd8a055e7','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('ebed3001-93f1-49ba-a935-3d463b0d76fc'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','2cbc2251-eb7d-4c69-a120-9a83785c994b','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('aa7c3492-be44-46dd-983e-478623edc0be'::uuid,'f75758d8-2fcd-40ba-9432-3ff3032a71d1','d9ad3878-4b94-4722-bbaf-d4b8080f339d','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false); + + +-- inserting params for ISLH +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('7e2e4b79-2f4c-451e-a28f-df1ad61c4f3b'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','164050e3-e35b-480d-bf6e-ed2fab86f370','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('adff4edb-1f78-45de-9269-29016d09d597'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','e6096350-9ac4-40aa-90c4-bbdff6e0b194','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('2239c77e-e073-47f3-aed7-e1edc6b8a9a4'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','45fce5ce-6a4c-4a6c-ab37-16ee0133628c','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('53ab68f9-8da7-48fa-80ac-bf91f05a4650'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','b9739817-6408-4829-8719-1e26f8a9ceb3','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('e9d3bc63-bc4a-43d0-98f1-48e3e51d2307'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','0c95581d-67de-48ae-a54b-a3748851d613','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('6632cbad-3fa0-46b8-a1ac-0b2bb5123401'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','b79978a7-21b7-4656-af83-25585acffb20','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('4ef58b87-8a93-44ec-b5c8-5b5779d8392e'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','599bbc21-8d1d-4039-9a89-ff52e3582144','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('d53e5f61-e92f-4ecb-9e1d-72ab8a99790f'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','a335e38a-7d95-4ba3-9c8b-75a5e00948bc','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('d94d2c5e-e91a-47d1-96b3-1c5d68a745dd'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','b03af5dc-7701-4e22-a986-d1889a2a8f27','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('7b8db256-e881-451e-a722-6431784e957f'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','add5114b-2a23-4e23-92b3-6dd0778dfc33','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('fedbd62d-d2fa-42b1-b6f6-c9c07e8c4014'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','a1d31d35-c87d-4a7d-b0b8-8b2646b96e43','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('c7a66e66-dabe-4f0b-a70d-f9639e87761a'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','2e091a7d-a1fd-4017-9f2d-73ad752a30c2','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('18f8b39f-37a5-4536-85d8-4b8b0a6bff94'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','739bbc23-cd08-4612-8e5d-da992202344e','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('7f20ed2e-1bdf-4370-b028-1251c07d3da1'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','95ee2e21-b232-4d74-9ec5-218564a8a8b9','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('a5be6b6f-e007-4d9f-8b1b-63e8ed5c4337'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','9de7fd2a-75c7-4c5c-ba5d-1a92f0b2f5f4','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('df5665d7-7b3d-487d-9d71-95d0e2832ae1'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','d87d82da-3ac2-44e8-bce0-cb4de40f9a72','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('c08f7ab1-6c3c-4627-b22f-1e987ba6f4f2'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','1e6257e9-757d-4d59-8846-727dd8a055e7','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('4d9ed9b0-957d-4e6a-a3d4-5e2e2784ef62'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','14a93209-370d-42f3-8ca2-479c953be839','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('6acb30b9-65a0-4902-85ed-1acb6f4ac930'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','5335e243-ab5b-4906-b84f-bd8c35ba64b3','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('fd83c2ba-0c59-4598-81d6-b56cc8d9979d'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','2cbc2251-eb7d-4c69-a120-9a83785c994b','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('b370c895-e356-4d2c-a200-c2c67ac51011'::uuid,'9f3d551a-0725-430e-897e-80ee9add3ae9','597bb77e-0ce7-4ba2-9624-24300962625f','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false); + +-- inserting params fo IOSFSC +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('f61ab040-dab4-4505-906d-d9a3a5da3515'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','0c95581d-67de-48ae-a54b-a3748851d613','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('421053c8-7b4a-44fc-8c73-14b72755c1f7'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','117da2f5-fff0-41e0-bba1-837124373098','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('8f8a8783-0ca2-4f0f-961f-07d1e3cdbf64'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','14a93209-370d-42f3-8ca2-479c953be839','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('ab332c8f-f46e-4d49-b29a-6adf7e67f9c7'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','1e6257e9-757d-4d59-8846-727dd8a055e7','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('a6e24b83-9cb4-4e56-9e38-7bdbd9d5c5fe'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','84d86517-9b88-4520-8d67-5ba892b85d10','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('4e05a7c7-bd0a-4c94-a99b-f052a8812aef'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','54c9cc4e-0d46-4956-b92e-be9847f894de','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('8ec5cd5d-d249-42e9-b11d-76a243d4045f'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','6ba0aeca-19f8-4247-a317-fffa81c5d5c1','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('2b6b9d89-65f3-4291-9e44-48d18d2a4070'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','a1d31d35-c87d-4a7d-b0b8-8b2646b96e43','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('a3d3c08f-d2a3-4ad1-b85f-b1daee12d71c'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','adeb57e5-6b1c-4c0f-b5c9-9e57e600303f','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('94d45661-82ac-479f-b7be-8e7a50ad46db'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','b79978a7-21b7-4656-af83-25585acffb20','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('c897cfa3-9b06-47b5-8e12-522f0897e59a'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','b9739817-6408-4829-8719-1e26f8a9ceb3','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('61832b60-2a2d-4e35-a799-b7ff9fa6a01e'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','d87d82da-3ac2-44e8-bce0-cb4de40f9a72','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('46531d32-91a5-4d98-a206-d0f1e14e2ff4'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','cd6d6ddf-7104-4d24-a8d6-d37fed61defe','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('a1d4f95e-f28f-4b6a-b83f-8f328f2b2498'::uuid,'81e29d0c-02a6-4a7a-be02-554deb3ee49e','f9753611-4b3e-4bf5-8e00-6d9ce9900f50','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false); + + +-- inserting params fo IDSFSC +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('25d90d5b-c58f-45e7-8c60-e7f63a0535b6'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','0c95581d-67de-48ae-a54b-a3748851d613','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('8bb29da3-32e7-4e98-b241-63b8c8c81c3b'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','117da2f5-fff0-41e0-bba1-837124373098','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('b8c12287-dcf6-4f88-bf7b-f7e99283f23d'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','14a93209-370d-42f3-8ca2-479c953be839','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('3b9f3eab-8e18-4888-81f4-c442b4e951cf'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','1e6257e9-757d-4d59-8846-727dd8a055e7','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('7a8430f7-ff55-4a80-b174-e1d4a2f21f25'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','70eecf7f-beae-4906-95ba-cbfe6797cf3a','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('2eaf2e5b-254e-48f0-a2c5-98d04087293f'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','54c9cc4e-0d46-4956-b92e-be9847f894de','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('71f571c5-99a0-420a-b375-bb859e3488a2'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','6ba0aeca-19f8-4247-a317-fffa81c5d5c1','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('cce73f9e-e3db-4d7f-a908-d9985f1b3f27'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','a1d31d35-c87d-4a7d-b0b8-8b2646b96e43','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('46c559e5-9f49-4b7e-98b6-b9d8e2a4e2cf'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','adeb57e5-6b1c-4c0f-b5c9-9e57e600303f','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('4512b905-cb68-4087-90b5-74e80ba9ec16'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','b79978a7-21b7-4656-af83-25585acffb20','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('d9887f60-e930-4f95-b53e-e9f0d8a445d3'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','b9739817-6408-4829-8719-1e26f8a9ceb3','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('f65b54fa-0e1c-45cc-b7f4-b41d356b970d'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','d87d82da-3ac2-44e8-bce0-cb4de40f9a72','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',true), + ('ce36b6e0-bcbf-4e96-9c5e-bd93fe9084c9'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','cd6d6ddf-7104-4d24-a8d6-d37fed61defe','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), + ('d9acb388-09a5-464b-bb50-bf418b25e96a'::uuid,'690a5fc1-0ea5-4554-8294-a367b5daefa9','f9753611-4b3e-4bf5-8e00-6d9ce9900f50','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false); + +-- inserting params fo IHPK +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('d9acb388-09a5-464b-bb50-bf418b25e96b'::uuid,'67ba1eaf-6ffd-49de-9a69-497be7789877','a1d31d35-c87d-4a7d-b0b8-8b2646b96e43','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), -- ContractCode + ('0b31db7a-fbab-4e49-8526-00458ac3900c'::uuid,'67ba1eaf-6ffd-49de-9a69-497be7789877','597bb77e-0ce7-4ba2-9624-24300962625f','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), -- PerUnitCents + ('9b0a74e3-afc4-4f42-8eb3-828f80fbfaf0'::uuid,'67ba1eaf-6ffd-49de-9a69-497be7789877','95ee2e21-b232-4d74-9ec5-218564a8a8b9','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false); -- IsPeak + +-- inserting params fo IHUPK +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('cb110853-6b1d-452b-9607-345721a70313'::uuid,'56e91c2d-015d-4243-9657-3ed34867abaa','a1d31d35-c87d-4a7d-b0b8-8b2646b96e43','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), -- ContractCode + ('cc95d5df-1167-4fe9-8682-07f8fbe7c286'::uuid,'56e91c2d-015d-4243-9657-3ed34867abaa','597bb77e-0ce7-4ba2-9624-24300962625f','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false), -- PerUnitCents + ('759bd482-b2f6-461b-a898-792415efa5f1'::uuid,'56e91c2d-015d-4243-9657-3ed34867abaa','95ee2e21-b232-4d74-9ec5-218564a8a8b9','2024-12-26 15:55:50.041957','2024-12-26 15:55:50.041957',false); -- IsPeak diff --git a/migrations/app/schema/20241227202424_insert_transportation_offices_camp_pendelton.up.sql b/migrations/app/schema/20241227202424_insert_transportation_offices_camp_pendelton.up.sql new file mode 100644 index 00000000000..dfe3c278ed9 --- /dev/null +++ b/migrations/app/schema/20241227202424_insert_transportation_offices_camp_pendelton.up.sql @@ -0,0 +1,8 @@ +--update address for Camp Pendelton transportation office +update addresses set postal_code = '92054', us_post_region_cities_id = 'd0c818dc-1e6c-416a-bfa9-2762efebaed1' where id = 'af2ebb73-54fe-46e0-9525-a4568ceb9e0e'; + +--fix Camp Pendelton transportation office spelling +update transportation_offices set name = 'PPPO DMO Camp Pendleton' where id = 'f50eb7f5-960a-46e8-aa64-6025b44132ab'; + +--associate duty location to Camp Pendleton transportation office +update duty_locations set transportation_office_id = 'f50eb7f5-960a-46e8-aa64-6025b44132ab' where id = '6e320acb-47b6-45e0-80f4-9d8dc1e20812'; diff --git a/migrations/app/schema/20250103180420_update_pricing_proc_to_use_local_price_variable.up.sql b/migrations/app/schema/20250103180420_update_pricing_proc_to_use_local_price_variable.up.sql new file mode 100644 index 00000000000..d5b3a911dc3 --- /dev/null +++ b/migrations/app/schema/20250103180420_update_pricing_proc_to_use_local_price_variable.up.sql @@ -0,0 +1,125 @@ +-- updating the pricing proc to use local variables when updating service items +-- there was an issue where it was retaining the previous value when the port fuel surcharges couldn't be priced but were updating anyway +CREATE OR REPLACE PROCEDURE update_service_item_pricing( + shipment_id UUID, + mileage INT +) AS +' +DECLARE + shipment RECORD; + service_item RECORD; + escalated_price NUMERIC; + estimated_price NUMERIC; + o_rate_area_id UUID; + d_rate_area_id UUID; + contract_id UUID; + service_code TEXT; + o_zip_code TEXT; + d_zip_code TEXT; + distance NUMERIC; -- This will be replaced by mileage + estimated_fsc_multiplier NUMERIC; + fuel_price NUMERIC; + cents_above_baseline NUMERIC; + price_difference NUMERIC; +BEGIN + SELECT ms.id, ms.pickup_address_id, ms.destination_address_id, ms.requested_pickup_date, ms.prime_estimated_weight + INTO shipment + FROM mto_shipments ms + WHERE ms.id = shipment_id; + + IF shipment IS NULL THEN + RAISE EXCEPTION ''Shipment with ID % not found'', shipment_id; + END IF; + + -- exit the proc if prime_estimated_weight is NULL + IF shipment.prime_estimated_weight IS NULL THEN + RETURN; + END IF; + + -- loop through service items in the shipment + FOR service_item IN + SELECT si.id, si.re_service_id + FROM mto_service_items si + WHERE si.mto_shipment_id = shipment_id + LOOP + -- get the service code for the current service item to determine calculation + SELECT code + INTO service_code + FROM re_services + WHERE id = service_item.re_service_id; + + CASE + WHEN service_code IN (''ISLH'', ''UBP'') THEN + contract_id := get_contract_id(shipment.requested_pickup_date); + o_rate_area_id := get_rate_area_id(shipment.pickup_address_id, service_item.re_service_id, contract_id); + d_rate_area_id := get_rate_area_id(shipment.destination_address_id, service_item.re_service_id, contract_id); + escalated_price := calculate_escalated_price(o_rate_area_id, d_rate_area_id, service_item.re_service_id, contract_id, service_code); + + IF shipment.prime_estimated_weight IS NOT NULL THEN + estimated_price := ROUND((escalated_price * (shipment.prime_estimated_weight / 100)::NUMERIC) * 100, 0); + RAISE NOTICE ''%: Received estimated price of % (% * (% / 100)) cents'', service_code, estimated_price, escalated_price, shipment.prime_estimated_weight; + -- update the pricing_estimate value in mto_service_items + UPDATE mto_service_items + SET pricing_estimate = estimated_price + WHERE id = service_item.id; + END IF; + + WHEN service_code IN (''IHPK'', ''IUBPK'') THEN + -- perform IHPK/IUBPK-specific logic (no destination rate area) + contract_id := get_contract_id(shipment.requested_pickup_date); + o_rate_area_id := get_rate_area_id(shipment.pickup_address_id, service_item.re_service_id, contract_id); + escalated_price := calculate_escalated_price(o_rate_area_id, NULL, service_item.re_service_id, contract_id, service_code); + + IF shipment.prime_estimated_weight IS NOT NULL THEN + estimated_price := ROUND((escalated_price * (shipment.prime_estimated_weight / 100)::NUMERIC) * 100, 0); + RAISE NOTICE ''%: Received estimated price of % (% * (% / 100)) cents'', service_code, estimated_price, escalated_price, shipment.prime_estimated_weight; + -- update the pricing_estimate value in mto_service_items + UPDATE mto_service_items + SET pricing_estimate = estimated_price + WHERE id = service_item.id; + END IF; + + WHEN service_code IN (''IHUPK'', ''IUBUPK'') THEN + -- perform IHUPK/IUBUPK-specific logic (no origin rate area) + contract_id := get_contract_id(shipment.requested_pickup_date); + d_rate_area_id := get_rate_area_id(shipment.destination_address_id, service_item.re_service_id, contract_id); + escalated_price := calculate_escalated_price(NULL, d_rate_area_id, service_item.re_service_id, contract_id, service_code); + + IF shipment.prime_estimated_weight IS NOT NULL THEN + estimated_price := ROUND((escalated_price * (shipment.prime_estimated_weight / 100)::NUMERIC) * 100, 0); + RAISE NOTICE ''%: Received estimated price of % (% * (% / 100)) cents'', service_code, estimated_price, escalated_price, shipment.prime_estimated_weight; + -- update the pricing_estimate value in mto_service_items + UPDATE mto_service_items + SET pricing_estimate = estimated_price + WHERE id = service_item.id; + END IF; + + WHEN service_code IN (''POEFSC'', ''PODFSC'') THEN + -- use the passed mileage parameter + distance = mileage; + + -- getting FSC multiplier from re_fsc_multipliers + estimated_fsc_multiplier := get_fsc_multiplier(shipment.prime_estimated_weight); + + fuel_price := get_fuel_price(shipment.requested_pickup_date); + + price_difference := calculate_price_difference(fuel_price); + + -- calculate estimated price, return as cents + IF estimated_fsc_multiplier IS NOT NULL AND distance IS NOT NULL THEN + cents_above_baseline := distance * estimated_fsc_multiplier; + RAISE NOTICE ''Distance: % * FSC Multipler: % = $% cents above baseline of $2.50'', distance, estimated_fsc_multiplier, cents_above_baseline; + RAISE NOTICE ''The fuel price is % cents above the baseline ($% - $2.50 baseline)'', price_difference, fuel_price; + estimated_price := ROUND((cents_above_baseline * price_difference) * 100); + RAISE NOTICE ''Received estimated price of % cents for service_code: %.'', estimated_price, service_code; + + -- update the pricing_estimate value in mto_service_items + UPDATE mto_service_items + SET pricing_estimate = estimated_price + WHERE id = service_item.id; + END IF; + END CASE; + END LOOP; +END; +' +LANGUAGE plpgsql; \ No newline at end of file diff --git a/pkg/assets/sql_scripts/move_history_fetcher.sql b/pkg/assets/sql_scripts/move_history_fetcher.sql index 3f981dd6e70..dacacf55d78 100644 --- a/pkg/assets/sql_scripts/move_history_fetcher.sql +++ b/pkg/assets/sql_scripts/move_history_fetcher.sql @@ -64,7 +64,6 @@ WITH move AS ( -- Remove log for when shipment_seq_num updates AND NOT (audit_history.event_name = NULL AND audit_history.changed_data::TEXT LIKE '%shipment_seq_num%' AND LENGTH(audit_history.changed_data::TEXT) < 25) group by audit_history.id - ), move_orders AS ( SELECT @@ -181,7 +180,6 @@ WITH move AS ( audit_history JOIN service_item_dimensions ON service_item_dimensions.id = audit_history.object_id WHERE audit_history.table_name = 'mto_service_item_dimensions' - ), move_entitlements AS ( SELECT @@ -350,7 +348,8 @@ WITH move AS ( NULL, move_shipments.shipment_locator FROM audit_history - JOIN move_shipments ON move_shipments.destination_address_id = audit_history.object_id AND audit_history."table_name" = 'addresses' + JOIN shipment_logs ON (shipment_logs.changed_data->>'destination_address_id')::uuid = audit_history.object_id AND audit_history."table_name" = 'addresses' + JOIN move_shipments ON shipment_logs.object_id = move_shipments.id UNION SELECT audit_history.object_id, @@ -360,8 +359,9 @@ WITH move AS ( NULL, move_shipments.shipment_locator FROM audit_history - JOIN move_shipments ON move_shipments.secondary_delivery_address_id = audit_history.object_id AND audit_history."table_name" = 'addresses' - UNION + JOIN shipment_logs ON (shipment_logs.changed_data->>'secondary_delivery_address_id')::uuid = audit_history.object_id AND audit_history."table_name" = 'addresses' + JOIN move_shipments ON shipment_logs.object_id = move_shipments.id + UNION SELECT audit_history.object_id, 'tertiaryDestinationAddress', @@ -370,8 +370,9 @@ WITH move AS ( NULL, move_shipments.shipment_locator FROM audit_history - JOIN move_shipments ON move_shipments.tertiary_delivery_address_id = audit_history.object_id AND audit_history."table_name" = 'addresses' - UNION + JOIN shipment_logs ON (shipment_logs.changed_data->>'tertiary_delivery_address_id')::uuid = audit_history.object_id AND audit_history."table_name" = 'addresses' + JOIN move_shipments ON shipment_logs.object_id = move_shipments.id + UNION SELECT audit_history.object_id, 'pickupAddress', @@ -380,8 +381,9 @@ WITH move AS ( NULL, move_shipments.shipment_locator FROM audit_history - JOIN move_shipments ON move_shipments.pickup_address_id = audit_history.object_id AND audit_history."table_name" = 'addresses' - UNION + JOIN shipment_logs ON (shipment_logs.changed_data->>'pickup_address_id')::uuid = audit_history.object_id AND audit_history."table_name" = 'addresses' + JOIN move_shipments ON shipment_logs.object_id = move_shipments.id + UNION SELECT audit_history.object_id, 'secondaryPickupAddress', @@ -390,7 +392,8 @@ WITH move AS ( NULL, move_shipments.shipment_locator FROM audit_history - JOIN move_shipments ON move_shipments.secondary_pickup_address_id = audit_history.object_id AND audit_history."table_name" = 'addresses' + JOIN shipment_logs ON (shipment_logs.changed_data->>'secondary_pickup_address_id')::uuid = audit_history.object_id AND audit_history."table_name" = 'addresses' + JOIN move_shipments ON shipment_logs.object_id = move_shipments.id UNION SELECT audit_history.object_id, @@ -400,7 +403,8 @@ WITH move AS ( NULL, move_shipments.shipment_locator FROM audit_history - JOIN move_shipments ON move_shipments.tertiary_pickup_address_id = audit_history.object_id AND audit_history."table_name" = 'addresses' + JOIN shipment_logs ON (shipment_logs.changed_data->>'tertiary_pickup_address_id')::uuid = audit_history.object_id AND audit_history."table_name" = 'addresses' + JOIN move_shipments ON shipment_logs.object_id = move_shipments.id UNION SELECT audit_history.object_id, @@ -491,7 +495,6 @@ WITH move AS ( JOIN move_orders ON move_orders.uploaded_orders_id = documents.id LEFT JOIN uploads ON user_uploads.upload_id = uploads.id WHERE documents.service_member_id = move_orders.service_member_id - -- amended orders have the document id in the uploaded amended orders id column UNION SELECT @@ -507,7 +510,6 @@ WITH move AS ( JOIN move_orders ON move_orders.uploaded_amended_orders_id = documents.id LEFT JOIN uploads ON user_uploads.upload_id = uploads.id WHERE documents.service_member_id = move_orders.service_member_id - UNION SELECT user_uploads.id, @@ -732,7 +734,8 @@ WITH move AS ( gsr_appeals_logs - ) SELECT DISTINCT + ) +SELECT DISTINCT combined_logs.*, COALESCE(office_users.first_name, prime_user_first_name, service_members.first_name) AS session_user_first_name, COALESCE(office_users.last_name, service_members.last_name) AS session_user_last_name, @@ -748,4 +751,4 @@ FROM SELECT 'Prime' AS prime_user_first_name ) prime_users ON roles.role_type = 'prime' ORDER BY - action_tstamp_tx DESC + action_tstamp_tx DESC \ No newline at end of file diff --git a/pkg/factory/address_factory.go b/pkg/factory/address_factory.go index 91a49da4445..27d92999d00 100644 --- a/pkg/factory/address_factory.go +++ b/pkg/factory/address_factory.go @@ -1,7 +1,10 @@ package factory import ( + "database/sql" + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/testdatagen" @@ -24,15 +27,17 @@ func BuildAddress(db *pop.Connection, customs []Customization, traits []Trait) m } // Create default Address + beverlyHillsUsprc := uuid.FromStringOrNil("3b9f0ae6-3b2b-44a6-9fcd-8ead346648c4") address := models.Address{ - StreetAddress1: "123 Any Street", - StreetAddress2: models.StringPointer("P.O. Box 12345"), - StreetAddress3: models.StringPointer("c/o Some Person"), - City: "Beverly Hills", - State: "CA", - PostalCode: "90210", - County: models.StringPointer("LOS ANGELES"), - IsOconus: models.BoolPointer(false), + StreetAddress1: "123 Any Street", + StreetAddress2: models.StringPointer("P.O. Box 12345"), + StreetAddress3: models.StringPointer("c/o Some Person"), + City: "Beverly Hills", + State: "CA", + PostalCode: "90210", + County: models.StringPointer("LOS ANGELES"), + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &beverlyHillsUsprc, } // Find/create the Country if customization is provided @@ -56,7 +61,7 @@ func BuildAddress(db *pop.Connection, customs []Customization, traits []Trait) m // Overwrite values with those from customizations testdatagen.MergeModels(&address, cAddress) - // This helps assign counties when the factory is called for seed data or tests + // This helps assign counties & us_post_region_cities_id values when the factory is called for seed data or tests // Additionally, also only run if not 90210. 90210's county is by default populated if db != nil && address.PostalCode != "90210" { county, err := models.FindCountyByZipCode(db, address.PostalCode) @@ -72,6 +77,17 @@ func BuildAddress(db *pop.Connection, customs []Customization, traits []Trait) m address.County = models.StringPointer("db nil when created") } + if db != nil && address.PostalCode != "90210" && cAddress.UsPostRegionCityID == nil { + usprc, err := models.FindByZipCode(db, address.PostalCode) + if err != nil && err != sql.ErrNoRows { + address.UsPostRegionCityID = nil + address.UsPostRegionCity = nil + } else if usprc.ID != uuid.Nil { + address.UsPostRegionCityID = &usprc.ID + address.UsPostRegionCity = usprc + } + } + // If db is false, it's a stub. No need to create in database. if db != nil { mustCreate(db, &address) diff --git a/pkg/factory/address_factory_test.go b/pkg/factory/address_factory_test.go index 2e17e564605..7f72a13e9b6 100644 --- a/pkg/factory/address_factory_test.go +++ b/pkg/factory/address_factory_test.go @@ -40,6 +40,7 @@ func (suite *FactorySuite) TestBuildAddress() { suite.Equal(defaultPostalCode, address.PostalCode) suite.Equal(country.ID, *address.CountryId) suite.Equal(defaultCounty, *address.County) + suite.NotNil(*address.UsPostRegionCityID) }) suite.Run("Successful creation of an address with customization", func() { diff --git a/pkg/factory/admin_user_factory.go b/pkg/factory/admin_user_factory.go index 3cd1b1e752f..df04ea8ce24 100644 --- a/pkg/factory/admin_user_factory.go +++ b/pkg/factory/admin_user_factory.go @@ -52,6 +52,50 @@ func BuildAdminUser(db *pop.Connection, customs []Customization, traits []Trait) return adminUser } +// BuildSuperAdminUser creates an AdminUser with Super privileges +// Also creates, if not provided +// - User +// +// Params: +// - customs is a slice that will be modified by the factory +// - db can be set to nil to create a stubbed model that is not stored in DB. +func BuildSuperAdminUser(db *pop.Connection, customs []Customization, traits []Trait) models.AdminUser { + customs = setupCustomizations(customs, traits) + + // Find adminuser assertion and convert to models adminuser + var cAdminUser models.AdminUser + if result := findValidCustomization(customs, AdminUser); result != nil { + cAdminUser = result.Model.(models.AdminUser) + if result.LinkOnly { + return cAdminUser + } + } + + // Create the associated user model + user := BuildActiveUser(db, customs, nil) + + // create adminuser + adminUser := models.AdminUser{ + UserID: &user.ID, + User: user, + FirstName: "Leo", + LastName: "Spaceman", + Email: "super_leo_spaceman_admin@example.com", + Role: "SYSTEM_ADMIN", + Super: true, + Active: true, + } + // Overwrite values with those from assertions + testdatagen.MergeModels(&adminUser, cAdminUser) + + // If db is false, it's a stub. No need to create in database + if db != nil { + mustCreate(db, &adminUser) + } + + return adminUser +} + // BuildDefaultAdminUser returns an admin user with appropriate email // Also creates // - User @@ -59,6 +103,13 @@ func BuildDefaultAdminUser(db *pop.Connection) models.AdminUser { return BuildAdminUser(db, nil, []Trait{GetTraitAdminUserEmail}) } +// BuildDefaultSuperAdminUser returns an admin user with appropriate email and super privs +// Also creates +// - User +func BuildDefaultSuperAdminUser(db *pop.Connection) models.AdminUser { + return BuildSuperAdminUser(db, nil, []Trait{GetTraitAdminUserEmail}) +} + // ------------------------ // TRAITS // ------------------------ diff --git a/pkg/factory/domestic_other_prices.go b/pkg/factory/domestic_other_prices.go new file mode 100644 index 00000000000..b878e926d48 --- /dev/null +++ b/pkg/factory/domestic_other_prices.go @@ -0,0 +1,49 @@ +package factory + +import ( + "database/sql" + "log" + + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" +) + +func FetchOrMakeDomesticOtherPrice(db *pop.Connection, customs []Customization, traits []Trait) models.ReDomesticOtherPrice { + customs = setupCustomizations(customs, traits) + + var cReDomesticOtherPrice models.ReDomesticOtherPrice + if result := findValidCustomization(customs, ReDomesticOtherPrice); result != nil { + cReDomesticOtherPrice = result.Model.(models.ReDomesticOtherPrice) + if result.LinkOnly { + return cReDomesticOtherPrice + } + } + + // fetch first before creating + // the contractID, serviceID, peak, and schedule need to be unique + var reDomesticOtherPrice models.ReDomesticOtherPrice + if cReDomesticOtherPrice.ContractID != uuid.Nil && cReDomesticOtherPrice.ServiceID != uuid.Nil && cReDomesticOtherPrice.Schedule != 0 { + err := db.Where("contract_id = ? AND service_id = ? AND is_peak_period = ? AND schedule = ?", + cReDomesticOtherPrice.ContractID, + cReDomesticOtherPrice.ServiceID, + cReDomesticOtherPrice.IsPeakPeriod, + cReDomesticOtherPrice.Schedule). + First(&reDomesticOtherPrice) + if err != nil && err != sql.ErrNoRows { + log.Panic(err) + } + return reDomesticOtherPrice + } + + // Overwrite values with those from customizations + testdatagen.MergeModels(&reDomesticOtherPrice, cReDomesticOtherPrice) + + // If db is false, it's a stub. No need to create in database + if db != nil { + mustCreate(db, &reDomesticOtherPrice) + } + return reDomesticOtherPrice +} diff --git a/pkg/factory/domestic_other_prices_test.go b/pkg/factory/domestic_other_prices_test.go new file mode 100644 index 00000000000..a1e0ae08f11 --- /dev/null +++ b/pkg/factory/domestic_other_prices_test.go @@ -0,0 +1,30 @@ +package factory + +import ( + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/unit" +) + +func (suite *FactorySuite) TestFetchOrMakeDomesticOtherPrice() { + suite.Run("Successful fetch of domestic other price", func() { + + id, err := uuid.FromString("51393fa4-b31c-40fe-bedf-b692703c46eb") + suite.NoError(err) + reService := FetchReServiceByCode(suite.DB(), models.ReServiceCodeDLH) + + domesticOtherPrice := FetchOrMakeDomesticOtherPrice(suite.DB(), []Customization{ + { + Model: models.ReDomesticOtherPrice{ + ContractID: id, + ServiceID: reService.ID, + IsPeakPeriod: true, + Schedule: 1, + PriceCents: unit.Cents(945), + }, + }, + }, nil) + suite.NotNil(domesticOtherPrice) + }) +} diff --git a/pkg/factory/domestic_service_area_price.go b/pkg/factory/domestic_service_area_price.go new file mode 100644 index 00000000000..ff9110849a8 --- /dev/null +++ b/pkg/factory/domestic_service_area_price.go @@ -0,0 +1,45 @@ +package factory + +import ( + "database/sql" + "log" + + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" +) + +func FetchOrMakeDomesticServiceAreaPrice(db *pop.Connection, customs []Customization, traits []Trait) models.ReDomesticServiceAreaPrice { + customs = setupCustomizations(customs, traits) + + var cReDomesticServiceAreaPrice models.ReDomesticServiceAreaPrice + if result := findValidCustomization(customs, ReDomesticServiceAreaPrice); result != nil { + cReDomesticServiceAreaPrice = result.Model.(models.ReDomesticServiceAreaPrice) + if result.LinkOnly { + return cReDomesticServiceAreaPrice + } + } + + // fetch first before creating + // the contractID, serviceID, peak, and schedule need to be unique + var reDomesticServiceAreaPrice models.ReDomesticServiceAreaPrice + if cReDomesticServiceAreaPrice.ContractID != uuid.Nil && cReDomesticServiceAreaPrice.ServiceID != uuid.Nil && cReDomesticServiceAreaPrice.DomesticServiceAreaID != uuid.Nil { + err := db.Where("contract_id = ? AND service_id = ? AND domestic_service_area_id = ? AND is_peak_period = ?", + cReDomesticServiceAreaPrice.ContractID, + cReDomesticServiceAreaPrice.ServiceID, + cReDomesticServiceAreaPrice.DomesticServiceAreaID, + cReDomesticServiceAreaPrice.IsPeakPeriod). + First(&reDomesticServiceAreaPrice) + if err != nil && err != sql.ErrNoRows { + log.Panic(err) + } else if err == sql.ErrNoRows { + // if it isn't found, then we need to create it + testdatagen.MergeModels(&reDomesticServiceAreaPrice, cReDomesticServiceAreaPrice) + mustCreate(db, &reDomesticServiceAreaPrice) + } + return reDomesticServiceAreaPrice + } + return reDomesticServiceAreaPrice +} diff --git a/pkg/factory/domestic_service_area_price_test.go b/pkg/factory/domestic_service_area_price_test.go new file mode 100644 index 00000000000..11f0258c8b4 --- /dev/null +++ b/pkg/factory/domestic_service_area_price_test.go @@ -0,0 +1,38 @@ +package factory + +import ( + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +func (suite *FactorySuite) TestFetchOrMakeDomesticServiceAreaPrice() { + suite.Run("Successful fetch of domestic service area price", func() { + + id, err := uuid.FromString("51393fa4-b31c-40fe-bedf-b692703c46eb") + suite.NoError(err) + reService := FetchReServiceByCode(suite.DB(), models.ReServiceCodeDLH) + serviceArea := testdatagen.FetchOrMakeReDomesticServiceArea(suite.DB(), testdatagen.Assertions{ + ReDomesticServiceArea: models.ReDomesticServiceArea{ + ServiceArea: "004", + ServicesSchedule: 2, + }, + ReContract: testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}), + }) + + domesticServiceAreaPrice := FetchOrMakeDomesticServiceAreaPrice(suite.DB(), []Customization{ + { + Model: models.ReDomesticServiceAreaPrice{ + ContractID: id, + ServiceID: reService.ID, + DomesticServiceAreaID: serviceArea.ID, + IsPeakPeriod: true, + PriceCents: unit.Cents(945), + }, + }, + }, nil) + suite.NotNil(domesticServiceAreaPrice) + }) +} diff --git a/pkg/factory/shared.go b/pkg/factory/shared.go index bc49aed27ff..33cb656197b 100644 --- a/pkg/factory/shared.go +++ b/pkg/factory/shared.go @@ -80,6 +80,8 @@ var PrimeUpload CustomType = "PrimeUpload" var ProgearWeightTicket CustomType = "ProgearWeightTicket" var ProofOfServiceDoc CustomType = "ProofOfServiceDoc" var ReService CustomType = "ReService" +var ReDomesticOtherPrice CustomType = "ReDomesticOtherPrice" +var ReDomesticServiceAreaPrice CustomType = "ReDomesticServiceAreaPrice" var Role CustomType = "Role" var ServiceItemParamKey CustomType = "ServiceItemParamKey" var ServiceParam CustomType = "ServiceParam" @@ -146,6 +148,8 @@ var defaultTypesMap = map[string]CustomType{ "models.ProgearWeightTicket": ProgearWeightTicket, "models.ProofOfServiceDoc": ProofOfServiceDoc, "models.ReService": ReService, + "models.ReDomesticOtherPrice": ReDomesticOtherPrice, + "models.ReDomesticServiceAreaPrice": ReDomesticServiceAreaPrice, "models.ServiceItemParamKey": ServiceItemParamKey, "models.ServiceMember": ServiceMember, "models.ServiceRequestDocument": ServiceRequestDocument, diff --git a/pkg/factory/user_factory.go b/pkg/factory/user_factory.go index 269021160ff..05aea8aef76 100644 --- a/pkg/factory/user_factory.go +++ b/pkg/factory/user_factory.go @@ -48,6 +48,42 @@ func BuildUser(db *pop.Connection, customs []Customization, traits []Trait) mode return user } +// BuildActiveUser creates a User +// It does not create Roles or UsersRoles. To create a User associated with certain roles, use BuildUserAndUsersRoles +// Params: +// - customs is a slice that will be modified by the factory +// - db can be set to nil to create a stubbed model that is not stored in DB. +func BuildActiveUser(db *pop.Connection, customs []Customization, traits []Trait) models.User { + customs = setupCustomizations(customs, traits) + + // Find user assertion and convert to models user + var cUser models.User + if result := findValidCustomization(customs, User); result != nil { + cUser = result.Model.(models.User) + if result.LinkOnly { + return cUser + } + } + + // create user + OktaID := MakeRandomString(20) + user := models.User{ + OktaID: OktaID, + OktaEmail: "first.last@okta.mil", + Active: true, + } + + // Overwrite values with those from assertions + testdatagen.MergeModels(&user, cUser) + + // If db is false, it's a stub. No need to create in database + if db != nil { + mustCreate(db, &user) + } + + return user +} + // BuildUserAndUsersRoles creates a User // - If the user has Roles in the customizations, Roles and UsersRoles will also be created // diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 361f666c222..3b8e14d3e66 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -6403,6 +6403,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true @@ -23143,6 +23148,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 c1351734062..53aee4aa8cf 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 52039d783cc..3f034c7a1f9 100644 --- a/pkg/gen/primeapi/embedded_spec.go +++ b/pkg/gen/primeapi/embedded_spec.go @@ -1063,7 +1063,7 @@ func init() { }, "/payment-requests": { "post": { - "description": "Creates a new instance of a paymentRequest and is assigned the status ` + "`" + `PENDING` + "`" + `.\nA move task order can have multiple payment requests, and\na final payment request can be marked using boolean ` + "`" + `isFinal` + "`" + `.\n\nIf a ` + "`" + `PENDING` + "`" + ` payment request is recalculated,\na new payment request is created and the original request is\nmarked with the status ` + "`" + `DEPRECATED` + "`" + `.\n\n**NOTE**: In order to create a payment request for most service items, the shipment *must*\nbe updated with the ` + "`" + `PrimeActualWeight` + "`" + ` value via [updateMTOShipment](#operation/updateMTOShipment).\n\n**FSC - Fuel Surcharge** service items require ` + "`" + `ActualPickupDate` + "`" + ` to be updated on the shipment.\n\nA service item can be on several payment requests in the case of partial payment requests and payments.\n\nIn the request, if no params are necessary, then just the ` + "`" + `serviceItem` + "`" + ` ` + "`" + `id` + "`" + ` is required. For example:\n` + "`" + `` + "`" + `` + "`" + `json\n{\n \"isFinal\": false,\n \"moveTaskOrderID\": \"uuid\",\n \"serviceItems\": [\n {\n \"id\": \"uuid\",\n },\n {\n \"id\": \"uuid\",\n \"params\": [\n {\n \"key\": \"Service Item Parameter Name\",\n \"value\": \"Service Item Parameter Value\"\n }\n ]\n }\n ],\n \"pointOfContact\": \"string\"\n}\n` + "`" + `` + "`" + `` + "`" + `\n\nSIT Service Items \u0026 Accepted Payment Request Parameters:\n---\nIf ` + "`" + `WeightBilled` + "`" + ` is not provided then the full shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) will be considered in the calculation.\n\n**NOTE**: Diversions have a unique calcuation for payment requests without a ` + "`" + `WeightBilled` + "`" + ` parameter.\n\nIf you created a payment request for a diversion and ` + "`" + `WeightBilled` + "`" + ` is not provided, then the following will be used in the calculation:\n- The lowest shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) found in the diverted shipment chain.\n- The lowest reweigh weight found in the diverted shipment chain.\n\nThe diverted shipment chain is created by referencing the ` + "`" + `diversion` + "`" + ` boolean, ` + "`" + `divertedFromShipmentId` + "`" + ` UUID, and matching destination to pickup addresses.\nIf the chain cannot be established it will fall back to the ` + "`" + `PrimeActualWeight` + "`" + ` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations.\nThe lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found.\n\n**DOFSIT - Domestic origin 1st day SIT**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOASIT - Domestic origin add'l SIT** *(SITPaymentRequestStart \u0026 SITPaymentRequestEnd are **REQUIRED**)*\n*To create a paymentRequest for this service item, the ` + "`" + `SITPaymentRequestStart` + "`" + ` and ` + "`" + `SITPaymentRequestEnd` + "`" + ` dates must not overlap previously requested SIT dates.*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n },\n {\n \"key\": \"SITPaymentRequestStart\",\n \"value\": \"date\"\n },\n {\n \"key\": \"SITPaymentRequestEnd\",\n \"value\": \"date\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOPSIT - Domestic origin SIT pickup**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOSHUT - Domestic origin shuttle service**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDFSIT - Domestic destination 1st day SIT**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDASIT - Domestic destination add'l SIT** *(SITPaymentRequestStart \u0026 SITPaymentRequestEnd are **REQUIRED**)*\n*To create a paymentRequest for this service item, the ` + "`" + `SITPaymentRequestStart` + "`" + ` and ` + "`" + `SITPaymentRequestEnd` + "`" + ` dates must not overlap previously requested SIT dates.*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n },\n {\n \"key\": \"SITPaymentRequestStart\",\n \"value\": \"date\"\n },\n {\n \"key\": \"SITPaymentRequestEnd\",\n \"value\": \"date\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDDSIT - Domestic destination SIT delivery**\n*To create a paymentRequest for this service item, it must first have a final address set via [updateMTOServiceItem](#operation/updateMTOServiceItem).*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDSHUT - Domestic destination shuttle service**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n---\n", + "description": "Creates a new instance of a paymentRequest and is assigned the status ` + "`" + `PENDING` + "`" + `.\nA move task order can have multiple payment requests, and\na final payment request can be marked using boolean ` + "`" + `isFinal` + "`" + `.\n\nIf a ` + "`" + `PENDING` + "`" + ` payment request is recalculated,\na new payment request is created and the original request is\nmarked with the status ` + "`" + `DEPRECATED` + "`" + `.\n\n**NOTE**: In order to create a payment request for most service items, the shipment *must*\nbe updated with the ` + "`" + `PrimeActualWeight` + "`" + ` value via [updateMTOShipment](#operation/updateMTOShipment).\n\nIf ` + "`" + `WeightBilled` + "`" + ` is not provided then the full shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) will be considered in the calculation.\n\n**NOTE**: Diversions have a unique calcuation for payment requests without a ` + "`" + `WeightBilled` + "`" + ` parameter.\n\nIf you created a payment request for a diversion and ` + "`" + `WeightBilled` + "`" + ` is not provided, then the following will be used in the calculation:\n- The lowest shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) found in the diverted shipment chain.\n- The lowest reweigh weight found in the diverted shipment chain.\n\nThe diverted shipment chain is created by referencing the ` + "`" + `diversion` + "`" + ` boolean, ` + "`" + `divertedFromShipmentId` + "`" + ` UUID, and matching destination to pickup addresses.\nIf the chain cannot be established it will fall back to the ` + "`" + `PrimeActualWeight` + "`" + ` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations.\nThe lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found.\n\nA service item can be on several payment requests in the case of partial payment requests and payments.\n\nIn the request, if no params are necessary, then just the ` + "`" + `serviceItem` + "`" + ` ` + "`" + `id` + "`" + ` is required. For example:\n` + "`" + `` + "`" + `` + "`" + `json\n{\n \"isFinal\": false,\n \"moveTaskOrderID\": \"uuid\",\n \"serviceItems\": [\n {\n \"id\": \"uuid\",\n },\n {\n \"id\": \"uuid\",\n \"params\": [\n {\n \"key\": \"Service Item Parameter Name\",\n \"value\": \"Service Item Parameter Value\"\n }\n ]\n }\n ],\n \"pointOfContact\": \"string\"\n}\n` + "`" + `` + "`" + `` + "`" + `\n\nDomestic Basic Service Items \u0026 Accepted Payment Request Parameters:\n---\n\n**DLH - Domestic Linehaul**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DSH - Domestic Shorthaul**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**FSC - Fuel Surcharge**\n**NOTE**: FSC requires ` + "`" + `ActualPickupDate` + "`" + ` to be updated on the shipment.\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DUPK - Domestic Unpacking**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DPK - Domestic Packing**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DNPK - Domestic NTS Packing**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DPK - Domestic Packing**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOP - Domestic Origin Price**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDP - Domestic Destination Price**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\nDomestic SIT Service Items \u0026 Accepted Payment Request Parameters:\n---\n\n**DOFSIT - Domestic origin 1st day SIT**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOASIT - Domestic origin add'l SIT** *(SITPaymentRequestStart \u0026 SITPaymentRequestEnd are **REQUIRED**)*\n*To create a paymentRequest for this service item, the ` + "`" + `SITPaymentRequestStart` + "`" + ` and ` + "`" + `SITPaymentRequestEnd` + "`" + ` dates must not overlap previously requested SIT dates.*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n },\n {\n \"key\": \"SITPaymentRequestStart\",\n \"value\": \"date\"\n },\n {\n \"key\": \"SITPaymentRequestEnd\",\n \"value\": \"date\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOPSIT - Domestic origin SIT pickup**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOSHUT - Domestic origin shuttle service**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDFSIT - Domestic destination 1st day SIT**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDASIT - Domestic destination add'l SIT** *(SITPaymentRequestStart \u0026 SITPaymentRequestEnd are **REQUIRED**)*\n*To create a paymentRequest for this service item, the ` + "`" + `SITPaymentRequestStart` + "`" + ` and ` + "`" + `SITPaymentRequestEnd` + "`" + ` dates must not overlap previously requested SIT dates.*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n },\n {\n \"key\": \"SITPaymentRequestStart\",\n \"value\": \"date\"\n },\n {\n \"key\": \"SITPaymentRequestEnd\",\n \"value\": \"date\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDDSIT - Domestic destination SIT delivery**\n*To create a paymentRequest for this service item, it must first have a final address set via [updateMTOServiceItem](#operation/updateMTOServiceItem).*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDSHUT - Domestic destination shuttle service**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n---\n\nInternational Basic Service Items \u0026 Accepted Payment Request Parameters:\n---\nJust like domestic shipments \u0026 service items, if ` + "`" + `WeightBilled` + "`" + ` is not provided then the full shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) will be considered in the calculation.\n**NOTE**: ` + "`" + `POEFSC` + "`" + ` \u0026 ` + "`" + `PODFSC` + "`" + ` service items must have a port associated on the service item in order to successfully add it to a payment request. To update the port of a service item, you must use the (#operation/updateMTOServiceItem) endpoint.\n\n**ISLH - International Shipping \u0026 Linehaul**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**IHPK - International HHG Pack**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**IHUPK - International HHG Unpack**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**POEFSC - International Port of Embarkation Fuel Surcharge**\n **NOTE**: POEFSC requires ` + "`" + `ActualPickupDate` + "`" + ` to be updated on the shipment \u0026 ` + "`" + `POELocation` + "`" + ` on the service item.\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**PODFSC - International Port of Debarkation Fuel Surcharge**\n**NOTE**: PODFSC requires ` + "`" + `ActualPickupDate` + "`" + ` to be updated on the shipment \u0026 ` + "`" + `PODLocation` + "`" + ` on the service item.\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n---\n", "consumes": [ "application/json" ], @@ -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 @@ -5921,7 +5926,7 @@ func init() { }, "/payment-requests": { "post": { - "description": "Creates a new instance of a paymentRequest and is assigned the status ` + "`" + `PENDING` + "`" + `.\nA move task order can have multiple payment requests, and\na final payment request can be marked using boolean ` + "`" + `isFinal` + "`" + `.\n\nIf a ` + "`" + `PENDING` + "`" + ` payment request is recalculated,\na new payment request is created and the original request is\nmarked with the status ` + "`" + `DEPRECATED` + "`" + `.\n\n**NOTE**: In order to create a payment request for most service items, the shipment *must*\nbe updated with the ` + "`" + `PrimeActualWeight` + "`" + ` value via [updateMTOShipment](#operation/updateMTOShipment).\n\n**FSC - Fuel Surcharge** service items require ` + "`" + `ActualPickupDate` + "`" + ` to be updated on the shipment.\n\nA service item can be on several payment requests in the case of partial payment requests and payments.\n\nIn the request, if no params are necessary, then just the ` + "`" + `serviceItem` + "`" + ` ` + "`" + `id` + "`" + ` is required. For example:\n` + "`" + `` + "`" + `` + "`" + `json\n{\n \"isFinal\": false,\n \"moveTaskOrderID\": \"uuid\",\n \"serviceItems\": [\n {\n \"id\": \"uuid\",\n },\n {\n \"id\": \"uuid\",\n \"params\": [\n {\n \"key\": \"Service Item Parameter Name\",\n \"value\": \"Service Item Parameter Value\"\n }\n ]\n }\n ],\n \"pointOfContact\": \"string\"\n}\n` + "`" + `` + "`" + `` + "`" + `\n\nSIT Service Items \u0026 Accepted Payment Request Parameters:\n---\nIf ` + "`" + `WeightBilled` + "`" + ` is not provided then the full shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) will be considered in the calculation.\n\n**NOTE**: Diversions have a unique calcuation for payment requests without a ` + "`" + `WeightBilled` + "`" + ` parameter.\n\nIf you created a payment request for a diversion and ` + "`" + `WeightBilled` + "`" + ` is not provided, then the following will be used in the calculation:\n- The lowest shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) found in the diverted shipment chain.\n- The lowest reweigh weight found in the diverted shipment chain.\n\nThe diverted shipment chain is created by referencing the ` + "`" + `diversion` + "`" + ` boolean, ` + "`" + `divertedFromShipmentId` + "`" + ` UUID, and matching destination to pickup addresses.\nIf the chain cannot be established it will fall back to the ` + "`" + `PrimeActualWeight` + "`" + ` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations.\nThe lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found.\n\n**DOFSIT - Domestic origin 1st day SIT**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOASIT - Domestic origin add'l SIT** *(SITPaymentRequestStart \u0026 SITPaymentRequestEnd are **REQUIRED**)*\n*To create a paymentRequest for this service item, the ` + "`" + `SITPaymentRequestStart` + "`" + ` and ` + "`" + `SITPaymentRequestEnd` + "`" + ` dates must not overlap previously requested SIT dates.*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n },\n {\n \"key\": \"SITPaymentRequestStart\",\n \"value\": \"date\"\n },\n {\n \"key\": \"SITPaymentRequestEnd\",\n \"value\": \"date\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOPSIT - Domestic origin SIT pickup**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOSHUT - Domestic origin shuttle service**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDFSIT - Domestic destination 1st day SIT**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDASIT - Domestic destination add'l SIT** *(SITPaymentRequestStart \u0026 SITPaymentRequestEnd are **REQUIRED**)*\n*To create a paymentRequest for this service item, the ` + "`" + `SITPaymentRequestStart` + "`" + ` and ` + "`" + `SITPaymentRequestEnd` + "`" + ` dates must not overlap previously requested SIT dates.*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n },\n {\n \"key\": \"SITPaymentRequestStart\",\n \"value\": \"date\"\n },\n {\n \"key\": \"SITPaymentRequestEnd\",\n \"value\": \"date\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDDSIT - Domestic destination SIT delivery**\n*To create a paymentRequest for this service item, it must first have a final address set via [updateMTOServiceItem](#operation/updateMTOServiceItem).*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDSHUT - Domestic destination shuttle service**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n---\n", + "description": "Creates a new instance of a paymentRequest and is assigned the status ` + "`" + `PENDING` + "`" + `.\nA move task order can have multiple payment requests, and\na final payment request can be marked using boolean ` + "`" + `isFinal` + "`" + `.\n\nIf a ` + "`" + `PENDING` + "`" + ` payment request is recalculated,\na new payment request is created and the original request is\nmarked with the status ` + "`" + `DEPRECATED` + "`" + `.\n\n**NOTE**: In order to create a payment request for most service items, the shipment *must*\nbe updated with the ` + "`" + `PrimeActualWeight` + "`" + ` value via [updateMTOShipment](#operation/updateMTOShipment).\n\nIf ` + "`" + `WeightBilled` + "`" + ` is not provided then the full shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) will be considered in the calculation.\n\n**NOTE**: Diversions have a unique calcuation for payment requests without a ` + "`" + `WeightBilled` + "`" + ` parameter.\n\nIf you created a payment request for a diversion and ` + "`" + `WeightBilled` + "`" + ` is not provided, then the following will be used in the calculation:\n- The lowest shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) found in the diverted shipment chain.\n- The lowest reweigh weight found in the diverted shipment chain.\n\nThe diverted shipment chain is created by referencing the ` + "`" + `diversion` + "`" + ` boolean, ` + "`" + `divertedFromShipmentId` + "`" + ` UUID, and matching destination to pickup addresses.\nIf the chain cannot be established it will fall back to the ` + "`" + `PrimeActualWeight` + "`" + ` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations.\nThe lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found.\n\nA service item can be on several payment requests in the case of partial payment requests and payments.\n\nIn the request, if no params are necessary, then just the ` + "`" + `serviceItem` + "`" + ` ` + "`" + `id` + "`" + ` is required. For example:\n` + "`" + `` + "`" + `` + "`" + `json\n{\n \"isFinal\": false,\n \"moveTaskOrderID\": \"uuid\",\n \"serviceItems\": [\n {\n \"id\": \"uuid\",\n },\n {\n \"id\": \"uuid\",\n \"params\": [\n {\n \"key\": \"Service Item Parameter Name\",\n \"value\": \"Service Item Parameter Value\"\n }\n ]\n }\n ],\n \"pointOfContact\": \"string\"\n}\n` + "`" + `` + "`" + `` + "`" + `\n\nDomestic Basic Service Items \u0026 Accepted Payment Request Parameters:\n---\n\n**DLH - Domestic Linehaul**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DSH - Domestic Shorthaul**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**FSC - Fuel Surcharge**\n**NOTE**: FSC requires ` + "`" + `ActualPickupDate` + "`" + ` to be updated on the shipment.\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DUPK - Domestic Unpacking**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DPK - Domestic Packing**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DNPK - Domestic NTS Packing**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DPK - Domestic Packing**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOP - Domestic Origin Price**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDP - Domestic Destination Price**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\nDomestic SIT Service Items \u0026 Accepted Payment Request Parameters:\n---\n\n**DOFSIT - Domestic origin 1st day SIT**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOASIT - Domestic origin add'l SIT** *(SITPaymentRequestStart \u0026 SITPaymentRequestEnd are **REQUIRED**)*\n*To create a paymentRequest for this service item, the ` + "`" + `SITPaymentRequestStart` + "`" + ` and ` + "`" + `SITPaymentRequestEnd` + "`" + ` dates must not overlap previously requested SIT dates.*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n },\n {\n \"key\": \"SITPaymentRequestStart\",\n \"value\": \"date\"\n },\n {\n \"key\": \"SITPaymentRequestEnd\",\n \"value\": \"date\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOPSIT - Domestic origin SIT pickup**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DOSHUT - Domestic origin shuttle service**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDFSIT - Domestic destination 1st day SIT**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDASIT - Domestic destination add'l SIT** *(SITPaymentRequestStart \u0026 SITPaymentRequestEnd are **REQUIRED**)*\n*To create a paymentRequest for this service item, the ` + "`" + `SITPaymentRequestStart` + "`" + ` and ` + "`" + `SITPaymentRequestEnd` + "`" + ` dates must not overlap previously requested SIT dates.*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n },\n {\n \"key\": \"SITPaymentRequestStart\",\n \"value\": \"date\"\n },\n {\n \"key\": \"SITPaymentRequestEnd\",\n \"value\": \"date\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDDSIT - Domestic destination SIT delivery**\n*To create a paymentRequest for this service item, it must first have a final address set via [updateMTOServiceItem](#operation/updateMTOServiceItem).*\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**DDSHUT - Domestic destination shuttle service**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n---\n\nInternational Basic Service Items \u0026 Accepted Payment Request Parameters:\n---\nJust like domestic shipments \u0026 service items, if ` + "`" + `WeightBilled` + "`" + ` is not provided then the full shipment weight (` + "`" + `PrimeActualWeight` + "`" + `) will be considered in the calculation.\n**NOTE**: ` + "`" + `POEFSC` + "`" + ` \u0026 ` + "`" + `PODFSC` + "`" + ` service items must have a port associated on the service item in order to successfully add it to a payment request. To update the port of a service item, you must use the (#operation/updateMTOServiceItem) endpoint.\n\n**ISLH - International Shipping \u0026 Linehaul**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**IHPK - International HHG Pack**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**IHUPK - International HHG Unpack**\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**POEFSC - International Port of Embarkation Fuel Surcharge**\n **NOTE**: POEFSC requires ` + "`" + `ActualPickupDate` + "`" + ` to be updated on the shipment \u0026 ` + "`" + `POELocation` + "`" + ` on the service item.\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n\n**PODFSC - International Port of Debarkation Fuel Surcharge**\n**NOTE**: PODFSC requires ` + "`" + `ActualPickupDate` + "`" + ` to be updated on the shipment \u0026 ` + "`" + `PODLocation` + "`" + ` on the service item.\n` + "`" + `` + "`" + `` + "`" + `json\n \"params\": [\n {\n \"key\": \"WeightBilled\",\n \"value\": \"integer\"\n }\n ]\n` + "`" + `` + "`" + `` + "`" + `\n---\n", "consumes": [ "application/json" ], @@ -6108,6 +6113,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/primeapi/primeoperations/payment_request/create_payment_request.go b/pkg/gen/primeapi/primeoperations/payment_request/create_payment_request.go index 1e98d93abe7..d1ca6a38ab8 100644 --- a/pkg/gen/primeapi/primeoperations/payment_request/create_payment_request.go +++ b/pkg/gen/primeapi/primeoperations/payment_request/create_payment_request.go @@ -45,7 +45,17 @@ marked with the status `DEPRECATED`. **NOTE**: In order to create a payment request for most service items, the shipment *must* be updated with the `PrimeActualWeight` value via [updateMTOShipment](#operation/updateMTOShipment). -**FSC - Fuel Surcharge** service items require `ActualPickupDate` to be updated on the shipment. +If `WeightBilled` is not provided then the full shipment weight (`PrimeActualWeight`) will be considered in the calculation. + +**NOTE**: Diversions have a unique calcuation for payment requests without a `WeightBilled` parameter. + +If you created a payment request for a diversion and `WeightBilled` is not provided, then the following will be used in the calculation: +- The lowest shipment weight (`PrimeActualWeight`) found in the diverted shipment chain. +- The lowest reweigh weight found in the diverted shipment chain. + +The diverted shipment chain is created by referencing the `diversion` boolean, `divertedFromShipmentId` UUID, and matching destination to pickup addresses. +If the chain cannot be established it will fall back to the `PrimeActualWeight` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations. +The lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found. A service item can be on several payment requests in the case of partial payment requests and payments. @@ -74,19 +84,120 @@ In the request, if no params are necessary, then just the `serviceItem` `id` is ``` -SIT Service Items & Accepted Payment Request Parameters: +Domestic Basic Service Items & Accepted Payment Request Parameters: --- -If `WeightBilled` is not provided then the full shipment weight (`PrimeActualWeight`) will be considered in the calculation. -**NOTE**: Diversions have a unique calcuation for payment requests without a `WeightBilled` parameter. +**DLH - Domestic Linehaul** +```json -If you created a payment request for a diversion and `WeightBilled` is not provided, then the following will be used in the calculation: -- The lowest shipment weight (`PrimeActualWeight`) found in the diverted shipment chain. -- The lowest reweigh weight found in the diverted shipment chain. + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] -The diverted shipment chain is created by referencing the `diversion` boolean, `divertedFromShipmentId` UUID, and matching destination to pickup addresses. -If the chain cannot be established it will fall back to the `PrimeActualWeight` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations. -The lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found. +``` + +**DSH - Domestic Shorthaul** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**FSC - Fuel Surcharge** +**NOTE**: FSC requires `ActualPickupDate` to be updated on the shipment. +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DUPK - Domestic Unpacking** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DPK - Domestic Packing** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DNPK - Domestic NTS Packing** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DPK - Domestic Packing** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DOP - Domestic Origin Price** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DDP - Domestic Destination Price** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +Domestic SIT Service Items & Accepted Payment Request Parameters: +--- **DOFSIT - Domestic origin 1st day SIT** ```json @@ -201,6 +312,76 @@ The lowest weight found is the true shipment weight, and thus we search the chai } ] +``` +--- + +International Basic Service Items & Accepted Payment Request Parameters: +--- +Just like domestic shipments & service items, if `WeightBilled` is not provided then the full shipment weight (`PrimeActualWeight`) will be considered in the calculation. +**NOTE**: `POEFSC` & `PODFSC` service items must have a port associated on the service item in order to successfully add it to a payment request. To update the port of a service item, you must use the (#operation/updateMTOServiceItem) endpoint. + +**ISLH - International Shipping & Linehaul** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**IHPK - International HHG Pack** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**IHUPK - International HHG Unpack** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**POEFSC - International Port of Embarkation Fuel Surcharge** + + **NOTE**: POEFSC requires `ActualPickupDate` to be updated on the shipment & `POELocation` on the service item. + +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**PODFSC - International Port of Debarkation Fuel Surcharge** +**NOTE**: PODFSC requires `ActualPickupDate` to be updated on the shipment & `PODLocation` on the service item. +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` --- */ diff --git a/pkg/gen/primeclient/payment_request/payment_request_client.go b/pkg/gen/primeclient/payment_request/payment_request_client.go index 82eae72610e..b10b46d87cf 100644 --- a/pkg/gen/primeclient/payment_request/payment_request_client.go +++ b/pkg/gen/primeclient/payment_request/payment_request_client.go @@ -52,7 +52,17 @@ marked with the status `DEPRECATED`. **NOTE**: In order to create a payment request for most service items, the shipment *must* be updated with the `PrimeActualWeight` value via [updateMTOShipment](#operation/updateMTOShipment). -**FSC - Fuel Surcharge** service items require `ActualPickupDate` to be updated on the shipment. +If `WeightBilled` is not provided then the full shipment weight (`PrimeActualWeight`) will be considered in the calculation. + +**NOTE**: Diversions have a unique calcuation for payment requests without a `WeightBilled` parameter. + +If you created a payment request for a diversion and `WeightBilled` is not provided, then the following will be used in the calculation: +- The lowest shipment weight (`PrimeActualWeight`) found in the diverted shipment chain. +- The lowest reweigh weight found in the diverted shipment chain. + +The diverted shipment chain is created by referencing the `diversion` boolean, `divertedFromShipmentId` UUID, and matching destination to pickup addresses. +If the chain cannot be established it will fall back to the `PrimeActualWeight` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations. +The lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found. A service item can be on several payment requests in the case of partial payment requests and payments. @@ -81,19 +91,120 @@ In the request, if no params are necessary, then just the `serviceItem` `id` is ``` -SIT Service Items & Accepted Payment Request Parameters: +Domestic Basic Service Items & Accepted Payment Request Parameters: --- -If `WeightBilled` is not provided then the full shipment weight (`PrimeActualWeight`) will be considered in the calculation. -**NOTE**: Diversions have a unique calcuation for payment requests without a `WeightBilled` parameter. +**DLH - Domestic Linehaul** +```json -If you created a payment request for a diversion and `WeightBilled` is not provided, then the following will be used in the calculation: -- The lowest shipment weight (`PrimeActualWeight`) found in the diverted shipment chain. -- The lowest reweigh weight found in the diverted shipment chain. + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] -The diverted shipment chain is created by referencing the `diversion` boolean, `divertedFromShipmentId` UUID, and matching destination to pickup addresses. -If the chain cannot be established it will fall back to the `PrimeActualWeight` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations. -The lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found. +``` + +**DSH - Domestic Shorthaul** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**FSC - Fuel Surcharge** +**NOTE**: FSC requires `ActualPickupDate` to be updated on the shipment. +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DUPK - Domestic Unpacking** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DPK - Domestic Packing** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DNPK - Domestic NTS Packing** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DPK - Domestic Packing** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DOP - Domestic Origin Price** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**DDP - Domestic Destination Price** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +Domestic SIT Service Items & Accepted Payment Request Parameters: +--- **DOFSIT - Domestic origin 1st day SIT** ```json @@ -208,6 +319,76 @@ The lowest weight found is the true shipment weight, and thus we search the chai } ] +``` +--- + +International Basic Service Items & Accepted Payment Request Parameters: +--- +Just like domestic shipments & service items, if `WeightBilled` is not provided then the full shipment weight (`PrimeActualWeight`) will be considered in the calculation. +**NOTE**: `POEFSC` & `PODFSC` service items must have a port associated on the service item in order to successfully add it to a payment request. To update the port of a service item, you must use the (#operation/updateMTOServiceItem) endpoint. + +**ISLH - International Shipping & Linehaul** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**IHPK - International HHG Pack** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**IHUPK - International HHG Unpack** +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**POEFSC - International Port of Embarkation Fuel Surcharge** + + **NOTE**: POEFSC requires `ActualPickupDate` to be updated on the shipment & `POELocation` on the service item. + +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + +``` + +**PODFSC - International Port of Debarkation Fuel Surcharge** +**NOTE**: PODFSC requires `ActualPickupDate` to be updated on the shipment & `PODLocation` on the service item. +```json + + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` --- */ 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 f0468e10884..f6ffd9298ba 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 @@ -4006,6 +4011,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 bb5a844cfe0..e788d625932 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 @@ -1889,6 +1894,9 @@ func init() { } ] }, + "destinationRateArea": { + "$ref": "#/definitions/RateArea" + }, "destinationSitAuthEndDate": { "description": "The SIT authorized end date for destination SIT.", "type": "string", @@ -1953,6 +1961,9 @@ func init() { "x-nullable": true, "example": 4500 }, + "originRateArea": { + "$ref": "#/definitions/RateArea" + }, "originSitAuthEndDate": { "description": "The SIT authorized end date for origin SIT.", "type": "string", @@ -2621,6 +2632,9 @@ func init() { "destinationAddress": { "$ref": "#/definitions/PPMDestinationAddress" }, + "destinationRateArea": { + "$ref": "#/definitions/RateArea" + }, "eTag": { "description": "A hash unique to this shipment that should be used as the \"If-Match\" header for any updates.", "type": "string", @@ -2704,6 +2718,9 @@ func init() { "x-nullable": true, "x-omitempty": false }, + "originRateArea": { + "$ref": "#/definitions/RateArea" + }, "pickupAddress": { "$ref": "#/definitions/Address" }, @@ -3123,6 +3140,32 @@ func init() { "$ref": "#/definitions/ProofOfServiceDoc" } }, + "RateArea": { + "description": "Rate area info for OCONUS postal code", + "type": "object", + "required": [ + "id", + "rateAreaId", + "rateAreaName" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "example": "1f2270c7-7166-40ae-981e-b200ebdf3054" + }, + "rateAreaId": { + "description": "Rate area code", + "type": "string", + "example": "US8101000" + }, + "rateAreaName": { + "description": "Rate area name", + "type": "string", + "example": "Alaska (Zone) I" + } + } + }, "ReServiceCode": { "description": "This is the full list of service items that can be found on a shipment. Not all service items\nmay be requested by the Prime, but may be returned in a response.\n\nDocumentation of all the service items will be provided.\n", "type": "string", @@ -4659,6 +4702,11 @@ func init() { "x-nullable": true, "example": "LOS ANGELES" }, + "destinationGbloc": { + "type": "string", + "pattern": "^[A-Z]{4}$", + "x-nullable": true + }, "eTag": { "type": "string", "readOnly": true @@ -6143,6 +6191,9 @@ func init() { } ] }, + "destinationRateArea": { + "$ref": "#/definitions/RateArea" + }, "destinationSitAuthEndDate": { "description": "The SIT authorized end date for destination SIT.", "type": "string", @@ -6207,6 +6258,9 @@ func init() { "x-nullable": true, "example": 4500 }, + "originRateArea": { + "$ref": "#/definitions/RateArea" + }, "originSitAuthEndDate": { "description": "The SIT authorized end date for origin SIT.", "type": "string", @@ -6875,6 +6929,9 @@ func init() { "destinationAddress": { "$ref": "#/definitions/PPMDestinationAddress" }, + "destinationRateArea": { + "$ref": "#/definitions/RateArea" + }, "eTag": { "description": "A hash unique to this shipment that should be used as the \"If-Match\" header for any updates.", "type": "string", @@ -6958,6 +7015,9 @@ func init() { "x-nullable": true, "x-omitempty": false }, + "originRateArea": { + "$ref": "#/definitions/RateArea" + }, "pickupAddress": { "$ref": "#/definitions/Address" }, @@ -7377,6 +7437,32 @@ func init() { "$ref": "#/definitions/ProofOfServiceDoc" } }, + "RateArea": { + "description": "Rate area info for OCONUS postal code", + "type": "object", + "required": [ + "id", + "rateAreaId", + "rateAreaName" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "example": "1f2270c7-7166-40ae-981e-b200ebdf3054" + }, + "rateAreaId": { + "description": "Rate area code", + "type": "string", + "example": "US8101000" + }, + "rateAreaName": { + "description": "Rate area name", + "type": "string", + "example": "Alaska (Zone) I" + } + } + }, "ReServiceCode": { "description": "This is the full list of service items that can be found on a shipment. Not all service items\nmay be requested by the Prime, but may be returned in a response.\n\nDocumentation of all the service items will be provided.\n", "type": "string", 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/gen/primev3messages/m_t_o_shipment_without_service_items.go b/pkg/gen/primev3messages/m_t_o_shipment_without_service_items.go index 1813fb3c19b..6fff06e8de3 100644 --- a/pkg/gen/primev3messages/m_t_o_shipment_without_service_items.go +++ b/pkg/gen/primev3messages/m_t_o_shipment_without_service_items.go @@ -89,6 +89,9 @@ type MTOShipmentWithoutServiceItems struct { Address } `json:"destinationAddress,omitempty"` + // destination rate area + DestinationRateArea *RateArea `json:"destinationRateArea,omitempty"` + // The SIT authorized end date for destination SIT. // Format: date DestinationSitAuthEndDate *strfmt.Date `json:"destinationSitAuthEndDate,omitempty"` @@ -138,6 +141,9 @@ type MTOShipmentWithoutServiceItems struct { // Example: 4500 NtsRecordedWeight *int64 `json:"ntsRecordedWeight,omitempty"` + // origin rate area + OriginRateArea *RateArea `json:"originRateArea,omitempty"` + // The SIT authorized end date for origin SIT. // Format: date OriginSitAuthEndDate *strfmt.Date `json:"originSitAuthEndDate,omitempty"` @@ -274,6 +280,10 @@ func (m *MTOShipmentWithoutServiceItems) Validate(formats strfmt.Registry) error res = append(res, err) } + if err := m.validateDestinationRateArea(formats); err != nil { + res = append(res, err) + } + if err := m.validateDestinationSitAuthEndDate(formats); err != nil { res = append(res, err) } @@ -302,6 +312,10 @@ func (m *MTOShipmentWithoutServiceItems) Validate(formats strfmt.Registry) error res = append(res, err) } + if err := m.validateOriginRateArea(formats); err != nil { + res = append(res, err) + } + if err := m.validateOriginSitAuthEndDate(formats); err != nil { res = append(res, err) } @@ -511,6 +525,25 @@ func (m *MTOShipmentWithoutServiceItems) validateDestinationAddress(formats strf return nil } +func (m *MTOShipmentWithoutServiceItems) validateDestinationRateArea(formats strfmt.Registry) error { + if swag.IsZero(m.DestinationRateArea) { // not required + return nil + } + + if m.DestinationRateArea != nil { + if err := m.DestinationRateArea.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("destinationRateArea") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("destinationRateArea") + } + return err + } + } + + return nil +} + func (m *MTOShipmentWithoutServiceItems) validateDestinationSitAuthEndDate(formats strfmt.Registry) error { if swag.IsZero(m.DestinationSitAuthEndDate) { // not required return nil @@ -639,6 +672,25 @@ func (m *MTOShipmentWithoutServiceItems) validateMoveTaskOrderID(formats strfmt. return nil } +func (m *MTOShipmentWithoutServiceItems) validateOriginRateArea(formats strfmt.Registry) error { + if swag.IsZero(m.OriginRateArea) { // not required + return nil + } + + if m.OriginRateArea != nil { + if err := m.OriginRateArea.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("originRateArea") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("originRateArea") + } + return err + } + } + + return nil +} + func (m *MTOShipmentWithoutServiceItems) validateOriginSitAuthEndDate(formats strfmt.Registry) error { if swag.IsZero(m.OriginSitAuthEndDate) { // not required return nil @@ -1062,6 +1114,10 @@ func (m *MTOShipmentWithoutServiceItems) ContextValidate(ctx context.Context, fo res = append(res, err) } + if err := m.contextValidateDestinationRateArea(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateDestinationType(ctx, formats); err != nil { res = append(res, err) } @@ -1086,6 +1142,10 @@ func (m *MTOShipmentWithoutServiceItems) ContextValidate(ctx context.Context, fo res = append(res, err) } + if err := m.contextValidateOriginRateArea(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidatePickupAddress(ctx, formats); err != nil { res = append(res, err) } @@ -1261,6 +1321,27 @@ func (m *MTOShipmentWithoutServiceItems) contextValidateDestinationAddress(ctx c return nil } +func (m *MTOShipmentWithoutServiceItems) contextValidateDestinationRateArea(ctx context.Context, formats strfmt.Registry) error { + + if m.DestinationRateArea != nil { + + if swag.IsZero(m.DestinationRateArea) { // not required + return nil + } + + if err := m.DestinationRateArea.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("destinationRateArea") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("destinationRateArea") + } + return err + } + } + + return nil +} + func (m *MTOShipmentWithoutServiceItems) contextValidateDestinationType(ctx context.Context, formats strfmt.Registry) error { if m.DestinationType != nil { @@ -1339,6 +1420,27 @@ func (m *MTOShipmentWithoutServiceItems) contextValidateMoveTaskOrderID(ctx cont return nil } +func (m *MTOShipmentWithoutServiceItems) contextValidateOriginRateArea(ctx context.Context, formats strfmt.Registry) error { + + if m.OriginRateArea != nil { + + if swag.IsZero(m.OriginRateArea) { // not required + return nil + } + + if err := m.OriginRateArea.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("originRateArea") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("originRateArea") + } + return err + } + } + + return nil +} + func (m *MTOShipmentWithoutServiceItems) contextValidatePickupAddress(ctx context.Context, formats strfmt.Registry) error { return nil diff --git a/pkg/gen/primev3messages/p_p_m_shipment.go b/pkg/gen/primev3messages/p_p_m_shipment.go index 89d6cd51c5f..a765ef630c1 100644 --- a/pkg/gen/primev3messages/p_p_m_shipment.go +++ b/pkg/gen/primev3messages/p_p_m_shipment.go @@ -61,6 +61,9 @@ type PPMShipment struct { // Required: true DestinationAddress *PPMDestinationAddress `json:"destinationAddress"` + // destination rate area + DestinationRateArea *RateArea `json:"destinationRateArea,omitempty"` + // A hash unique to this shipment that should be used as the "If-Match" header for any updates. // Required: true // Read Only: true @@ -117,6 +120,9 @@ type PPMShipment struct { // The max amount the government will pay the service member to move their belongings based on the moving date, locations, and shipment weight. MaxIncentive *int64 `json:"maxIncentive"` + // origin rate area + OriginRateArea *RateArea `json:"originRateArea,omitempty"` + // pickup address // Required: true PickupAddress *Address `json:"pickupAddress"` @@ -217,6 +223,10 @@ func (m *PPMShipment) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateDestinationRateArea(formats); err != nil { + res = append(res, err) + } + if err := m.validateETag(formats); err != nil { res = append(res, err) } @@ -229,6 +239,10 @@ func (m *PPMShipment) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateOriginRateArea(formats); err != nil { + res = append(res, err) + } + if err := m.validatePickupAddress(formats); err != nil { res = append(res, err) } @@ -372,6 +386,25 @@ func (m *PPMShipment) validateDestinationAddress(formats strfmt.Registry) error return nil } +func (m *PPMShipment) validateDestinationRateArea(formats strfmt.Registry) error { + if swag.IsZero(m.DestinationRateArea) { // not required + return nil + } + + if m.DestinationRateArea != nil { + if err := m.DestinationRateArea.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("destinationRateArea") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("destinationRateArea") + } + return err + } + } + + return nil +} + func (m *PPMShipment) validateETag(formats strfmt.Registry) error { if err := validate.RequiredString("eTag", "body", m.ETag); err != nil { @@ -407,6 +440,25 @@ func (m *PPMShipment) validateID(formats strfmt.Registry) error { return nil } +func (m *PPMShipment) validateOriginRateArea(formats strfmt.Registry) error { + if swag.IsZero(m.OriginRateArea) { // not required + return nil + } + + if m.OriginRateArea != nil { + if err := m.OriginRateArea.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("originRateArea") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("originRateArea") + } + return err + } + } + + return nil +} + func (m *PPMShipment) validatePickupAddress(formats strfmt.Registry) error { if err := validate.Required("pickupAddress", "body", m.PickupAddress); err != nil { @@ -634,6 +686,10 @@ func (m *PPMShipment) ContextValidate(ctx context.Context, formats strfmt.Regist res = append(res, err) } + if err := m.contextValidateDestinationRateArea(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateETag(ctx, formats); err != nil { res = append(res, err) } @@ -642,6 +698,10 @@ func (m *PPMShipment) ContextValidate(ctx context.Context, formats strfmt.Regist res = append(res, err) } + if err := m.contextValidateOriginRateArea(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidatePickupAddress(ctx, formats); err != nil { res = append(res, err) } @@ -710,6 +770,27 @@ func (m *PPMShipment) contextValidateDestinationAddress(ctx context.Context, for return nil } +func (m *PPMShipment) contextValidateDestinationRateArea(ctx context.Context, formats strfmt.Registry) error { + + if m.DestinationRateArea != nil { + + if swag.IsZero(m.DestinationRateArea) { // not required + return nil + } + + if err := m.DestinationRateArea.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("destinationRateArea") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("destinationRateArea") + } + return err + } + } + + return nil +} + func (m *PPMShipment) contextValidateETag(ctx context.Context, formats strfmt.Registry) error { if err := validate.ReadOnly(ctx, "eTag", "body", string(m.ETag)); err != nil { @@ -728,6 +809,27 @@ func (m *PPMShipment) contextValidateID(ctx context.Context, formats strfmt.Regi return nil } +func (m *PPMShipment) contextValidateOriginRateArea(ctx context.Context, formats strfmt.Registry) error { + + if m.OriginRateArea != nil { + + if swag.IsZero(m.OriginRateArea) { // not required + return nil + } + + if err := m.OriginRateArea.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("originRateArea") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("originRateArea") + } + return err + } + } + + return nil +} + func (m *PPMShipment) contextValidatePickupAddress(ctx context.Context, formats strfmt.Registry) error { if m.PickupAddress != nil { diff --git a/pkg/gen/primev3messages/rate_area.go b/pkg/gen/primev3messages/rate_area.go new file mode 100644 index 00000000000..2d98c400ebb --- /dev/null +++ b/pkg/gen/primev3messages/rate_area.go @@ -0,0 +1,113 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package primev3messages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// RateArea Rate area info for OCONUS postal code +// +// swagger:model RateArea +type RateArea struct { + + // id + // Example: 1f2270c7-7166-40ae-981e-b200ebdf3054 + // Required: true + // Format: uuid + ID *strfmt.UUID `json:"id"` + + // Rate area code + // Example: US8101000 + // Required: true + RateAreaID *string `json:"rateAreaId"` + + // Rate area name + // Example: Alaska (Zone) I + // Required: true + RateAreaName *string `json:"rateAreaName"` +} + +// Validate validates this rate area +func (m *RateArea) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRateAreaID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRateAreaName(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *RateArea) validateID(formats strfmt.Registry) error { + + if err := validate.Required("id", "body", m.ID); err != nil { + return err + } + + if err := validate.FormatOf("id", "body", "uuid", m.ID.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *RateArea) validateRateAreaID(formats strfmt.Registry) error { + + if err := validate.Required("rateAreaId", "body", m.RateAreaID); err != nil { + return err + } + + return nil +} + +func (m *RateArea) validateRateAreaName(formats strfmt.Registry) error { + + if err := validate.Required("rateAreaName", "body", m.RateAreaName); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this rate area based on context it is used +func (m *RateArea) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *RateArea) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *RateArea) UnmarshalBinary(b []byte) error { + var res RateArea + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/handlers/adminapi/moves_test.go b/pkg/handlers/adminapi/moves_test.go index 81e166a35e8..bfc3291df46 100644 --- a/pkg/handlers/adminapi/moves_test.go +++ b/pkg/handlers/adminapi/moves_test.go @@ -117,6 +117,8 @@ func (suite *HandlerSuite) TestUpdateMoveHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) return UpdateMoveHandler{ suite.HandlerConfig(), diff --git a/pkg/handlers/ghcapi/move_task_order_test.go b/pkg/handlers/ghcapi/move_task_order_test.go index 10e9630fc44..e93d756cf58 100644 --- a/pkg/handlers/ghcapi/move_task_order_test.go +++ b/pkg/handlers/ghcapi/move_task_order_test.go @@ -189,6 +189,8 @@ func (suite *HandlerSuite) TestUpdateMoveTaskOrderHandlerIntegrationSuccess() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) siCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -313,6 +315,8 @@ func (suite *HandlerSuite) TestUpdateMoveTaskOrderHandlerIntegrationWithIncomple mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { @@ -401,6 +405,8 @@ func (suite *HandlerSuite) TestUpdateMTOStatusServiceCounselingCompletedHandler( mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) siCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := UpdateMTOStatusServiceCounselingCompletedHandlerFunc{ @@ -620,6 +626,8 @@ func (suite *HandlerSuite) TestUpdateMoveTIORemarksHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/handlers/ghcapi/mto_service_items_test.go b/pkg/handlers/ghcapi/mto_service_items_test.go index b1ff7e9c405..23071c64635 100644 --- a/pkg/handlers/ghcapi/mto_service_items_test.go +++ b/pkg/handlers/ghcapi/mto_service_items_test.go @@ -312,6 +312,8 @@ func (suite *HandlerSuite) TestUpdateMTOServiceItemStatusHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -564,6 +566,8 @@ func (suite *HandlerSuite) TestUpdateMTOServiceItemStatusHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) mtoServiceItemStatusUpdater := mtoserviceitem.NewMTOServiceItemUpdater(planner, queryBuilder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) @@ -625,6 +629,8 @@ func (suite *HandlerSuite) TestUpdateMTOServiceItemStatusHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) mtoServiceItemStatusUpdater := mtoserviceitem.NewMTOServiceItemUpdater(planner, queryBuilder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) @@ -759,6 +765,8 @@ func (suite *HandlerSuite) TestUpdateServiceItemSitEntryDateHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) diff --git a/pkg/handlers/ghcapi/mto_shipment.go b/pkg/handlers/ghcapi/mto_shipment.go index 9a49401e612..0ab30d6ed1a 100644 --- a/pkg/handlers/ghcapi/mto_shipment.go +++ b/pkg/handlers/ghcapi/mto_shipment.go @@ -1,11 +1,13 @@ package ghcapi import ( + "errors" "fmt" "github.com/go-openapi/runtime/middleware" "github.com/gobuffalo/validate/v3" "github.com/gofrs/uuid" + "github.com/lib/pq" "go.uber.org/zap" "github.com/transcom/mymove/pkg/appcontext" @@ -378,6 +380,16 @@ func (h UpdateShipmentHandler) Handle(params mtoshipmentops.UpdateMTOShipmentPar appCtx.Logger().Error("ghcapi.UpdateShipmentHandler error", zap.Error(e.Unwrap())) } + // Try to unwrap the error to access the underlying pq.Error (aka error from the db) + var pqErr *pq.Error + if errors.As(e.Unwrap(), &pqErr) { + appCtx.Logger().Error("QueryError message", zap.String("databaseError", pqErr.Message)) + databaseErrorMessage := fmt.Sprintf("Database error: %s", pqErr.Message) + return mtoshipmentops.NewUpdateMTOShipmentInternalServerError().WithPayload( + &ghcmessages.Error{Message: &databaseErrorMessage}, + ), err + } + msg := fmt.Sprintf("%v | Instance: %v", handlers.FmtString(err.Error()), h.GetTraceIDFromRequest(params.HTTPRequest)) return mtoshipmentops.NewUpdateMTOShipmentInternalServerError().WithPayload( diff --git a/pkg/handlers/ghcapi/mto_shipment_test.go b/pkg/handlers/ghcapi/mto_shipment_test.go index dd60fdf7eee..03096892d58 100644 --- a/pkg/handlers/ghcapi/mto_shipment_test.go +++ b/pkg/handlers/ghcapi/mto_shipment_test.go @@ -603,6 +603,8 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) approver := mtoshipment.NewShipmentApprover( mtoshipment.NewShipmentRouter(), @@ -2123,6 +2125,8 @@ func (suite *HandlerSuite) TestRequestShipmentReweighHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveRouter := moveservices.NewMoveRouter() moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -2181,6 +2185,8 @@ func (suite *HandlerSuite) TestRequestShipmentReweighHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveRouter := moveservices.NewMoveRouter() moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -2236,6 +2242,8 @@ func (suite *HandlerSuite) TestRequestShipmentReweighHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveRouter := moveservices.NewMoveRouter() moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -2292,6 +2300,8 @@ func (suite *HandlerSuite) TestRequestShipmentReweighHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveRouter := moveservices.NewMoveRouter() moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -2349,6 +2359,8 @@ func (suite *HandlerSuite) TestRequestShipmentReweighHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveRouter := moveservices.NewMoveRouter() moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -2405,6 +2417,8 @@ func (suite *HandlerSuite) TestRequestShipmentReweighHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveRouter := moveservices.NewMoveRouter() moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -2743,6 +2757,8 @@ func (suite *HandlerSuite) TestApproveSITExtensionHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -2883,6 +2899,8 @@ func (suite *HandlerSuite) CreateApprovedSITDurationUpdate() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -2966,6 +2984,8 @@ func (suite *HandlerSuite) CreateApprovedSITDurationUpdate() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -3139,6 +3159,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( builder, @@ -3223,6 +3245,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( builder, @@ -3279,6 +3303,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( builder, @@ -3331,6 +3357,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( builder, @@ -3378,6 +3406,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( builder, @@ -3462,6 +3492,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandlerUsingPPM() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( @@ -3673,6 +3705,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandlerUsingPPM() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( builder, @@ -3823,6 +3857,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandlerUsingPPM() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( builder, @@ -4028,6 +4064,8 @@ func (suite *HandlerSuite) TestUpdateShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveRouter := moveservices.NewMoveRouter() moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester()) @@ -4698,6 +4736,8 @@ func (suite *HandlerSuite) TestUpdateSITServiceItemCustomerExpenseHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) updater := mtoserviceitem.NewMTOServiceItemUpdater(planner, builder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) req := httptest.NewRequest("PATCH", fmt.Sprintf("/shipments/%s/sit-service-item/convert-to-customer-expense", approvedShipment.ID.String()), nil) @@ -4774,6 +4814,8 @@ func (suite *HandlerSuite) TestUpdateSITServiceItemCustomerExpenseHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) updater := mtoserviceitem.NewMTOServiceItemUpdater(planner, builder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) req := httptest.NewRequest("PATCH", fmt.Sprintf("/shipments/%s/sit-service-item/convert-to-customer-expense", approvedShipment.ID.String()), nil) diff --git a/pkg/handlers/ghcapi/orders_test.go b/pkg/handlers/ghcapi/orders_test.go index 93fdc901600..6d3bd880903 100644 --- a/pkg/handlers/ghcapi/orders_test.go +++ b/pkg/handlers/ghcapi/orders_test.go @@ -398,6 +398,8 @@ func (suite *HandlerSuite) TestUpdateOrderHandlerWithAmendedUploads() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/handlers/ghcapi/ppm_shipment_test.go b/pkg/handlers/ghcapi/ppm_shipment_test.go index b476066d53a..d5e358feb4a 100644 --- a/pkg/handlers/ghcapi/ppm_shipment_test.go +++ b/pkg/handlers/ghcapi/ppm_shipment_test.go @@ -388,7 +388,7 @@ func (suite *HandlerSuite) TestGetPPMSITEstimatedCostHandler() { ppmShipment.DestinationAddress = destinationAddress mockedPlanner := &routemocks.Planner{} mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "90210", "30813").Return(2294, nil) + "90210", "30813", false, false).Return(2294, nil) }) setUpGetCostRequestAndParams := func() ppmsitops.GetPPMSITEstimatedCostParams { diff --git a/pkg/handlers/internalapi/mto_shipment_test.go b/pkg/handlers/internalapi/mto_shipment_test.go index 4d2b557286f..f227a704629 100644 --- a/pkg/handlers/internalapi/mto_shipment_test.go +++ b/pkg/handlers/internalapi/mto_shipment_test.go @@ -77,6 +77,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandlerV1() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/handlers/primeapi/allowed_payment_service_item_params.go b/pkg/handlers/primeapi/allowed_payment_service_item_params.go index cc40edc4ff8..809582a592a 100644 --- a/pkg/handlers/primeapi/allowed_payment_service_item_params.go +++ b/pkg/handlers/primeapi/allowed_payment_service_item_params.go @@ -67,6 +67,21 @@ var ( models.ReServiceCodeDOSFSC: { models.ServiceItemParamNameWeightBilled, }, + models.ReServiceCodeISLH: { + models.ServiceItemParamNameWeightBilled, + }, + models.ReServiceCodeIHPK: { + models.ServiceItemParamNameWeightBilled, + }, + models.ReServiceCodeIHUPK: { + models.ServiceItemParamNameWeightBilled, + }, + models.ReServiceCodePOEFSC: { + models.ServiceItemParamNameWeightBilled, + }, + models.ReServiceCodePODFSC: { + models.ServiceItemParamNameWeightBilled, + }, } ) diff --git a/pkg/handlers/primeapi/move_task_order_test.go b/pkg/handlers/primeapi/move_task_order_test.go index 2bfa5abfbcc..b659b9386d9 100644 --- a/pkg/handlers/primeapi/move_task_order_test.go +++ b/pkg/handlers/primeapi/move_task_order_test.go @@ -1710,6 +1710,8 @@ func (suite *HandlerSuite) TestUpdateMTOPostCounselingInfo() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { @@ -1794,6 +1796,8 @@ func (suite *HandlerSuite) TestUpdateMTOPostCounselingInfo() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/handlers/primeapi/mto_service_item.go b/pkg/handlers/primeapi/mto_service_item.go index 86740aa0523..7dad26d373b 100644 --- a/pkg/handlers/primeapi/mto_service_item.go +++ b/pkg/handlers/primeapi/mto_service_item.go @@ -44,6 +44,26 @@ func (h CreateMTOServiceItemHandler) Handle(params mtoserviceitemops.CreateMTOSe return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { + // ** Create service item can not be done for PPM shipment **/ + shipment, err := models.FetchShipmentByID(appCtx.DB(), uuid.FromStringOrNil(params.Body.MtoShipmentID().String())) + if err != nil { + appCtx.Logger().Error("primeapi.CreateMTOServiceItemHandler Error Fetch Shipment", zap.Error(err)) + switch err { + case models.ErrFetchNotFound: + return mtoserviceitemops.NewCreateMTOServiceItemNotFound().WithPayload(payloads.ClientError(handlers.NotFoundMessage, "Fetch Shipment", h.GetTraceIDFromRequest(params.HTTPRequest))), err + default: + return mtoserviceitemops.NewCreateMTOServiceItemInternalServerError().WithPayload(payloads.InternalServerError(nil, h.GetTraceIDFromRequest(params.HTTPRequest))), err + } + } + + if shipment.ShipmentType == models.MTOShipmentTypePPM { + verrs := validate.NewErrors() + verrs.Add("mtoShipmentID", params.Body.MtoShipmentID().String()) + appCtx.Logger().Error("primeapi.CreateMTOServiceItemHandler - Create Service Item is not allowed for PPM shipments", zap.Error(verrs)) + return mtoserviceitemops.NewCreateMTOServiceItemUnprocessableEntity().WithPayload(payloads.ValidationError( + "Create Service Item is not allowed for PPM shipments", h.GetTraceIDFromRequest(params.HTTPRequest), verrs)), verrs + } + /** Feature Flag - Alaska **/ isAlaskaEnabled := false featureFlagName := "enable_alaska" diff --git a/pkg/handlers/primeapi/mto_service_item_test.go b/pkg/handlers/primeapi/mto_service_item_test.go index 196a4c0b860..e408e4085b8 100644 --- a/pkg/handlers/primeapi/mto_service_item_test.go +++ b/pkg/handlers/primeapi/mto_service_item_test.go @@ -43,16 +43,33 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mtoServiceItem models.MTOServiceItem } - makeSubtestData := func() (subtestData *localSubtestData) { + makeSubtestDataWithPPMShipmentType := func(isPPM bool) (subtestData *localSubtestData) { subtestData = &localSubtestData{} + mtoShipmentID, _ := uuid.NewV4() mto := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) - subtestData.mtoShipment = factory.BuildMTOShipment(suite.DB(), []factory.Customization{ - { - Model: mto, - LinkOnly: true, - }, - }, nil) + if isPPM { + subtestData.mtoShipment = factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: mto, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + ID: mtoShipmentID, + ShipmentType: models.MTOShipmentTypePPM, + }, + }, + }, nil) + } else { + subtestData.mtoShipment = factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: mto, + LinkOnly: true, + }, + }, nil) + } + factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeDOFSIT) req := httptest.NewRequest("POST", "/mto-service-items", nil) sitEntryDate := time.Now() @@ -86,6 +103,10 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { return subtestData } + makeSubtestData := func() (subtestData *localSubtestData) { + return makeSubtestDataWithPPMShipmentType(false) + } + suite.Run("Successful POST - Integration Test", func() { subtestData := makeSubtestData() moveRouter := moverouter.NewMoveRouter() @@ -94,6 +115,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -152,6 +175,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -290,6 +315,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -334,6 +361,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -427,6 +456,68 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { // Validate outgoing payload suite.NoError(responsePayload.Validate(strfmt.Default)) }) + + suite.Run("POST failure - Shipment fetch not found", func() { + subtestData := makeSubtestDataWithPPMShipmentType(true) + moveRouter := moverouter.NewMoveRouter() + planner := &routemocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + ).Return(400, nil) + creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) + handler := CreateMTOServiceItemHandler{ + suite.HandlerConfig(), + creator, + mtoChecker, + } + + // Validate incoming payload + suite.NoError(subtestData.params.Body.Validate(strfmt.Default)) + + // we are going to mock fake UUID to force NOT FOUND ERROR + subtestData.params.Body.SetMtoShipmentID(subtestData.params.Body.ID()) + + response := handler.Handle(subtestData.params) + suite.IsType(&mtoserviceitemops.CreateMTOServiceItemNotFound{}, response) + typedResponse := response.(*mtoserviceitemops.CreateMTOServiceItemNotFound) + + // Validate outgoing payload + suite.NoError(typedResponse.Payload.Validate(strfmt.Default)) + + suite.Contains(*typedResponse.Payload.Detail, "Fetch Shipment") + }) + + suite.Run("POST failure - 422 - PPM not allowed to create service item", func() { + subtestData := makeSubtestDataWithPPMShipmentType(true) + moveRouter := moverouter.NewMoveRouter() + planner := &routemocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + ).Return(400, nil) + creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) + handler := CreateMTOServiceItemHandler{ + suite.HandlerConfig(), + creator, + mtoChecker, + } + + // Validate incoming payload + suite.NoError(subtestData.params.Body.Validate(strfmt.Default)) + + response := handler.Handle(subtestData.params) + suite.IsType(&mtoserviceitemops.CreateMTOServiceItemUnprocessableEntity{}, response) + typedResponse := response.(*mtoserviceitemops.CreateMTOServiceItemUnprocessableEntity) + + // Validate outgoing payload + suite.NoError(typedResponse.Payload.Validate(strfmt.Default)) + + suite.Contains(*typedResponse.Payload.Detail, "Create Service Item is not allowed for PPM shipments") + suite.Contains(typedResponse.Payload.InvalidFields["mtoShipmentID"][0], subtestData.params.Body.MtoShipmentID().String()) + }) } func (suite *HandlerSuite) TestCreateMTOServiceItemDomesticCratingHandler() { @@ -485,6 +576,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDomesticCratingHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -522,6 +615,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDomesticCratingHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -645,6 +740,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -691,6 +788,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -760,6 +859,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -844,6 +945,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandlerWithDOFSITNoA mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -954,6 +1057,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandlerWithDOFSITWit mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1127,6 +1232,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1201,6 +1308,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1234,6 +1343,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1290,6 +1401,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1344,6 +1457,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1542,6 +1657,8 @@ func (suite *HandlerSuite) TestUpdateMTOServiceItemDDDSIT() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) subtestData.handler = UpdateMTOServiceItemHandler{ suite.HandlerConfig(), @@ -1825,6 +1942,8 @@ func (suite *HandlerSuite) TestUpdateMTOServiceItemDOPSIT() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) subtestData.handler = UpdateMTOServiceItemHandler{ suite.HandlerConfig(), diff --git a/pkg/handlers/primeapi/mto_shipment_address_test.go b/pkg/handlers/primeapi/mto_shipment_address_test.go index 8a45daa4156..cb662b28dfe 100644 --- a/pkg/handlers/primeapi/mto_shipment_address_test.go +++ b/pkg/handlers/primeapi/mto_shipment_address_test.go @@ -47,6 +47,8 @@ func (suite *HandlerSuite) TestUpdateMTOShipmentAddressHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) // Create handler handler := UpdateMTOShipmentAddressHandler{ diff --git a/pkg/handlers/primeapi/mto_shipment_test.go b/pkg/handlers/primeapi/mto_shipment_test.go index 5b602de7873..917e10cdfc6 100644 --- a/pkg/handlers/primeapi/mto_shipment_test.go +++ b/pkg/handlers/primeapi/mto_shipment_test.go @@ -203,6 +203,8 @@ func (suite *HandlerSuite) TestUpdateMTOShipmentStatusHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) moveRouter := moveservices.NewMoveRouter() addressUpdater := address.NewAddressUpdater() @@ -223,6 +225,8 @@ func (suite *HandlerSuite) TestUpdateMTOShipmentStatusHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) handler := UpdateMTOShipmentStatusHandler{ handlerConfig, @@ -414,6 +418,8 @@ func (suite *HandlerSuite) TestDeleteMTOShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/handlers/primeapi/payment_request_test.go b/pkg/handlers/primeapi/payment_request_test.go index fd4e8dd14da..114f03aa49b 100644 --- a/pkg/handlers/primeapi/payment_request_test.go +++ b/pkg/handlers/primeapi/payment_request_test.go @@ -28,7 +28,7 @@ import ( ) const ( - dlhTestServiceArea = "004" + dlhTestServiceArea = "042" dlhTestWeight = unit.Pound(4000) ) @@ -648,7 +648,7 @@ func (suite *HandlerSuite) setupDomesticLinehaulData() (models.Move, models.MTOS }, }) - baseLinehaulPrice := testdatagen.MakeReDomesticLinehaulPrice(suite.DB(), testdatagen.Assertions{ + baseLinehaulPrice := testdatagen.FetchOrMakeReDomesticLinehaulPrice(suite.DB(), testdatagen.Assertions{ ReDomesticLinehaulPrice: models.ReDomesticLinehaulPrice{ ContractID: contractYear.Contract.ID, Contract: contractYear.Contract, @@ -658,7 +658,7 @@ func (suite *HandlerSuite) setupDomesticLinehaulData() (models.Move, models.MTOS }, }) - _ = testdatagen.MakeReDomesticLinehaulPrice(suite.DB(), testdatagen.Assertions{ + _ = testdatagen.FetchOrMakeReDomesticLinehaulPrice(suite.DB(), testdatagen.Assertions{ ReDomesticLinehaulPrice: models.ReDomesticLinehaulPrice{ ContractID: contractYear.Contract.ID, Contract: contractYear.Contract, @@ -746,6 +746,8 @@ func (suite *HandlerSuite) TestCreatePaymentRequestHandlerNewPaymentRequestCreat mock.AnythingOfType("*appcontext.appContext"), "90210", "94535", + false, + false, ).Return(defaultZipDistance, nil) paymentRequestCreator := paymentrequest.NewPaymentRequestCreator( @@ -905,6 +907,8 @@ func (suite *HandlerSuite) TestCreatePaymentRequestHandlerInvalidMTOReferenceID( mock.AnythingOfType("*appcontext.appContext"), "90210", "94535", + false, + false, ).Return(defaultZipDistance, nil) paymentRequestCreator := paymentrequest.NewPaymentRequestCreator( @@ -943,13 +947,6 @@ func (suite *HandlerSuite) TestCreatePaymentRequestHandlerInvalidMTOReferenceID( suite.IsType(&paymentrequestop.CreatePaymentRequestUnprocessableEntity{}, response) typedResponse := response.(*paymentrequestop.CreatePaymentRequestUnprocessableEntity) - // Validate outgoing payload - // TODO: Can't validate the response because of the issue noted below. Figure out a way to - // either alter the service or relax the swagger requirements. - // suite.NoError(typedResponse.Payload.Validate(strfmt.Default)) - // CreatePaymentRequestCheck is returning apperror.InvalidCreateInputError without any validation errors - // so InvalidFields won't be added to the payload. - suite.Contains(*typedResponse.Payload.Detail, "has missing ReferenceID") }) @@ -970,6 +967,8 @@ func (suite *HandlerSuite) TestCreatePaymentRequestHandlerInvalidMTOReferenceID( mock.AnythingOfType("*appcontext.appContext"), "90210", "94535", + false, + false, ).Return(defaultZipDistance, nil) paymentRequestCreator := paymentrequest.NewPaymentRequestCreator( @@ -1007,13 +1006,6 @@ func (suite *HandlerSuite) TestCreatePaymentRequestHandlerInvalidMTOReferenceID( suite.IsType(&paymentrequestop.CreatePaymentRequestUnprocessableEntity{}, response) typedResponse := response.(*paymentrequestop.CreatePaymentRequestUnprocessableEntity) - // Validate outgoing payload - // TODO: Can't validate the response because of the issue noted below. Figure out a way to - // either alter the service or relax the swagger requirements. - // suite.NoError(typedResponse.Payload.Validate(strfmt.Default)) - // CreatePaymentRequestCheck is returning apperror.InvalidCreateInputError without any validation errors - // so InvalidFields won't be added to the payload. - suite.Contains(*typedResponse.Payload.Detail, "has missing ReferenceID") }) } diff --git a/pkg/handlers/primeapi/reweigh_test.go b/pkg/handlers/primeapi/reweigh_test.go index baa363a5d8a..d97b7f78d22 100644 --- a/pkg/handlers/primeapi/reweigh_test.go +++ b/pkg/handlers/primeapi/reweigh_test.go @@ -36,6 +36,8 @@ func (suite *HandlerSuite) TestUpdateReweighHandler() { mockPlanner.On("ZipTransitDistance", recalculateTestPickupZip, recalculateTestDestinationZip, + false, + false, ).Return(recalculateTestZip3Distance, nil) // Get shipment payment request recalculator service diff --git a/pkg/handlers/primeapiv2/mto_service_item.go b/pkg/handlers/primeapiv2/mto_service_item.go index e26332964c8..54ea0540344 100644 --- a/pkg/handlers/primeapiv2/mto_service_item.go +++ b/pkg/handlers/primeapiv2/mto_service_item.go @@ -42,6 +42,26 @@ func (h CreateMTOServiceItemHandler) Handle(params mtoserviceitemops.CreateMTOSe return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { + // ** Create service item can not be done for PPM shipment **/ + shipment, err := models.FetchShipmentByID(appCtx.DB(), uuid.FromStringOrNil(params.Body.MtoShipmentID().String())) + if err != nil { + appCtx.Logger().Error("primeapi.CreateMTOServiceItemHandler.v2 Error Fetch Shipment", zap.Error(err)) + switch err { + case models.ErrFetchNotFound: + return mtoserviceitemops.NewCreateMTOServiceItemNotFound().WithPayload(primeapipayloads.ClientError(handlers.NotFoundMessage, "Fetch Shipment", h.GetTraceIDFromRequest(params.HTTPRequest))), err + default: + return mtoserviceitemops.NewCreateMTOServiceItemInternalServerError().WithPayload(primeapipayloads.InternalServerError(nil, h.GetTraceIDFromRequest(params.HTTPRequest))), err + } + } + + if shipment.ShipmentType == models.MTOShipmentTypePPM { + verrs := validate.NewErrors() + verrs.Add("mtoShipmentID", params.Body.MtoShipmentID().String()) + appCtx.Logger().Error("primeapi.CreateMTOServiceItemHandler.v2 - Create Service Item is not allowed for PPM shipments", zap.Error(verrs)) + return mtoserviceitemops.NewCreateMTOServiceItemUnprocessableEntity().WithPayload(primeapipayloads.ValidationError( + "Create Service Item is not allowed for PPM shipments", h.GetTraceIDFromRequest(params.HTTPRequest), verrs)), verrs + } + /** Feature Flag - Alaska **/ isAlaskaEnabled := false featureFlagName := "enable_alaska" diff --git a/pkg/handlers/primeapiv2/mto_service_item_test.go b/pkg/handlers/primeapiv2/mto_service_item_test.go index ffe960ba4f4..8875c35a851 100644 --- a/pkg/handlers/primeapiv2/mto_service_item_test.go +++ b/pkg/handlers/primeapiv2/mto_service_item_test.go @@ -37,16 +37,33 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mtoServiceItem models.MTOServiceItem } - makeSubtestData := func() (subtestData *localSubtestData) { + makeSubtestDataWithPPMShipmentType := func(isPPM bool) (subtestData *localSubtestData) { subtestData = &localSubtestData{} + mtoShipmentID, _ := uuid.NewV4() + mto := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) - subtestData.mtoShipment = factory.BuildMTOShipment(suite.DB(), []factory.Customization{ - { - Model: mto, - LinkOnly: true, - }, - }, nil) + if isPPM { + subtestData.mtoShipment = factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: mto, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + ID: mtoShipmentID, + ShipmentType: models.MTOShipmentTypePPM, + }, + }, + }, nil) + } else { + subtestData.mtoShipment = factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: mto, + LinkOnly: true, + }, + }, nil) + } factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeDOFSIT) req := httptest.NewRequest("POST", "/mto-service-items", nil) sitEntryDate := time.Now() @@ -80,6 +97,10 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { return subtestData } + makeSubtestData := func() (subtestData *localSubtestData) { + return makeSubtestDataWithPPMShipmentType(false) + } + suite.Run("Successful POST - Integration Test", func() { subtestData := makeSubtestData() moveRouter := moverouter.NewMoveRouter() @@ -88,6 +109,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -146,6 +169,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -284,6 +309,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -328,6 +355,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -421,6 +450,68 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { // Validate outgoing payload suite.NoError(responsePayload.Validate(strfmt.Default)) }) + + suite.Run("POST failure - Shipment fetch not found", func() { + subtestData := makeSubtestDataWithPPMShipmentType(true) + moveRouter := moverouter.NewMoveRouter() + planner := &routemocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + ).Return(400, nil) + creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) + handler := CreateMTOServiceItemHandler{ + suite.HandlerConfig(), + creator, + mtoChecker, + } + + // Validate incoming payload + suite.NoError(subtestData.params.Body.Validate(strfmt.Default)) + + // we are going to mock fake UUID to force NOT FOUND ERROR + subtestData.params.Body.SetMtoShipmentID(subtestData.params.Body.ID()) + + response := handler.Handle(subtestData.params) + suite.IsType(&mtoserviceitemops.CreateMTOServiceItemNotFound{}, response) + typedResponse := response.(*mtoserviceitemops.CreateMTOServiceItemNotFound) + + // Validate outgoing payload + suite.NoError(typedResponse.Payload.Validate(strfmt.Default)) + + suite.Contains(*typedResponse.Payload.Detail, "Fetch Shipment") + }) + + suite.Run("POST failure - 422 - PPM not allowed to create service item", func() { + subtestData := makeSubtestDataWithPPMShipmentType(true) + moveRouter := moverouter.NewMoveRouter() + planner := &routemocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + ).Return(400, nil) + creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) + handler := CreateMTOServiceItemHandler{ + suite.HandlerConfig(), + creator, + mtoChecker, + } + + // Validate incoming payload + suite.NoError(subtestData.params.Body.Validate(strfmt.Default)) + + response := handler.Handle(subtestData.params) + suite.IsType(&mtoserviceitemops.CreateMTOServiceItemUnprocessableEntity{}, response) + typedResponse := response.(*mtoserviceitemops.CreateMTOServiceItemUnprocessableEntity) + + // Validate outgoing payload + suite.NoError(typedResponse.Payload.Validate(strfmt.Default)) + + suite.Contains(*typedResponse.Payload.Detail, "Create Service Item is not allowed for PPM shipments") + suite.Contains(typedResponse.Payload.InvalidFields["mtoShipmentID"][0], subtestData.params.Body.MtoShipmentID().String()) + }) } func (suite *HandlerSuite) TestCreateMTOServiceItemDomesticCratingHandler() { @@ -479,6 +570,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDomesticCratingHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -516,6 +609,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDomesticCratingHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -639,6 +734,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -685,6 +782,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -754,6 +853,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -838,6 +939,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandlerWithDOFSITNoA mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -946,6 +1049,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandlerWithDOFSITWit mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1113,6 +1218,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1187,6 +1294,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1220,6 +1329,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1276,6 +1387,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1340,6 +1453,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ diff --git a/pkg/handlers/primeapiv2/mto_shipment_test.go b/pkg/handlers/primeapiv2/mto_shipment_test.go index 314dc9c3a5f..aaf6ba180d6 100644 --- a/pkg/handlers/primeapiv2/mto_shipment_test.go +++ b/pkg/handlers/primeapiv2/mto_shipment_test.go @@ -52,6 +52,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/handlers/primeapiv3/api.go b/pkg/handlers/primeapiv3/api.go index e9c522f8d42..8365d280068 100644 --- a/pkg/handlers/primeapiv3/api.go +++ b/pkg/handlers/primeapiv3/api.go @@ -46,6 +46,7 @@ func NewPrimeAPI(handlerConfig handlers.HandlerConfig) *primev3operations.Mymove primeAPIV3.MoveTaskOrderGetMoveTaskOrderHandler = GetMoveTaskOrderHandler{ handlerConfig, movetaskorder.NewMoveTaskOrderFetcher(), + mtoshipment.NewMTOShipmentRateAreaFetcher(), } signedCertificationCreator := signedcertification.NewSignedCertificationCreator() diff --git a/pkg/handlers/primeapiv3/move_task_order.go b/pkg/handlers/primeapiv3/move_task_order.go index 8c9ffe918b0..e7b5f01149c 100644 --- a/pkg/handlers/primeapiv3/move_task_order.go +++ b/pkg/handlers/primeapiv3/move_task_order.go @@ -17,7 +17,8 @@ import ( // GetMoveTaskOrderHandler returns the details for a particular move type GetMoveTaskOrderHandler struct { handlers.HandlerConfig - moveTaskOrderFetcher services.MoveTaskOrderFetcher + moveTaskOrderFetcher services.MoveTaskOrderFetcher + shipmentRateAreaFinder services.ShipmentRateAreaFinder } // Handle fetches a move from the database using its UUID or move code @@ -104,7 +105,15 @@ func (h GetMoveTaskOrderHandler) Handle(params movetaskorderops.GetMoveTaskOrder } /** End of Feature Flag **/ - moveTaskOrderPayload := payloads.MoveTaskOrder(mto) + // Add oconus rate area information to payload + shipmentPostalCodeRateArea, err := h.shipmentRateAreaFinder.GetPrimeMoveShipmentOconusRateArea(appCtx, *mto) + if err != nil { + appCtx.Logger().Error("primeapi.GetMoveTaskOrderHandler error", zap.Error(err)) + return movetaskorderops.NewGetMoveTaskOrderInternalServerError().WithPayload( + payloads.InternalServerError(handlers.FmtString(err.Error()), h.GetTraceIDFromRequest(params.HTTPRequest))), err + } + + moveTaskOrderPayload := payloads.MoveTaskOrderWithShipmentOconusRateArea(mto, shipmentPostalCodeRateArea) return movetaskorderops.NewGetMoveTaskOrderOK().WithPayload(moveTaskOrderPayload), nil }) diff --git a/pkg/handlers/primeapiv3/move_task_order_test.go b/pkg/handlers/primeapiv3/move_task_order_test.go index cb76222e7ad..b1585307e7b 100644 --- a/pkg/handlers/primeapiv3/move_task_order_test.go +++ b/pkg/handlers/primeapiv3/move_task_order_test.go @@ -7,13 +7,19 @@ import ( "time" "github.com/go-openapi/strfmt" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/mock" + "github.com/transcom/mymove/pkg/apperror" "github.com/transcom/mymove/pkg/factory" movetaskorderops "github.com/transcom/mymove/pkg/gen/primev3api/primev3operations/move_task_order" "github.com/transcom/mymove/pkg/gen/primev3messages" "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/services/mocks" movetaskorder "github.com/transcom/mymove/pkg/services/move_task_order" + mtoshipment "github.com/transcom/mymove/pkg/services/mto_shipment" "github.com/transcom/mymove/pkg/testdatagen" "github.com/transcom/mymove/pkg/unit" ) @@ -36,12 +42,22 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { suite.NotNil(payload.ETag) } - suite.Run("Success with Prime-available move by ID", func() { + setupDefaultTestHandler := func() GetMoveTaskOrderHandler { + mockShipmentRateAreaFinder := &mocks.ShipmentRateAreaFinder{} + mockShipmentRateAreaFinder.On("GetPrimeMoveShipmentOconusRateArea", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.Move"), + ).Return(nil, nil) handler := GetMoveTaskOrderHandler{ suite.HandlerConfig(), movetaskorder.NewMoveTaskOrderFetcher(), + mockShipmentRateAreaFinder, } + return handler + } + suite.Run("Success with Prime-available move by ID", func() { + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) params := movetaskorderops.GetMoveTaskOrderParams{ HTTPRequest: request, @@ -66,10 +82,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success with Prime-available move by Locator", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) params := movetaskorderops.GetMoveTaskOrderParams{ HTTPRequest: request, @@ -94,10 +107,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success returns reweighs on shipments if they exist", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) params := movetaskorderops.GetMoveTaskOrderParams{ HTTPRequest: request, @@ -146,10 +156,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - returns sit extensions on shipments if they exist", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) params := movetaskorderops.GetMoveTaskOrderParams{ HTTPRequest: request, @@ -204,10 +211,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - filters shipments handled by an external vendor", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) // Create two shipments, one prime, one external. Only prime one should be returned. @@ -259,10 +263,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - returns shipment with attached PpmShipment", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) ppmShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ { @@ -295,10 +296,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { suite.Run("Success - returns all the fields at the mtoShipment level", func() { // This tests fields that aren't other structs and Addresses - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) destinationAddress := factory.BuildAddress(suite.DB(), nil, nil) destinationType := models.DestinationTypeHomeOfRecord @@ -422,10 +420,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - returns all the fields associated with StorageFacility within MtoShipments", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) params := movetaskorderops.GetMoveTaskOrderParams{ HTTPRequest: request, @@ -478,10 +473,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - returns all the fields associated with Agents within MtoShipments", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) params := movetaskorderops.GetMoveTaskOrderParams{ HTTPRequest: request, @@ -529,10 +521,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - return all base fields assoicated with the getMoveTaskOrder", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() now := time.Now() aWeekAgo := now.AddDate(0, 0, -7) upload := factory.BuildUpload(suite.DB(), nil, nil) @@ -581,10 +570,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - return all Order fields assoicated with the getMoveTaskOrder", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() currentAddress := factory.BuildAddress(suite.DB(), nil, nil) successMove := factory.BuildAvailableToPrimeMove(suite.DB(), []factory.Customization{ { @@ -676,10 +662,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - return all PaymentRequests fields assoicated with the getMoveTaskOrder", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) successShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ @@ -865,10 +848,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - return all MTOServiceItemBasic fields assoicated with the getMoveTaskOrder", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) successShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ @@ -944,10 +924,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - return all MTOServiceItemOriginSIT fields assoicated with the getMoveTaskOrder", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) successShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ @@ -1058,10 +1035,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - return all MTOServiceItemDestSIT fields assoicated with the getMoveTaskOrder", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) successShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ @@ -1183,10 +1157,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - return all MTOServiceItemShuttle fields assoicated with the getMoveTaskOrder", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) successShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ @@ -1269,10 +1240,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }) suite.Run("Success - return all MTOServiceItemDomesticCrating fields assoicated with the getMoveTaskOrder", func() { - handler := GetMoveTaskOrderHandler{ - suite.HandlerConfig(), - movetaskorder.NewMoveTaskOrderFetcher(), - } + handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) successShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ @@ -1400,6 +1368,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { handler := GetMoveTaskOrderHandler{ suite.HandlerConfig(), movetaskorder.NewMoveTaskOrderFetcher(), + mtoshipment.NewMTOShipmentRateAreaFetcher(), } failureMove := factory.BuildMove(suite.DB(), nil, nil) // default is not available to Prime params := movetaskorderops.GetMoveTaskOrderParams{ @@ -1421,4 +1390,181 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { suite.Contains(*movePayload.Detail, failureMove.ID.String()) }) + + suite.Run("Success - returns Oconus RateArea information", func() { + const fairbanksAlaskaPostalCode = "99716" + const anchorageAlaskaPostalCode = "99521" + + mockShipmentRateAreaFinder := &mocks.ShipmentRateAreaFinder{} + + // This tests fields that aren't other structs and Addresses + handler := GetMoveTaskOrderHandler{ + suite.HandlerConfig(), + movetaskorder.NewMoveTaskOrderFetcher(), + mockShipmentRateAreaFinder, + } + successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "1234 Street", + City: "Fairbanks", + State: "AK", + PostalCode: fairbanksAlaskaPostalCode, + }, + }, + }, nil) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "1234 Street", + City: "Anchorage", + State: "AK", + PostalCode: anchorageAlaskaPostalCode, + }, + }, + }, nil) + + // mock up ShipmentPostalCodeRateArea + shipmentPostalCodeRateArea := []services.ShipmentPostalCodeRateArea{ + { + PostalCode: fairbanksAlaskaPostalCode, + RateArea: &models.ReRateArea{ + ID: uuid.Must(uuid.NewV4()), + Code: fairbanksAlaskaPostalCode, + Name: fairbanksAlaskaPostalCode, + }, + }, + { + PostalCode: anchorageAlaskaPostalCode, + RateArea: &models.ReRateArea{ + ID: uuid.Must(uuid.NewV4()), + Code: anchorageAlaskaPostalCode, + Name: anchorageAlaskaPostalCode, + }, + }, + } + + mockShipmentRateAreaFinder.On("GetPrimeMoveShipmentOconusRateArea", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.Move"), + ).Return(&shipmentPostalCodeRateArea, nil) + + destinationType := models.DestinationTypeHomeOfRecord + now := time.Now() + nowDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) + yesterDate := nowDate.AddDate(0, 0, -1) + aWeekAgo := nowDate.AddDate(0, 0, -7) + successShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + ActualDeliveryDate: &nowDate, + CounselorRemarks: models.StringPointer("LGTM"), + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + DestinationType: &destinationType, + FirstAvailableDeliveryDate: &yesterDate, + Status: models.MTOShipmentStatusApproved, + NTSRecordedWeight: models.PoundPointer(unit.Pound(249)), + PrimeEstimatedWeight: models.PoundPointer(unit.Pound(980)), + PrimeEstimatedWeightRecordedDate: &aWeekAgo, + RequiredDeliveryDate: &nowDate, + ScheduledDeliveryDate: &nowDate, + }, + }, + { + Model: successMove, + LinkOnly: true, + }, + }, nil) + params := movetaskorderops.GetMoveTaskOrderParams{ + HTTPRequest: request, + MoveID: successMove.ID.String(), + } + + // Validate incoming payload: no body to validate + + response := handler.Handle(params) + suite.NotNil(response) + + suite.IsNotErrResponse(response) + suite.IsType(&movetaskorderops.GetMoveTaskOrderOK{}, response) + + moveResponse := response.(*movetaskorderops.GetMoveTaskOrderOK) + movePayload := moveResponse.Payload + + // Validate outgoing payload + suite.NoError(movePayload.Validate(strfmt.Default)) + + suite.Equal(movePayload.ID.String(), successMove.ID.String()) + + suite.NotNil(movePayload.AvailableToPrimeAt) + suite.NotEmpty(movePayload.AvailableToPrimeAt) + + suite.NotNil(movePayload.MtoShipments[0].OriginRateArea) + suite.NotNil(movePayload.MtoShipments[0].DestinationRateArea) + + suite.Equal(shipmentPostalCodeRateArea[0].RateArea.Code, *movePayload.MtoShipments[0].OriginRateArea.RateAreaID) + suite.Equal(shipmentPostalCodeRateArea[1].RateArea.Code, *movePayload.MtoShipments[0].DestinationRateArea.RateAreaID) + + suite.Equal(successShipment.ID, handlers.FmtUUIDToPop(movePayload.MtoShipments[0].ID)) + }) + + suite.Run("failure - error while attempting to retrieve Oconus RateArea information for shipments", func() { + + mockShipmentRateAreaFinder := &mocks.ShipmentRateAreaFinder{} + + // This tests fields that aren't other structs and Addresses + handler := GetMoveTaskOrderHandler{ + suite.HandlerConfig(), + movetaskorder.NewMoveTaskOrderFetcher(), + mockShipmentRateAreaFinder, + } + successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + + defaultAddress := factory.BuildAddress(suite.DB(), nil, nil) + + mockShipmentRateAreaFinder.On("GetPrimeMoveShipmentOconusRateArea", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.Move"), + ).Return(nil, apperror.InternalServerError{}) + + destinationType := models.DestinationTypeHomeOfRecord + now := time.Now() + nowDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) + yesterDate := nowDate.AddDate(0, 0, -1) + aWeekAgo := nowDate.AddDate(0, 0, -7) + factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + ActualDeliveryDate: &nowDate, + CounselorRemarks: models.StringPointer("LGTM"), + PickupAddressID: &defaultAddress.ID, + DestinationAddressID: &defaultAddress.ID, + DestinationType: &destinationType, + FirstAvailableDeliveryDate: &yesterDate, + Status: models.MTOShipmentStatusApproved, + NTSRecordedWeight: models.PoundPointer(unit.Pound(249)), + PrimeEstimatedWeight: models.PoundPointer(unit.Pound(980)), + PrimeEstimatedWeightRecordedDate: &aWeekAgo, + RequiredDeliveryDate: &nowDate, + ScheduledDeliveryDate: &nowDate, + }, + }, + { + Model: successMove, + LinkOnly: true, + }, + }, nil) + params := movetaskorderops.GetMoveTaskOrderParams{ + HTTPRequest: request, + MoveID: successMove.ID.String(), + } + + response := handler.Handle(params) + suite.NotNil(response) + + suite.IsType(&movetaskorderops.GetMoveTaskOrderInternalServerError{}, response) + }) } diff --git a/pkg/handlers/primeapiv3/mto_service_item.go b/pkg/handlers/primeapiv3/mto_service_item.go index 4b83e3d1fd2..ef7528e4ddf 100644 --- a/pkg/handlers/primeapiv3/mto_service_item.go +++ b/pkg/handlers/primeapiv3/mto_service_item.go @@ -42,6 +42,26 @@ func (h CreateMTOServiceItemHandler) Handle(params mtoserviceitemops.CreateMTOSe return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { + // ** Create service item can not be done for PPM shipment **/ + shipment, err := models.FetchShipmentByID(appCtx.DB(), uuid.FromStringOrNil(params.Body.MtoShipmentID().String())) + if err != nil { + appCtx.Logger().Error("primeapi.CreateMTOServiceItemHandler.v3 Error Fetch Shipment", zap.Error(err)) + switch err { + case models.ErrFetchNotFound: + return mtoserviceitemops.NewCreateMTOServiceItemNotFound().WithPayload(primeapipayloads.ClientError(handlers.NotFoundMessage, "Fetch Shipment", h.GetTraceIDFromRequest(params.HTTPRequest))), err + default: + return mtoserviceitemops.NewCreateMTOServiceItemInternalServerError().WithPayload(primeapipayloads.InternalServerError(nil, h.GetTraceIDFromRequest(params.HTTPRequest))), err + } + } + + if shipment.ShipmentType == models.MTOShipmentTypePPM { + verrs := validate.NewErrors() + verrs.Add("mtoShipmentID", params.Body.MtoShipmentID().String()) + appCtx.Logger().Error("primeapi.CreateMTOServiceItemHandler.v3 - Create Service Item is not allowed for PPM shipments", zap.Error(verrs)) + return mtoserviceitemops.NewCreateMTOServiceItemUnprocessableEntity().WithPayload(primeapipayloads.ValidationError( + "Create Service Item is not allowed for PPM shipments", h.GetTraceIDFromRequest(params.HTTPRequest), verrs)), verrs + } + /** Feature Flag - Alaska **/ isAlaskaEnabled := false featureFlagName := "enable_alaska" @@ -65,7 +85,7 @@ func (h CreateMTOServiceItemHandler) Handle(params mtoserviceitemops.CreateMTOSe verrs := validate.NewErrors() verrs.Add("modelType", fmt.Sprintf("allowed modelType() %v", mapKeys)) - appCtx.Logger().Error("primeapi.CreateMTOServiceItemHandler error", zap.Error(verrs)) + appCtx.Logger().Error("primeapi.CreateMTOServiceItemHandler.v3 error", zap.Error(verrs)) return mtoserviceitemops.NewCreateMTOServiceItemUnprocessableEntity().WithPayload(primeapipayloads.ValidationError( detailErr, h.GetTraceIDFromRequest(params.HTTPRequest), verrs)), verrs } diff --git a/pkg/handlers/primeapiv3/mto_service_item_test.go b/pkg/handlers/primeapiv3/mto_service_item_test.go index 1bc9362127c..e5f36265a03 100644 --- a/pkg/handlers/primeapiv3/mto_service_item_test.go +++ b/pkg/handlers/primeapiv3/mto_service_item_test.go @@ -37,16 +37,33 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mtoServiceItem models.MTOServiceItem } - makeSubtestData := func() (subtestData *localSubtestData) { + makeSubtestDataWithPPMShipmentType := func(isPPM bool) (subtestData *localSubtestData) { subtestData = &localSubtestData{} + mtoShipmentID, _ := uuid.NewV4() + mto := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) - subtestData.mtoShipment = factory.BuildMTOShipment(suite.DB(), []factory.Customization{ - { - Model: mto, - LinkOnly: true, - }, - }, nil) + if isPPM { + subtestData.mtoShipment = factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: mto, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + ID: mtoShipmentID, + ShipmentType: models.MTOShipmentTypePPM, + }, + }, + }, nil) + } else { + subtestData.mtoShipment = factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: mto, + LinkOnly: true, + }, + }, nil) + } factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeDOFSIT) req := httptest.NewRequest("POST", "/mto-service-items", nil) sitEntryDate := time.Now() @@ -81,12 +98,18 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { return subtestData } + makeSubtestData := func() (subtestData *localSubtestData) { + return makeSubtestDataWithPPMShipmentType(false) + } + suite.Run("Successful POST - Integration Test", func() { planner := &routemocks.Planner{} planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) subtestData := makeSubtestData() moveRouter := moverouter.NewMoveRouter() @@ -147,6 +170,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -285,6 +310,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -329,6 +356,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -422,6 +451,68 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemHandler() { // Validate outgoing payload suite.NoError(responsePayload.Validate(strfmt.Default)) }) + + suite.Run("POST failure - Shipment fetch not found", func() { + subtestData := makeSubtestDataWithPPMShipmentType(true) + moveRouter := moverouter.NewMoveRouter() + planner := &routemocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + ).Return(400, nil) + creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) + handler := CreateMTOServiceItemHandler{ + suite.HandlerConfig(), + creator, + mtoChecker, + } + + // Validate incoming payload + suite.NoError(subtestData.params.Body.Validate(strfmt.Default)) + + // we are going to mock fake UUID to force NOT FOUND ERROR + subtestData.params.Body.SetMtoShipmentID(subtestData.params.Body.ID()) + + response := handler.Handle(subtestData.params) + suite.IsType(&mtoserviceitemops.CreateMTOServiceItemNotFound{}, response) + typedResponse := response.(*mtoserviceitemops.CreateMTOServiceItemNotFound) + + // Validate outgoing payload + suite.NoError(typedResponse.Payload.Validate(strfmt.Default)) + + suite.Contains(*typedResponse.Payload.Detail, "Fetch Shipment") + }) + + suite.Run("POST failure - 422 - PPM not allowed to create service item", func() { + subtestData := makeSubtestDataWithPPMShipmentType(true) + moveRouter := moverouter.NewMoveRouter() + planner := &routemocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + ).Return(400, nil) + creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) + handler := CreateMTOServiceItemHandler{ + suite.HandlerConfig(), + creator, + mtoChecker, + } + + // Validate incoming payload + suite.NoError(subtestData.params.Body.Validate(strfmt.Default)) + + response := handler.Handle(subtestData.params) + suite.IsType(&mtoserviceitemops.CreateMTOServiceItemUnprocessableEntity{}, response) + typedResponse := response.(*mtoserviceitemops.CreateMTOServiceItemUnprocessableEntity) + + // Validate outgoing payload + suite.NoError(typedResponse.Payload.Validate(strfmt.Default)) + + suite.Contains(*typedResponse.Payload.Detail, "Create Service Item is not allowed for PPM shipments") + suite.Contains(typedResponse.Payload.InvalidFields["mtoShipmentID"][0], subtestData.params.Body.MtoShipmentID().String()) + }) } func (suite *HandlerSuite) TestCreateMTOServiceItemDomesticCratingHandler() { @@ -480,6 +571,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDomesticCratingHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -517,6 +610,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDomesticCratingHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -640,6 +735,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -686,6 +783,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -755,6 +854,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -839,6 +940,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandlerWithDOFSITNoA mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -947,6 +1050,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandlerWithDOFSITWit mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1114,6 +1219,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1188,6 +1295,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1221,6 +1330,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1277,6 +1388,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1341,6 +1454,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ @@ -1377,6 +1492,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemDestSITHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) handler := CreateMTOServiceItemHandler{ diff --git a/pkg/handlers/primeapiv3/mto_shipment_test.go b/pkg/handlers/primeapiv3/mto_shipment_test.go index 44c8f229e0d..308bdf4d462 100644 --- a/pkg/handlers/primeapiv3/mto_shipment_test.go +++ b/pkg/handlers/primeapiv3/mto_shipment_test.go @@ -58,6 +58,8 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/handlers/primeapiv3/payloads/model_to_payload.go b/pkg/handlers/primeapiv3/payloads/model_to_payload.go index f4cd6ab2609..2b93dd3480f 100644 --- a/pkg/handlers/primeapiv3/payloads/model_to_payload.go +++ b/pkg/handlers/primeapiv3/payloads/model_to_payload.go @@ -13,6 +13,7 @@ import ( "github.com/transcom/mymove/pkg/gen/primev3messages" "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" ) // MoveTaskOrder payload @@ -60,6 +61,31 @@ func MoveTaskOrder(moveTaskOrder *models.Move) *primev3messages.MoveTaskOrder { return payload } +func MoveTaskOrderWithShipmentOconusRateArea(moveTaskOrder *models.Move, shipmentRateArea *[]services.ShipmentPostalCodeRateArea) *primev3messages.MoveTaskOrder { + // create default payload + var payload = MoveTaskOrder(moveTaskOrder) + + // decorate payload with oconus rateArea information + if payload != nil && shipmentRateArea != nil { + // build map from incoming rateArea list to simplify rateArea lookup by postal code + var shipmentPostalCodeRateAreaLookupMap = make(map[string]services.ShipmentPostalCodeRateArea) + for _, ra := range *shipmentRateArea { + shipmentPostalCodeRateAreaLookupMap[ra.PostalCode] = ra + } + // Origin/Destination RateArea will be present on root shipment level for all non-PPM shipment types + for _, shipment := range payload.MtoShipments { + if shipment.PpmShipment != nil { + shipment.PpmShipment.OriginRateArea = PostalCodeToRateArea(shipment.PpmShipment.PickupAddress.PostalCode, shipmentPostalCodeRateAreaLookupMap) + shipment.PpmShipment.DestinationRateArea = PostalCodeToRateArea(shipment.PpmShipment.DestinationAddress.PostalCode, shipmentPostalCodeRateAreaLookupMap) + } else { + shipment.OriginRateArea = PostalCodeToRateArea(shipment.PickupAddress.PostalCode, shipmentPostalCodeRateAreaLookupMap) + shipment.DestinationRateArea = PostalCodeToRateArea(shipment.DestinationAddress.PostalCode, shipmentPostalCodeRateAreaLookupMap) + } + } + } + return payload +} + // Customer payload func Customer(customer *models.ServiceMember) *primev3messages.Customer { if customer == nil { @@ -205,16 +231,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, } } @@ -1066,3 +1093,14 @@ func Port(portLocation *models.PortLocation) *primev3messages.Port { Country: portLocation.Country.CountryName, } } + +// PostalCodeToRateArea converts postalCode into RateArea model to payload +func PostalCodeToRateArea(postalCode *string, shipmentPostalCodeRateAreaMap map[string]services.ShipmentPostalCodeRateArea) *primev3messages.RateArea { + if postalCode == nil { + return nil + } + if ra, ok := shipmentPostalCodeRateAreaMap[*postalCode]; ok { + return &primev3messages.RateArea{ID: handlers.FmtUUID(ra.RateArea.ID), RateAreaID: &ra.RateArea.Code, RateAreaName: &ra.RateArea.Name} + } + return nil +} diff --git a/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go b/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go index 5f7e78e43ed..8ef4396aa1d 100644 --- a/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go +++ b/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go @@ -1,6 +1,8 @@ package payloads import ( + "encoding/json" + "slices" "time" "github.com/go-openapi/strfmt" @@ -13,6 +15,7 @@ import ( "github.com/transcom/mymove/pkg/gen/primev3messages" "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/unit" ) @@ -104,7 +107,280 @@ func (suite *PayloadsSuite) TestMoveTaskOrder() { suite.Require().NotEmpty(returnedModel.MtoShipments) suite.Equal(basicMove.MTOShipments[0].PickupAddress.County, returnedModel.MtoShipments[0].PickupAddress.County) }) + + suite.Run("Success - payload with RateArea", func() { + cloneMove := func(orig *models.Move) (*models.Move, error) { + origJSON, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + clone := models.Move{} + if err = json.Unmarshal(origJSON, &clone); err != nil { + return nil, err + } + + return &clone, nil + } + + newMove, err := cloneMove(&basicMove) + suite.NotNil(newMove) + suite.Nil(err) + + const fairbanksAlaskaPostalCode = "99716" + const anchorageAlaskaPostalCode = "99521" + const wasillaAlaskaPostalCode = "99652" + + //clear MTOShipment and rebuild with specifics for test + newMove.MTOShipments = newMove.MTOShipments[:0] + + newMove.MTOShipments = append(newMove.MTOShipments, models.MTOShipment{ + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Fairbanks", + State: "AK", + PostalCode: fairbanksAlaskaPostalCode, + }, + DestinationAddress: &models.Address{ + 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{ + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Wasilla", + State: "AK", + PostalCode: wasillaAlaskaPostalCode, + }, + DestinationAddress: &models.Address{ + 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{ + ShipmentType: models.MTOShipmentTypePPM, + PPMShipment: &models.PPMShipment{ + ID: uuid.Must(uuid.NewV4()), + ApprovedAt: models.TimePointer(time.Now()), + Status: models.PPMShipmentStatusNeedsAdvanceApproval, + ActualMoveDate: models.TimePointer(time.Now()), + ActualPickupPostalCode: models.StringPointer("42444"), + ActualDestinationPostalCode: models.StringPointer("30813"), + HasReceivedAdvance: models.BoolPointer(true), + AdvanceAmountReceived: models.CentPointer(unit.Cents(340000)), + FinalIncentive: models.CentPointer(50000000), + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Wasilla", + State: "AK", + PostalCode: wasillaAlaskaPostalCode, + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Wasilla", + State: "AK", + PostalCode: wasillaAlaskaPostalCode, + }, + }, + }) + newMove.MTOShipments = append(newMove.MTOShipments, models.MTOShipment{ + PPMShipment: &models.PPMShipment{ + ID: uuid.Must(uuid.NewV4()), + ApprovedAt: models.TimePointer(time.Now()), + Status: models.PPMShipmentStatusNeedsAdvanceApproval, + ActualMoveDate: models.TimePointer(time.Now()), + ActualPickupPostalCode: models.StringPointer("42444"), + ActualDestinationPostalCode: models.StringPointer("30813"), + HasReceivedAdvance: models.BoolPointer(true), + AdvanceAmountReceived: models.CentPointer(unit.Cents(340000)), + FinalIncentive: models.CentPointer(50000000), + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Beverly Hills", + State: "CA", + PostalCode: "90210", + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Beverly Hills", + State: "CA", + PostalCode: "90210", + }, + }, + }) + 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", + DestinationGbloc: models.StringPointer("JEAT"), + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + StreetAddress2: &streetAddress2, + StreetAddress3: &streetAddress3, + City: "Beverly Hills", + State: "CA", + PostalCode: "90210", + DestinationGbloc: models.StringPointer("JEAT"), + }, + }) + + // no ShipmentPostalCodeRateArea passed in + returnedModel := MoveTaskOrderWithShipmentOconusRateArea(newMove, nil) + + suite.IsType(&primev3messages.MoveTaskOrder{}, returnedModel) + suite.Equal(strfmt.UUID(newMove.ID.String()), returnedModel.ID) + suite.Equal(newMove.Locator, returnedModel.MoveCode) + suite.Equal(strfmt.DateTime(newMove.CreatedAt), returnedModel.CreatedAt) + suite.Equal(handlers.FmtDateTimePtr(newMove.AvailableToPrimeAt), returnedModel.AvailableToPrimeAt) + suite.Equal(strfmt.UUID(newMove.OrdersID.String()), returnedModel.OrderID) + suite.Equal(ordersType, returnedModel.Order.OrdersType) + suite.Equal(shipmentGBLOC, returnedModel.Order.OriginDutyLocationGBLOC) + suite.Equal(referenceID, returnedModel.ReferenceID) + suite.Equal(strfmt.DateTime(newMove.UpdatedAt), returnedModel.UpdatedAt) + suite.NotEmpty(returnedModel.ETag) + suite.True(returnedModel.ExcessWeightQualifiedAt.Equal(strfmt.DateTime(*newMove.ExcessWeightQualifiedAt))) + suite.True(returnedModel.ExcessWeightAcknowledgedAt.Equal(strfmt.DateTime(*newMove.ExcessWeightAcknowledgedAt))) + suite.Require().NotNil(returnedModel.ExcessWeightUploadID) + suite.Equal(strfmt.UUID(newMove.ExcessWeightUploadID.String()), *returnedModel.ExcessWeightUploadID) + suite.Equal(factory.DefaultContractNumber, returnedModel.ContractNumber) + suite.Equal(models.SupplyAndServicesCostEstimate, returnedModel.Order.SupplyAndServicesCostEstimate) + suite.Equal(models.MethodOfPayment, returnedModel.Order.MethodOfPayment) + suite.Equal(models.NAICS, returnedModel.Order.Naics) + suite.Equal(packingInstructions, returnedModel.Order.PackingAndShippingInstructions) + suite.Require().NotEmpty(returnedModel.MtoShipments) + suite.Equal(newMove.MTOShipments[0].PickupAddress.County, returnedModel.MtoShipments[0].PickupAddress.County) + + // verify there are no RateArea set because no ShipmentPostalCodeRateArea passed in. + for _, shipment := range returnedModel.MtoShipments { + suite.Nil(shipment.OriginRateArea) + suite.Nil(shipment.DestinationRateArea) + if shipment.PpmShipment != nil { + suite.Nil(shipment.PpmShipment.OriginRateArea) + suite.Nil(shipment.PpmShipment.DestinationRateArea) + } + } + + // mock up ShipmentPostalCodeRateArea + shipmentPostalCodeRateArea := []services.ShipmentPostalCodeRateArea{ + { + PostalCode: fairbanksAlaskaPostalCode, + RateArea: &models.ReRateArea{ + ID: uuid.Must(uuid.NewV4()), + Code: fairbanksAlaskaPostalCode, + Name: fairbanksAlaskaPostalCode, + }, + }, + { + PostalCode: anchorageAlaskaPostalCode, + RateArea: &models.ReRateArea{ + ID: uuid.Must(uuid.NewV4()), + Code: anchorageAlaskaPostalCode, + Name: anchorageAlaskaPostalCode, + }, + }, + { + PostalCode: wasillaAlaskaPostalCode, + RateArea: &models.ReRateArea{ + ID: uuid.Must(uuid.NewV4()), + Code: wasillaAlaskaPostalCode, + Name: wasillaAlaskaPostalCode, + }, + }, + } + + returnedModel = MoveTaskOrderWithShipmentOconusRateArea(newMove, &shipmentPostalCodeRateArea) + + var shipmentPostalCodeRateAreaLookupMap = make(map[string]services.ShipmentPostalCodeRateArea) + for _, i := range shipmentPostalCodeRateArea { + shipmentPostalCodeRateAreaLookupMap[i.PostalCode] = i + } + + // test Alaska/Oconus PostCodes have associative RateArea for respective shipment + expectedAlaskaPostalCodes := []string{fairbanksAlaskaPostalCode, anchorageAlaskaPostalCode, wasillaAlaskaPostalCode} + for _, shipment := range returnedModel.MtoShipments { + if shipment.PpmShipment != nil { + suite.NotNil(shipment.PpmShipment.PickupAddress) + suite.NotNil(shipment.PpmShipment.DestinationAddress) + if slices.Contains(expectedAlaskaPostalCodes, *shipment.PpmShipment.PickupAddress.PostalCode) { + // verify mapping of RateArea is correct + ra, contains := shipmentPostalCodeRateAreaLookupMap[*shipment.PpmShipment.PickupAddress.PostalCode] + suite.True(contains) + suite.NotNil(shipment.PpmShipment.OriginRateArea) + // for testing purposes RateArea code/names are using postalCodes as value + suite.Equal(ra.PostalCode, *shipment.PpmShipment.PickupAddress.PostalCode) + suite.Equal(ra.PostalCode, *shipment.PpmShipment.OriginRateArea.RateAreaName) + } else { + suite.Nil(shipment.PpmShipment.OriginRateArea) + } + if slices.Contains(expectedAlaskaPostalCodes, *shipment.PpmShipment.DestinationAddress.PostalCode) { + ra, contains := shipmentPostalCodeRateAreaLookupMap[*shipment.PpmShipment.DestinationAddress.PostalCode] + suite.True(contains) + suite.NotNil(shipment.PpmShipment.DestinationRateArea) + suite.Equal(ra.PostalCode, *shipment.PpmShipment.DestinationAddress.PostalCode) + suite.Equal(ra.PostalCode, *shipment.PpmShipment.DestinationRateArea.RateAreaName) + } else { + suite.Nil(shipment.PpmShipment.DestinationRateArea) + } + // because it's PPM verify root doesnt have rateArea for org/dest + suite.Nil(shipment.OriginRateArea) + suite.Nil(shipment.DestinationRateArea) + } 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) + suite.NotNil(shipment.OriginRateArea) + suite.Equal(ra.PostalCode, *shipment.PickupAddress.PostalCode) + suite.Equal(ra.PostalCode, *shipment.OriginRateArea.RateAreaName) + suite.NotNil(shipment.OriginRateArea) + } else { + suite.Nil(shipment.OriginRateArea) + } + if slices.Contains(expectedAlaskaPostalCodes, *shipment.DestinationAddress.PostalCode) { + ra, contains := shipmentPostalCodeRateAreaLookupMap[*shipment.DestinationAddress.PostalCode] + suite.True(contains) + suite.NotNil(shipment.DestinationRateArea) + suite.Equal(ra.PostalCode, *shipment.DestinationAddress.PostalCode) + suite.Equal(ra.PostalCode, *shipment.DestinationRateArea.RateAreaName) + suite.NotNil(shipment.OriginRateArea) + } else { + suite.Nil(shipment.OriginRateArea) + } + } + } + }) } + func (suite *PayloadsSuite) TestReweigh() { id, _ := uuid.NewV4() shipmentID, _ := uuid.NewV4() diff --git a/pkg/handlers/supportapi/move_task_order_test.go b/pkg/handlers/supportapi/move_task_order_test.go index f10fde1438e..b1241de3f32 100644 --- a/pkg/handlers/supportapi/move_task_order_test.go +++ b/pkg/handlers/supportapi/move_task_order_test.go @@ -173,6 +173,8 @@ func (suite *HandlerSuite) TestMakeMoveAvailableHandlerIntegrationSuccess() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { @@ -388,6 +390,8 @@ func (suite *HandlerSuite) TestCreateMoveTaskOrderRequestHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/handlers/supportapi/mto_service_item_test.go b/pkg/handlers/supportapi/mto_service_item_test.go index 9b2a995255a..4c0596523a4 100644 --- a/pkg/handlers/supportapi/mto_service_item_test.go +++ b/pkg/handlers/supportapi/mto_service_item_test.go @@ -85,6 +85,8 @@ func (suite *HandlerSuite) TestUpdateMTOServiceItemStatusHandlerApproveSuccess() mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) handler := UpdateMTOServiceItemStatusHandler{handlerConfig, mtoserviceitem.NewMTOServiceItemUpdater(planner, queryBuilder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher), @@ -142,6 +144,8 @@ func (suite *HandlerSuite) TestUpdateMTOServiceItemStatusHandlerRejectSuccess() mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) handler := UpdateMTOServiceItemStatusHandler{handlerConfig, mtoserviceitem.NewMTOServiceItemUpdater(planner, queryBuilder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher), @@ -199,6 +203,8 @@ func (suite *HandlerSuite) TestUpdateMTOServiceItemStatusHandlerRejectionFailedN mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) handler := UpdateMTOServiceItemStatusHandler{handlerConfig, mtoserviceitem.NewMTOServiceItemUpdater(planner, queryBuilder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher), diff --git a/pkg/handlers/supportapi/mto_shipment_test.go b/pkg/handlers/supportapi/mto_shipment_test.go index c1a9bb7018b..9a3a0c07a85 100644 --- a/pkg/handlers/supportapi/mto_shipment_test.go +++ b/pkg/handlers/supportapi/mto_shipment_test.go @@ -97,6 +97,8 @@ func (suite *HandlerSuite) TestUpdateMTOShipmentStatusHandler() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) siCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) planner.On("Zip5TransitDistanceLineHaul", diff --git a/pkg/models/address.go b/pkg/models/address.go index e683f7771ab..d89a163c9aa 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:"-"` // 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_service_items.go b/pkg/models/mto_service_items.go index 822e568f227..446c822c1ee 100644 --- a/pkg/models/mto_service_items.go +++ b/pkg/models/mto_service_items.go @@ -154,7 +154,9 @@ func FetchServiceItem(db *pop.Connection, serviceItemID uuid.UUID) (MTOServiceIt err := db.Eager("SITDestinationOriginalAddress", "SITDestinationFinalAddress", "ReService", - "CustomerContacts").Where("id = ?", serviceItemID).First(&serviceItem) + "CustomerContacts", + "MTOShipment.PickupAddress", + "MTOShipment.DestinationAddress").Where("id = ?", serviceItemID).First(&serviceItem) if err != nil { if errors.Cause(err).Error() == RecordNotFoundErrorString { diff --git a/pkg/models/mto_shipments.go b/pkg/models/mto_shipments.go index da5e021a698..9c2506e3292 100644 --- a/pkg/models/mto_shipments.go +++ b/pkg/models/mto_shipments.go @@ -1,6 +1,7 @@ package models import ( + "database/sql" "fmt" "time" @@ -8,6 +9,7 @@ import ( "github.com/gobuffalo/validate/v3" "github.com/gobuffalo/validate/v3/validators" "github.com/gofrs/uuid" + "github.com/pkg/errors" "github.com/transcom/mymove/pkg/unit" ) @@ -280,6 +282,10 @@ func (m MTOShipment) ContainsAPPMShipment() bool { return m.PPMShipment != nil } +func (m MTOShipment) IsPPMShipment() bool { + return m.ShipmentType == MTOShipmentTypePPM +} + // determining the market code for a shipment based off of address isOconus value // this function takes in a shipment and returns the same shipment with the updated MarketCode value func DetermineShipmentMarketCode(shipment *MTOShipment) *MTOShipment { @@ -361,6 +367,33 @@ func DetermineMarketCode(address1 *Address, address2 *Address) (MarketCode, erro } } +// PortLocationInfo holds the ZIP code and port type for a shipment +// this is used in the db function/query below +type PortLocationInfo struct { + UsprZipID string `db:"uspr_zip_id"` + PortType string `db:"port_type"` +} + +// GetPortLocationForShipment gets the ZIP and port type associated with the port for the POEFSC/PODFSC service item in a shipment +func GetPortLocationInfoForShipment(db *pop.Connection, shipmentID uuid.UUID) (*string, *string, error) { + var portLocationInfo PortLocationInfo + + err := db.RawQuery("SELECT * FROM get_port_location_info_for_shipment($1)", shipmentID). + First(&portLocationInfo) + + if err != nil && err != sql.ErrNoRows { + return nil, nil, fmt.Errorf("error fetching port location for shipment ID: %s with error %w", shipmentID, err) + } + + // return the ZIP code and port type, or nil if not found + if portLocationInfo.UsprZipID != "" && portLocationInfo.PortType != "" { + return &portLocationInfo.UsprZipID, &portLocationInfo.PortType, nil + } + + // if nothing was found, return nil - just means we don't have the port info from Prime yet + return nil, nil, nil +} + func CreateApprovedServiceItemsForShipment(db *pop.Connection, shipment *MTOShipment) error { err := db.RawQuery("CALL create_approved_service_items_for_shipment($1)", shipment.ID).Exec() if err != nil { @@ -373,11 +406,44 @@ func CreateApprovedServiceItemsForShipment(db *pop.Connection, shipment *MTOShip // a db stored proc that will handle updating the pricing_estimate columns of basic service items for shipment types: // iHHG // iUB -func UpdateEstimatedPricingForShipmentBasicServiceItems(db *pop.Connection, shipment *MTOShipment) error { - err := db.RawQuery("CALL update_service_item_pricing($1)", shipment.ID).Exec() +func UpdateEstimatedPricingForShipmentBasicServiceItems(db *pop.Connection, shipment *MTOShipment, mileage *int) error { + err := db.RawQuery("CALL update_service_item_pricing($1, $2)", shipment.ID, mileage).Exec() if err != nil { return fmt.Errorf("error updating estimated pricing for shipment's service items: %w", err) } 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) + } + + 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 + err := db.Q().Find(&mtoShipment, shipmentID) + + if err != nil { + if errors.Cause(err).Error() == RecordNotFoundErrorString { + return nil, ErrFetchNotFound + } + return nil, err + } + return &mtoShipment, nil +} diff --git a/pkg/models/mto_shipments_test.go b/pkg/models/mto_shipments_test.go index 5f2a5f78864..4014aa90d12 100644 --- a/pkg/models/mto_shipments_test.go +++ b/pkg/models/mto_shipments_test.go @@ -324,3 +324,164 @@ func (suite *ModelSuite) TestCreateApprovedServiceItemsForShipment() { suite.Error(err) }) } + +func (suite *ModelSuite) TestFindShipmentByID() { + suite.Run("success - test find", func() { + shipment := factory.BuildMTOShipmentMinimal(suite.DB(), nil, nil) + _, err := models.FetchShipmentByID(suite.DB(), shipment.ID) + suite.NoError(err) + }) + + suite.Run("not found test find", func() { + notValidID := uuid.Must(uuid.NewV4()) + _, err := models.FetchShipmentByID(suite.DB(), notValidID) + suite.Error(err) + 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") + }) + 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") + }) +} diff --git a/pkg/models/port_test.go b/pkg/models/port_test.go new file mode 100644 index 00000000000..062f3ee7f59 --- /dev/null +++ b/pkg/models/port_test.go @@ -0,0 +1,27 @@ +package models_test + +import ( + "time" + + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/models" +) + +func (suite *ModelSuite) TestPort() { + suite.Run("test Port functions", func() { + port := models.Port{ + ID: uuid.Must(uuid.NewV4()), + PortCode: "PortCode", + PortType: "Both", + PortName: "PortName", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + suite.Equal(port.TableName(), "ports") + suite.Equal(port.PortName, "PortName") + suite.Equal(port.PortType.String(), "Both") + }) + +} diff --git a/pkg/models/re_rate_area.go b/pkg/models/re_rate_area.go index 167b70577ce..7b613b42a28 100644 --- a/pkg/models/re_rate_area.go +++ b/pkg/models/re_rate_area.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "time" "github.com/gobuffalo/pop/v6" @@ -53,3 +54,16 @@ func FetchReRateAreaItem(tx *pop.Connection, contractID uuid.UUID, code string) return &area, err } + +// a db stored proc that takes in an address id & a service code to get the rate area id for an address +func FetchRateAreaID(db *pop.Connection, addressID uuid.UUID, serviceID uuid.UUID, contractID uuid.UUID) (uuid.UUID, error) { + if addressID != uuid.Nil && serviceID != uuid.Nil && contractID != uuid.Nil { + var rateAreaID uuid.UUID + err := db.RawQuery("SELECT get_rate_area_id($1, $2, $3)", addressID, serviceID, contractID).First(&rateAreaID) + if err != nil { + return uuid.Nil, fmt.Errorf("error fetching rate area id for shipment ID: %s, service ID %s, and contract ID: %s: %s", addressID, serviceID, contractID, err) + } + return rateAreaID, nil + } + return uuid.Nil, fmt.Errorf("error fetching rate area ID - required parameters not provided") +} diff --git a/pkg/models/re_rate_area_test.go b/pkg/models/re_rate_area_test.go index a0769056783..87f310c2088 100644 --- a/pkg/models/re_rate_area_test.go +++ b/pkg/models/re_rate_area_test.go @@ -3,7 +3,9 @@ package models_test import ( "github.com/gofrs/uuid" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" ) func (suite *ModelSuite) TestReRateAreaValidation() { @@ -28,3 +30,23 @@ func (suite *ModelSuite) TestReRateAreaValidation() { suite.verifyValidationErrors(&emptyReRateArea, expErrors) }) } + +func (suite *ModelSuite) TestFetchRateAreaID() { + suite.Run("success - fetching a rate area ID", func() { + service := factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeIHPK) + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + address := factory.BuildAddress(suite.DB(), nil, nil) + rateAreaId, err := models.FetchRateAreaID(suite.DB(), address.ID, service.ID, contract.ID) + suite.NotNil(rateAreaId) + suite.NoError(err) + }) + + suite.Run("fail - receive error when not all values are provided", func() { + var nilUuid uuid.UUID + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + address := factory.BuildAddress(suite.DB(), nil, nil) + rateAreaId, err := models.FetchRateAreaID(suite.DB(), address.ID, nilUuid, contract.ID) + suite.Equal(uuid.Nil, rateAreaId) + suite.Error(err) + }) +} diff --git a/pkg/models/re_zip3s_test.go b/pkg/models/re_zip3s_test.go index 3395842216d..79a5d70c613 100644 --- a/pkg/models/re_zip3s_test.go +++ b/pkg/models/re_zip3s_test.go @@ -4,7 +4,6 @@ import ( "github.com/gofrs/uuid" "github.com/transcom/mymove/pkg/models" - "github.com/transcom/mymove/pkg/testdatagen" ) func (suite *ModelSuite) TestReZip3Validations() { @@ -48,11 +47,6 @@ func (suite *ModelSuite) TestReZip3Validations() { suite.Run("test FetchReZip3Item", func() { zip3 := "606" - testdatagen.MakeReZip3(suite.DB(), testdatagen.Assertions{ - ReZip3: models.ReZip3{ - Zip3: zip3, - }, - }) reZip3, err := models.FetchReZip3Item(suite.DB(), zip3) suite.Nil(err) diff --git a/pkg/models/service_item_param_key.go b/pkg/models/service_item_param_key.go index 3bfd789dcc4..0c637cc7d92 100644 --- a/pkg/models/service_item_param_key.go +++ b/pkg/models/service_item_param_key.go @@ -61,6 +61,10 @@ const ( ServiceItemParamNameNTSPackingFactor ServiceItemParamName = "NTSPackingFactor" // ServiceItemParamNameNumberDaysSIT is the param key name NumberDaysSIT ServiceItemParamNameNumberDaysSIT ServiceItemParamName = "NumberDaysSIT" + // ServiceItemParamNamePerUnitCents is the param key name PerUnitCents + ServiceItemParamNamePerUnitCents ServiceItemParamName = "PerUnitCents" + // ServiceItemParamNamePortZip is the param key name PortZip + ServiceItemParamNamePortZip ServiceItemParamName = "PortZip" // ServiceItemParamNamePriceAreaDest is the param key name PriceAreaDest ServiceItemParamNamePriceAreaDest ServiceItemParamName = "PriceAreaDest" // ServiceItemParamNamePriceAreaIntlDest is the param key name PriceAreaIntlDest @@ -275,6 +279,8 @@ var ValidServiceItemParamNames = []ServiceItemParamName{ ServiceItemParamNameStandaloneCrateCap, ServiceItemParamNameUncappedRequestTotal, ServiceItemParamNameLockedPriceCents, + ServiceItemParamNamePerUnitCents, + ServiceItemParamNamePortZip, } // ValidServiceItemParamNameStrings lists all valid service item param key names @@ -349,6 +355,8 @@ var ValidServiceItemParamNameStrings = []string{ string(ServiceItemParamNameStandaloneCrateCap), string(ServiceItemParamNameUncappedRequestTotal), string(ServiceItemParamNameLockedPriceCents), + string(ServiceItemParamNamePerUnitCents), + string(ServiceItemParamNamePortZip), } // ValidServiceItemParamTypes lists all valid service item param types diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go index be5f2515e49..7f05aa59ee9 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go @@ -49,6 +49,25 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service // Now calculate the distance between zips pickupZip := r.PickupAddress.PostalCode destinationZip := r.DestinationAddress.PostalCode + + // if the shipment is international, we need to change the respective ZIP to use the port ZIP and not the address ZIP + if mtoShipment.MarketCode == models.MarketCodeInternational { + portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), *mtoShipmentID) + if err != nil { + return "", err + } + if portZip != nil && portType != nil { + // if the port type is POEFSC this means the shipment is CONUS -> OCONUS (pickup -> port) + // if the port type is PODFSC this means the shipment is OCONUS -> CONUS (port -> destination) + if *portType == models.ReServiceCodePOEFSC.String() { + destinationZip = *portZip + } else if *portType == models.ReServiceCodePODFSC.String() { + pickupZip = *portZip + } + } else { + return "", apperror.NewNotFoundError(*mtoShipmentID, "looking for port ZIP for shipment") + } + } errorMsgForPickupZip := fmt.Sprintf("Shipment must have valid pickup zipcode. Received: %s", pickupZip) errorMsgForDestinationZip := fmt.Sprintf("Shipment must have valid destination zipcode. Received: %s", destinationZip) if len(pickupZip) < 5 { @@ -83,7 +102,7 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service } if mtoShipment.DeliveryAddressUpdate != nil && mtoShipment.DeliveryAddressUpdate.Status == models.ShipmentAddressUpdateStatusApproved { - distanceMiles, err = planner.ZipTransitDistance(appCtx, pickupZip, mtoShipment.DeliveryAddressUpdate.NewAddress.PostalCode) + distanceMiles, err = planner.ZipTransitDistance(appCtx, pickupZip, mtoShipment.DeliveryAddressUpdate.NewAddress.PostalCode, false, false) if err != nil { return "", err } @@ -91,14 +110,15 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service } } - if mtoShipment.Distance != nil && mtoShipment.ShipmentType != models.MTOShipmentTypePPM { + internationalShipment := mtoShipment.MarketCode == models.MarketCodeInternational + if mtoShipment.Distance != nil && mtoShipment.ShipmentType != models.MTOShipmentTypePPM && !internationalShipment { return strconv.Itoa(mtoShipment.Distance.Int()), nil } if pickupZip == destinationZip { distanceMiles = 1 } else { - distanceMiles, err = planner.ZipTransitDistance(appCtx, pickupZip, destinationZip) + distanceMiles, err = planner.ZipTransitDistance(appCtx, pickupZip, destinationZip, false, internationalShipment) if err != nil { return "", err } diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go index 4f826c52009..f4fa18c8790 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go @@ -63,6 +63,68 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceLookup() { suite.Equal(unit.Miles(defaultZipDistance), *mtoShipment.Distance) }) + suite.Run("Calculate transit zip distance for international shipment with port data", func() { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + portLocation := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "SEA", + }, + }, + }, nil) + mtoServiceItem := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.ReService{ + Code: models.ReServiceCodePOEFSC, + }, + }, + { + Model: models.Address{ + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.MTOServiceItem{ + POELocationID: &portLocation.ID, + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + }, []factory.Trait{ + factory.GetTraitAvailableToPrimeMove, + }) + + paymentRequest := factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: mtoServiceItem.MoveTaskOrder, + LinkOnly: true, + }, + }, nil) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, paymentRequest.ID, paymentRequest.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + distanceStr, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + expected := strconv.Itoa(defaultInternationalZipDistance) + suite.Equal(expected, distanceStr) + + var mtoShipment models.MTOShipment + err = suite.DB().Find(&mtoShipment, mtoServiceItem.MTOShipmentID) + suite.NoError(err) + + suite.Equal(unit.Miles(defaultInternationalZipDistance), *mtoShipment.Distance) + }) + suite.Run("Calculate zip distance lookup without a saved service item", func() { ppmShipment := factory.BuildPPMShipment(suite.DB(), nil, nil) @@ -79,7 +141,7 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceLookup() { suite.NoError(err) planner := suite.planner.(*mocks.Planner) - planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, ppmShipment.DestinationAddress.PostalCode) + planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, ppmShipment.DestinationAddress.PostalCode, false, false) err = suite.DB().Reload(&ppmShipment.Shipment) suite.NoError(err) @@ -110,7 +172,7 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceLookup() { suite.NoError(err) planner := suite.planner.(*mocks.Planner) - planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, ppmShipment.DestinationAddress.PostalCode) + planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, ppmShipment.DestinationAddress.PostalCode, false, false) err = suite.DB().Reload(&ppmShipment.Shipment) suite.NoError(err) @@ -143,7 +205,7 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceLookup() { suite.NoError(err) planner := suite.planner.(*mocks.Planner) - planner.AssertNotCalled(suite.T(), "ZipTransitDistance", appContext, shipment.PickupAddress.PostalCode, shipment.DestinationAddress.PostalCode) + planner.AssertNotCalled(suite.T(), "ZipTransitDistance", appContext, shipment.PickupAddress.PostalCode, shipment.DestinationAddress.PostalCode, false, false) err = suite.DB().Reload(&shipment) suite.NoError(err) diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_sit_dest_lookup.go b/pkg/payment_request/service_param_value_lookups/distance_zip_sit_dest_lookup.go index 2136d7556bc..7ddc8651946 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_sit_dest_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_sit_dest_lookup.go @@ -33,7 +33,7 @@ func (r DistanceZipSITDestLookup) lookup(appCtx appcontext.AppContext, keyData * if destZip == finalDestZip { distanceMiles = 1 } else { - distanceMiles, distanceErr = planner.ZipTransitDistance(appCtx, destZip, finalDestZip) + distanceMiles, distanceErr = planner.ZipTransitDistance(appCtx, destZip, finalDestZip, false, false) } if distanceErr != nil { diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_sit_dest_lookup_test.go b/pkg/payment_request/service_param_value_lookups/distance_zip_sit_dest_lookup_test.go index 8231238b831..997fcb409e7 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_sit_dest_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_sit_dest_lookup_test.go @@ -211,6 +211,8 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceZipSITDestLookup() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(0, errors.New("error with ZipTransitDistance")) paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), errorPlanner, mtoServiceItemSameZip3, paymentRequest.ID, paymentRequest.MoveTaskOrderID, nil) diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_sit_origin_lookup.go b/pkg/payment_request/service_param_value_lookups/distance_zip_sit_origin_lookup.go index e452adbed10..52178be8d65 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_sit_origin_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_sit_origin_lookup.go @@ -39,7 +39,7 @@ func (r DistanceZipSITOriginLookup) lookup(appCtx appcontext.AppContext, keyData if originZip == actualOriginZip { distanceMiles = 1 } else { - distanceMiles, distanceErr = planner.ZipTransitDistance(appCtx, originZip, actualOriginZip) + distanceMiles, distanceErr = planner.ZipTransitDistance(appCtx, originZip, actualOriginZip, false, false) } if distanceErr != nil { return "", distanceErr diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_sit_origin_lookup_test.go b/pkg/payment_request/service_param_value_lookups/distance_zip_sit_origin_lookup_test.go index 31d766db385..6c055f7fd6b 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_sit_origin_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_sit_origin_lookup_test.go @@ -184,6 +184,8 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceZipSITOriginLookup() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(0, errors.New("error with ZipTransitDistance")) paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), errorPlanner, mtoServiceItemSameZip3, paymentRequest.ID, paymentRequest.MoveTaskOrderID, nil) diff --git a/pkg/payment_request/service_param_value_lookups/eia_fuel_price_lookup.go b/pkg/payment_request/service_param_value_lookups/eia_fuel_price_lookup.go index 9e5e29cc188..9d60fd9e7b1 100644 --- a/pkg/payment_request/service_param_value_lookups/eia_fuel_price_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/eia_fuel_price_lookup.go @@ -22,7 +22,7 @@ func (r EIAFuelPriceLookup) lookup(appCtx appcontext.AppContext, _ *ServiceItemP // Make sure there is an actual pickup date since ActualPickupDate is nullable actualPickupDate := r.MTOShipment.ActualPickupDate if actualPickupDate == nil { - return "", fmt.Errorf("not found looking for pickup address") + return "", fmt.Errorf("not found looking for shipment pickup date") } // Find the GHCDieselFuelPrice object effective before the shipment's ActualPickupDate and ends after the ActualPickupDate diff --git a/pkg/payment_request/service_param_value_lookups/eia_fuel_price_lookup_test.go b/pkg/payment_request/service_param_value_lookups/eia_fuel_price_lookup_test.go index ded105300ab..2b310cbcdc0 100644 --- a/pkg/payment_request/service_param_value_lookups/eia_fuel_price_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/eia_fuel_price_lookup_test.go @@ -207,53 +207,16 @@ func (suite *ServiceParamValueLookupsSuite) TestEIAFuelPriceLookupWithInvalidAct suite.Run("lookup GHC diesel fuel price with nil actual pickup date", func() { setupTestData() - - paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, paymentRequest.ID, paymentRequest.MoveTaskOrderID, nil) + var shipment models.MTOShipment + err := suite.DB().Find(&shipment, mtoServiceItem.MTOShipmentID) suite.FatalNoError(err) - _, err = paramLookup.ServiceParamValue(suite.AppContextForTest(), key) - suite.Error(err) - suite.Contains(err.Error(), "EIAFuelPriceLookup with error Not found Looking for GHCDieselFuelPrice") - }) -} - -func (suite *ServiceParamValueLookupsSuite) TestEIAFuelPriceLookupWithNoGHCDieselFuelPriceData() { - key := models.ServiceItemParamNameEIAFuelPrice - var mtoServiceItem models.MTOServiceItem - var paymentRequest models.PaymentRequest - actualPickupDate := time.Date(2020, time.July, 15, 0, 0, 0, 0, time.UTC) - - setupTestData := func() { - testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ - ReContractYear: models.ReContractYear{ - StartDate: time.Now().Add(-24 * time.Hour), - EndDate: time.Now().Add(24 * time.Hour), - }, - }) - mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ - { - Model: models.MTOShipment{ - ActualPickupDate: &actualPickupDate, - }, - }, - }, []factory.Trait{ - factory.GetTraitAvailableToPrimeMove, - }) - - paymentRequest = factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ - { - Model: mtoServiceItem.MoveTaskOrder, - LinkOnly: true, - }, - }, nil) - } - - suite.Run("lookup GHC diesel fuel price with no data", func() { - setupTestData() + shipment.ActualPickupDate = nil + suite.MustSave(&shipment) paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, paymentRequest.ID, paymentRequest.MoveTaskOrderID, nil) suite.FatalNoError(err) _, err = paramLookup.ServiceParamValue(suite.AppContextForTest(), key) suite.Error(err) - suite.Contains(err.Error(), "Looking for GHCDieselFuelPrice") + suite.Contains(err.Error(), "EIAFuelPriceLookup with error not found looking for shipment pickup date") }) } diff --git a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go new file mode 100644 index 00000000000..b339fbf43dd --- /dev/null +++ b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go @@ -0,0 +1,90 @@ +package serviceparamvaluelookups + +import ( + "fmt" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services/ghcrateengine" +) + +// PerUnitCents does lookup on the per unit cents value associated with a service item +type PerUnitCentsLookup struct { + ServiceItem models.MTOServiceItem + MTOShipment models.MTOShipment +} + +func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemParamKeyData) (string, error) { + serviceID := p.ServiceItem.ReServiceID + contractID := s.ContractID + if p.MTOShipment.RequestedPickupDate == nil { + return "", fmt.Errorf("requested pickup date is required for shipment with id: %s", p.MTOShipment.ID) + } + + switch p.ServiceItem.ReService.Code { + case models.ReServiceCodeIHPK: + // IHPK: Need rate area ID for the pickup address + rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(*p.MTOShipment.RequestedPickupDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("rate_area_id = ?", rateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IHPK per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, and rateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, rateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + + case models.ReServiceCodeIHUPK: + // IHUPK: Need rate area ID for the destination address + rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(*p.MTOShipment.RequestedPickupDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("rate_area_id = ?", rateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IHUPK per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, and rateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, rateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + + case models.ReServiceCodeISLH: + // ISLH: Need rate area IDs for origin and destination + originRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for origin address for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) + } + destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.DestinationAddressID, serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for destination address for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(*p.MTOShipment.RequestedPickupDate) + var reIntlPrice models.ReIntlPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("origin_rate_area_id = ?", originRateAreaID). + Where("destination_rate_area_id = ?", destRateAreaID). + First(&reIntlPrice) + if err != nil { + return "", fmt.Errorf("error fetching ISLH per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, originRateAreaID: %s, and destRateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, originRateAreaID, destRateAreaID, err) + } + return reIntlPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + + default: + return "", fmt.Errorf("unsupported service code to retrieve service item param PerUnitCents") + } +} diff --git a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go new file mode 100644 index 00000000000..9937f86217b --- /dev/null +++ b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go @@ -0,0 +1,163 @@ +package serviceparamvaluelookups + +import ( + "time" + + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" +) + +func (suite *ServiceParamValueLookupsSuite) TestPerUnitCentsLookup() { + key := models.ServiceItemParamNamePerUnitCents + var mtoServiceItem models.MTOServiceItem + setupTestData := func(serviceCode models.ReServiceCode) { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.ReService{ + Code: serviceCode, + }, + }, + }, []factory.Trait{factory.GetTraitAvailableToPrimeMove}) + + } + + suite.Run("success - returns perUnitCent value for IHPK", func() { + setupTestData(models.ReServiceCodeIHPK) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "6997") + }) + + suite.Run("success - returns perUnitCent value for IHUPK", func() { + setupTestData(models.ReServiceCodeIHUPK) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "752") + }) + + suite.Run("success - returns perUnitCent value for ISLH", func() { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + }, + }, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + mtoServiceItem := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeISLH, + }, + }, + }, nil) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "1605") + }) + + suite.Run("failure - unauthorized service code", func() { + setupTestData(models.ReServiceCodeDUPK) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.Error(err) + suite.Equal(perUnitCents, "") + }) + + suite.Run("failure - no requested pickup date on shipment", func() { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.ReService{ + Code: models.ReServiceCodeIHPK, + }, + }, + { + Model: models.MTOShipment{ + RequestedPickupDate: nil, + }, + }, + }, []factory.Trait{factory.GetTraitAvailableToPrimeMove}) + + mtoServiceItem.MTOShipment.RequestedPickupDate = nil + suite.MustSave(&mtoServiceItem.MTOShipment) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.Error(err) + suite.Equal(perUnitCents, "") + }) +} diff --git a/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go b/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go new file mode 100644 index 00000000000..3ea8be94315 --- /dev/null +++ b/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go @@ -0,0 +1,35 @@ +package serviceparamvaluelookups + +import ( + "fmt" + + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" +) + +// PortZipLookup does lookup on the shipment and finds the port zip +// The mileage calculated is from port <-> pickup/destination so this value is important +type PortZipLookup struct { + ServiceItem models.MTOServiceItem +} + +func (p PortZipLookup) lookup(appCtx appcontext.AppContext, _ *ServiceItemParamKeyData) (string, error) { + var portLocationID *uuid.UUID + if p.ServiceItem.PODLocationID != nil { + portLocationID = p.ServiceItem.PODLocationID + } else if p.ServiceItem.POELocationID != nil { + portLocationID = p.ServiceItem.POELocationID + } else { + return "", fmt.Errorf("unable to find port zip for service item id: %s", p.ServiceItem.ID) + } + var portLocation models.PortLocation + err := appCtx.DB().Q(). + EagerPreload("UsPostRegionCity"). + Where("id = $1", portLocationID).First(&portLocation) + if err != nil { + return "", fmt.Errorf("unable to find port zip with id %s", portLocationID) + } + return portLocation.UsPostRegionCity.UsprZipID, nil +} diff --git a/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go b/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go new file mode 100644 index 00000000000..4410ba8e198 --- /dev/null +++ b/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go @@ -0,0 +1,114 @@ +package serviceparamvaluelookups + +import ( + "time" + + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" +) + +func (suite *ServiceParamValueLookupsSuite) TestPortZipLookup() { + key := models.ServiceItemParamNamePortZip + var mtoServiceItem models.MTOServiceItem + setupTestData := func(serviceCode models.ReServiceCode, portID uuid.UUID) { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + if serviceCode == models.ReServiceCodePOEFSC { + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.ReService{ + Code: serviceCode, + }, + }, + { + Model: models.MTOServiceItem{ + POELocationID: &portID, + }, + }, + }, []factory.Trait{factory.GetTraitAvailableToPrimeMove}) + } else { + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.ReService{ + Code: serviceCode, + }, + }, + { + Model: models.MTOServiceItem{ + PODLocationID: &portID, + }, + }, + }, []factory.Trait{factory.GetTraitAvailableToPrimeMove}) + } + } + + suite.Run("success - returns PortZip value for POEFSC", func() { + port := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "SEA", + }, + }, + }, nil) + setupTestData(models.ReServiceCodePOEFSC, port.ID) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + portZip, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(portZip, port.UsPostRegionCity.UsprZipID) + }) + + suite.Run("success - returns PortZip value for PODFSC", func() { + port := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "PDX", + }, + }, + }, nil) + setupTestData(models.ReServiceCodePODFSC, port.ID) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + portZip, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(portZip, port.UsPostRegionCity.UsprZipID) + }) + + suite.Run("failure - no port zip on service item", func() { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.ReService{ + Code: models.ReServiceCodePOEFSC, + }, + }, + { + Model: models.MTOServiceItem{ + POELocationID: nil, + }, + }, + }, []factory.Trait{factory.GetTraitAvailableToPrimeMove}) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + _, err = paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.Error(err) + }) +} diff --git a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go index 0bb499be70b..33775af842b 100644 --- a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go +++ b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go @@ -21,6 +21,7 @@ type ServiceItemParamKeyData struct { MTOServiceItem models.MTOServiceItem PaymentRequestID uuid.UUID MoveTaskOrderID uuid.UUID + ContractID uuid.UUID ContractCode string mtoShipmentID *uuid.UUID paramCache *ServiceParamsCache @@ -85,6 +86,8 @@ var ServiceItemParamsWithLookups = []models.ServiceItemParamName{ models.ServiceItemParamNameStandaloneCrate, models.ServiceItemParamNameStandaloneCrateCap, models.ServiceItemParamNameLockedPriceCents, + models.ServiceItemParamNamePerUnitCents, + models.ServiceItemParamNamePortZip, } // ServiceParamLookupInitialize initializes service parameter lookup @@ -120,6 +123,7 @@ func ServiceParamLookupInitialize( to this query. Otherwise the contract_code field could be added to the MTO. */ ContractCode: contract.Code, + ContractID: contract.ID, } // @@ -430,6 +434,15 @@ func InitializeLookups(appCtx appcontext.AppContext, shipment models.MTOShipment ServiceItem: serviceItem, } + lookups[models.ServiceItemParamNamePerUnitCents] = PerUnitCentsLookup{ + ServiceItem: serviceItem, + MTOShipment: shipment, + } + + lookups[models.ServiceItemParamNamePortZip] = PortZipLookup{ + ServiceItem: serviceItem, + } + return lookups } diff --git a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go index bc5587b7941..7b8307f147c 100644 --- a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go +++ b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go @@ -29,6 +29,7 @@ import ( ) const defaultZipDistance = 1234 +const defaultInternationalZipDistance = 1800 type ServiceParamValueLookupsSuite struct { *testingsuite.PopTestSuite @@ -46,7 +47,16 @@ func TestServiceParamValueLookupsSuite(t *testing.T) { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(defaultZipDistance, nil) + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + true, + ).Return(defaultInternationalZipDistance, nil) ts := &ServiceParamValueLookupsSuite{ PopTestSuite: testingsuite.NewPopTestSuite(testingsuite.CurrentPackage(), testingsuite.WithPerTestTransaction()), diff --git a/pkg/route/dtod_planner.go b/pkg/route/dtod_planner.go index 383de1dd923..61cf7addb26 100644 --- a/pkg/route/dtod_planner.go +++ b/pkg/route/dtod_planner.go @@ -44,7 +44,7 @@ func (p *dtodPlanner) Zip3TransitDistance(_ appcontext.AppContext, _ string, _ s } // ZipTransitDistance calculates the distance between two valid Zips -func (p *dtodPlanner) ZipTransitDistance(appCtx appcontext.AppContext, source string, destination string) (int, error) { +func (p *dtodPlanner) ZipTransitDistance(appCtx appcontext.AppContext, source string, destination string, useDTOD bool, isInternationalShipment bool) (int, error) { if len(source) < 5 { source = fmt.Sprintf("%05s", source) } diff --git a/pkg/route/dtod_planner_test.go b/pkg/route/dtod_planner_test.go index c8978eea831..1315f531fc6 100644 --- a/pkg/route/dtod_planner_test.go +++ b/pkg/route/dtod_planner_test.go @@ -18,7 +18,6 @@ import ( "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/route/ghcmocks" - "github.com/transcom/mymove/pkg/testdatagen" "github.com/transcom/mymove/pkg/testingsuite" ) @@ -99,19 +98,9 @@ func (suite *GHCTestSuite) TestDTODZipTransitDistance() { mock.Anything, ).Return(soapResponseForDistance("150.33"), nil) - sourceZip3 := "303" - destinationZip3 := "309" - testdatagen.MakeZip3Distance(suite.DB(), testdatagen.Assertions{ - Zip3Distance: models.Zip3Distance{ - FromZip3: sourceZip3, - ToZip3: destinationZip3, - DistanceMiles: 150, - }, - }) - plannerMileage := NewDTODZip5Distance(fakeUsername, fakePassword, testSoapClient, false) planner := NewDTODPlanner(plannerMileage) - distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "30907", "30301") + distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "30907", "30301", false, false) suite.NoError(err) suite.Equal(150, distance) }) @@ -125,7 +114,7 @@ func (suite *GHCTestSuite) TestDTODZipTransitDistance() { plannerMileage := NewDTODZip5Distance(fakeUsername, fakePassword, testSoapClient, false) planner := NewDTODPlanner(plannerMileage) - distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "30907", "30901") + distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "30907", "30901", false, false) suite.Error(err) suite.Equal(0, distance) }) diff --git a/pkg/route/here_planner.go b/pkg/route/here_planner.go index e8d40c85521..e7114dd1bf2 100644 --- a/pkg/route/here_planner.go +++ b/pkg/route/here_planner.go @@ -219,7 +219,7 @@ func (p *herePlanner) Zip5TransitDistance(appCtx appcontext.AppContext, source s } // ZipTransitDistance calculates the distance between two valid Zip5s; it is used by the PPM flow -func (p *herePlanner) ZipTransitDistance(_ appcontext.AppContext, _ string, _ string) (int, error) { +func (p *herePlanner) ZipTransitDistance(_ appcontext.AppContext, _ string, _ string, _ bool, _ bool) (int, error) { // This might get retired after we transition over fully to GHC. panic("implement me") diff --git a/pkg/route/hhg_planner.go b/pkg/route/hhg_planner.go index 544d8f5eed7..88e1caa04a5 100644 --- a/pkg/route/hhg_planner.go +++ b/pkg/route/hhg_planner.go @@ -46,7 +46,7 @@ func (p *hhgPlanner) Zip3TransitDistance(_ appcontext.AppContext, _ string, _ st } // ZipTransitDistance calculates the distance between two valid Zips -func (p *hhgPlanner) ZipTransitDistance(appCtx appcontext.AppContext, source string, destination string) (int, error) { +func (p *hhgPlanner) ZipTransitDistance(appCtx appcontext.AppContext, source string, destination string, useDTOD bool, isInternationalShipment bool) (int, error) { sourceZip5 := source if len(source) < 5 { sourceZip5 = fmt.Sprintf("%05s", source) @@ -58,7 +58,7 @@ func (p *hhgPlanner) ZipTransitDistance(appCtx appcontext.AppContext, source str sourceZip3 := sourceZip5[0:3] destZip3 := destZip5[0:3] - if sourceZip3 == destZip3 { + if sourceZip3 == destZip3 || useDTOD { if sourceZip5 == destZip5 { return 1, nil } @@ -67,18 +67,21 @@ func (p *hhgPlanner) ZipTransitDistance(appCtx appcontext.AppContext, source str // Get reZip3s for origin and destination to compare base point cities. // Dont throw/return errors from this. If we dont find them, we'll just use randMcNallyZip3Distance - sourceReZip3, sErr := models.FetchReZip3Item(appCtx.DB(), sourceZip3) - if sErr != nil { - appCtx.Logger().Error("Failed to fetch the reZip3 item for sourceZip3", zap.Error(sErr)) - } - destinationReZip3, dErr := models.FetchReZip3Item(appCtx.DB(), destZip3) - if dErr != nil { - appCtx.Logger().Error("Failed to fetch the reZip3 item for destinationZip3", zap.Error(dErr)) - } + // this only applies to domestic shipments + if !isInternationalShipment { + sourceReZip3, sErr := models.FetchReZip3Item(appCtx.DB(), sourceZip3) + if sErr != nil { + appCtx.Logger().Error("Failed to fetch the reZip3 item for sourceZip3", zap.Error(sErr)) + } + destinationReZip3, dErr := models.FetchReZip3Item(appCtx.DB(), destZip3) + if dErr != nil { + appCtx.Logger().Error("Failed to fetch the reZip3 item for destinationZip3", zap.Error(dErr)) + } - // Different zip3, same base point city, use DTOD - if sourceReZip3 != nil && destinationReZip3 != nil && sourceReZip3.BasePointCity == destinationReZip3.BasePointCity { - return p.dtodPlannerMileage.DTODZip5Distance(appCtx, source, destination) + // Different zip3, same base point city, use DTOD + if sourceReZip3 != nil && destinationReZip3 != nil && sourceReZip3.BasePointCity == destinationReZip3.BasePointCity { + return p.dtodPlannerMileage.DTODZip5Distance(appCtx, source, destination) + } } return randMcNallyZip3Distance(appCtx, sourceZip3, destZip3) diff --git a/pkg/route/hhg_planner_test.go b/pkg/route/hhg_planner_test.go index e443788fd28..15c4fb03d14 100644 --- a/pkg/route/hhg_planner_test.go +++ b/pkg/route/hhg_planner_test.go @@ -15,7 +15,6 @@ import ( "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/route/ghcmocks" - "github.com/transcom/mymove/pkg/testdatagen" ) func (suite *GHCTestSuite) TestHHGTransitDistance() { @@ -77,21 +76,11 @@ func (suite *GHCTestSuite) TestHHGZipTransitDistance() { mock.Anything, ).Return(soapResponseForDistance("150.33"), nil) - sourceZip3 := "303" - destinationZip3 := "309" - testdatagen.MakeZip3Distance(suite.DB(), testdatagen.Assertions{ - Zip3Distance: models.Zip3Distance{ - FromZip3: sourceZip3, - ToZip3: destinationZip3, - DistanceMiles: 150, - }, - }) - plannerMileage := NewDTODZip5Distance(fakeUsername, fakePassword, testSoapClient, false) planner := NewHHGPlanner(plannerMileage) - distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "30907", "30301") + distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "30907", "30301", false, false) suite.NoError(err) - suite.Equal(150, distance) + suite.Equal(149, distance) }) suite.Run("ZipTransitDistance returns a distance of 1 if origin and dest zips are the same", func() { @@ -99,7 +88,7 @@ func (suite *GHCTestSuite) TestHHGZipTransitDistance() { plannerMileage := NewDTODZip5Distance(fakeUsername, fakePassword, testSoapClient, false) planner := NewHHGPlanner(plannerMileage) - distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "11201", "11201") + distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "11201", "11201", false, false) suite.NoError(err) suite.Equal(1, distance) }) @@ -112,30 +101,11 @@ func (suite *GHCTestSuite) TestHHGZipTransitDistance() { mock.Anything, ).Return(soapResponseForDistance("166"), nil) - // Create two zip3s in the same base point city (Miami) - testdatagen.MakeReZip3(suite.DB(), testdatagen.Assertions{ - ReZip3: models.ReZip3{ - Zip3: "330", - BasePointCity: "Miami", - State: "FL", - }, - }) - testdatagen.MakeReZip3(suite.DB(), testdatagen.Assertions{ - ReZip3: models.ReZip3{ - Zip3: "331", - BasePointCity: "Miami", - State: "FL", - }, - ReDomesticServiceArea: models.ReDomesticServiceArea{ - ServiceArea: "005", - }, - }) - plannerMileage := NewDTODZip5Distance(fakeUsername, fakePassword, testSoapClient, false) planner := NewHHGPlanner(plannerMileage) // Get distance between two zips in the same base point city - distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "33169", "33040") + distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "33169", "33040", false, false) suite.NoError(err) // Ensure DTOD was used for distance @@ -151,7 +121,7 @@ func (suite *GHCTestSuite) TestHHGZipTransitDistance() { plannerMileage := NewDTODZip5Distance(fakeUsername, fakePassword, testSoapClient, false) planner := NewHHGPlanner(plannerMileage) - distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "30907", "30901") + distance, err := planner.ZipTransitDistance(suite.AppContextForTest(), "30907", "30901", false, false) suite.Error(err) suite.Equal(0, distance) }) diff --git a/pkg/route/mocks/Planner.go b/pkg/route/mocks/Planner.go index 2cf26622df2..f6ce16a0e45 100644 --- a/pkg/route/mocks/Planner.go +++ b/pkg/route/mocks/Planner.go @@ -156,9 +156,9 @@ func (_m *Planner) Zip5TransitDistanceLineHaul(appCtx appcontext.AppContext, sou return r0, r1 } -// ZipTransitDistance provides a mock function with given fields: appCtx, source, destination -func (_m *Planner) ZipTransitDistance(appCtx appcontext.AppContext, source string, destination string) (int, error) { - ret := _m.Called(appCtx, source, destination) +// ZipTransitDistance provides a mock function with given fields: appCtx, source, destination, useDTOD, isInternationalShipment +func (_m *Planner) ZipTransitDistance(appCtx appcontext.AppContext, source string, destination string, useDTOD bool, isInternationalShipment bool) (int, error) { + ret := _m.Called(appCtx, source, destination, useDTOD, isInternationalShipment) if len(ret) == 0 { panic("no return value specified for ZipTransitDistance") @@ -166,17 +166,17 @@ func (_m *Planner) ZipTransitDistance(appCtx appcontext.AppContext, source strin var r0 int var r1 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, string) (int, error)); ok { - return rf(appCtx, source, destination) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, string, bool, bool) (int, error)); ok { + return rf(appCtx, source, destination, useDTOD, isInternationalShipment) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, string) int); ok { - r0 = rf(appCtx, source, destination) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, string, bool, bool) int); ok { + r0 = rf(appCtx, source, destination, useDTOD, isInternationalShipment) } else { r0 = ret.Get(0).(int) } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, string) error); ok { - r1 = rf(appCtx, source, destination) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, string, bool, bool) error); ok { + r1 = rf(appCtx, source, destination, useDTOD, isInternationalShipment) } else { r1 = ret.Error(1) } diff --git a/pkg/route/planner.go b/pkg/route/planner.go index ccc349be6d9..591dcec643b 100644 --- a/pkg/route/planner.go +++ b/pkg/route/planner.go @@ -120,7 +120,7 @@ type Planner interface { // Zip5TransitDistanceLineHaul is used by PPM flow and checks for minimum distance restriction as PPM doesn't allow short hauls // New code should probably make the minimum checks after calling Zip5TransitDistance over using this method Zip5TransitDistanceLineHaul(appCtx appcontext.AppContext, source string, destination string) (int, error) - ZipTransitDistance(appCtx appcontext.AppContext, source string, destination string) (int, error) + ZipTransitDistance(appCtx appcontext.AppContext, source string, destination string, useDTOD bool, isInternationalShipment bool) (int, error) Zip3TransitDistance(appCtx appcontext.AppContext, source string, destination string) (int, error) Zip5TransitDistance(appCtx appcontext.AppContext, source string, destination string) (int, error) } diff --git a/pkg/route/planner_test.go b/pkg/route/planner_test.go index eb574873278..4a0d968c1f9 100644 --- a/pkg/route/planner_test.go +++ b/pkg/route/planner_test.go @@ -109,7 +109,7 @@ func (suite *PlannerFullSuite) TestZipDistance() { {zip1: "902101234", zip2: caZip, distanceMin: 30, distanceMax: 49}, } for _, ts := range tests { - distance, err := suite.planner.ZipTransitDistance(suite.AppContextForTest(), ts.zip1, ts.zip2) + distance, err := suite.planner.ZipTransitDistance(suite.AppContextForTest(), ts.zip1, ts.zip2, false, false) if len(ts.zip1) > 5 { suite.Error(err) suite.Equal(distance, 0) diff --git a/pkg/route/rand_mcnally_distance_test.go b/pkg/route/rand_mcnally_distance_test.go index 1710194636c..d8b227a5080 100644 --- a/pkg/route/rand_mcnally_distance_test.go +++ b/pkg/route/rand_mcnally_distance_test.go @@ -1,27 +1,20 @@ package route -import ( - "github.com/transcom/mymove/pkg/testdatagen" -) - func (suite *GHCTestSuite) TestRandMcNallyZip3Distance() { suite.Run("test basic distance check", func() { - testdatagen.MakeDefaultZip3Distance(suite.DB()) distance, err := randMcNallyZip3Distance(suite.AppContextForTest(), "010", "011") suite.NoError(err) - suite.Equal(24, distance) + suite.Equal(12, distance) }) suite.Run("fromZip3 is greater than toZip3", func() { - testdatagen.MakeDefaultZip3Distance(suite.DB()) distance, err := randMcNallyZip3Distance(suite.AppContextForTest(), "011", "010") suite.NoError(err) - suite.Equal(24, distance) + suite.Equal(12, distance) }) suite.Run("fromZip3 is the same as toZip3", func() { - testdatagen.MakeDefaultZip3Distance(suite.DB()) distance, err := randMcNallyZip3Distance(suite.AppContextForTest(), "010", "010") suite.Equal(0, distance) suite.NotNil(err) diff --git a/pkg/services/ghc_rate_engine.go b/pkg/services/ghc_rate_engine.go index 5d17e0388ce..2247e3d7426 100644 --- a/pkg/services/ghc_rate_engine.go +++ b/pkg/services/ghc_rate_engine.go @@ -232,3 +232,35 @@ type DomesticOriginSITFuelSurchargePricer interface { ) ParamsPricer } + +// IntlShippingAndLinehaulPricer prices international shipping and linehaul for a move +// +//go:generate mockery --name IntlShippingAndLinehaulPricer +type IntlShippingAndLinehaulPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, distance unit.Miles, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} + +// IntlHHGPackPricer prices international packing for an iHHG shipment within a move +// +//go:generate mockery --name IntlHHGPackPricer +type IntlHHGPackPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} + +// IntlHHGUnpackPricer prices international unpacking for an iHHG shipment within a move +// +//go:generate mockery --name IntlHHGUnpackPricer +type IntlHHGUnpackPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} + +// IntlPortFuelSurchargePricer prices the POEFSC/PODFSC service items on an iHHG shipment within a move +// +//go:generate mockery --name IntlPortFuelSurchargePricer +type IntlPortFuelSurchargePricer interface { + Price(appCtx appcontext.AppContext, actualPickupDate time.Time, distance unit.Miles, weight unit.Pound, fscWeightBasedDistanceMultiplier float64, eiaFuelPrice unit.Millicents) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} diff --git a/pkg/services/ghcdieselfuelprice/ghc_diesel_fuel_price_storer_test.go b/pkg/services/ghcdieselfuelprice/ghc_diesel_fuel_price_storer_test.go index 95188677e61..bbd563fc017 100644 --- a/pkg/services/ghcdieselfuelprice/ghc_diesel_fuel_price_storer_test.go +++ b/pkg/services/ghcdieselfuelprice/ghc_diesel_fuel_price_storer_test.go @@ -32,8 +32,8 @@ func (suite *GHCDieselFuelPriceServiceSuite) Test_ghcDieselFuelPriceStorer() { var ghcDieselFuelPrice models.GHCDieselFuelPrice err = suite.DB().Last(&ghcDieselFuelPrice) suite.NoError(err) - suite.Equal("2020-06-22T00:00:00Z", ghcDieselFuelPrice.PublicationDate.Format(time.RFC3339)) - suite.Equal(unit.Millicents(265900), ghcDieselFuelPrice.FuelPriceInMillicents) + suite.Equal("2020-04-06T00:00:00Z", ghcDieselFuelPrice.PublicationDate.Format(time.RFC3339)) + suite.Equal(unit.Millicents(254800), ghcDieselFuelPrice.FuelPriceInMillicents) }) diff --git a/pkg/services/ghcimport/import_re_domestic_linehaul_prices_test.go b/pkg/services/ghcimport/import_re_domestic_linehaul_prices_test.go index 7d79bf37b98..32b16b4fced 100644 --- a/pkg/services/ghcimport/import_re_domestic_linehaul_prices_test.go +++ b/pkg/services/ghcimport/import_re_domestic_linehaul_prices_test.go @@ -45,7 +45,7 @@ func (suite *GHCRateEngineImportSuite) Test_importREDomesticLinehaulPrices() { func (suite *GHCRateEngineImportSuite) helperVerifyDomesticLinehaulCount() { count, err := suite.DB().Count(&models.ReDomesticLinehaulPrice{}) suite.NoError(err) - suite.Equal(240, count) + suite.Equal(13800, count) } func (suite *GHCRateEngineImportSuite) helperCheckDomesticLinehaulValue() { diff --git a/pkg/services/ghcimport/import_re_domestic_other_prices_test.go b/pkg/services/ghcimport/import_re_domestic_other_prices_test.go index 8cf12ef8cbd..ca14e7f4ad9 100644 --- a/pkg/services/ghcimport/import_re_domestic_other_prices_test.go +++ b/pkg/services/ghcimport/import_re_domestic_other_prices_test.go @@ -85,7 +85,7 @@ func (suite *GHCRateEngineImportSuite) Test_importREDomesticOtherPricesFailures( func (suite *GHCRateEngineImportSuite) helperVerifyDomesticOtherPrices() { count, err := suite.DB().Count(&models.ReDomesticOtherPrice{}) suite.NoError(err) - suite.Equal(24, count) + suite.Equal(48, count) } func (suite *GHCRateEngineImportSuite) helperCheckDomesticOtherPriceValue() { diff --git a/pkg/services/ghcimport/import_re_domestic_service_area_prices_test.go b/pkg/services/ghcimport/import_re_domestic_service_area_prices_test.go index 7f89bb5f431..955a5ed3e63 100644 --- a/pkg/services/ghcimport/import_re_domestic_service_area_prices_test.go +++ b/pkg/services/ghcimport/import_re_domestic_service_area_prices_test.go @@ -70,7 +70,7 @@ func (suite *GHCRateEngineImportSuite) Test_importREDomesticServiceAreaPricesFai func (suite *GHCRateEngineImportSuite) helperVerifyDomesticServiceAreaPrices() { count, err := suite.DB().Count(&models.ReDomesticServiceAreaPrice{}) suite.NoError(err) - suite.Equal(70, count) + suite.Equal(3234, count) } func (suite *GHCRateEngineImportSuite) helperCheckDomesticServiceAreaPriceValue() { diff --git a/pkg/services/ghcimport/import_re_intl_other_prices_test.go b/pkg/services/ghcimport/import_re_intl_other_prices_test.go index 8818f531cc8..8dc6fd4ce47 100644 --- a/pkg/services/ghcimport/import_re_intl_other_prices_test.go +++ b/pkg/services/ghcimport/import_re_intl_other_prices_test.go @@ -49,7 +49,7 @@ func (suite *GHCRateEngineImportSuite) Test_importREInternationalOtherPrices() { func (suite *GHCRateEngineImportSuite) helperVerifyInternationalOtherPrices() { count, err := suite.DB().Count(&models.ReIntlOtherPrice{}) suite.NoError(err) - suite.Equal(180, count) + suite.Equal(2580, count) } func (suite *GHCRateEngineImportSuite) helperCheckInternationalOtherPriceRecords() { diff --git a/pkg/services/ghcimport/import_re_intl_prices_test.go b/pkg/services/ghcimport/import_re_intl_prices_test.go index 13d9f74ffd8..ae159e3197d 100644 --- a/pkg/services/ghcimport/import_re_intl_prices_test.go +++ b/pkg/services/ghcimport/import_re_intl_prices_test.go @@ -102,7 +102,7 @@ func (suite *GHCRateEngineImportSuite) Test_getRateAreaIDForKind() { func (suite *GHCRateEngineImportSuite) helperVerifyInternationalPrices() { count, err := suite.DB().Count(&models.ReIntlPrice{}) suite.NoError(err) - suite.Equal(276, count) + suite.Equal(46640, count) } func (suite *GHCRateEngineImportSuite) helperCheckInternationalPriceValues() { diff --git a/pkg/services/ghcrateengine/domestic_destination_first_day_sit_pricer_test.go b/pkg/services/ghcrateengine/domestic_destination_first_day_sit_pricer_test.go index b3589c58e11..f03ba1e4a53 100644 --- a/pkg/services/ghcrateengine/domestic_destination_first_day_sit_pricer_test.go +++ b/pkg/services/ghcrateengine/domestic_destination_first_day_sit_pricer_test.go @@ -14,11 +14,11 @@ import ( const ( ddfsitTestServiceArea = "456" ddfsitTestIsPeakPeriod = false - ddfsitTestBasePriceCents = unit.Cents(525) + ddfsitTestBasePriceCents = unit.Cents(1770) ddfsitTestContractYearName = "DDFSIT Test Year" ddfsitTestEscalationCompounded = 1.052 ddfsitTestWeight = unit.Pound(3300) - ddfsitTestPriceCents = unit.Cents(18216) + ddfsitTestPriceCents = unit.Cents(61446) ) var ddfsitTestRequestedPickupDate = time.Date(testdatagen.TestYear, time.January, 5, 7, 33, 11, 456, time.UTC) diff --git a/pkg/services/ghcrateengine/domestic_destination_sit_delivery_pricer_test.go b/pkg/services/ghcrateengine/domestic_destination_sit_delivery_pricer_test.go index a1b0950e605..25976f78a0d 100644 --- a/pkg/services/ghcrateengine/domestic_destination_sit_delivery_pricer_test.go +++ b/pkg/services/ghcrateengine/domestic_destination_sit_delivery_pricer_test.go @@ -22,8 +22,8 @@ const ( dddsitTestWeightUpper = unit.Pound(4999) dddsitTestMilesLower = 251 dddsitTestMilesUpper = 500 - dddsitTestDomesticOtherBasePriceCents = unit.Cents(2518) - dddsitTestDomesticLinehaulBasePriceMillicents = unit.Millicents(6500) + dddsitTestDomesticOtherBasePriceCents = unit.Cents(21796) + dddsitTestDomesticLinehaulBasePriceMillicents = unit.Millicents(237900) dddsitTestDomesticServiceAreaBasePriceCents = unit.Cents(153) ) @@ -35,7 +35,7 @@ func (suite *GHCRateEngineServiceSuite) TestDomesticDestinationSITDeliveryPricer distance := unit.Miles(37) pricer := NewDomesticDestinationSITDeliveryPricer() - expectedPrice := unit.Cents(58365) // dddsitTestDomesticServiceAreaBasePriceCents * (dddsitTestWeight / 100) * distance * dddsitTestEscalationCompounded + expectedPrice := unit.Cents(505125) // dddsitTestDomesticServiceAreaBasePriceCents * (dddsitTestWeight / 100) * distance * dddsitTestEscalationCompounded suite.Run("success using PaymentServiceItemParams", func() { suite.setupDomesticOtherPrice(models.ReServiceCodeDDDSIT, dddsitTestSchedule, dddsitTestIsPeakPeriod, dddsitTestDomesticOtherBasePriceCents, dddsitTestContractYearName, dddsitTestEscalationCompounded) @@ -49,7 +49,6 @@ func (suite *GHCRateEngineServiceSuite) TestDomesticDestinationSITDeliveryPricer {Key: models.ServiceItemParamNameContractYearName, Value: dddsitTestContractYearName}, {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(dddsitTestEscalationCompounded)}, {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(dddsitTestIsPeakPeriod)}, - // {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(dddsitTestDomesticServiceAreaBasePriceCents)}, {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(dddsitTestDomesticOtherBasePriceCents)}, } suite.validatePricerCreatedParams(expectedParams, displayParams) @@ -103,16 +102,7 @@ func (suite *GHCRateEngineServiceSuite) TestDomesticDestinationSITDeliveryPricer suite.Contains(err.Error(), "invalid SIT final destination postal code") }) - suite.Run("bad SIT final destination service area using ServiceAreaLookup", func() { - suite.setupDomesticServiceAreaPrice(models.ReServiceCodeDSH, "", dddsitTestIsPeakPeriod, dddsitTestDomesticServiceAreaBasePriceCents, dddsitTestContractYearName, dddsitTestEscalationCompounded) - - _, _, err := pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, dddsitTestRequestedPickupDate, dddsitTestWeight, "111", dddsitTestSchedule, zipDest, zipSITDest, distance) - suite.Error(err) - suite.Contains(err.Error(), "could not fetch domestic destination SIT delivery rate") - }) - suite.Run("error from shorthaul pricer", func() { - //suite.setupDomesticServiceAreaPrice(models.ReServiceCodeDSH, dddsitTestServiceArea, dddsitTestIsPeakPeriod, dddsitTestDomesticServiceAreaBasePriceCents, dddsitTestContractYearName, dddsitTestEscalationCompounded) suite.setupDomesticOtherPrice(models.ReServiceCodeDDDSIT, dddsitTestSchedule, dddsitTestIsPeakPeriod, dddsitTestDomesticOtherBasePriceCents, dddsitTestContractYearName, dddsitTestEscalationCompounded) _, _, err := pricer.Price(suite.AppContextForTest(), "BOGUS", dddsitTestRequestedPickupDate, dddsitTestWeight, dddsitTestServiceArea, dddsitTestSchedule, zipDest, zipSITDest, distance) @@ -123,12 +113,11 @@ func (suite *GHCRateEngineServiceSuite) TestDomesticDestinationSITDeliveryPricer func (suite *GHCRateEngineServiceSuite) TestDomesticDestinationSITDeliveryPricer50PlusMilesDiffZip3s() { zipDest := "30907" - zipSITDest := "36106" // different zip3 - //sitServiceAreaDest := "020" // different service Area + zipSITDest := "36106" // different zip3 distance := unit.Miles(305) // > 50 miles pricer := NewDomesticDestinationSITDeliveryPricer() - expectedPrice := unit.Cents(45979) + expectedPrice := unit.Cents(1681313) suite.Run("success using PaymentServiceItemParams", func() { suite.setupDomesticLinehaulPrice(dddsitTestServiceArea, dddsitTestIsPeakPeriod, dddsitTestWeightLower, dddsitTestWeightUpper, dddsitTestMilesLower, dddsitTestMilesUpper, dddsitTestDomesticLinehaulBasePriceMillicents, dddsitTestContractYearName, dddsitTestEscalationCompounded) @@ -170,7 +159,7 @@ func (suite *GHCRateEngineServiceSuite) TestDomesticDestinationSITDeliveryPricer distance := unit.Miles(37) // <= 50 miles pricer := NewDomesticDestinationSITDeliveryPricer() - expectedPrice := unit.Cents(58365) + expectedPrice := unit.Cents(505125) suite.Run("success using PaymentServiceItemParams", func() { suite.setupDomesticOtherPrice(models.ReServiceCodeDDDSIT, dddsitTestSchedule, dddsitTestIsPeakPeriod, dddsitTestDomesticOtherBasePriceCents, dddsitTestContractYearName, dddsitTestEscalationCompounded) diff --git a/pkg/services/ghcrateengine/domestic_linehaul_pricer_test.go b/pkg/services/ghcrateengine/domestic_linehaul_pricer_test.go index 8a5b187fc2c..5365abdbb3c 100644 --- a/pkg/services/ghcrateengine/domestic_linehaul_pricer_test.go +++ b/pkg/services/ghcrateengine/domestic_linehaul_pricer_test.go @@ -18,12 +18,12 @@ const ( dlhTestWeightUpper = unit.Pound(4999) dlhTestMilesLower = 1001 dlhTestMilesUpper = 1500 - dlhTestBasePriceMillicents = unit.Millicents(5111) + dlhTestBasePriceMillicents = unit.Millicents(388600) dlhTestContractYearName = "DLH Test Year" dlhTestEscalationCompounded = 1.04071 dlhTestDistance = unit.Miles(1201) dlhTestWeight = unit.Pound(4001) - dlhPriceCents = unit.Cents(254676) + dlhPriceCents = unit.Cents(19432233) ) var dlhRequestedPickupDate = time.Date(testdatagen.TestYear, time.June, 5, 7, 33, 11, 456, time.UTC) @@ -32,7 +32,6 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticLinehaul() { linehaulServicePricer := NewDomesticLinehaulPricer() suite.Run("success using PaymentServiceItemParams", func() { - // serviceArea := "sa0" suite.setupDomesticLinehaulPrice(dlhTestServiceArea, dlhTestIsPeakPeriod, dlhTestWeightLower, dlhTestWeightUpper, dlhTestMilesLower, dlhTestMilesUpper, dlhTestBasePriceMillicents, dlhTestContractYearName, dlhTestEscalationCompounded) paymentServiceItem := suite.setupDomesticLinehaulServiceItem() priceCents, displayParams, err := linehaulServicePricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) @@ -87,7 +86,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticLinehaul() { // < 50 mile distance with PPM priceCents, _, err := linehaulServicePricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, dlhRequestedPickupDate, unit.Miles(49), dlhTestWeight, dlhTestServiceArea, isPPM) suite.NoError(err) - suite.Equal(unit.Cents(10391), priceCents) + suite.Equal(unit.Cents(526980), priceCents) }) suite.Run("successfully finds linehaul price for ppm with distance < 50 miles with PriceUsingParams method", func() { @@ -202,7 +201,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticLinehaul() { _, _, err = linehaulServicePricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, time.Date(testdatagen.TestYear+1, 1, 1, 1, 1, 1, 1, time.UTC), dlhTestDistance, dlhTestWeight, dlhTestServiceArea, isPPM) suite.Error(err) - suite.Contains(err.Error(), "could not fetch domestic linehaul rate") + suite.Contains(err.Error(), "could not lookup contract year") }) } diff --git a/pkg/services/ghcrateengine/domestic_nts_pack_pricer_test.go b/pkg/services/ghcrateengine/domestic_nts_pack_pricer_test.go index ce75ab4599c..b3c1c6c441f 100644 --- a/pkg/services/ghcrateengine/domestic_nts_pack_pricer_test.go +++ b/pkg/services/ghcrateengine/domestic_nts_pack_pricer_test.go @@ -18,9 +18,9 @@ const ( dnpkTestWeight = unit.Pound(2100) dnpkTestServicesScheduleOrigin = 1 dnpkTestContractYearName = "DNPK Test Year" - dnpkTestBasePriceCents = unit.Cents(6333) + dnpkTestBasePriceCents = unit.Cents(6544) dnpkTestFactor = 1.35 - dnpkTestPriceCents = unit.Cents(186855) + dnpkTestPriceCents = unit.Cents(193064) ) var dnpkTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 5, 5, 5, 5, time.UTC) @@ -116,15 +116,18 @@ func (suite *GHCRateEngineServiceSuite) setupDomesticNTSPackPrices(schedule int, }) packService := factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeDPK) - otherPrice := models.ReDomesticOtherPrice{ - ContractID: contractYear.Contract.ID, - ServiceID: packService.ID, - IsPeakPeriod: isPeakPeriod, - Schedule: schedule, - PriceCents: priceCents, - } - suite.MustSave(&otherPrice) + factory.FetchOrMakeDomesticOtherPrice(suite.DB(), []factory.Customization{ + { + Model: models.ReDomesticOtherPrice{ + ContractID: contractYear.Contract.ID, + ServiceID: packService.ID, + IsPeakPeriod: isPeakPeriod, + Schedule: schedule, + PriceCents: priceCents, + }, + }, + }, nil) ntsPackService := factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeDNPK) shipmentTypePrice := models.ReShipmentTypePrice{ diff --git a/pkg/services/ghcrateengine/domestic_origin_sit_pickup_pricer_test.go b/pkg/services/ghcrateengine/domestic_origin_sit_pickup_pricer_test.go index 3a443ee47f7..cdf36569170 100644 --- a/pkg/services/ghcrateengine/domestic_origin_sit_pickup_pricer_test.go +++ b/pkg/services/ghcrateengine/domestic_origin_sit_pickup_pricer_test.go @@ -22,9 +22,9 @@ const ( dopsitTestWeightUpper = unit.Pound(4999) dopsitTestMilesLower = 51 dopsitTestMilesUpper = 250 - dopsitTestDomesticOtherBasePriceCents = unit.Cents(2810) - dopsitTestDomesticLinehaulBasePriceMillicents = unit.Millicents(4455) - dopsitTestDomesticServiceAreaBasePriceCents = unit.Cents(223) + dopsitTestDomesticOtherBasePriceCents = unit.Cents(25030) + dopsitTestDomesticLinehaulBasePriceMillicents = unit.Millicents(200300) + dopsitTestDomesticServiceAreaBasePriceCents = unit.Cents(25030) ) var dopsitTestRequestedPickupDate = time.Date(testdatagen.TestYear, time.July, 5, 10, 22, 11, 456, time.UTC) @@ -35,7 +35,7 @@ func (suite *GHCRateEngineServiceSuite) TestDomesticOriginSITPickupPricerSameZip distance := unit.Miles(12) // distance will follow pricer logic for moves under 50 miles pricer := NewDomesticOriginSITPickupPricer() - expectedPrice := unit.Cents(10613) // dopsitTestDomesticServiceAreaBasePriceCents * (dopsitTestWeight / 100) * distance * dopsitTestEscalationCompounded + expectedPrice := unit.Cents(1190859) // dopsitTestDomesticServiceAreaBasePriceCents * (dopsitTestWeight / 100) * distance * dopsitTestEscalationCompounded suite.Run("success using PaymentServiceItemParams", func() { suite.setupDomesticOtherPrice(models.ReServiceCodeDOPSIT, dopsitTestSchedule, dopsitTestIsPeakPeriod, dopsitTestDomesticServiceAreaBasePriceCents, dopsitTestContractYearName, dopsitTestEscalationCompounded) @@ -113,7 +113,7 @@ func (suite *GHCRateEngineServiceSuite) TestDomesticOriginSITPickupPricer50PlusM distance := unit.Miles(77) // > 50 miles pricer := NewDomesticOriginSITPickupPricer() - expectedPrice := unit.Cents(16485) + expectedPrice := unit.Cents(733738) suite.Run("success using PaymentServiceItemParams", func() { suite.setupDomesticLinehaulPrice(dopsitTestServiceArea, dopsitTestIsPeakPeriod, dopsitTestWeightLower, dopsitTestWeightUpper, dopsitTestMilesLower, dopsitTestMilesUpper, dopsitTestDomesticLinehaulBasePriceMillicents, dopsitTestContractYearName, dopsitTestEscalationCompounded) @@ -154,7 +154,7 @@ func (suite *GHCRateEngineServiceSuite) TestDomesticOriginSITPickupPricer50Miles distance := unit.Miles(23) // <= 50 miles pricer := NewDomesticOriginSITPickupPricer() - expectedPrice := unit.Cents(133689) + expectedPrice := unit.Cents(1190859) suite.Run("success using PaymentServiceItemParams", func() { suite.setupDomesticOtherPrice(models.ReServiceCodeDOPSIT, dopsitTestSchedule, dopsitTestIsPeakPeriod, dopsitTestDomesticOtherBasePriceCents, dopsitTestContractYearName, dopsitTestEscalationCompounded) diff --git a/pkg/services/ghcrateengine/domestic_pack_pricer_test.go b/pkg/services/ghcrateengine/domestic_pack_pricer_test.go index 3939f72c43c..a043c440fa2 100644 --- a/pkg/services/ghcrateengine/domestic_pack_pricer_test.go +++ b/pkg/services/ghcrateengine/domestic_pack_pricer_test.go @@ -18,8 +18,8 @@ const ( dpkTestWeight = unit.Pound(2100) dpkTestServicesScheduleOrigin = 1 dpkTestContractYearName = "DPK Test Year" - dpkTestBasePriceCents = unit.Cents(146) - dpkTestPriceCents = unit.Cents(3192) + dpkTestBasePriceCents = unit.Cents(6544) + dpkTestPriceCents = unit.Cents(143010) ) var dpkTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) diff --git a/pkg/services/ghcrateengine/domestic_unpack_pricer_test.go b/pkg/services/ghcrateengine/domestic_unpack_pricer_test.go index 41b5c834f80..f914f56857c 100644 --- a/pkg/services/ghcrateengine/domestic_unpack_pricer_test.go +++ b/pkg/services/ghcrateengine/domestic_unpack_pricer_test.go @@ -18,8 +18,8 @@ const ( dupkTestWeight = unit.Pound(3600) dupkTestServicesScheduleDest = 1 dupkTestContractYearName = "DUPK Test Year" - dupkTestBasePriceCents = unit.Cents(123) - dupkTestPriceCents = unit.Cents(5436) + dupkTestBasePriceCents = unit.Cents(8334) + dupkTestPriceCents = unit.Cents(369360) ) var dupkTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC).AddDate(0, 0, -1) diff --git a/pkg/services/ghcrateengine/fuel_surcharge_pricer.go b/pkg/services/ghcrateengine/fuel_surcharge_pricer.go index 7371a7f397f..72b71bafce6 100644 --- a/pkg/services/ghcrateengine/fuel_surcharge_pricer.go +++ b/pkg/services/ghcrateengine/fuel_surcharge_pricer.go @@ -27,7 +27,7 @@ func NewFuelSurchargePricer() services.FuelSurchargePricer { return &fuelSurchargePricer{} } -// Price determines the price for a counseling service +// Price determines the price for fuel surcharge func (p fuelSurchargePricer) Price(_ appcontext.AppContext, actualPickupDate time.Time, distance unit.Miles, weight unit.Pound, fscWeightBasedDistanceMultiplier float64, eiaFuelPrice unit.Millicents, isPPM bool) (unit.Cents, services.PricingDisplayParams, error) { // Validate parameters if actualPickupDate.IsZero() { diff --git a/pkg/services/ghcrateengine/ghc_rate_engine_service_test.go b/pkg/services/ghcrateengine/ghc_rate_engine_service_test.go index 0ed120492f8..8fcd52d9d34 100644 --- a/pkg/services/ghcrateengine/ghc_rate_engine_service_test.go +++ b/pkg/services/ghcrateengine/ghc_rate_engine_service_test.go @@ -38,7 +38,7 @@ func (suite *GHCRateEngineServiceSuite) setupTaskOrderFeeData(code models.ReServ } func (suite *GHCRateEngineServiceSuite) setupDomesticOtherPrice(code models.ReServiceCode, schedule int, isPeakPeriod bool, priceCents unit.Cents, contractYearName string, escalationCompounded float64) { - contractYear := testdatagen.MakeReContractYear(suite.DB(), + contractYear := testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ Name: contractYearName, @@ -48,19 +48,23 @@ func (suite *GHCRateEngineServiceSuite) setupDomesticOtherPrice(code models.ReSe service := factory.FetchReServiceByCode(suite.DB(), code) - otherPrice := models.ReDomesticOtherPrice{ - ContractID: contractYear.Contract.ID, - ServiceID: service.ID, - IsPeakPeriod: isPeakPeriod, - Schedule: schedule, - PriceCents: priceCents, - } + otherPrice := factory.FetchOrMakeDomesticOtherPrice(suite.DB(), []factory.Customization{ + { + Model: models.ReDomesticOtherPrice{ + ContractID: contractYear.Contract.ID, + ServiceID: service.ID, + IsPeakPeriod: isPeakPeriod, + Schedule: schedule, + PriceCents: priceCents, + }, + }, + }, nil) suite.MustSave(&otherPrice) } func (suite *GHCRateEngineServiceSuite) setupDomesticAccessorialPrice(code models.ReServiceCode, schedule int, perUnitCents unit.Cents, contractYearName string, escalationCompounded float64) { - contractYear := testdatagen.MakeReContractYear(suite.DB(), + contractYear := testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ Name: contractYearName, @@ -81,7 +85,7 @@ func (suite *GHCRateEngineServiceSuite) setupDomesticAccessorialPrice(code model } func (suite *GHCRateEngineServiceSuite) setupDomesticServiceAreaPrice(code models.ReServiceCode, serviceAreaCode string, isPeakPeriod bool, priceCents unit.Cents, contractYearName string, escalationCompounded float64) { - contractYear := testdatagen.MakeReContractYear(suite.DB(), + contractYear := testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ Name: contractYearName, @@ -91,27 +95,30 @@ func (suite *GHCRateEngineServiceSuite) setupDomesticServiceAreaPrice(code model service := factory.FetchReServiceByCode(suite.DB(), code) - serviceArea := testdatagen.MakeReDomesticServiceArea(suite.DB(), + serviceArea := testdatagen.FetchOrMakeReDomesticServiceArea(suite.DB(), testdatagen.Assertions{ ReDomesticServiceArea: models.ReDomesticServiceArea{ + ContractID: contractYear.Contract.ID, Contract: contractYear.Contract, ServiceArea: serviceAreaCode, }, }) - serviceAreaPrice := models.ReDomesticServiceAreaPrice{ - ContractID: contractYear.Contract.ID, - ServiceID: service.ID, - IsPeakPeriod: isPeakPeriod, - DomesticServiceAreaID: serviceArea.ID, - PriceCents: priceCents, - } - - suite.MustSave(&serviceAreaPrice) + factory.FetchOrMakeDomesticServiceAreaPrice(suite.DB(), []factory.Customization{ + { + Model: models.ReDomesticServiceAreaPrice{ + ContractID: contractYear.Contract.ID, + ServiceID: service.ID, + IsPeakPeriod: isPeakPeriod, + DomesticServiceAreaID: serviceArea.ID, + PriceCents: priceCents, + }, + }, + }, nil) } func (suite *GHCRateEngineServiceSuite) setupDomesticLinehaulPrice(serviceAreaCode string, isPeakPeriod bool, weightLower unit.Pound, weightUpper unit.Pound, milesLower int, milesUpper int, priceMillicents unit.Millicents, contractYearName string, escalationCompounded float64) { - contractYear := testdatagen.MakeReContractYear(suite.DB(), + contractYear := testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ Name: contractYearName, @@ -119,30 +126,18 @@ func (suite *GHCRateEngineServiceSuite) setupDomesticLinehaulPrice(serviceAreaCo }, }) - serviceArea := testdatagen.MakeReDomesticServiceArea(suite.DB(), + testdatagen.FetchOrMakeReDomesticServiceArea(suite.DB(), testdatagen.Assertions{ ReDomesticServiceArea: models.ReDomesticServiceArea{ + ContractID: contractYear.Contract.ID, Contract: contractYear.Contract, ServiceArea: serviceAreaCode, }, }) - - baseLinehaulPrice := models.ReDomesticLinehaulPrice{ - ContractID: contractYear.Contract.ID, - WeightLower: weightLower, - WeightUpper: weightUpper, - MilesLower: milesLower, - MilesUpper: milesUpper, - IsPeakPeriod: isPeakPeriod, - DomesticServiceAreaID: serviceArea.ID, - PriceMillicents: priceMillicents, - } - - suite.MustSave(&baseLinehaulPrice) } func (suite *GHCRateEngineServiceSuite) setupShipmentTypePrice(code models.ReServiceCode, market models.Market, factor float64, contractYearName string, escalationCompounded float64) { - contractYear := testdatagen.MakeReContractYear(suite.DB(), + contractYear := testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ Name: contractYearName, diff --git a/pkg/services/ghcrateengine/intl_hhg_pack_pricer.go b/pkg/services/ghcrateengine/intl_hhg_pack_pricer.go new file mode 100644 index 00000000000..12090aa3bde --- /dev/null +++ b/pkg/services/ghcrateengine/intl_hhg_pack_pricer.go @@ -0,0 +1,45 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlHHGPackPricer struct { +} + +func NewIntlHHGPackPricer() services.IntlHHGPackPricer { + return &intlHHGPackPricer{} +} + +func (p intlHHGPackPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlPackUnpack(appCtx, models.ReServiceCodeIHPK, contractCode, referenceDate, weight, perUnitCents) +} + +func (p intlHHGPackPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_hhg_pack_pricer_test.go b/pkg/services/ghcrateengine/intl_hhg_pack_pricer_test.go new file mode 100644 index 00000000000..9674775b906 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_hhg_pack_pricer_test.go @@ -0,0 +1,115 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + ihpkTestContractYearName = "Base Period Year 1" + ihpkTestPerUnitCents = unit.Cents(15000) + ihpkTestTotalCost = unit.Cents(315000) + ihpkTestIsPeakPeriod = true + ihpkTestEscalationCompounded = 1.0000 + ihpkTestWeight = unit.Pound(2100) + ihpkTestPriceCents = unit.Cents(193064) +) + +var ihpkTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlHHGPackPricer() { + pricer := NewIntlHHGPackPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlPackServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(ihpkTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: ihpkTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(ihpkTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(ihpkTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(ihpkTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlPackServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlPackServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIHPK, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: ihpkTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(ihpkTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(ihpkTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/intl_hhg_unpack_pricer.go b/pkg/services/ghcrateengine/intl_hhg_unpack_pricer.go new file mode 100644 index 00000000000..d4cb95dd315 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_hhg_unpack_pricer.go @@ -0,0 +1,45 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlHHGUnpackPricer struct { +} + +func NewIntlHHGUnpackPricer() services.IntlHHGUnpackPricer { + return &intlHHGUnpackPricer{} +} + +func (p intlHHGUnpackPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlPackUnpack(appCtx, models.ReServiceCodeIHUPK, contractCode, referenceDate, weight, perUnitCents) +} + +func (p intlHHGUnpackPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_hhg_unpack_pricer_test.go b/pkg/services/ghcrateengine/intl_hhg_unpack_pricer_test.go new file mode 100644 index 00000000000..322027b37b5 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_hhg_unpack_pricer_test.go @@ -0,0 +1,114 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + ihupkTestContractYearName = "Base Period Year 1" + ihupkTestPerUnitCents = unit.Cents(1200) + ihupkTestTotalCost = unit.Cents(25200) + ihupkTestIsPeakPeriod = true + ihupkTestEscalationCompounded = 1.0000 + ihpukTestWeight = unit.Pound(2100) +) + +var ihupkTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlHHGUnpackPricer() { + pricer := NewIntlHHGUnpackPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlUnpackServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(ihupkTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: ihupkTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(ihupkTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(ihupkTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(ihupkTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlPackServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlUnpackServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIHUPK, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: ihupkTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(ihupkTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(ihpukTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/intl_port_fuel_surcharge_pricer.go b/pkg/services/ghcrateengine/intl_port_fuel_surcharge_pricer.go new file mode 100644 index 00000000000..e970f29a1c0 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_port_fuel_surcharge_pricer.go @@ -0,0 +1,105 @@ +package ghcrateengine + +import ( + "database/sql" + "fmt" + "math" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/apperror" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type portFuelSurchargePricer struct { +} + +func NewPortFuelSurchargePricer() services.IntlPortFuelSurchargePricer { + return &portFuelSurchargePricer{} +} + +func (p portFuelSurchargePricer) Price(_ appcontext.AppContext, actualPickupDate time.Time, distance unit.Miles, weight unit.Pound, fscWeightBasedDistanceMultiplier float64, eiaFuelPrice unit.Millicents) (unit.Cents, services.PricingDisplayParams, error) { + // Validate parameters + if actualPickupDate.IsZero() { + return 0, nil, errors.New("ActualPickupDate is required") + } + if distance <= 0 { + return 0, nil, errors.New("Distance must be greater than 0") + } + if weight < minIntlWeightHHG { + return 0, nil, fmt.Errorf("weight must be a minimum of %d", minIntlWeightHHG) + } + if fscWeightBasedDistanceMultiplier == 0 { + return 0, nil, errors.New("WeightBasedDistanceMultiplier is required") + } + if eiaFuelPrice == 0 { + return 0, nil, errors.New("EIAFuelPrice is required") + } + + fscPriceDifferenceInCents := (eiaFuelPrice - baseGHCDieselFuelPrice).Float64() / 1000.0 + fscMultiplier := fscWeightBasedDistanceMultiplier * distance.Float64() + fscPrice := fscMultiplier * fscPriceDifferenceInCents * 100 + totalCost := unit.Cents(math.Round(fscPrice)) + + displayParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameFSCPriceDifferenceInCents, Value: FormatFloat(fscPriceDifferenceInCents, 1)}, + {Key: models.ServiceItemParamNameFSCMultiplier, Value: FormatFloat(fscMultiplier, 7)}, + } + + return totalCost, displayParams, nil +} + +func (p portFuelSurchargePricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + actualPickupDate, err := getParamTime(params, models.ServiceItemParamNameActualPickupDate) + if err != nil { + return unit.Cents(0), nil, err + } + + var paymentServiceItem models.PaymentServiceItem + mtoShipment := params[0].PaymentServiceItem.MTOServiceItem.MTOShipment + + if mtoShipment.ID == uuid.Nil { + err = appCtx.DB().Eager("MTOServiceItem", "MTOServiceItem.MTOShipment").Find(&paymentServiceItem, params[0].PaymentServiceItemID) + if err != nil { + switch err { + case sql.ErrNoRows: + return unit.Cents(0), nil, apperror.NewNotFoundError(params[0].PaymentServiceItemID, "looking for PaymentServiceItem") + default: + return unit.Cents(0), nil, apperror.NewQueryError("PaymentServiceItem", err, "") + } + } + mtoShipment = paymentServiceItem.MTOServiceItem.MTOShipment + } + + distance, err := getParamInt(params, models.ServiceItemParamNameDistanceZip) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + fscWeightBasedDistanceMultiplier, err := getParamFloat(params, models.ServiceItemParamNameFSCWeightBasedDistanceMultiplier) + if err != nil { + return unit.Cents(0), nil, err + } + + eiaFuelPrice, err := getParamInt(params, models.ServiceItemParamNameEIAFuelPrice) + if err != nil { + return unit.Cents(0), nil, err + } + + _, err = getParamString(params, models.ServiceItemParamNamePortZip) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, actualPickupDate, unit.Miles(distance), unit.Pound(weightBilled), fscWeightBasedDistanceMultiplier, unit.Millicents(eiaFuelPrice)) +} diff --git a/pkg/services/ghcrateengine/intl_port_fuel_surcharge_pricer_test.go b/pkg/services/ghcrateengine/intl_port_fuel_surcharge_pricer_test.go new file mode 100644 index 00000000000..ce64f248c22 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_port_fuel_surcharge_pricer_test.go @@ -0,0 +1,209 @@ +package ghcrateengine + +import ( + "fmt" + "time" + + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/apperror" + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + intlPortFscTestDistance = unit.Miles(2276) + intlPortFscTestWeight = unit.Pound(4025) + intlPortFscWeightDistanceMultiplier = float64(0.000417) + intlPortFscFuelPrice = unit.Millicents(281400) + intlPortFscPriceCents = unit.Cents(2980) + intlPortFscPortZip = "99505" +) + +var intlPortFscActualPickupDate = time.Date(testdatagen.TestYear, time.June, 5, 7, 33, 11, 456, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlPortFuelSurchargePricer() { + intlPortFuelSurchargePricer := NewPortFuelSurchargePricer() + + intlPortFscPriceDifferenceInCents := (intlPortFscFuelPrice - baseGHCDieselFuelPrice).Float64() / 1000.0 + intlPortFscMultiplier := intlPortFscWeightDistanceMultiplier * intlPortFscTestDistance.Float64() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupPortFuelSurchargeServiceItem() + priceCents, displayParams, err := intlPortFuelSurchargePricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(intlPortFscPriceCents, priceCents) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameFSCPriceDifferenceInCents, Value: FormatFloat(intlPortFscPriceDifferenceInCents, 1)}, + {Key: models.ServiceItemParamNameFSCMultiplier, Value: FormatFloat(intlPortFscMultiplier, 7)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("success without PaymentServiceItemParams", func() { + priceCents, _, err := intlPortFuelSurchargePricer.Price(suite.AppContextForTest(), intlPortFscActualPickupDate, intlPortFscTestDistance, intlPortFscTestWeight, intlPortFscWeightDistanceMultiplier, intlPortFscFuelPrice) + suite.NoError(err) + suite.Equal(intlPortFscPriceCents, priceCents) + }) + + suite.Run("sending PaymentServiceItemParams without expected param", func() { + _, _, err := intlPortFuelSurchargePricer.PriceUsingParams(suite.AppContextForTest(), models.PaymentServiceItemParams{}) + suite.Error(err) + }) + + suite.Run("fails using PaymentServiceItemParams with below minimum weight for WeightBilled", func() { + paymentServiceItem := suite.setupPortFuelSurchargeServiceItem() + paramsWithBelowMinimumWeight := paymentServiceItem.PaymentServiceItemParams + weightBilledIndex := 2 + if paramsWithBelowMinimumWeight[weightBilledIndex].ServiceItemParamKey.Key != models.ServiceItemParamNameWeightBilled { + suite.FailNow("failed", "Test needs to adjust the weight of %s but the index is pointing to %s ", models.ServiceItemParamNameWeightBilled, paramsWithBelowMinimumWeight[4].ServiceItemParamKey.Key) + } + paramsWithBelowMinimumWeight[weightBilledIndex].Value = "200" + priceCents, _, err := intlPortFuelSurchargePricer.PriceUsingParams(suite.AppContextForTest(), paramsWithBelowMinimumWeight) + if suite.Error(err) { + suite.Equal("weight must be a minimum of 500", err.Error()) + suite.Equal(unit.Cents(0), priceCents) + } + }) + + suite.Run("FSC is negative if fuel price from EIA is below $2.50", func() { + priceCents, _, err := intlPortFuelSurchargePricer.Price(suite.AppContextForTest(), intlPortFscActualPickupDate, intlPortFscTestDistance, intlPortFscTestWeight, intlPortFscWeightDistanceMultiplier, 242400) + suite.NoError(err) + suite.Equal(unit.Cents(-721), priceCents) + }) + + suite.Run("Price validation errors", func() { + + // No actual pickup date + _, _, err := intlPortFuelSurchargePricer.Price(suite.AppContextForTest(), time.Time{}, intlPortFscTestDistance, intlPortFscTestWeight, intlPortFscWeightDistanceMultiplier, intlPortFscFuelPrice) + suite.Error(err) + suite.Equal("ActualPickupDate is required", err.Error()) + + // No distance + _, _, err = intlPortFuelSurchargePricer.Price(suite.AppContextForTest(), intlPortFscActualPickupDate, unit.Miles(0), intlPortFscTestWeight, intlPortFscWeightDistanceMultiplier, intlPortFscFuelPrice) + suite.Error(err) + suite.Equal("Distance must be greater than 0", err.Error()) + + // No weight + _, _, err = intlPortFuelSurchargePricer.Price(suite.AppContextForTest(), intlPortFscActualPickupDate, intlPortFscTestDistance, unit.Pound(0), intlPortFscWeightDistanceMultiplier, intlPortFscFuelPrice) + suite.Error(err) + suite.Equal(fmt.Sprintf("weight must be a minimum of %d", minDomesticWeight), err.Error()) + + // No weight based distance multiplier + _, _, err = intlPortFuelSurchargePricer.Price(suite.AppContextForTest(), intlPortFscActualPickupDate, intlPortFscTestDistance, intlPortFscTestWeight, 0, intlPortFscFuelPrice) + suite.Error(err) + suite.Equal("WeightBasedDistanceMultiplier is required", err.Error()) + + // No EIA fuel price + _, _, err = intlPortFuelSurchargePricer.Price(suite.AppContextForTest(), intlPortFscActualPickupDate, intlPortFscTestDistance, intlPortFscTestWeight, intlPortFscWeightDistanceMultiplier, 0) + suite.Error(err) + suite.Equal("EIAFuelPrice is required", err.Error()) + }) + + suite.Run("PriceUsingParams validation errors", func() { + paymentServiceItem := suite.setupPortFuelSurchargeServiceItem() + paramsWithBelowMinimumWeight := paymentServiceItem.PaymentServiceItemParams + weightBilledIndex := 2 + if paramsWithBelowMinimumWeight[weightBilledIndex].ServiceItemParamKey.Key != models.ServiceItemParamNameWeightBilled { + suite.FailNow("failed", "Test needs to adjust the weight of %s but the index is pointing to %s ", models.ServiceItemParamNameWeightBilled, paramsWithBelowMinimumWeight[4].ServiceItemParamKey.Key) + } + paramsWithBelowMinimumWeight[weightBilledIndex].Value = "200" + + // No ActualPickupDate + missingActualPickupDate := suite.removeOnePaymentServiceItem(paymentServiceItem.PaymentServiceItemParams, models.ServiceItemParamNameActualPickupDate) + _, _, err := intlPortFuelSurchargePricer.PriceUsingParams(suite.AppContextForTest(), missingActualPickupDate) + suite.Error(err) + suite.Equal("could not find param with key ActualPickupDate", err.Error()) + + // No WeightBilled + missingWeightBilled := suite.removeOnePaymentServiceItem(paymentServiceItem.PaymentServiceItemParams, models.ServiceItemParamNameWeightBilled) + _, _, err = intlPortFuelSurchargePricer.PriceUsingParams(suite.AppContextForTest(), missingWeightBilled) + suite.Error(err) + suite.Equal("could not find param with key WeightBilled", err.Error()) + + // No FSCWeightBasedDistanceMultiplier + missingFSCWeightBasedDistanceMultiplier := suite.removeOnePaymentServiceItem(paymentServiceItem.PaymentServiceItemParams, models.ServiceItemParamNameFSCWeightBasedDistanceMultiplier) + _, _, err = intlPortFuelSurchargePricer.PriceUsingParams(suite.AppContextForTest(), missingFSCWeightBasedDistanceMultiplier) + suite.Error(err) + suite.Equal("could not find param with key FSCWeightBasedDistanceMultiplier", err.Error()) + + // No EIAFuelPrice + missingEIAFuelPrice := suite.removeOnePaymentServiceItem(paymentServiceItem.PaymentServiceItemParams, models.ServiceItemParamNameEIAFuelPrice) + _, _, err = intlPortFuelSurchargePricer.PriceUsingParams(suite.AppContextForTest(), missingEIAFuelPrice) + suite.Error(err) + suite.Equal("could not find param with key EIAFuelPrice", err.Error()) + }) + + suite.Run("can't find distance", func() { + paymentServiceItem := suite.setupPortFuelSurchargeServiceItem() + paramsWithBelowMinimumWeight := paymentServiceItem.PaymentServiceItemParams + weightBilledIndex := 2 + if paramsWithBelowMinimumWeight[weightBilledIndex].ServiceItemParamKey.Key != models.ServiceItemParamNameWeightBilled { + suite.FailNow("failed", "Test needs to adjust the weight of %s but the index is pointing to %s ", models.ServiceItemParamNameWeightBilled, paramsWithBelowMinimumWeight[4].ServiceItemParamKey.Key) + } + paramsWithBelowMinimumWeight[weightBilledIndex].Value = "200" + + paramsWithBadReference := paymentServiceItem.PaymentServiceItemParams + paramsWithBadReference[0].PaymentServiceItemID = uuid.Nil + _, _, err := intlPortFuelSurchargePricer.PriceUsingParams(suite.AppContextForTest(), paramsWithBadReference) + suite.Error(err) + suite.IsType(apperror.NotFoundError{}, err) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupPortFuelSurchargeServiceItem() models.PaymentServiceItem { + model := factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodePOEFSC, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameActualPickupDate, + KeyType: models.ServiceItemParamTypeDate, + Value: intlPortFscActualPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNameDistanceZip, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(intlPortFscTestDistance)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(intlPortFscTestWeight)), + }, + { + Key: models.ServiceItemParamNameFSCWeightBasedDistanceMultiplier, + KeyType: models.ServiceItemParamTypeDecimal, + Value: fmt.Sprintf("%.7f", intlPortFscWeightDistanceMultiplier), + }, + { + Key: models.ServiceItemParamNameEIAFuelPrice, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(intlPortFscFuelPrice)), + }, + { + Key: models.ServiceItemParamNamePortZip, + KeyType: models.ServiceItemParamTypeString, + Value: intlPortFscPortZip, + }, + }, nil, nil, + ) + + var mtoServiceItem models.MTOServiceItem + err := suite.DB().Eager("MTOShipment").Find(&mtoServiceItem, model.MTOServiceItemID) + suite.NoError(err) + + distance := intlPortFscTestDistance + mtoServiceItem.MTOShipment.Distance = &distance + err = suite.DB().Save(&mtoServiceItem.MTOShipment) + suite.NoError(err) + + // the testdatagen factory has some dirty shipment data that we don't want to pass through to the pricer in the test + model.PaymentServiceItemParams[0].PaymentServiceItem.MTOServiceItem = models.MTOServiceItem{} + + return model +} diff --git a/pkg/services/ghcrateengine/intl_shipping_and_linehaul_pricer.go b/pkg/services/ghcrateengine/intl_shipping_and_linehaul_pricer.go new file mode 100644 index 00000000000..3d0ea35c3ba --- /dev/null +++ b/pkg/services/ghcrateengine/intl_shipping_and_linehaul_pricer.go @@ -0,0 +1,108 @@ +package ghcrateengine + +import ( + "fmt" + "math" + "time" + + "github.com/pkg/errors" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +const islhPricerMinimumWeight = unit.Pound(500) + +type intlShippingAndLinehaulPricer struct { +} + +func NewIntlShippingAndLinehaulPricer() services.IntlShippingAndLinehaulPricer { + return &intlShippingAndLinehaulPricer{} +} + +func (p intlShippingAndLinehaulPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, distance unit.Miles, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + if len(contractCode) == 0 { + return 0, nil, errors.New("ContractCode is required") + } + if referenceDate.IsZero() { + return 0, nil, errors.New("referenceDate is required") + } + if weight < islhPricerMinimumWeight { + return 0, nil, fmt.Errorf("weight must be at least %d", islhPricerMinimumWeight) + } + if perUnitCents == 0 { + return 0, nil, errors.New("PerUnitCents is required") + } + + isPeakPeriod := IsPeakPeriod(referenceDate) + + contract, err := fetchContractByContractCode(appCtx, contractCode) + if err != nil { + return 0, nil, fmt.Errorf("could not find contract with code: %s: %w", contractCode, err) + } + + basePrice := float64(perUnitCents) + escalatedPrice, contractYear, err := escalatePriceForContractYear( + appCtx, + contract.ID, + referenceDate, + true, + basePrice) + if err != nil { + return 0, nil, fmt.Errorf("could not calculate escalated price: %w", err) + } + + escalatedPrice = escalatedPrice * weight.ToCWTFloat64() + totalPriceCents := unit.Cents(math.Round(escalatedPrice)) + + params := services.PricingDisplayParams{ + { + Key: models.ServiceItemParamNameContractYearName, + Value: contractYear.Name, + }, + { + Key: models.ServiceItemParamNameEscalationCompounded, + Value: FormatEscalation(contractYear.EscalationCompounded), + }, + { + Key: models.ServiceItemParamNameIsPeak, + Value: FormatBool(isPeakPeriod), + }, + { + Key: models.ServiceItemParamNamePriceRateOrFactor, + Value: FormatCents(unit.Cents(perUnitCents)), + }} + + return totalPriceCents, params, nil +} + +func (p intlShippingAndLinehaulPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + distance, err := getParamInt(params, models.ServiceItemParamNameDistanceZip) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, unit.Miles(distance), unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_shipping_and_linehaul_pricer_test.go b/pkg/services/ghcrateengine/intl_shipping_and_linehaul_pricer_test.go new file mode 100644 index 00000000000..ef3407b04f0 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_shipping_and_linehaul_pricer_test.go @@ -0,0 +1,144 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + islhTestContractYearName = "Base Period Year 1" + islhTestPerUnitCents = unit.Cents(15000) + islhTestTotalCost = unit.Cents(315000) + islhTestIsPeakPeriod = true + islhTestEscalationCompounded = 1.0000 + islhTestWeight = unit.Pound(2100) + islhTestDistance = unit.Miles(1201) +) + +var islhTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlShippingAndLinehaulPricer() { + pricer := NewIntlShippingAndLinehaulPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlShippingAndLinehaulServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(islhTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: islhTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(islhTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(islhTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(islhTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlShippingAndLinehaulServiceItem() + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // DistanceZip + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameDistanceZip)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) + + suite.Run("Price validation errors", func() { + + // No contract code + _, _, err := pricer.Price(suite.AppContextForTest(), "", islhTestRequestedPickupDate, islhTestDistance, islhTestWeight, islhTestPerUnitCents.Int()) + suite.Error(err) + suite.Equal("ContractCode is required", err.Error()) + + // No reference date + _, _, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, time.Time{}, islhTestDistance, islhTestWeight, islhTestPerUnitCents.Int()) + suite.Error(err) + suite.Equal("referenceDate is required", err.Error()) + + // No weight + _, _, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, islhTestRequestedPickupDate, islhTestDistance, 0, islhTestPerUnitCents.Int()) + suite.Error(err) + suite.Equal(fmt.Sprintf("weight must be at least %d", minIntlWeightHHG), err.Error()) + + // No per unit cents + _, _, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, islhTestRequestedPickupDate, islhTestDistance, islhTestWeight, 0) + suite.Error(err) + suite.Equal("PerUnitCents is required", err.Error()) + + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlShippingAndLinehaulServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeISLH, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameDistanceZip, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(islhTestDistance)), + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: islhTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(islhTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(islhTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/pricer_helpers.go b/pkg/services/ghcrateengine/pricer_helpers.go index faa802afb0b..cb804b0da49 100644 --- a/pkg/services/ghcrateengine/pricer_helpers.go +++ b/pkg/services/ghcrateengine/pricer_helpers.go @@ -448,10 +448,10 @@ func createPricerGeneratedParams(appCtx appcontext.AppContext, paymentServiceIte Where("key = ?", param.Key). First(&serviceItemParamKey) if err != nil { - return paymentServiceItemParams, fmt.Errorf("Unable to find service item param key for %v", param.Key) + return paymentServiceItemParams, fmt.Errorf("unable to find service item param key for %v", param.Key) } if serviceItemParamKey.Origin != models.ServiceItemParamOriginPricer { - return paymentServiceItemParams, fmt.Errorf("Service item param key is not a pricer param. Param key: %v", serviceItemParamKey.Key) + return paymentServiceItemParams, fmt.Errorf("service item param key is not a pricer param. Param key: %v", serviceItemParamKey.Key) } // Create the PaymentServiceItemParam from the PricingDisplayParam and store it in the DB diff --git a/pkg/services/ghcrateengine/pricer_helpers_intl.go b/pkg/services/ghcrateengine/pricer_helpers_intl.go new file mode 100644 index 00000000000..924dad55537 --- /dev/null +++ b/pkg/services/ghcrateengine/pricer_helpers_intl.go @@ -0,0 +1,66 @@ +package ghcrateengine + +import ( + "fmt" + "math" + "time" + + "github.com/pkg/errors" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +func priceIntlPackUnpack(appCtx appcontext.AppContext, packUnpackCode models.ReServiceCode, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + if packUnpackCode != models.ReServiceCodeIHPK && packUnpackCode != models.ReServiceCodeIHUPK { + return 0, nil, fmt.Errorf("unsupported pack/unpack code of %s", packUnpackCode) + } + if len(contractCode) == 0 { + return 0, nil, errors.New("ContractCode is required") + } + if referenceDate.IsZero() { + return 0, nil, errors.New("ReferenceDate is required") + } + if perUnitCents == 0 { + return 0, nil, errors.New("PerUnitCents is required") + } + + isPeakPeriod := IsPeakPeriod(referenceDate) + + contract, err := fetchContractByContractCode(appCtx, contractCode) + if err != nil { + return 0, nil, fmt.Errorf("could not find contract with code: %s: %w", contractCode, err) + } + + basePrice := float64(perUnitCents) + escalatedPrice, contractYear, err := escalatePriceForContractYear(appCtx, contract.ID, referenceDate, false, basePrice) + if err != nil { + return 0, nil, fmt.Errorf("could not calculate escalated price: %w", err) + } + + escalatedPrice = escalatedPrice * weight.ToCWTFloat64() + totalCost := unit.Cents(math.Round(escalatedPrice)) + + displayParams := services.PricingDisplayParams{ + { + Key: models.ServiceItemParamNameContractYearName, + Value: contractYear.Name, + }, + { + Key: models.ServiceItemParamNamePriceRateOrFactor, + Value: FormatCents(unit.Cents(perUnitCents)), + }, + { + Key: models.ServiceItemParamNameIsPeak, + Value: FormatBool(isPeakPeriod), + }, + { + Key: models.ServiceItemParamNameEscalationCompounded, + Value: FormatEscalation(contractYear.EscalationCompounded), + }, + } + + return totalCost, displayParams, nil +} diff --git a/pkg/services/ghcrateengine/pricer_helpers_intl_test.go b/pkg/services/ghcrateengine/pricer_helpers_intl_test.go new file mode 100644 index 00000000000..19539e4c976 --- /dev/null +++ b/pkg/services/ghcrateengine/pricer_helpers_intl_test.go @@ -0,0 +1,46 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" +) + +func (suite *GHCRateEngineServiceSuite) TestPriceIntlPackUnpack() { + suite.Run("success with IHPK", func() { + suite.setupIntlPackServiceItem() + totalCost, displayParams, err := priceIntlPackUnpack(suite.AppContextForTest(), models.ReServiceCodeIHPK, testdatagen.DefaultContractCode, ihpkTestRequestedPickupDate, ihpkTestWeight, ihpkTestPerUnitCents.Int()) + suite.NoError(err) + suite.Equal(ihpkTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: ihpkTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(ihpkTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(ihpkTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(ihpkTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("Invalid parameters to Price", func() { + suite.setupIntlPackServiceItem() + _, _, err := priceIntlPackUnpack(suite.AppContextForTest(), models.ReServiceCodeDLH, testdatagen.DefaultContractCode, ihpkTestRequestedPickupDate, ihpkTestWeight, ihpkTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "unsupported pack/unpack code") + + _, _, err = priceIntlPackUnpack(suite.AppContextForTest(), models.ReServiceCodeIHPK, "", ihpkTestRequestedPickupDate, ihpkTestWeight, ihpkTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ContractCode is required") + + _, _, err = priceIntlPackUnpack(suite.AppContextForTest(), models.ReServiceCodeIHPK, testdatagen.DefaultContractCode, time.Time{}, ihpkTestWeight, ihpkTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ReferenceDate is required") + + _, _, err = priceIntlPackUnpack(suite.AppContextForTest(), models.ReServiceCodeIHPK, testdatagen.DefaultContractCode, ihpkTestRequestedPickupDate, ihpkTestWeight, 0) + suite.Error(err) + suite.Contains(err.Error(), "PerUnitCents is required") + }) + +} diff --git a/pkg/services/ghcrateengine/pricer_helpers_test.go b/pkg/services/ghcrateengine/pricer_helpers_test.go index 9adfa70f58e..06b9ec30044 100644 --- a/pkg/services/ghcrateengine/pricer_helpers_test.go +++ b/pkg/services/ghcrateengine/pricer_helpers_test.go @@ -253,7 +253,7 @@ func (suite *GHCRateEngineServiceSuite) Test_priceDomesticPickupDeliverySITSameZ suite.setupDomesticOtherPrice(models.ReServiceCodeDDDSIT, dddsitTestSchedule, dddsitTestIsPeakPeriod, dddsitTestDomesticOtherBasePriceCents, dshContractName, dddsitTestEscalationCompounded) priceCents, displayParams, err := priceDomesticPickupDeliverySIT(suite.AppContextForTest(), models.ReServiceCodeDDDSIT, testdatagen.DefaultContractCode, dddsitTestRequestedPickupDate, dddsitTestWeight, dddsitTestServiceArea, dddsitTestSchedule, dshZipDest, dshZipSITDest, dshDistance) suite.NoError(err) - expectedPrice := unit.Cents(58365) // dddsitTestDomesticServiceAreaBasePriceCents * (dddsitTestWeight / 100) * distance * dddsitTestEscalationCompounded + expectedPrice := unit.Cents(505125) // dddsitTestDomesticServiceAreaBasePriceCents * (dddsitTestWeight / 100) * distance * dddsitTestEscalationCompounded suite.Equal(expectedPrice, priceCents) expectedParams := services.PricingDisplayParams{ @@ -308,7 +308,7 @@ func (suite *GHCRateEngineServiceSuite) Test_priceDomesticPickupDeliverySIT50Plu suite.setupDomesticLinehaulPrice(dddsitTestServiceArea, dddsitTestIsPeakPeriod, dddsitTestWeightLower, dddsitTestWeightUpper, dddsitTestMilesLower, dddsitTestMilesUpper, dddsitTestDomesticLinehaulBasePriceMillicents, dlhContractName, dddsitTestEscalationCompounded) priceCents, displayParams, err := priceDomesticPickupDeliverySIT(suite.AppContextForTest(), models.ReServiceCodeDDDSIT, testdatagen.DefaultContractCode, dddsitTestRequestedPickupDate, dddsitTestWeight, dddsitTestServiceArea, dddsitTestSchedule, dlhZipDest, dlhZipSITDest, dlhDistance) suite.NoError(err) - expectedPrice := unit.Cents(45979) + expectedPrice := unit.Cents(1681313) suite.Equal(expectedPrice, priceCents) @@ -338,7 +338,7 @@ func (suite *GHCRateEngineServiceSuite) Test_priceDomesticPickupDeliverySIT50Mil suite.setupDomesticOtherPrice(models.ReServiceCodeDDDSIT, dddsitTestSchedule, dddsitTestIsPeakPeriod, dddsitTestDomesticOtherBasePriceCents, domContractName, dddsitTestEscalationCompounded) priceCents, displayParams, err := priceDomesticPickupDeliverySIT(suite.AppContextForTest(), models.ReServiceCodeDDDSIT, testdatagen.DefaultContractCode, dddsitTestRequestedPickupDate, dddsitTestWeight, dddsitTestServiceArea, dddsitTestSchedule, domOtherZipDest, domOtherZipSITDest, domOtherDistance) suite.NoError(err) - expectedPrice := unit.Cents(58365) + expectedPrice := unit.Cents(505125) suite.Equal(expectedPrice, priceCents) expectedParams := services.PricingDisplayParams{ @@ -378,7 +378,7 @@ func (suite *GHCRateEngineServiceSuite) Test_priceDomesticPickupDeliverySIT50Mil suite.setupDomesticOtherPrice(models.ReServiceCodeDDDSIT, dddsitTestSchedule, dddsitTestIsPeakPeriod, dddsitTestDomesticOtherBasePriceCents, domContractName, dddsitTestEscalationCompounded) priceCents, displayParams, err := priceDomesticPickupDeliverySIT(suite.AppContextForTest(), models.ReServiceCodeDDDSIT, testdatagen.DefaultContractCode, dddsitTestRequestedPickupDate, dddsitTestWeight, dddsitTestServiceArea, dddsitTestSchedule, domOtherZipDest, domOtherZipSITDest, domOtherDistance) suite.NoError(err) - expectedPrice := unit.Cents(58365) + expectedPrice := unit.Cents(505125) suite.Equal(expectedPrice, priceCents) expectedParams := services.PricingDisplayParams{ @@ -529,7 +529,7 @@ func (suite *GHCRateEngineServiceSuite) Test_createPricerGeneratedParams() { _, err := createPricerGeneratedParams(suite.AppContextForTest(), subtestData.paymentServiceItem.ID, invalidParam) suite.Error(err) - suite.Contains(err.Error(), "Service item param key is not a pricer param") + suite.Contains(err.Error(), "service item param key is not a pricer param") }) suite.Run("errors if no PricingParms passed from the Pricer", func() { diff --git a/pkg/services/ghcrateengine/pricer_query_helpers.go b/pkg/services/ghcrateengine/pricer_query_helpers.go index 5fc88dd7d5e..84cde4fc64c 100644 --- a/pkg/services/ghcrateengine/pricer_query_helpers.go +++ b/pkg/services/ghcrateengine/pricer_query_helpers.go @@ -103,6 +103,16 @@ func fetchContractsByContractId(appCtx appcontext.AppContext, contractID uuid.UU return contracts, nil } +func fetchContractByContractCode(appCtx appcontext.AppContext, contractCode string) (models.ReContract, error) { + var contract models.ReContract + err := appCtx.DB().Where("code = $1", contractCode).First(&contract) + if err != nil { + return models.ReContract{}, err + } + + return contract, nil +} + func fetchShipmentTypePrice(appCtx appcontext.AppContext, contractCode string, serviceCode models.ReServiceCode, market models.Market) (models.ReShipmentTypePrice, error) { var shipmentTypePrice models.ReShipmentTypePrice err := appCtx.DB().Q(). diff --git a/pkg/services/ghcrateengine/service_item_pricer.go b/pkg/services/ghcrateengine/service_item_pricer.go index 81ad0a42cf5..a673f832b63 100644 --- a/pkg/services/ghcrateengine/service_item_pricer.go +++ b/pkg/services/ghcrateengine/service_item_pricer.go @@ -36,7 +36,6 @@ func (p serviceItemPricer) PriceServiceItem(appCtx appcontext.AppContext, item m // createPricerGeneratedParams will throw an error if pricingParams is an empty slice // currently our pricers are returning empty slices for pricingParams // once all pricers have been updated to return pricingParams - // TODO: this conditional logic should be removed var displayParams models.PaymentServiceItemParams if len(pricingParams) > 0 { displayParams, err = createPricerGeneratedParams(appCtx, item.ID, pricingParams) @@ -94,6 +93,16 @@ func PricerForServiceItem(serviceCode models.ReServiceCode) (services.ParamsPric return NewDomesticOriginSITPickupPricer(), nil case models.ReServiceCodeDDDSIT: return NewDomesticDestinationSITDeliveryPricer(), nil + case models.ReServiceCodeISLH: + return NewIntlShippingAndLinehaulPricer(), nil + case models.ReServiceCodeIHPK: + return NewIntlHHGPackPricer(), nil + case models.ReServiceCodeIHUPK: + return NewIntlHHGUnpackPricer(), nil + case models.ReServiceCodePOEFSC: + return NewPortFuelSurchargePricer(), nil + case models.ReServiceCodePODFSC: + return NewPortFuelSurchargePricer(), nil default: // TODO: We may want a different error type here after all pricers have been implemented return nil, apperror.NewNotImplementedError(fmt.Sprintf("pricer not found for code %s", serviceCode)) diff --git a/pkg/services/ghcrateengine/shared.go b/pkg/services/ghcrateengine/shared.go index 3f89a29cb40..171d8668bb1 100644 --- a/pkg/services/ghcrateengine/shared.go +++ b/pkg/services/ghcrateengine/shared.go @@ -9,6 +9,9 @@ import ( // minDomesticWeight is the minimum weight used in domestic calculations (weights below this are upgraded to the min) const minDomesticWeight = unit.Pound(500) +// minIntlWeightHHG is the minimum weight used in intl calculations (weights below this are upgraded to the min) +const minIntlWeightHHG = unit.Pound(500) + // dateInYear represents a specific date in a year (without caring what year it is) type dateInYear struct { month time.Month diff --git a/pkg/services/mocks/IntlHHGPackPricer.go b/pkg/services/mocks/IntlHHGPackPricer.go new file mode 100644 index 00000000000..a12bed589ec --- /dev/null +++ b/pkg/services/mocks/IntlHHGPackPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlHHGPackPricer is an autogenerated mock type for the IntlHHGPackPricer type +type IntlHHGPackPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, weight, perUnitCents +func (_m *IntlHHGPackPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlHHGPackPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlHHGPackPricer creates a new instance of IntlHHGPackPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlHHGPackPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlHHGPackPricer { + mock := &IntlHHGPackPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/IntlHHGUnpackPricer.go b/pkg/services/mocks/IntlHHGUnpackPricer.go new file mode 100644 index 00000000000..d06f97791d5 --- /dev/null +++ b/pkg/services/mocks/IntlHHGUnpackPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlHHGUnpackPricer is an autogenerated mock type for the IntlHHGUnpackPricer type +type IntlHHGUnpackPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, weight, perUnitCents +func (_m *IntlHHGUnpackPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlHHGUnpackPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlHHGUnpackPricer creates a new instance of IntlHHGUnpackPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlHHGUnpackPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlHHGUnpackPricer { + mock := &IntlHHGUnpackPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/IntlPortFuelSurchargePricer.go b/pkg/services/mocks/IntlPortFuelSurchargePricer.go new file mode 100644 index 00000000000..1780857c419 --- /dev/null +++ b/pkg/services/mocks/IntlPortFuelSurchargePricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlPortFuelSurchargePricer is an autogenerated mock type for the IntlPortFuelSurchargePricer type +type IntlPortFuelSurchargePricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, actualPickupDate, distance, weight, fscWeightBasedDistanceMultiplier, eiaFuelPrice +func (_m *IntlPortFuelSurchargePricer) Price(appCtx appcontext.AppContext, actualPickupDate time.Time, distance unit.Miles, weight unit.Pound, fscWeightBasedDistanceMultiplier float64, eiaFuelPrice unit.Millicents) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, actualPickupDate, distance, weight, fscWeightBasedDistanceMultiplier, eiaFuelPrice) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, time.Time, unit.Miles, unit.Pound, float64, unit.Millicents) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, actualPickupDate, distance, weight, fscWeightBasedDistanceMultiplier, eiaFuelPrice) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, time.Time, unit.Miles, unit.Pound, float64, unit.Millicents) unit.Cents); ok { + r0 = rf(appCtx, actualPickupDate, distance, weight, fscWeightBasedDistanceMultiplier, eiaFuelPrice) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, time.Time, unit.Miles, unit.Pound, float64, unit.Millicents) services.PricingDisplayParams); ok { + r1 = rf(appCtx, actualPickupDate, distance, weight, fscWeightBasedDistanceMultiplier, eiaFuelPrice) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, time.Time, unit.Miles, unit.Pound, float64, unit.Millicents) error); ok { + r2 = rf(appCtx, actualPickupDate, distance, weight, fscWeightBasedDistanceMultiplier, eiaFuelPrice) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlPortFuelSurchargePricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlPortFuelSurchargePricer creates a new instance of IntlPortFuelSurchargePricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlPortFuelSurchargePricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlPortFuelSurchargePricer { + mock := &IntlPortFuelSurchargePricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/IntlShippingAndLinehaulPricer.go b/pkg/services/mocks/IntlShippingAndLinehaulPricer.go new file mode 100644 index 00000000000..c6e4ca2b557 --- /dev/null +++ b/pkg/services/mocks/IntlShippingAndLinehaulPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlShippingAndLinehaulPricer is an autogenerated mock type for the IntlShippingAndLinehaulPricer type +type IntlShippingAndLinehaulPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, distance, weight, perUnitCents +func (_m *IntlShippingAndLinehaulPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, distance unit.Miles, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, distance, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, distance, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, distance, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, distance, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, distance, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlShippingAndLinehaulPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlShippingAndLinehaulPricer creates a new instance of IntlShippingAndLinehaulPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlShippingAndLinehaulPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlShippingAndLinehaulPricer { + mock := &IntlShippingAndLinehaulPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/ShipmentRateAreaFinder.go b/pkg/services/mocks/ShipmentRateAreaFinder.go new file mode 100644 index 00000000000..663a74a3cb8 --- /dev/null +++ b/pkg/services/mocks/ShipmentRateAreaFinder.go @@ -0,0 +1,61 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" +) + +// ShipmentRateAreaFinder is an autogenerated mock type for the ShipmentRateAreaFinder type +type ShipmentRateAreaFinder struct { + mock.Mock +} + +// GetPrimeMoveShipmentOconusRateArea provides a mock function with given fields: appCtx, move +func (_m *ShipmentRateAreaFinder) GetPrimeMoveShipmentOconusRateArea(appCtx appcontext.AppContext, move models.Move) (*[]services.ShipmentPostalCodeRateArea, error) { + ret := _m.Called(appCtx, move) + + if len(ret) == 0 { + panic("no return value specified for GetPrimeMoveShipmentOconusRateArea") + } + + var r0 *[]services.ShipmentPostalCodeRateArea + var r1 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.Move) (*[]services.ShipmentPostalCodeRateArea, error)); ok { + return rf(appCtx, move) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.Move) *[]services.ShipmentPostalCodeRateArea); ok { + r0 = rf(appCtx, move) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*[]services.ShipmentPostalCodeRateArea) + } + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.Move) error); ok { + r1 = rf(appCtx, move) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewShipmentRateAreaFinder creates a new instance of ShipmentRateAreaFinder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewShipmentRateAreaFinder(t interface { + mock.TestingT + Cleanup(func()) +}) *ShipmentRateAreaFinder { + mock := &ShipmentRateAreaFinder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/move_history/move_history_fetcher_test.go b/pkg/services/move_history/move_history_fetcher_test.go index af89aee3bff..9da8d13f3bb 100644 --- a/pkg/services/move_history/move_history_fetcher_test.go +++ b/pkg/services/move_history/move_history_fetcher_test.go @@ -375,6 +375,8 @@ func (suite *MoveHistoryServiceSuite) TestMoveHistoryFetcherScenarios() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) updater := mtoserviceitem.NewMTOServiceItemUpdater(planner, builder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) move := factory.BuildApprovalsRequestedMove(suite.DB(), nil, nil) @@ -550,6 +552,8 @@ func (suite *MoveHistoryServiceSuite) TestMoveHistoryFetcherScenarios() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -623,6 +627,8 @@ func (suite *MoveHistoryServiceSuite) TestMoveHistoryFetcherScenarios() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) 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..1d30aef5ceb 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 = models.StringPointer("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..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 @@ -344,6 +344,102 @@ 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 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{ diff --git a/pkg/services/move_task_order/move_task_order_updater_test.go b/pkg/services/move_task_order/move_task_order_updater_test.go index bd1e001e494..ed4df5b58d5 100644 --- a/pkg/services/move_task_order/move_task_order_updater_test.go +++ b/pkg/services/move_task_order/move_task_order_updater_test.go @@ -59,6 +59,8 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_UpdateStatusSer mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) mtoUpdater := mt.NewMoveTaskOrderUpdater( queryBuilder, @@ -346,6 +348,8 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_UpdatePostCouns mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { @@ -513,6 +517,8 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_ShowHide() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { @@ -754,6 +760,8 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) serviceItemCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) mtoUpdater := mt.NewMoveTaskOrderUpdater(queryBuilder, serviceItemCreator, moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), ppmEstimator) @@ -792,6 +800,8 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) serviceItemCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) mtoUpdater := mt.NewMoveTaskOrderUpdater(queryBuilder, serviceItemCreator, moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), ppmEstimator) @@ -1041,6 +1051,8 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_UpdatePPMType() mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/services/mto_service_item/mto_service_item_creator.go b/pkg/services/mto_service_item/mto_service_item_creator.go index 2ab0802badd..6bac7e4ec89 100644 --- a/pkg/services/mto_service_item/mto_service_item_creator.go +++ b/pkg/services/mto_service_item/mto_service_item_creator.go @@ -128,7 +128,7 @@ func (o *mtoServiceItemCreator) findEstimatedPrice(appCtx appcontext.AppContext, return 0, err } if mtoShipment.PickupAddress != nil && mtoShipment.DestinationAddress != nil { - distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, mtoShipment.DestinationAddress.PostalCode) + distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, mtoShipment.DestinationAddress.PostalCode, false, false) if err != nil { return 0, err } @@ -144,7 +144,7 @@ func (o *mtoServiceItemCreator) findEstimatedPrice(appCtx appcontext.AppContext, return 0, err } if mtoShipment.PickupAddress != nil && mtoShipment.DestinationAddress != nil { - distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, mtoShipment.DestinationAddress.PostalCode) + distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, mtoShipment.DestinationAddress.PostalCode, false, false) if err != nil { return 0, err } @@ -167,7 +167,7 @@ func (o *mtoServiceItemCreator) findEstimatedPrice(appCtx appcontext.AppContext, } if mtoShipment.PickupAddress != nil && mtoShipment.DestinationAddress != nil { - distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, mtoShipment.DestinationAddress.PostalCode) + distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, mtoShipment.DestinationAddress.PostalCode, false, false) if err != nil { return 0, err } @@ -303,14 +303,14 @@ func (o *mtoServiceItemCreator) calculateSITDeliveryMiles(appCtx appcontext.AppC originalSITAddressZip = mtoShipment.PickupAddress.PostalCode } if mtoShipment.PickupAddress != nil && originalSITAddressZip != "" { - distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, originalSITAddressZip) + distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, originalSITAddressZip, false, false) } } if serviceItem.ReService.Code == models.ReServiceCodeDDFSIT || serviceItem.ReService.Code == models.ReServiceCodeDDASIT || serviceItem.ReService.Code == models.ReServiceCodeDDSFSC || serviceItem.ReService.Code == models.ReServiceCodeDDDSIT { // Creation: Destination SIT: distance between shipment destination address & service item destination address if mtoShipment.DestinationAddress != nil && serviceItem.SITDestinationFinalAddress != nil { - distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.DestinationAddress.PostalCode, serviceItem.SITDestinationFinalAddress.PostalCode) + distance, err = o.planner.ZipTransitDistance(appCtx, mtoShipment.DestinationAddress.PostalCode, serviceItem.SITDestinationFinalAddress.PostalCode, false, false) } } if err != nil { diff --git a/pkg/services/mto_service_item/mto_service_item_creator_test.go b/pkg/services/mto_service_item/mto_service_item_creator_test.go index e9799db0278..d2a7709b9ff 100644 --- a/pkg/services/mto_service_item/mto_service_item_creator_test.go +++ b/pkg/services/mto_service_item/mto_service_item_creator_test.go @@ -193,6 +193,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateMTOServiceItemWithInvalidMove mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) serviceItemForUnapprovedMove := suite.buildValidServiceItemWithInvalidMove() @@ -221,6 +223,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateMTOServiceItem() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -1031,6 +1035,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateOriginSITServiceItem() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -1080,6 +1086,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateOriginSITServiceItem() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -1154,6 +1162,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateOriginSITServiceItem() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -1280,6 +1290,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateOriginSITServiceItem() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -1314,6 +1326,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateOriginSITServiceItem() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -1347,6 +1361,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateOriginSITServiceItem() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -1422,6 +1438,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateOriginSITServiceItemFailToCre mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -1456,6 +1474,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateDestSITServiceItem() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -1755,6 +1775,8 @@ func (suite *MTOServiceItemServiceSuite) TestCreateDestSITServiceItem() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) createdServiceItems, _, err := creator.CreateMTOServiceItem(suite.AppContextForTest(), &serviceItemDDASIT) diff --git a/pkg/services/mto_service_item/mto_service_item_updater.go b/pkg/services/mto_service_item/mto_service_item_updater.go index 9ffb413c26d..b14f266fc4b 100644 --- a/pkg/services/mto_service_item/mto_service_item_updater.go +++ b/pkg/services/mto_service_item/mto_service_item_updater.go @@ -244,7 +244,7 @@ func (p *mtoServiceItemUpdater) updateServiceItem(appCtx appcontext.AppContext, if serviceItem.ReService.Code == models.ReServiceCodeDDDSIT || serviceItem.ReService.Code == models.ReServiceCodeDDSFSC { // Destination SIT: distance between shipment destination address & service item ORIGINAL destination address - milesCalculated, err := p.planner.ZipTransitDistance(appCtx, mtoShipment.DestinationAddress.PostalCode, serviceItem.SITDestinationOriginalAddress.PostalCode) + milesCalculated, err := p.planner.ZipTransitDistance(appCtx, mtoShipment.DestinationAddress.PostalCode, serviceItem.SITDestinationOriginalAddress.PostalCode, false, false) if err != nil { return nil, err } @@ -256,7 +256,7 @@ func (p *mtoServiceItemUpdater) updateServiceItem(appCtx appcontext.AppContext, if serviceItem.ReService.Code == models.ReServiceCodeDOPSIT || serviceItem.ReService.Code == models.ReServiceCodeDOSFSC { // Origin SIT: distance between shipment pickup address & service item ORIGINAL pickup address - milesCalculated, err := p.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, serviceItem.SITOriginHHGOriginalAddress.PostalCode) + milesCalculated, err := p.planner.ZipTransitDistance(appCtx, mtoShipment.PickupAddress.PostalCode, serviceItem.SITOriginHHGOriginalAddress.PostalCode, false, false) if err != nil { return nil, err } @@ -384,7 +384,7 @@ func (p *mtoServiceItemUpdater) UpdateMTOServiceItemPrime( func calculateOriginSITRequiredDeliveryDate(appCtx appcontext.AppContext, shipment models.MTOShipment, planner route.Planner, sitCustomerContacted *time.Time, sitDepartureDate *time.Time) (*time.Time, error) { // Get a distance calculation between pickup and destination addresses. - distance, err := planner.ZipTransitDistance(appCtx, shipment.PickupAddress.PostalCode, shipment.DestinationAddress.PostalCode) + distance, err := planner.ZipTransitDistance(appCtx, shipment.PickupAddress.PostalCode, shipment.DestinationAddress.PostalCode, false, false) if err != nil { return nil, apperror.NewUnprocessableEntityError("cannot calculate distance between pickup and destination addresses") @@ -612,6 +612,38 @@ func (p *mtoServiceItemUpdater) UpdateMTOServiceItem( // Make the update and create a InvalidInputError if there were validation issues verrs, updateErr := txnAppCtx.DB().ValidateAndUpdate(validServiceItem) + // if the port information was updated, then we need to update the basic service item pricing since distance has changed + // this only applies to international shipments + if oldServiceItem.POELocationID != mtoServiceItem.POELocationID || oldServiceItem.PODLocationID != mtoServiceItem.PODLocationID { + shipment := oldServiceItem.MTOShipment + if shipment.PickupAddress != nil && shipment.DestinationAddress != nil && + (mtoServiceItem.POELocation != nil && mtoServiceItem.POELocation.UsPostRegionCity.UsprZipID != "" || + mtoServiceItem.PODLocation != nil && mtoServiceItem.PODLocation.UsPostRegionCity.UsprZipID != "") { + var pickupZip string + var destZip string + // if the port type is POEFSC this means the shipment is CONUS -> OCONUS (pickup -> port) + // if the port type is PODFSC this means the shipment is OCONUS -> CONUS (port -> destination) + if mtoServiceItem.POELocation != nil { + pickupZip = shipment.PickupAddress.PostalCode + destZip = mtoServiceItem.POELocation.UsPostRegionCity.UsprZipID + } else if mtoServiceItem.PODLocation != nil { + pickupZip = mtoServiceItem.PODLocation.UsPostRegionCity.UsprZipID + destZip = shipment.DestinationAddress.PostalCode + } + // we need to get the mileage from DTOD first, the db proc will consume that + mileage, err := p.planner.ZipTransitDistance(appCtx, pickupZip, destZip, true, true) + if err != nil { + return err + } + + // update the service item pricing if relevant fields have changed + err = models.UpdateEstimatedPricingForShipmentBasicServiceItems(appCtx.DB(), &shipment, &mileage) + if err != nil { + return err + } + } + } + // If there were validation errors create an InvalidInputError type if verrs != nil && verrs.HasAny() { return apperror.NewInvalidInputError(validServiceItem.ID, updateErr, verrs, "Invalid input found while updating the service item.") diff --git a/pkg/services/mto_service_item/mto_service_item_updater_test.go b/pkg/services/mto_service_item/mto_service_item_updater_test.go index 2f6a52310ea..efb730a8f18 100644 --- a/pkg/services/mto_service_item/mto_service_item_updater_test.go +++ b/pkg/services/mto_service_item/mto_service_item_updater_test.go @@ -49,6 +49,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) updater := NewMTOServiceItemUpdater(planner, builder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) @@ -326,6 +328,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, nil) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -450,6 +454,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, nil) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -578,6 +584,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, nil) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -704,6 +712,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, nil) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -780,6 +790,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, nil) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -850,6 +862,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, nil) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -958,6 +972,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, nil) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -1073,6 +1089,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, nil) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -1233,6 +1251,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, nil) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -1343,6 +1363,8 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1234, apperror.UnprocessableEntityError{}) ghcDomesticTransitTime := models.GHCDomesticTransitTime{ @@ -1371,6 +1393,203 @@ func (suite *MTOServiceItemServiceSuite) TestMTOServiceItemUpdater() { suite.Error(err) suite.IsType(apperror.UnprocessableEntityError{}, err) }) + + suite.Run("Successful update of port service item with updated pricing estimates of basic iHHG service items ", func() { + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + "50314", + "98158", + true, + true, + ).Return(1000, nil) + + ghcDomesticTransitTime := models.GHCDomesticTransitTime{ + MaxDaysTransitTime: 12, + WeightLbsLower: 0, + WeightLbsUpper: 10000, + DistanceMilesLower: 0, + DistanceMilesUpper: 10000, + } + _, _ = suite.DB().ValidateAndCreate(&ghcDomesticTransitTime) + + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "50314") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, + }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + pickupDate := time.Now() + requestedPickup := time.Now() + estimatedWeight := unit.Pound(1212) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusApproved, + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + ScheduledPickupDate: &pickupDate, + RequestedPickupDate: &requestedPickup, + PrimeEstimatedWeight: &estimatedWeight, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + // building service items with NO pricing estimates + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeISLH, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHUPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + portLocation := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "PDX", + }, + }, + }, nil) + poefsc := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodePOEFSC, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + { + Model: portLocation, + LinkOnly: true, + Type: &factory.PortLocations.PortOfEmbarkation, + }, + }, nil) + + eTag := etag.GenerateEtag(poefsc.UpdatedAt) + + // update the port + newServiceItemPrime := poefsc + newServiceItemPrime.POELocation.Port.PortCode = "SEA" + + // Update MTO service item + _, err = updater.UpdateMTOServiceItemPrime(suite.AppContextForTest(), &newServiceItemPrime, planner, shipment, eTag) + suite.NoError(err) + + // checking the service item data + var serviceItems []models.MTOServiceItem + err = suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", shipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err) + + suite.Equal(4, len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + // because the estimated weight is provided & POEFSC has a port location now, estimated pricing should be updated + suite.NotNil(serviceItems[i].PricingEstimate) + } + }) } func (suite *MTOServiceItemServiceSuite) TestValidateUpdateMTOServiceItem() { @@ -1959,6 +2178,8 @@ func (suite *MTOServiceItemServiceSuite) TestUpdateMTOServiceItemStatus() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) updater := NewMTOServiceItemUpdater(planner, builder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) diff --git a/pkg/services/mto_shipment.go b/pkg/services/mto_shipment.go index a409c6db436..187d290cb9f 100644 --- a/pkg/services/mto_shipment.go +++ b/pkg/services/mto_shipment.go @@ -153,3 +153,15 @@ type ShipmentSITStatus interface { CalculateShipmentSITAllowance(appCtx appcontext.AppContext, shipment models.MTOShipment) (int, error) RetrieveShipmentSIT(appCtx appcontext.AppContext, shipment models.MTOShipment) (models.SITServiceItemGroupings, error) } + +type ShipmentPostalCodeRateArea struct { + PostalCode string + RateArea *models.ReRateArea +} + +// ShipmentRateAreaFinder is the interface to retrieve Oconus RateArea info for shipment +// +//go:generate mockery --name ShipmentRateAreaFinder +type ShipmentRateAreaFinder interface { + GetPrimeMoveShipmentOconusRateArea(appCtx appcontext.AppContext, move models.Move) (*[]ShipmentPostalCodeRateArea, error) +} diff --git a/pkg/services/mto_shipment/mto_shipment_address_updater.go b/pkg/services/mto_shipment/mto_shipment_address_updater.go index edab362940f..b000991dea4 100644 --- a/pkg/services/mto_shipment/mto_shipment_address_updater.go +++ b/pkg/services/mto_shipment/mto_shipment_address_updater.go @@ -72,9 +72,9 @@ func UpdateOriginSITServiceItemSITDeliveryMiles(planner route.Planner, shipment // Origin SIT: distance between shipment pickup address & service item ORIGINAL pickup address if serviceItem.SITOriginHHGOriginalAddress != nil { - milesCalculated, err = planner.ZipTransitDistance(appCtx, newAddress.PostalCode, serviceItem.SITOriginHHGOriginalAddress.PostalCode) + milesCalculated, err = planner.ZipTransitDistance(appCtx, newAddress.PostalCode, serviceItem.SITOriginHHGOriginalAddress.PostalCode, false, false) } else { - milesCalculated, err = planner.ZipTransitDistance(appCtx, oldAddress.PostalCode, newAddress.PostalCode) + milesCalculated, err = planner.ZipTransitDistance(appCtx, oldAddress.PostalCode, newAddress.PostalCode, false, false) } if err != nil { return nil, err diff --git a/pkg/services/mto_shipment/mto_shipment_address_updater_test.go b/pkg/services/mto_shipment/mto_shipment_address_updater_test.go index 44c773ef06f..da9621dd279 100644 --- a/pkg/services/mto_shipment/mto_shipment_address_updater_test.go +++ b/pkg/services/mto_shipment/mto_shipment_address_updater_test.go @@ -19,6 +19,8 @@ func (suite *MTOShipmentServiceSuite) TestUpdateMTOShipmentAddress() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) addressCreator := address.NewAddressCreator() addressUpdater := address.NewAddressUpdater() @@ -169,6 +171,8 @@ func (suite *MTOShipmentServiceSuite) TestUpdateMTOShipmentAddress() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(465, nil) mtoServiceItems, _ := UpdateOriginSITServiceItemSITDeliveryMiles(planner, &externalShipment, &newAddress, &oldAddress, suite.AppContextForTest()) suite.Equal(2, len(*mtoServiceItems)) diff --git a/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher.go b/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher.go new file mode 100644 index 00000000000..bbc290dba9e --- /dev/null +++ b/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher.go @@ -0,0 +1,115 @@ +package mtoshipment + +import ( + "database/sql" + "fmt" + "time" + + "github.com/gofrs/uuid" + "golang.org/x/exp/slices" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/apperror" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" +) + +type mtoShipmentRateAreaFetcher struct { +} + +// NewMTOShipmentFetcher creates a new MTOShipmentFetcher struct that supports ListMTOShipments +func NewMTOShipmentRateAreaFetcher() services.ShipmentRateAreaFinder { + return &mtoShipmentRateAreaFetcher{} +} + +func (f mtoShipmentRateAreaFetcher) GetPrimeMoveShipmentOconusRateArea(appCtx appcontext.AppContext, moveTaskOrder models.Move) (*[]services.ShipmentPostalCodeRateArea, error) { + if moveTaskOrder.AvailableToPrimeAt == nil { + return nil, apperror.NewUnprocessableEntityError("Move not available to the Prime, unable to retrieve move shipment oconus rateArea") + } + + contract, err := fetchContract(appCtx, *moveTaskOrder.AvailableToPrimeAt) + if err != nil { + return nil, err + } + + // build set of postalCodes to fetch rateArea for + var postalCodes = make([]string, 0) + for _, shipment := range moveTaskOrder.MTOShipments { + if shipment.PickupAddress != nil { + if !slices.Contains(postalCodes, shipment.PickupAddress.PostalCode) { + postalCodes = append(postalCodes, shipment.PickupAddress.PostalCode) + } + } + if shipment.DestinationAddress != nil { + if !slices.Contains(postalCodes, shipment.DestinationAddress.PostalCode) { + postalCodes = append(postalCodes, shipment.DestinationAddress.PostalCode) + } + } + if shipment.PPMShipment != nil { + if shipment.PPMShipment.PickupAddress != nil { + if !slices.Contains(postalCodes, shipment.PPMShipment.PickupAddress.PostalCode) { + postalCodes = append(postalCodes, shipment.PPMShipment.PickupAddress.PostalCode) + } + } + if shipment.PPMShipment.DestinationAddress != nil { + if !slices.Contains(postalCodes, shipment.PPMShipment.DestinationAddress.PostalCode) { + postalCodes = append(postalCodes, shipment.PPMShipment.DestinationAddress.PostalCode) + } + } + } + } + + ra, err := fetchRateArea(appCtx, contract.ID, postalCodes) + if err != nil { + return nil, err + } + + return ra, nil +} + +func fetchRateArea(appCtx appcontext.AppContext, contractId uuid.UUID, postalCode []string) (*[]services.ShipmentPostalCodeRateArea, error) { + var rateArea = make([]services.ShipmentPostalCodeRateArea, 0) + for _, code := range postalCode { + ra, err := fetchOconusRateAreaByPostalCode(appCtx, contractId, code) + if err != nil { + if err != sql.ErrNoRows { + return nil, apperror.NewQueryError("GetRateArea", err, fmt.Sprintf("error retrieving rateArea for contractId:%s, postalCode:%s", contractId, code)) + } + } else { + rateArea = append(rateArea, services.ShipmentPostalCodeRateArea{PostalCode: code, RateArea: ra}) + } + } + return &rateArea, nil +} + +func fetchOconusRateAreaByPostalCode(appCtx appcontext.AppContext, contractId uuid.UUID, postalCode string) (*models.ReRateArea, error) { + var area models.ReRateArea + + err := appCtx.DB().Q().RawQuery(`select + re_rate_areas.* + from v_locations + join re_oconus_rate_areas on re_oconus_rate_areas.us_post_region_cities_id = v_locations.uprc_id + join re_rate_areas on re_oconus_rate_areas.rate_area_id = re_rate_areas.id and v_locations.uspr_zip_id = ? + and re_rate_areas.contract_id = ?`, + postalCode, contractId).First(&area) + + if err != nil { + return nil, err + } + + return &area, err +} + +func fetchContract(appCtx appcontext.AppContext, date time.Time) (*models.ReContract, error) { + var contractYear models.ReContractYear + err := appCtx.DB().EagerPreload("Contract").Where("? between start_date and end_date", date). + First(&contractYear) + if err != nil { + if err == sql.ErrNoRows { + return nil, apperror.NewNotFoundError(uuid.Nil, fmt.Sprintf("no contract year found for %s", date.String())) + } + return nil, err + } + + return &contractYear.Contract, nil +} diff --git a/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher_test.go b/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher_test.go new file mode 100644 index 00000000000..976e03ac2e1 --- /dev/null +++ b/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher_test.go @@ -0,0 +1,445 @@ +package mtoshipment + +import ( + "database/sql" + "fmt" + "time" + + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/apperror" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" +) + +const testContractCode = "TEST" +const testContractName = "Test Contract" +const fairbanksAlaskaPostalCode = "99716" +const anchorageAlaskaPostalCode = "99521" +const wasillaAlaskaPostalCode = "99652" + +func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { + shipmentRateAreaFetcher := NewMTOShipmentRateAreaFetcher() + + suite.Run("test mapping of one rateArea to many postCodes and one rateArea to one", func() { + availableToPrimeAtTime := time.Now().Add(-500 * time.Hour) + testMove := models.Move{ + AvailableToPrimeAt: &availableToPrimeAtTime, + MTOShipments: models.MTOShipments{ + models.MTOShipment{ + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Fairbanks", + State: "AK", + PostalCode: fairbanksAlaskaPostalCode, + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Anchorage", + State: "AK", + PostalCode: anchorageAlaskaPostalCode, + }, + }, + models.MTOShipment{ + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Beverly Hills", + State: "CA", + PostalCode: "90210", + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "San Diego", + State: "CA", + PostalCode: "92075", + }, + }, + models.MTOShipment{ + PPMShipment: &models.PPMShipment{ + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Wasilla", + State: "AK", + PostalCode: wasillaAlaskaPostalCode, + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Wasilla", + State: "AK", + PostalCode: wasillaAlaskaPostalCode, + }, + }, + }, + }, + } + + // create test contract + contract, err := suite.createContract(suite.AppContextForTest(), testContractCode, testContractName) + suite.NotNil(contract) + suite.FatalNoError(err) + + // setup contract year within availableToPrimeAtTime time + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: availableToPrimeAtTime, + EndDate: time.Now(), + ContractID: contract.ID, + }, + }) + + setupRateArea := func(contract models.ReContract) models.ReRateArea { + rateAreaCode := uuid.Must(uuid.NewV4()).String()[0:5] + rateArea := models.ReRateArea{ + ID: uuid.Must(uuid.NewV4()), + ContractID: contract.ID, + IsOconus: true, + Code: rateAreaCode, + Name: fmt.Sprintf("Alaska-%s", rateAreaCode), + Contract: contract, + } + verrs, err := suite.DB().ValidateAndCreate(&rateArea) + if verrs.HasAny() { + suite.Fail(verrs.Error()) + } + if err != nil { + suite.Fail(err.Error()) + } + return rateArea + } + + setupRateAreaToPostalCodeData := func(rateArea models.ReRateArea, postalCode string) models.ReRateArea { + // fetch US by country id + us_countryId := uuid.FromStringOrNil("c390ced2-89e1-418d-bbff-f8a79b89c4b6") + us_country, err := models.FetchCountryByID(suite.DB(), us_countryId) + suite.NotNil(us_country) + suite.FatalNoError(err) + + usprc, err := findUsPostRegionCityByZipCode(suite.AppContextForTest(), postalCode) + suite.NotNil(usprc) + suite.FatalNoError(err) + + oconusRateArea := testOnlyOconusRateArea{ + ID: uuid.Must(uuid.NewV4()), + RateAreaId: rateArea.ID, + CountryId: us_country.ID, + UsPostRegionCityId: usprc.ID, + Active: true, + } + verrs, err := suite.DB().ValidateAndCreate(&oconusRateArea) + if verrs.HasAny() { + suite.Fail(verrs.Error()) + } + if err != nil { + suite.Fail(err.Error()) + } + + return rateArea + } + + setupRateAreaToManyPostalCodesData := func(contract models.ReContract, testPostalCode []string) models.ReRateArea { + rateArea := setupRateArea(contract) + for _, postalCode := range testPostalCode { + setupRateAreaToPostalCodeData(rateArea, postalCode) + } + return rateArea + } + + // setup Fairbanks and Anchorage to have same RateArea + rateArea1 := setupRateAreaToManyPostalCodesData(*contract, []string{fairbanksAlaskaPostalCode, anchorageAlaskaPostalCode}) + // setup Wasilla to have it's own RateArea + rateArea2 := setupRateAreaToPostalCodeData(setupRateArea(*contract), wasillaAlaskaPostalCode) + + shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentOconusRateArea(suite.AppContextForTest(), testMove) + suite.NotNil(shipmentPostalCodeRateArea) + suite.FatalNoError(err) + suite.Equal(3, len(*shipmentPostalCodeRateArea)) + + isRateAreaEquals := func(expectedRateArea models.ReRateArea, postalCode string, shipmentPostalCodeRateArea *[]services.ShipmentPostalCodeRateArea) bool { + var shipmentPostalCodeRateAreaLookupMap = make(map[string]services.ShipmentPostalCodeRateArea) + for _, i := range *shipmentPostalCodeRateArea { + shipmentPostalCodeRateAreaLookupMap[i.PostalCode] = i + } + if _, ok := shipmentPostalCodeRateAreaLookupMap[postalCode]; !ok { + return false + } + return (shipmentPostalCodeRateAreaLookupMap[postalCode].RateArea.ID == expectedRateArea.ID && shipmentPostalCodeRateAreaLookupMap[postalCode].RateArea.Name == expectedRateArea.Name && shipmentPostalCodeRateAreaLookupMap[postalCode].RateArea.Code == expectedRateArea.Code) + } + + suite.Equal(true, isRateAreaEquals(rateArea1, fairbanksAlaskaPostalCode, shipmentPostalCodeRateArea)) + suite.Equal(true, isRateAreaEquals(rateArea1, anchorageAlaskaPostalCode, shipmentPostalCodeRateArea)) + suite.Equal(true, isRateAreaEquals(rateArea2, wasillaAlaskaPostalCode, shipmentPostalCodeRateArea)) + + suite.Equal(false, isRateAreaEquals(rateArea2, fairbanksAlaskaPostalCode, shipmentPostalCodeRateArea)) + suite.Equal(false, isRateAreaEquals(rateArea2, anchorageAlaskaPostalCode, shipmentPostalCodeRateArea)) + suite.Equal(false, isRateAreaEquals(rateArea1, wasillaAlaskaPostalCode, shipmentPostalCodeRateArea)) + }) + + suite.Run("no oconus rateArea found returns empty array", func() { + availableToPrimeAtTime := time.Now().Add(-500 * time.Hour) + testMove := models.Move{ + AvailableToPrimeAt: &availableToPrimeAtTime, + MTOShipments: models.MTOShipments{ + models.MTOShipment{ + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Beverly Hills", + State: "CA", + PostalCode: "90210", + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "San Diego", + State: "CA", + PostalCode: "92075", + }, + }, + models.MTOShipment{ + PPMShipment: &models.PPMShipment{ + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "NY", + State: "NY", + PostalCode: "11220", + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Beverly Hills", + State: "CA", + PostalCode: "90210", + }, + }, + }, + }, + } + + // create test contract + contract, err := suite.createContract(suite.AppContextForTest(), testContractCode, testContractName) + suite.NotNil(contract) + suite.FatalNoError(err) + + // setup contract year within availableToPrimeAtTime time + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: availableToPrimeAtTime, + EndDate: time.Now(), + ContractID: contract.ID, + }, + }) + + shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentOconusRateArea(suite.AppContextForTest(), testMove) + suite.NotNil(shipmentPostalCodeRateArea) + suite.Equal(0, len(*shipmentPostalCodeRateArea)) + suite.Nil(err) + }) + + suite.Run("not available to prime error", func() { + testMove := models.Move{ + MTOShipments: models.MTOShipments{ + models.MTOShipment{ + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Fairbanks", + State: "AK", + PostalCode: fairbanksAlaskaPostalCode, + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Anchorage", + State: "AK", + PostalCode: anchorageAlaskaPostalCode, + }, + }, + }, + } + + shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentOconusRateArea(suite.AppContextForTest(), testMove) + suite.Nil(shipmentPostalCodeRateArea) + suite.NotNil(err) + suite.IsType(apperror.UnprocessableEntityError{}, err) + }) + + suite.Run("contract for move not found", func() { + availableToPrimeAtTime := time.Now().Add(-500 * time.Hour) + testMove := models.Move{ + AvailableToPrimeAt: &availableToPrimeAtTime, + MTOShipments: models.MTOShipments{ + models.MTOShipment{ + PickupAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Fairbanks", + State: "AK", + PostalCode: fairbanksAlaskaPostalCode, + }, + DestinationAddress: &models.Address{ + StreetAddress1: "123 Main St", + City: "Anchorage", + State: "AK", + PostalCode: anchorageAlaskaPostalCode, + }, + }, + }, + } + + // create test contract + contract, err := suite.createContract(suite.AppContextForTest(), testContractCode, testContractName) + suite.NotNil(contract) + suite.FatalNoError(err) + + // setup contract year within availableToPrimeAtTime time + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now(), + EndDate: time.Now().Add(5 * time.Hour), + ContractID: contract.ID, + }, + }) + + shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentOconusRateArea(suite.AppContextForTest(), testMove) + suite.Nil(shipmentPostalCodeRateArea) + suite.NotNil(err) + suite.IsType(apperror.NotFoundError{}, err) + }) +} + +func (suite *MTOShipmentServiceSuite) TestFetchRateAreaByPostalCode() { + // fetch US by country id + us_countryId := uuid.FromStringOrNil("c390ced2-89e1-418d-bbff-f8a79b89c4b6") + us_country, err := models.FetchCountryByID(suite.DB(), us_countryId) + suite.NotNil(us_country) + suite.FatalNoError(err) + + // create test contract + contract, err := suite.createContract(suite.AppContextForTest(), testContractCode, testContractName) + suite.NotNil(contract) + suite.FatalNoError(err) + + // create rateArea associated to contract + rateArea := models.ReRateArea{ + ID: uuid.Must(uuid.NewV4()), + ContractID: contract.ID, + IsOconus: true, + Code: "SomeAlaskaCode", + Name: "Alaska", + Contract: *contract, + } + verrs, err := suite.DB().ValidateAndCreate(&rateArea) + if verrs.HasAny() { + suite.Fail(verrs.Error()) + } + if err != nil { + suite.Fail(err.Error()) + } + + const alaskaPostalCode = "99506" + + usprc, err := findUsPostRegionCityByZipCode(suite.AppContextForTest(), alaskaPostalCode) + suite.NotNil(usprc) + suite.FatalNoError(err) + + oconusRateArea := testOnlyOconusRateArea{ + ID: uuid.Must(uuid.NewV4()), + RateAreaId: rateArea.ID, + CountryId: us_country.ID, + UsPostRegionCityId: usprc.ID, + Active: true, + } + verrs, err = suite.DB().ValidateAndCreate(&oconusRateArea) + if verrs.HasAny() { + suite.Fail(verrs.Error()) + } + if err != nil { + suite.Fail(err.Error()) + } + + match, err := fetchOconusRateAreaByPostalCode(suite.AppContextForTest(), contract.ID, alaskaPostalCode) + suite.NotNil(match) + suite.FatalNoError(err) +} + +func (suite *MTOShipmentServiceSuite) TestFetchRateAreaByPostalCodeNotFound() { + _, err := fetchOconusRateAreaByPostalCode(suite.AppContextForTest(), uuid.FromStringOrNil("51393fa4-b31c-40fe-bedf-b692703c46eb"), "90210") + suite.NotNil(err) +} + +func (suite *MTOShipmentServiceSuite) TestFetchContract() { + // create test contract + expectedContract, err := suite.createContract(suite.AppContextForTest(), testContractCode, testContractName) + suite.NotNil(expectedContract) + suite.FatalNoError(err) + + time := time.Now().Add(-50 * time.Hour) + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time, + EndDate: time, + ContractID: expectedContract.ID, + }, + }) + contract, err := fetchContract(suite.AppContextForTest(), time) + suite.NotNil(contract) + suite.Nil(err) + suite.Equal(expectedContract.ID, contract.ID) +} + +func (suite *MTOShipmentServiceSuite) TestFetchContractNotFound() { + _, err := fetchContract(suite.AppContextForTest(), time.Now()) + suite.NotNil(err) +} + +func (suite *MTOShipmentServiceSuite) createContract(appCtx appcontext.AppContext, contractCode string, contractName string) (*models.ReContract, error) { + + // See if contract code already exists. + exists, err := appCtx.DB().Where("code = ?", testContractCode).Exists(&models.ReContract{}) + if err != nil { + return nil, fmt.Errorf("could not determine if contract code [%s] existed: %w", testContractCode, err) + } + if exists { + return nil, fmt.Errorf("the provided contract code [%s] already exists", testContractCode) + } + + // Contract code is new; insert it. + contract := models.ReContract{ + Code: contractCode, + Name: contractName, + } + verrs, err := appCtx.DB().ValidateAndSave(&contract) + if verrs.HasAny() { + return nil, fmt.Errorf("validation errors when saving contract [%+v]: %w", contract, verrs) + } + if err != nil { + return nil, fmt.Errorf("could not save contract [%+v]: %w", contract, err) + } + + return &contract, nil +} + +func findUsPostRegionCityByZipCode(appCtx appcontext.AppContext, zipCode string) (*models.UsPostRegionCity, error) { + var usprc models.UsPostRegionCity + err := appCtx.DB().Where("uspr_zip_id = ?", zipCode).First(&usprc) + if err != nil { + switch err { + case sql.ErrNoRows: + return nil, fmt.Errorf("No UsPostRegionCity found for provided zip code %s", zipCode) + default: + return nil, err + } + } + return &usprc, nil +} + +// **** This model is specifically for testing only to allow both R/W (READ,INSERTS). models.OconusRateArea is (R)READONLY. *** +type testOnlyOconusRateArea struct { + ID uuid.UUID `json:"id" db:"id"` + RateAreaId uuid.UUID `json:"rate_area_id" db:"rate_area_id"` + CountryId uuid.UUID `json:"country_id" db:"country_id"` + UsPostRegionCityId uuid.UUID `json:"us_post_region_cities_id" db:"us_post_region_cities_id"` + Active bool `json:"active" db:"active"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +func (o testOnlyOconusRateArea) TableName() string { + return "re_oconus_rate_areas" +} diff --git a/pkg/services/mto_shipment/mto_shipment_updater.go b/pkg/services/mto_shipment/mto_shipment_updater.go index 92ca8ba58c8..b4b0d266e0a 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater.go +++ b/pkg/services/mto_shipment/mto_shipment_updater.go @@ -850,6 +850,52 @@ func (f *mtoShipmentUpdater) updateShipmentRecord(appCtx appcontext.AppContext, return err } + // if the shipment has an estimated weight, we need to update the service item pricing + // we only need to do this if the estimated weight, primary addresses, and pickup date are being updated since those all impact pricing + // we will compare data here to see if we even need to update the pricing + if newShipment.MarketCode == models.MarketCodeInternational && + (newShipment.PrimeEstimatedWeight != nil || + newShipment.PickupAddress != nil && newShipment.PickupAddress.PostalCode != dbShipment.PickupAddress.PostalCode || + newShipment.DestinationAddress != nil && newShipment.DestinationAddress.PostalCode != dbShipment.DestinationAddress.PostalCode || + newShipment.RequestedPickupDate != nil && newShipment.RequestedPickupDate.Format("2006-01-02") != dbShipment.RequestedPickupDate.Format("2006-01-02")) { + + portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), newShipment.ID) + if err != nil { + return err + } + // if we don't have the port data, then we won't worry about pricing POEFSC/PODFSC because we need the distance from/to the ports + if portZip != nil && portType != nil { + var pickupZip string + var destZip string + // if the port type is POEFSC this means the shipment is CONUS -> OCONUS (pickup -> port) + // if the port type is PODFSC this means the shipment is OCONUS -> CONUS (port -> destination) + if *portType == models.ReServiceCodePOEFSC.String() { + pickupZip = newShipment.PickupAddress.PostalCode + destZip = *portZip + } else if *portType == models.ReServiceCodePODFSC.String() { + pickupZip = *portZip + destZip = newShipment.DestinationAddress.PostalCode + } + // we need to get the mileage from DTOD first, the db proc will consume that + mileage, err := f.planner.ZipTransitDistance(appCtx, pickupZip, destZip, true, true) + if err != nil { + return err + } + + // update the service item pricing if relevant fields have changed + err = models.UpdateEstimatedPricingForShipmentBasicServiceItems(appCtx.DB(), newShipment, &mileage) + if err != nil { + return err + } + } else { + // if we don't have the port data, that's okay - we can update the other service items except for PODFSC/POEFSC + err = models.UpdateEstimatedPricingForShipmentBasicServiceItems(appCtx.DB(), newShipment, nil) + if err != nil { + return err + } + } + } + // // Perform shipment recalculate payment request // @@ -1027,7 +1073,7 @@ func (o *mtoShipmentStatusUpdater) setRequiredDeliveryDate(appCtx appcontext.App pickupLocation = shipment.PickupAddress deliveryLocation = shipment.DestinationAddress } - requiredDeliveryDate, calcErr := CalculateRequiredDeliveryDate(appCtx, o.planner, *pickupLocation, *deliveryLocation, *shipment.ScheduledPickupDate, weight.Int()) + requiredDeliveryDate, calcErr := CalculateRequiredDeliveryDate(appCtx, o.planner, *pickupLocation, *deliveryLocation, *shipment.ScheduledPickupDate, weight.Int(), shipment.MarketCode) if calcErr != nil { return calcErr } @@ -1144,7 +1190,7 @@ func reServiceCodesForShipment(shipment models.MTOShipment) []models.ReServiceCo // CalculateRequiredDeliveryDate function is used to get a distance calculation using the pickup and destination addresses. It then uses // the value returned to make a fetch on the ghc_domestic_transit_times table and returns a required delivery date // based on the max_days_transit_time. -func CalculateRequiredDeliveryDate(appCtx appcontext.AppContext, planner route.Planner, pickupAddress models.Address, destinationAddress models.Address, pickupDate time.Time, weight int) (*time.Time, error) { +func CalculateRequiredDeliveryDate(appCtx appcontext.AppContext, planner route.Planner, pickupAddress models.Address, destinationAddress models.Address, pickupDate time.Time, weight int, marketCode models.MarketCode) (*time.Time, error) { // Okay, so this is something to get us able to take care of the 20 day condition over in the gdoc linked in this // story: https://dp3.atlassian.net/browse/MB-1141 // We unfortunately didn't get a lot of guidance regarding vicinity. So for now we're taking zip codes that are the @@ -1156,8 +1202,9 @@ func CalculateRequiredDeliveryDate(appCtx appcontext.AppContext, planner route.P "99615", "99619", "99624", "99643", "99644", "99697", "99650", "99801", "99802", "99803", "99811", "99812", "99950", "99824", "99850", "99901", "99928", "99950", "99835"} + internationalShipment := marketCode == models.MarketCodeInternational // Get a distance calculation between pickup and destination addresses. - distance, err := planner.ZipTransitDistance(appCtx, pickupAddress.PostalCode, destinationAddress.PostalCode) + distance, err := planner.ZipTransitDistance(appCtx, pickupAddress.PostalCode, destinationAddress.PostalCode, false, internationalShipment) if err != nil { return nil, err } @@ -1308,11 +1355,11 @@ func UpdateDestinationSITServiceItemsSITDeliveryMiles(planner route.Planner, app if TOOApprovalRequired { if serviceItem.SITDestinationOriginalAddress != nil { // if TOO approval was required, shipment destination address has been updated at this point - milesCalculated, err = planner.ZipTransitDistance(appCtx, shipment.DestinationAddress.PostalCode, serviceItem.SITDestinationOriginalAddress.PostalCode) + milesCalculated, err = planner.ZipTransitDistance(appCtx, shipment.DestinationAddress.PostalCode, serviceItem.SITDestinationOriginalAddress.PostalCode, false, false) } } else { // if TOO approval was not required, use the newAddress - milesCalculated, err = planner.ZipTransitDistance(appCtx, newAddress.PostalCode, serviceItem.SITDestinationOriginalAddress.PostalCode) + milesCalculated, err = planner.ZipTransitDistance(appCtx, newAddress.PostalCode, serviceItem.SITDestinationOriginalAddress.PostalCode, false, false) } if err != nil { return err diff --git a/pkg/services/mto_shipment/mto_shipment_updater_test.go b/pkg/services/mto_shipment/mto_shipment_updater_test.go index aabab4f1a16..ba8cc3b7b89 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater_test.go +++ b/pkg/services/mto_shipment/mto_shipment_updater_test.go @@ -50,6 +50,8 @@ func (suite *MTOShipmentServiceSuite) TestMTOShipmentUpdater() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(1000, nil) moveRouter := moveservices.NewMoveRouter() moveWeights := moveservices.NewMoveWeights(NewShipmentReweighRequester()) @@ -457,6 +459,409 @@ func (suite *MTOShipmentServiceSuite) TestMTOShipmentUpdater() { suite.Equal(updatedShipment.MarketCode, models.MarketCodeInternational) }) + suite.Run("Successful update on international shipment with estimated weight results in the update of estimated pricing for basic service items", func() { + setupTestData() + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + "50314", + "99505", + false, + true, + ).Return(1000, nil) + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + "97220", + "99505", + true, + true, + ).Return(1000, nil) + + ghcDomesticTransitTime := models.GHCDomesticTransitTime{ + MaxDaysTransitTime: 12, + WeightLbsLower: 0, + WeightLbsUpper: 10000, + DistanceMilesLower: 0, + DistanceMilesUpper: 10000, + } + _, _ = suite.DB().ValidateAndCreate(&ghcDomesticTransitTime) + + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "50314") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, + }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + pickupDate := now.AddDate(0, 0, 10) + requestedPickup := time.Now() + oldShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusApproved, + PrimeEstimatedWeight: nil, + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + ScheduledPickupDate: &pickupDate, + RequestedPickupDate: &requestedPickup, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: oldShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeISLH, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: oldShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: oldShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHUPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + portLocation := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "PDX", + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: oldShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodePOEFSC, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + { + Model: portLocation, + LinkOnly: true, + Type: &factory.PortLocations.PortOfDebarkation, + }, + }, nil) + + eTag := etag.GenerateEtag(oldShipment.UpdatedAt) + + updatedShipment := models.MTOShipment{ + ID: oldShipment.ID, + PrimeEstimatedWeight: &primeEstimatedWeight, + } + + session := auth.Session{} + _, err = mtoShipmentUpdaterPrime.UpdateMTOShipment(suite.AppContextWithSessionForTest(&session), &updatedShipment, eTag, "test") + suite.NoError(err) + + // checking the service item data + var serviceItems []models.MTOServiceItem + err = suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", oldShipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err) + + suite.Equal(4, len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + // because the estimated weight is provided & POEFSC has a port location, estimated pricing should be updated + suite.NotNil(serviceItems[i].PricingEstimate) + } + }) + + suite.Run("Successful update on international shipment with estimated weight results in the update of estimated pricing for basic service items except for port fuel surcharge", func() { + setupTestData() + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + "50314", + "99505", + false, + true, + ).Return(1000, nil) + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + "50314", + "97220", + true, + true, + ).Return(1000, nil) + + ghcDomesticTransitTime := models.GHCDomesticTransitTime{ + MaxDaysTransitTime: 12, + WeightLbsLower: 0, + WeightLbsUpper: 10000, + DistanceMilesLower: 0, + DistanceMilesUpper: 10000, + } + _, _ = suite.DB().ValidateAndCreate(&ghcDomesticTransitTime) + + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "50314") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, + }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + pickupDate := now.AddDate(0, 0, 10) + requestedPickup := time.Now() + dbShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusApproved, + PrimeEstimatedWeight: nil, + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + ScheduledPickupDate: &pickupDate, + RequestedPickupDate: &requestedPickup, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: dbShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeISLH, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: dbShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: dbShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHUPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + + // this will not have a port location and pricing shouldn't be updated + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: dbShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodePODFSC, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: nil, + }, + }, + }, nil) + + eTag := etag.GenerateEtag(dbShipment.UpdatedAt) + + shipment := models.MTOShipment{ + ID: dbShipment.ID, + PrimeEstimatedWeight: &primeEstimatedWeight, + } + + session := auth.Session{} + _, err = mtoShipmentUpdaterPrime.UpdateMTOShipment(suite.AppContextWithSessionForTest(&session), &shipment, eTag, "test") + suite.NoError(err) + + // checking the service item data + var serviceItems []models.MTOServiceItem + err = suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", dbShipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err) + + suite.Equal(4, len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + if serviceItems[i].ReService.Code != models.ReServiceCodePODFSC { + suite.NotNil(serviceItems[i].PricingEstimate) + } else if serviceItems[i].ReService.Code == models.ReServiceCodePODFSC { + suite.Nil(serviceItems[i].PricingEstimate) + } + } + }) + suite.Run("Successful update to a minimal MTO shipment", func() { setupTestData() @@ -1127,10 +1532,6 @@ func (suite *MTOShipmentServiceSuite) TestMTOShipmentUpdater() { suite.Run("Prime not able to update an existing prime estimated weight", func() { setupTestData() - // This test was added because of a bug that nullified the ApprovedDate - // when ScheduledPickupDate was included in the payload. See PR #6919. - // ApprovedDate affects shipment diversions, so we want to make sure it - // never gets nullified, regardless of which fields are being updated. move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) oldShipment := factory.BuildMTOShipmentMinimal(suite.DB(), []factory.Customization{ { @@ -1795,6 +2196,8 @@ func (suite *MTOShipmentServiceSuite) TestUpdateMTOShipmentStatus() { mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), + false, + false, ).Return(500, nil).Run(func(args mock.Arguments) { TransitDistancePickupArg = args.Get(1).(string) TransitDistanceDestinationArg = args.Get(2).(string) @@ -2949,6 +3352,8 @@ func (suite *MTOShipmentServiceSuite) TestUpdateStatusServiceItems() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) siCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) updater := NewMTOShipmentStatusUpdater(builder, siCreator, planner) @@ -2989,7 +3394,16 @@ func (suite *MTOShipmentServiceSuite) TestUpdateStatusServiceItems() { err = appCtx.DB().EagerPreload("ReService").Where("mto_shipment_id = ?", updatedShipment.ID).All(&serviceItems) suite.NoError(err) - suite.Equal(models.ReServiceCodeDLH, serviceItems[0].ReService.Code) + foundDLH := false + for _, serviceItem := range serviceItems { + if serviceItem.ReService.Code == models.ReServiceCodeDLH { + foundDLH = true + break + } + } + + // at least one service item should have the DLH code + suite.True(foundDLH, "Expected to find at least one service item with ReService code DLH") }) suite.Run("Shipments with same origin/destination ZIP3 have shorthaul service item", func() { diff --git a/pkg/services/mto_shipment/rules.go b/pkg/services/mto_shipment/rules.go index 46663cc2c00..738db7f8d61 100644 --- a/pkg/services/mto_shipment/rules.go +++ b/pkg/services/mto_shipment/rules.go @@ -343,7 +343,7 @@ func checkPrimeValidationsOnModel(planner route.Planner) validator { weight = older.NTSRecordedWeight } requiredDeliveryDate, err := CalculateRequiredDeliveryDate(appCtx, planner, *latestPickupAddress, - *latestDestinationAddress, *latestSchedPickupDate, weight.Int()) + *latestDestinationAddress, *latestSchedPickupDate, weight.Int(), older.MarketCode) if err != nil { verrs.Add("requiredDeliveryDate", err.Error()) } diff --git a/pkg/services/mto_shipment/shipment_approver.go b/pkg/services/mto_shipment/shipment_approver.go index 52e849e469b..34021fcb3e2 100644 --- a/pkg/services/mto_shipment/shipment_approver.go +++ b/pkg/services/mto_shipment/shipment_approver.go @@ -77,14 +77,61 @@ func (f *shipmentApprover) ApproveShipment(appCtx appcontext.AppContext, shipmen } } - // create international shipment service items - if shipment.ShipmentType == models.MTOShipmentTypeHHG && shipment.MarketCode == models.MarketCodeInternational { - err := models.CreateApprovedServiceItemsForShipment(appCtx.DB(), shipment) - if err != nil { - return shipment, err - } - } transactionError := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { + // create international shipment service items before approving + // we use a database proc to create the basic auto-approved service items + if shipment.ShipmentType == models.MTOShipmentTypeHHG && shipment.MarketCode == models.MarketCodeInternational { + err := models.CreateApprovedServiceItemsForShipment(appCtx.DB(), shipment) + if err != nil { + return err + } + + // Update the service item pricing if we have the estimated weight + if shipment.PrimeEstimatedWeight != nil { + portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), shipment.ID) + if err != nil { + return err + } + // if we don't have the port data, then we won't worry about pricing + if portZip != nil && portType != nil { + var pickupZip string + var destZip string + // if the port type is POEFSC this means the shipment is CONUS -> OCONUS (pickup -> port) + // if the port type is PODFSC this means the shipment is OCONUS -> CONUS (port -> destination) + if *portType == models.ReServiceCodePOEFSC.String() { + pickupZip = shipment.PickupAddress.PostalCode + destZip = *portZip + } else if *portType == models.ReServiceCodePODFSC.String() { + pickupZip = *portZip + destZip = shipment.DestinationAddress.PostalCode + } + // we need to get the mileage from DTOD first, the db proc will consume that + mileage, err := f.planner.ZipTransitDistance(appCtx, pickupZip, destZip, true, true) + if err != nil { + return err + } + + // update the service item pricing if relevant fields have changed + err = models.UpdateEstimatedPricingForShipmentBasicServiceItems(appCtx.DB(), shipment, &mileage) + if err != nil { + return err + } + } else { + // if we don't have the port data, that's okay - we can update the other service items except for PODFSC/POEFSC + err = models.UpdateEstimatedPricingForShipmentBasicServiceItems(appCtx.DB(), shipment, nil) + if err != nil { + return err + } + } + } + } else { + // after approving shipment, shipment level service items must be created (this is for domestic shipments only) + err = f.createShipmentServiceItems(txnAppCtx, shipment) + if err != nil { + return err + } + } + verrs, err := txnAppCtx.DB().ValidateAndSave(shipment) if verrs != nil && verrs.HasAny() { invalidInputError := apperror.NewInvalidInputError(shipment.ID, nil, verrs, "There was an issue with validating the updates") @@ -95,11 +142,6 @@ func (f *shipmentApprover) ApproveShipment(appCtx appcontext.AppContext, shipmen return err } - // after approving shipment, shipment level service items must be created - err = f.createShipmentServiceItems(txnAppCtx, shipment) - if err != nil { - return err - } return nil }) @@ -169,7 +211,7 @@ func (f *shipmentApprover) setRequiredDeliveryDate(appCtx appcontext.AppContext, deliveryLocation = shipment.DestinationAddress weight = shipment.PrimeEstimatedWeight.Int() } - requiredDeliveryDate, calcErr := CalculateRequiredDeliveryDate(appCtx, f.planner, *pickupLocation, *deliveryLocation, *shipment.ScheduledPickupDate, weight) + requiredDeliveryDate, calcErr := CalculateRequiredDeliveryDate(appCtx, f.planner, *pickupLocation, *deliveryLocation, *shipment.ScheduledPickupDate, weight, shipment.MarketCode) if calcErr != nil { return calcErr } diff --git a/pkg/services/mto_shipment/shipment_approver_test.go b/pkg/services/mto_shipment/shipment_approver_test.go index 943cd4edb72..9444dd7cb2c 100644 --- a/pkg/services/mto_shipment/shipment_approver_test.go +++ b/pkg/services/mto_shipment/shipment_approver_test.go @@ -14,7 +14,6 @@ import ( "github.com/transcom/mymove/pkg/etag" "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" - "github.com/transcom/mymove/pkg/route" "github.com/transcom/mymove/pkg/route/mocks" "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/ghcrateengine" @@ -91,6 +90,8 @@ func (suite *MTOShipmentServiceSuite) createApproveShipmentSubtestData() (subtes mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) siCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) subtestData.planner = &mocks.Planner{} @@ -190,50 +191,100 @@ func (suite *MTOShipmentServiceSuite) createApproveShipmentSubtestData() (subtes } func (suite *MTOShipmentServiceSuite) TestApproveShipment() { - suite.Run("If the international mtoShipment is approved successfully it should create pre approved mtoServiceItems", func() { - internationalShipment := factory.BuildMTOShipment(suite.AppContextForTest().DB(), []factory.Customization{ + suite.Run("If the international mtoShipment is approved successfully it should create pre approved mtoServiceItems and should NOT update pricing without port data", func() { + move := factory.BuildAvailableToPrimeMove(suite.DB(), []factory.Customization{ { Model: models.Move{ Status: models.MoveStatusAPPROVED, }, + }}, nil) + + // we need to get the usPostRegionCityIDs based off of the ZIP for the addresses + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "50314") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ { Model: models.Address{ - StreetAddress1: "Tester Address", - City: "Des Moines", - State: "IA", - PostalCode: "50314", - IsOconus: models.BoolPointer(false), + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, }, - Type: &factory.Addresses.PickupAddress, }, + }, nil) + + pickupDate := time.Now() + internationalShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ { Model: models.MTOShipment{ - MarketCode: "i", - Status: models.MTOShipmentStatusSubmitted, + Status: models.MTOShipmentStatusSubmitted, + MarketCode: models.MarketCodeInternational, + PrimeEstimatedWeight: models.PoundPointer(unit.Pound(4000)), + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + RequestedPickupDate: &pickupDate, }, }, { - Model: models.Address{ - StreetAddress1: "JBER", - City: "Anchorage", - State: "AK", - PostalCode: "99505", - IsOconus: models.BoolPointer(true), - }, - Type: &factory.Addresses.DeliveryAddress, + Model: move, + LinkOnly: true, }, }, nil) internationalShipmentEtag := etag.GenerateEtag(internationalShipment.UpdatedAt) + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + shipmentRouter := NewShipmentRouter() + moveWeights := moverouter.NewMoveWeights(NewShipmentReweighRequester()) var serviceItemCreator services.MTOServiceItemCreator - var planner route.Planner - var moveWeights services.MoveWeights + appCtx := suite.AppContextWithSessionForTest(&auth.Session{ + ApplicationName: auth.OfficeApp, + OfficeUserID: uuid.Must(uuid.NewV4()), + }) + + ghcDomesticTransitTime := models.GHCDomesticTransitTime{ + MaxDaysTransitTime: 12, + WeightLbsLower: 0, + WeightLbsUpper: 10000, + DistanceMilesLower: 0, + DistanceMilesUpper: 10000, + } + _, _ = suite.DB().ValidateAndCreate(&ghcDomesticTransitTime) + + planner := &mocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + true, + ).Return(500, nil) // Approve international shipment shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights) - _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) + _, err = shipmentApprover.ApproveShipment(appCtx, internationalShipment.ID, internationalShipmentEtag) suite.NoError(err) // Get created pre approved service items @@ -252,6 +303,12 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { for i := 0; i < len(serviceItems); i++ { actualReServiceCode := serviceItems[i].ReService.Code suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode)) + // we should have pricing data on all but the POEFSC since we don't have the port data yet + if serviceItems[i].ReService.Code != models.ReServiceCodePOEFSC { + suite.NotNil(serviceItems[i].PricingEstimate) + } else if serviceItems[i].ReService.Code == models.ReServiceCodePOEFSC { + suite.Nil(serviceItems[i].PricingEstimate) + } } }) @@ -287,6 +344,8 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(500, nil) preApprovalTime := time.Now() @@ -406,6 +465,8 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { mock.AnythingOfType("*appcontext.appContext"), createdShipment.PickupAddress.PostalCode, createdShipment.DestinationAddress.PostalCode, + false, + false, ).Return(500, nil) shipmentHeavyEtag := etag.GenerateEtag(shipmentHeavy.UpdatedAt) @@ -681,6 +742,8 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), + false, + false, ).Return(500, nil).Run(func(args mock.Arguments) { TransitDistancePickupArg = args.Get(1).(string) TransitDistanceDestinationArg = args.Get(2).(string) @@ -735,6 +798,8 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), + false, + false, ).Return(500, nil) suite.Equal(8000, *shipment.MoveTaskOrder.Orders.Entitlement.AuthorizedWeight()) @@ -775,6 +840,8 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), + false, + false, ).Return(500, nil) shipmentEtag := etag.GenerateEtag(shipment.UpdatedAt) diff --git a/pkg/services/mto_shipment/shipment_deleter_test.go b/pkg/services/mto_shipment/shipment_deleter_test.go index 1e0ca2796f6..35c86bc68ac 100644 --- a/pkg/services/mto_shipment/shipment_deleter_test.go +++ b/pkg/services/mto_shipment/shipment_deleter_test.go @@ -29,6 +29,8 @@ func (suite *MTOShipmentServiceSuite) TestShipmentDeleter() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { @@ -259,6 +261,8 @@ func (suite *MTOShipmentServiceSuite) TestPrimeShipmentDeleter() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/services/payment_request/payment_request_creator.go b/pkg/services/payment_request/payment_request_creator.go index 0b301193287..63b1f0950a7 100644 --- a/pkg/services/payment_request/payment_request_creator.go +++ b/pkg/services/payment_request/payment_request_creator.go @@ -402,7 +402,6 @@ func (p *paymentRequestCreator) createPaymentServiceItem(appCtx appcontext.AppCo paymentServiceItem.PaymentRequestID = paymentRequest.ID paymentServiceItem.PaymentRequest = *paymentRequest paymentServiceItem.Status = models.PaymentServiceItemStatusRequested - // No pricing at this point, so skipping the PriceCents field. paymentServiceItem.RequestedAt = requestedAt verrs, err := appCtx.DB().ValidateAndCreate(&paymentServiceItem) diff --git a/pkg/services/payment_request/payment_request_creator_test.go b/pkg/services/payment_request/payment_request_creator_test.go index 61edc1c6fa8..cc8d50d01d2 100644 --- a/pkg/services/payment_request/payment_request_creator_test.go +++ b/pkg/services/payment_request/payment_request_creator_test.go @@ -327,6 +327,8 @@ func (suite *PaymentRequestServiceSuite) TestCreatePaymentRequest() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(0, nil) }) @@ -568,6 +570,8 @@ func (suite *PaymentRequestServiceSuite) TestCreatePaymentRequest() { mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(0, nil) failingCreator := NewPaymentRequestCreator(planner, failingServiceItemPricer) @@ -1346,7 +1350,7 @@ func (suite *PaymentRequestServiceSuite) TestCreatePaymentRequestCheckOnNTSRelea testZip3Distance := 1234 // ((testOriginalWeight / 100.0) * testZip3Distance * testDLHRate * testEscalationCompounded) / 1000 - testDLHTotalPrice := unit.Cents(279407) + testDLHTotalPrice := unit.Cents(17485484) // // Test data setup @@ -1459,6 +1463,8 @@ func (suite *PaymentRequestServiceSuite) TestCreatePaymentRequestCheckOnNTSRelea mock.AnythingOfType("*appcontext.appContext"), testStorageFacilityZip, testDestinationZip, + false, + false, ).Return(testZip3Distance, nil) // Create an initial payment request. diff --git a/pkg/services/payment_request/payment_request_recalculator_test.go b/pkg/services/payment_request/payment_request_recalculator_test.go index 4a91e7f9a10..7d5120dcd55 100644 --- a/pkg/services/payment_request/payment_request_recalculator_test.go +++ b/pkg/services/payment_request/payment_request_recalculator_test.go @@ -55,6 +55,8 @@ func (suite *PaymentRequestServiceSuite) TestRecalculatePaymentRequestSuccess() mock.AnythingOfType("*appcontext.appContext"), recalculateTestPickupZip, recalculateTestDestinationZip, + false, + false, ).Return(recalculateTestZip3Distance, nil) // Create an initial payment request. @@ -164,7 +166,7 @@ func (suite *PaymentRequestServiceSuite) TestRecalculatePaymentRequestSuccess() { paymentRequest: &oldPaymentRequest, serviceCode: models.ReServiceCodeDLH, - priceCents: unit.Cents(279407), + priceCents: unit.Cents(17485484), paramsToCheck: []paramMap{ {models.ServiceItemParamNameWeightOriginal, strTestOriginalWeight}, {models.ServiceItemParamNameWeightBilled, strTestOriginalWeight}, @@ -182,7 +184,7 @@ func (suite *PaymentRequestServiceSuite) TestRecalculatePaymentRequestSuccess() { paymentRequest: &oldPaymentRequest, serviceCode: models.ReServiceCodeDOASIT, - priceCents: unit.Cents(254910), + priceCents: unit.Cents(41633), paramsToCheck: []paramMap{ {models.ServiceItemParamNameWeightOriginal, strTestOriginalWeight}, {models.ServiceItemParamNameWeightBilled, strTestOriginalWeight}, @@ -208,7 +210,7 @@ func (suite *PaymentRequestServiceSuite) TestRecalculatePaymentRequestSuccess() isNewPaymentRequest: true, paymentRequest: newPaymentRequest, serviceCode: models.ReServiceCodeDLH, - priceCents: unit.Cents(261045), + priceCents: unit.Cents(16336383), paramsToCheck: []paramMap{ {models.ServiceItemParamNameWeightOriginal, strTestChangedOriginalWeight}, {models.ServiceItemParamNameWeightBilled, strTestChangedOriginalWeight}, @@ -228,7 +230,7 @@ func (suite *PaymentRequestServiceSuite) TestRecalculatePaymentRequestSuccess() isNewPaymentRequest: true, paymentRequest: newPaymentRequest, serviceCode: models.ReServiceCodeDOASIT, - priceCents: unit.Cents(238158), // Price same as before since new weight still in same weight bracket + priceCents: unit.Cents(38897), // Price same as before since new weight still in same weight bracket paramsToCheck: []paramMap{ {models.ServiceItemParamNameWeightOriginal, strTestChangedOriginalWeight}, {models.ServiceItemParamNameWeightBilled, strTestChangedOriginalWeight}, @@ -295,6 +297,8 @@ func (suite *PaymentRequestServiceSuite) TestRecalculatePaymentRequestErrors() { mock.AnythingOfType("*appcontext.appContext"), recalculateTestPickupZip, recalculateTestDestinationZip, + false, + false, ).Return(recalculateTestZip3Distance, nil) // Create an initial payment request. @@ -478,21 +482,6 @@ func (suite *PaymentRequestServiceSuite) setupRecalculateData1() (models.Move, m } suite.MustSave(&domServiceAreaPriceDOP) - // Domestic Pack - dpkService := factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeDPK) - - // Domestic Other Price - domOtherPriceDPK := models.ReDomesticOtherPrice{ - ContractID: contractYear.Contract.ID, - ServiceID: dpkService.ID, - IsPeakPeriod: false, - Schedule: 2, - PriceCents: recalculateTestDomOtherPrice, - Contract: contractYear.Contract, - Service: dpkService, - } - suite.MustSave(&domOtherPriceDPK) - // Build up a payment request with service item references for creating a payment request. paymentRequestArg := models.PaymentRequest{ MoveTaskOrderID: moveTaskOrder.ID, diff --git a/pkg/services/payment_request/payment_request_shipment_recalculate_test.go b/pkg/services/payment_request/payment_request_shipment_recalculate_test.go index 238a622bc90..d32beaa61b7 100644 --- a/pkg/services/payment_request/payment_request_shipment_recalculate_test.go +++ b/pkg/services/payment_request/payment_request_shipment_recalculate_test.go @@ -27,6 +27,8 @@ func (suite *PaymentRequestServiceSuite) TestRecalculateShipmentPaymentRequestSu mock.AnythingOfType("*appcontext.appContext"), recalculateTestPickupZip, recalculateTestDestinationZip, + false, + false, ).Return(recalculateTestZip3Distance, nil) // Create an initial payment request. @@ -136,6 +138,8 @@ func (suite *PaymentRequestServiceSuite) TestRecalculateShipmentPaymentRequestEr mock.AnythingOfType("*appcontext.appContext"), recalculateTestPickupZip, recalculateTestDestinationZip, + false, + false, ).Return(recalculateTestZip3Distance, nil) creator := NewPaymentRequestCreator(mockPlanner, ghcrateengine.NewServiceItemPricer()) diff --git a/pkg/services/port_location/port_location_fetcher.go b/pkg/services/port_location/port_location_fetcher.go index fb26549934f..9d25b2d94a2 100644 --- a/pkg/services/port_location/port_location_fetcher.go +++ b/pkg/services/port_location/port_location_fetcher.go @@ -17,7 +17,7 @@ func NewPortLocationFetcher() services.PortLocationFetcher { func (p *portLocationFetcher) FetchPortLocationByPortCode(appCtx appcontext.AppContext, portCode string) (*models.PortLocation, error) { portLocation := models.PortLocation{} - err := appCtx.DB().Eager("Port").Where("is_active = TRUE").InnerJoin("ports p", "port_id = p.id").Where("p.port_code = $1", portCode).First(&portLocation) + err := appCtx.DB().Eager("Port", "UsPostRegionCity").Where("is_active = TRUE").InnerJoin("ports p", "port_id = p.id").Where("p.port_code = $1", portCode).First(&portLocation) if err != nil { return nil, apperror.NewQueryError("PortLocation", err, "") } diff --git a/pkg/services/postal_codes/postal_code_validator_test.go b/pkg/services/postal_codes/postal_code_validator_test.go index 80da05fce2d..13986827a60 100644 --- a/pkg/services/postal_codes/postal_code_validator_test.go +++ b/pkg/services/postal_codes/postal_code_validator_test.go @@ -59,29 +59,10 @@ func (suite *ValidatePostalCodeTestSuite) TestValidatePostalCode() { suite.Contains(err.Error(), "not found in postal_code_to_gblocs") }) - suite.Run("Postal code is not in zip3_distances table", func() { - testPostalCode := "30183" - factory.FetchOrBuildPostalCodeToGBLOC(suite.DB(), testPostalCode, "CNNQ") - - valid, err := postalCodeValidator.ValidatePostalCode(suite.AppContextForTest(), testPostalCode) - - suite.False(valid) - suite.Error(err) - suite.IsType(&apperror.UnsupportedPostalCodeError{}, err) - suite.Contains(err.Error(), "not found in zip3_distances") - }) - suite.Run("Contract year cannot be found", func() { testPostalCode := "30183" factory.FetchOrBuildPostalCodeToGBLOC(suite.DB(), testPostalCode, "CNNQ") - testdatagen.MakeZip3Distance(suite.DB(), testdatagen.Assertions{ - Zip3Distance: models.Zip3Distance{ - FromZip3: testPostalCode[:3], - ToZip3: "993", - }, - }) - suite.buildContractYear(testdatagen.GHCTestYear - 1) valid, err := postalCodeValidator.ValidatePostalCode(suite.AppContextForTest(), testPostalCode) @@ -91,128 +72,11 @@ func (suite *ValidatePostalCodeTestSuite) TestValidatePostalCode() { suite.IsType(&apperror.UnsupportedPostalCodeError{}, err) suite.Contains(err.Error(), "could not find contract year") }) - - suite.Run("Postal code is not in re_zip3s table", func() { - testPostalCode := "30183" - factory.FetchOrBuildPostalCodeToGBLOC(suite.DB(), testPostalCode, "CNNQ") - - testdatagen.MakeZip3Distance(suite.DB(), testdatagen.Assertions{ - Zip3Distance: models.Zip3Distance{ - FromZip3: testPostalCode[:3], - ToZip3: "993", - }, - }) - - suite.buildContractYear(testdatagen.GHCTestYear) - - valid, err := postalCodeValidator.ValidatePostalCode(suite.AppContextForTest(), testPostalCode) - - suite.False(valid) - suite.Error(err) - suite.IsType(&apperror.UnsupportedPostalCodeError{}, err) - suite.Contains(err.Error(), "not found in re_zip3s") - }) - - suite.Run("Postal code is not in re_zip5_rate_areas table", func() { - testPostalCode := "32102" - factory.FetchOrBuildPostalCodeToGBLOC(suite.DB(), testPostalCode, "CNNQ") - - testdatagen.MakeZip3Distance(suite.DB(), testdatagen.Assertions{ - Zip3Distance: models.Zip3Distance{ - FromZip3: testPostalCode[:3], - ToZip3: "993", - }, - }) - - reContractYear := suite.buildContractYear(testdatagen.GHCTestYear) - serviceArea := testdatagen.MakeDefaultReDomesticServiceArea(suite.DB()) - testdatagen.MakeReZip3(suite.DB(), testdatagen.Assertions{ - ReZip3: models.ReZip3{ - Zip3: testPostalCode[:3], - Contract: reContractYear.Contract, - DomesticServiceArea: serviceArea, - HasMultipleRateAreas: true, - }, - }) - - valid, err := postalCodeValidator.ValidatePostalCode(suite.AppContextForTest(), testPostalCode) - - suite.False(valid) - suite.Error(err) - suite.IsType(&apperror.UnsupportedPostalCodeError{}, err) - suite.Contains(err.Error(), "not found in re_zip5_rate_areas") - }) - - suite.Run("Valid postal code for zip3 with single rate area", func() { - testPostalCode := "30813" - factory.FetchOrBuildPostalCodeToGBLOC(suite.DB(), testPostalCode, "CNNQ") - - testdatagen.MakeZip3Distance(suite.DB(), testdatagen.Assertions{ - Zip3Distance: models.Zip3Distance{ - FromZip3: testPostalCode[:3], - ToZip3: "993", - }, - }) - - reContractYear := suite.buildContractYear(testdatagen.GHCTestYear) - serviceArea := testdatagen.MakeDefaultReDomesticServiceArea(suite.DB()) - testdatagen.MakeReZip3(suite.DB(), testdatagen.Assertions{ - ReZip3: models.ReZip3{ - Zip3: testPostalCode[:3], - Contract: reContractYear.Contract, - DomesticServiceArea: serviceArea, - }, - }) - - valid, err := postalCodeValidator.ValidatePostalCode(suite.AppContextForTest(), testPostalCode) - - suite.True(valid) - suite.NoError(err) - }) - - suite.Run("Valid postal code for zip3 with multiple rate areas", func() { - testPostalCode := "32102" - factory.FetchOrBuildPostalCodeToGBLOC(suite.DB(), testPostalCode, "CNNQ") - - testdatagen.MakeZip3Distance(suite.DB(), testdatagen.Assertions{ - Zip3Distance: models.Zip3Distance{ - FromZip3: testPostalCode[:3], - ToZip3: "993", - }, - }) - - reContractYear := suite.buildContractYear(testdatagen.GHCTestYear) - serviceArea := testdatagen.MakeDefaultReDomesticServiceArea(suite.DB()) - testdatagen.MakeReZip3(suite.DB(), testdatagen.Assertions{ - ReZip3: models.ReZip3{ - Zip3: testPostalCode[:3], - Contract: reContractYear.Contract, - DomesticServiceArea: serviceArea, - HasMultipleRateAreas: true, - }, - }) - - rateArea := testdatagen.FetchOrMakeReRateArea(suite.DB(), testdatagen.Assertions{ - ReContractYear: reContractYear, - }) - testdatagen.MakeReZip5RateArea(suite.DB(), testdatagen.Assertions{ - ReZip5RateArea: models.ReZip5RateArea{ - Zip5: testPostalCode, - }, - ReContract: reContractYear.Contract, - ReRateArea: rateArea, - }) - - valid, err := postalCodeValidator.ValidatePostalCode(suite.AppContextForTest(), testPostalCode) - - suite.True(valid) - suite.NoError(err) - }) } 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), diff --git a/pkg/services/ppm_closeout/ppm_closeout_test.go b/pkg/services/ppm_closeout/ppm_closeout_test.go index 2564a06b468..c1479c140ea 100644 --- a/pkg/services/ppm_closeout/ppm_closeout_test.go +++ b/pkg/services/ppm_closeout/ppm_closeout_test.go @@ -184,53 +184,42 @@ func (suite *PPMCloseoutSuite) TestPPMShipmentCreator() { dpkService := factory.FetchReServiceByCode(suite.AppContextForTest().DB(), models.ReServiceCodeDPK) - testdatagen.FetchOrMakeReDomesticOtherPrice(suite.AppContextForTest().DB(), testdatagen.Assertions{ - ReDomesticOtherPrice: models.ReDomesticOtherPrice{ - ContractID: originDomesticServiceArea.ContractID, - Contract: originDomesticServiceArea.Contract, - ServiceID: dpkService.ID, - Service: dpkService, - IsPeakPeriod: false, - Schedule: 3, - PriceCents: 7395, + factory.FetchOrMakeDomesticOtherPrice(suite.DB(), []factory.Customization{ + { + Model: models.ReDomesticOtherPrice{ + ContractID: originDomesticServiceArea.ContractID, + ServiceID: dpkService.ID, + IsPeakPeriod: true, + Schedule: 3, + PriceCents: 7395, + }, }, - }) - testdatagen.FetchOrMakeReDomesticOtherPrice(suite.AppContextForTest().DB(), testdatagen.Assertions{ - ReDomesticOtherPrice: models.ReDomesticOtherPrice{ - ContractID: originDomesticServiceArea.ContractID, - Contract: originDomesticServiceArea.Contract, - ServiceID: dpkService.ID, - Service: dpkService, - IsPeakPeriod: true, - Schedule: 3, - PriceCents: 7395, - }, - }) + }, nil) dupkService := factory.FetchReServiceByCode(suite.AppContextForTest().DB(), models.ReServiceCodeDUPK) - testdatagen.FetchOrMakeReDomesticOtherPrice(suite.AppContextForTest().DB(), testdatagen.Assertions{ - ReDomesticOtherPrice: models.ReDomesticOtherPrice{ - ContractID: destDomesticServiceArea.ContractID, - Contract: destDomesticServiceArea.Contract, - ServiceID: dupkService.ID, - Service: dupkService, - IsPeakPeriod: false, - Schedule: 2, - PriceCents: 597, + factory.FetchOrMakeDomesticOtherPrice(suite.DB(), []factory.Customization{ + { + Model: models.ReDomesticOtherPrice{ + ContractID: destDomesticServiceArea.ContractID, + ServiceID: dupkService.ID, + IsPeakPeriod: false, + Schedule: 2, + PriceCents: 597, + }, }, - }) - testdatagen.FetchOrMakeReDomesticOtherPrice(suite.AppContextForTest().DB(), testdatagen.Assertions{ - ReDomesticOtherPrice: models.ReDomesticOtherPrice{ - ContractID: destDomesticServiceArea.ContractID, - Contract: destDomesticServiceArea.Contract, - ServiceID: dupkService.ID, - Service: dupkService, - IsPeakPeriod: true, - Schedule: 2, - PriceCents: 597, + }, nil) + factory.FetchOrMakeDomesticOtherPrice(suite.DB(), []factory.Customization{ + { + Model: models.ReDomesticOtherPrice{ + ContractID: destDomesticServiceArea.ContractID, + ServiceID: dupkService.ID, + IsPeakPeriod: true, + Schedule: 2, + PriceCents: 597, + }, }, - }) + }, nil) dofsitService := factory.FetchReServiceByCode(suite.AppContextForTest().DB(), models.ReServiceCodeDOFSIT) @@ -348,7 +337,7 @@ func (suite *PPMCloseoutSuite) TestPPMShipmentCreator() { appCtx := suite.AppContextForTest() mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) mockedPaymentRequestHelper.On( "FetchServiceParamsForServiceItems", @@ -404,7 +393,7 @@ func (suite *PPMCloseoutSuite) TestPPMShipmentCreator() { appCtx := suite.AppContextForTest() mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) mockedPaymentRequestHelper.On( "FetchServiceParamsForServiceItems", diff --git a/pkg/services/ppmshipment/ppm_estimator_test.go b/pkg/services/ppmshipment/ppm_estimator_test.go index 4826d396a33..71b0749a7ea 100644 --- a/pkg/services/ppmshipment/ppm_estimator_test.go +++ b/pkg/services/ppmshipment/ppm_estimator_test.go @@ -492,25 +492,25 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) linehaul, fuel, origin, dest, packing, unpacking, _, err := ppmEstimator.PriceBreakdown(suite.AppContextForTest(), &ppmShipment) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) suite.Equal(unit.Pound(4000), *ppmShipment.EstimatedWeight) - suite.Equal(unit.Cents(37841824), linehaul) + suite.Equal(unit.Cents(43384128), linehaul) suite.Equal(unit.Cents(3004), fuel) - suite.Equal(unit.Cents(16160), origin) + suite.Equal(unit.Cents(21760), origin) suite.Equal(unit.Cents(33280), dest) - suite.Equal(unit.Cents(295800), packing) + suite.Equal(unit.Cents(290000), packing) suite.Equal(unit.Cents(23880), unpacking) total := linehaul + fuel + origin + dest + packing + unpacking - suite.Equal(unit.Cents(38213948), total) + suite.Equal(unit.Cents(43756052), total) }) suite.Run("Estimated Incentive", func() { @@ -537,18 +537,18 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) suite.Equal(oldPPMShipment.PickupAddress.PostalCode, newPPM.PickupAddress.PostalCode) suite.Equal(unit.Pound(5000), *newPPM.EstimatedWeight) - suite.Equal(unit.Cents(70064364), *ppmEstimate) + suite.Equal(unit.Cents(80249474), *ppmEstimate) }) suite.Run("Estimated Incentive - Success using db authorize weight and not estimated incentive", func() { @@ -574,13 +574,13 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) suite.Equal(oldPPMShipment.PickupAddress.PostalCode, newPPM.PickupAddress.PostalCode) @@ -608,18 +608,18 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) suite.Equal(oldPPMShipment.PickupAddress.PostalCode, newPPM.PickupAddress.PostalCode) suite.Equal(unit.Pound(5000), *newPPM.EstimatedWeight) - suite.Equal(unit.Cents(70064364), *ppmEstimate) + suite.Equal(unit.Cents(80249474), *ppmEstimate) }) suite.Run("Estimated Incentive - Success when old Estimated Incentive is zero", func() { @@ -642,18 +642,18 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) suite.Equal(oldPPMShipment.PickupAddress.PostalCode, newPPM.PickupAddress.PostalCode) suite.Equal(unit.Pound(5000), *newPPM.EstimatedWeight) - suite.Equal(unit.Cents(70064364), *ppmEstimate) + suite.Equal(unit.Cents(80249474), *ppmEstimate) }) suite.Run("Estimated Incentive - Success - clears advance and advance requested values", func() { @@ -678,13 +678,13 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil).Once() + "50309", "30813", false, false).Return(2294, nil).Once() ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) suite.Nil(newPPM.HasRequestedAdvance) suite.Nil(newPPM.AdvanceAmountRequested) - suite.Equal(unit.Cents(38213948), *ppmEstimate) + suite.Equal(unit.Cents(43754569), *ppmEstimate) }) suite.Run("Estimated Incentive - does not change when required fields are the same", func() { @@ -764,16 +764,16 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { mock.AnythingOfType("[]models.MTOServiceItem")).Return(serviceParams, nil) mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) maxIncentive, err := ppmEstimator.MaxIncentive(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) - suite.Equal(unit.Cents(112102682), *maxIncentive) + suite.Equal(unit.Cents(128398858), *maxIncentive) }) suite.Run("Max Incentive - Success - is skipped when Estimated Weight is missing", func() { @@ -825,13 +825,13 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmFinal, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) suite.Equal(oldPPMShipment.ActualPickupPostalCode, newPPM.ActualPickupPostalCode) @@ -839,7 +839,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { originalWeight, newWeight := SumWeightTickets(oldPPMShipment, newPPM) suite.Equal(unit.Pound(5000), originalWeight) suite.Equal(unit.Pound(5000), newWeight) - suite.Equal(unit.Cents(70064364), *ppmFinal) + suite.Equal(unit.Cents(80249474), *ppmFinal) }) suite.Run("Final Incentive - Success with allowable weight less than net weight", func() { @@ -876,12 +876,12 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { mock.AnythingOfType("[]models.MTOServiceItem")).Return(serviceParams, nil) // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles - mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "50309", "30813").Return(2294, nil) + mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "50309", "30813", false, false).Return(2294, nil) ppmFinal, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) - mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "50309", "30813") + mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) suite.Equal(oldPPMShipment.ActualPickupPostalCode, newPPM.ActualPickupPostalCode) @@ -889,7 +889,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { originalWeight, newWeight := SumWeightTickets(oldPPMShipment, newPPM) suite.Equal(unit.Pound(5000), originalWeight) suite.Equal(unit.Pound(5000), newWeight) - suite.Equal(unit.Cents(70064364), *ppmFinal) + suite.Equal(unit.Cents(80249474), *ppmFinal) // Repeat the above shipment with an allowable weight less than the net weight weightOverride = unit.Pound(19500) @@ -927,12 +927,12 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmFinalIncentiveLimitedByAllowableWeight, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) - mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "50309", "30813") + mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) suite.Equal(oldPPMShipment.ActualPickupPostalCode, newPPM.ActualPickupPostalCode) @@ -982,13 +982,13 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmFinal, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) suite.Equal(oldPPMShipment.ActualPickupPostalCode, newPPM.ActualPickupPostalCode) @@ -996,7 +996,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { originalWeight, newWeight := SumWeightTickets(oldPPMShipment, newPPM) suite.Equal(unit.Pound(4000), originalWeight) suite.Equal(unit.Pound(5000), newWeight) - suite.Equal(unit.Cents(70064364), *ppmFinal) + suite.Equal(unit.Cents(80249474), *ppmFinal) }) suite.Run("Final Incentive - Success with disregarding rejected weight tickets", func() { @@ -1039,13 +1039,13 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmFinal, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) originalWeight, newWeight := SumWeightTickets(oldPPMShipment, newPPM) @@ -1101,19 +1101,19 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmFinal, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) originalWeight, newWeight := SumWeightTickets(oldPPMShipment, newPPM) suite.Equal(unit.Pound(8000), originalWeight) suite.Equal(unit.Pound(4000), newWeight) - suite.Equal(unit.Cents(38213948), *ppmFinal) + suite.Equal(unit.Cents(43756052), *ppmFinal) suite.NotEqual(oldPPMShipment.FinalIncentive, *ppmFinal) }) @@ -1171,19 +1171,19 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { // DTOD distance is going to be less than the HHG Rand McNally distance of 2361 miles mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) ppmFinal, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), oldPPMShipment, &newPPM) suite.NilOrNoVerrs(err) mockedPlanner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813") + "50309", "30813", false, false) mockedPaymentRequestHelper.AssertCalled(suite.T(), "FetchServiceParamsForServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]models.MTOServiceItem")) originalWeight, newWeight := SumWeightTickets(oldPPMShipment, newPPM) suite.Equal(unit.Pound(8000), originalWeight) suite.Equal(unit.Pound(3000), newWeight) - suite.Equal(unit.Cents(28661212), *ppmFinal) + suite.Equal(unit.Cents(32817790), *ppmFinal) suite.NotEqual(oldPPMShipment.FinalIncentive, *ppmFinal) }) @@ -1666,13 +1666,13 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }, nil) mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) _, estimatedSITCost, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), models.PPMShipment{}, &shipmentOriginSIT) suite.NoError(err) suite.NotNil(estimatedSITCost) - suite.Equal(50660, estimatedSITCost.Int()) + suite.Equal(56660, estimatedSITCost.Int()) }) suite.Run("Success - Destination First Day and Additional Day SIT", func() { @@ -1728,7 +1728,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }, nil) mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) _, estimatedSITCost, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), models.PPMShipment{}, &shipmentDestinationSIT) @@ -1790,7 +1790,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }, }, nil) mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30813").Return(2294, nil) + "50309", "30813", false, false).Return(2294, nil) _, estimatedSITCost, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), models.PPMShipment{}, &shipmentOriginSIT) @@ -1980,10 +1980,10 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { shipmentDifferentDeparture.ExpectedDepartureDate = originalShipment.ExpectedDepartureDate.Add(time.Hour * 24 * 70) mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "90211", "30813").Return(2294, nil) + "90211", "30813", false, false).Return(2294, nil) mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "30814").Return(2290, nil) + "50309", "30814", false, false).Return(2290, nil) // SIT specific field changes will likely cause the price to change, although adjusting dates may not change // the total number of days in SIT. diff --git a/pkg/services/ppmshipment/ppm_shipment_updater_test.go b/pkg/services/ppmshipment/ppm_shipment_updater_test.go index 8d21e74baf6..607491af64c 100644 --- a/pkg/services/ppmshipment/ppm_shipment_updater_test.go +++ b/pkg/services/ppmshipment/ppm_shipment_updater_test.go @@ -1504,7 +1504,7 @@ func (suite *PPMShipmentSuite) TestUpdatePPMShipment() { originalPPM.DestinationAddress = destinationAddress mockedPlanner := &routemocks.Planner{} mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "90210", "30813").Return(2294, nil) + "90210", "30813", false, false).Return(2294, nil) updatedPPM, err := subtestData.ppmShipmentUpdater.UpdatePPMShipmentSITEstimatedCost(appCtx, &originalPPM) @@ -1560,7 +1560,7 @@ func (suite *PPMShipmentSuite) TestUpdatePPMShipment() { originalPPM.DestinationAddress = destinationAddress mockedPlanner := &routemocks.Planner{} mockedPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "90210", "30813").Return(2294, nil) + "90210", "30813", false, false).Return(2294, nil) updatedPPM, err := subtestData.ppmShipmentUpdater.UpdatePPMShipmentSITEstimatedCost(appCtx, &originalPPM) diff --git a/pkg/services/reweigh/reweigh_updater_test.go b/pkg/services/reweigh/reweigh_updater_test.go index ad2ab0da981..77805ecf4ac 100644 --- a/pkg/services/reweigh/reweigh_updater_test.go +++ b/pkg/services/reweigh/reweigh_updater_test.go @@ -31,6 +31,8 @@ func (suite *ReweighSuite) TestReweighUpdater() { mockPlanner.On("ZipTransitDistance", recalculateTestPickupZip, recalculateTestDestinationZip, + false, + false, ).Return(recalculateTestZip3Distance, nil) // Get shipment payment request recalculator service diff --git a/pkg/services/shipment_address_update/shipment_address_update_requester.go b/pkg/services/shipment_address_update/shipment_address_update_requester.go index b9bcf918264..a0ed36fccdd 100644 --- a/pkg/services/shipment_address_update/shipment_address_update_requester.go +++ b/pkg/services/shipment_address_update/shipment_address_update_requester.go @@ -41,7 +41,7 @@ func NewShipmentAddressUpdateRequester(planner route.Planner, addressCreator ser func (f *shipmentAddressUpdateRequester) isAddressChangeDistanceOver50(appCtx appcontext.AppContext, addressUpdate models.ShipmentAddressUpdate) (bool, error) { //We calculate and set the distance between the old and new address - distance, err := f.planner.ZipTransitDistance(appCtx, addressUpdate.OriginalAddress.PostalCode, addressUpdate.NewAddress.PostalCode) + distance, err := f.planner.ZipTransitDistance(appCtx, addressUpdate.OriginalAddress.PostalCode, addressUpdate.NewAddress.PostalCode, false, false) if err != nil { return false, err } @@ -92,11 +92,11 @@ func (f *shipmentAddressUpdateRequester) doesDeliveryAddressUpdateChangeMileageB return false, nil } - previousDistance, err := f.planner.ZipTransitDistance(appCtx, originalPickupAddress.PostalCode, originalDeliveryAddress.PostalCode) + previousDistance, err := f.planner.ZipTransitDistance(appCtx, originalPickupAddress.PostalCode, originalDeliveryAddress.PostalCode, false, false) if err != nil { return false, err } - newDistance, err := f.planner.ZipTransitDistance(appCtx, originalPickupAddress.PostalCode, newDeliveryAddress.PostalCode) + newDistance, err := f.planner.ZipTransitDistance(appCtx, originalPickupAddress.PostalCode, newDeliveryAddress.PostalCode, false, false) if err != nil { return false, err } @@ -308,14 +308,14 @@ func (f *shipmentAddressUpdateRequester) RequestShipmentDeliveryAddressUpdate(ap if addressUpdate.NewSitDistanceBetween != nil { distanceBetweenOld = *addressUpdate.NewSitDistanceBetween } else { - distanceBetweenOld, err = f.planner.ZipTransitDistance(appCtx, addressUpdate.SitOriginalAddress.PostalCode, addressUpdate.OriginalAddress.PostalCode) + distanceBetweenOld, err = f.planner.ZipTransitDistance(appCtx, addressUpdate.SitOriginalAddress.PostalCode, addressUpdate.OriginalAddress.PostalCode, false, false) } if err != nil { return nil, err } // calculating distance between the new address update & the SIT - distanceBetweenNew, err = f.planner.ZipTransitDistance(appCtx, addressUpdate.SitOriginalAddress.PostalCode, addressUpdate.NewAddress.PostalCode) + distanceBetweenNew, err = f.planner.ZipTransitDistance(appCtx, addressUpdate.SitOriginalAddress.PostalCode, addressUpdate.NewAddress.PostalCode, false, false) if err != nil { return nil, err } @@ -472,7 +472,6 @@ func (f *shipmentAddressUpdateRequester) ReviewShipmentAddressChange(appCtx appc addressUpdate.Status = models.ShipmentAddressUpdateStatusApproved addressUpdate.OfficeRemarks = &tooRemarks shipment.DestinationAddress = &addressUpdate.NewAddress - shipment.DestinationAddressID = &addressUpdate.NewAddressID var haulPricingTypeHasChanged bool if shipment.ShipmentType == models.MTOShipmentTypeHHG { @@ -566,7 +565,6 @@ func (f *shipmentAddressUpdateRequester) ReviewShipmentAddressChange(appCtx appc } } } - if tooApprovalStatus == models.ShipmentAddressUpdateStatusRejected { addressUpdate.Status = models.ShipmentAddressUpdateStatusRejected addressUpdate.OfficeRemarks = &tooRemarks @@ -604,6 +602,48 @@ func (f *shipmentAddressUpdateRequester) ReviewShipmentAddressChange(appCtx appc return err } + // if the shipment has an estimated weight, we need to update the service item pricing since we know the distances have changed + // this only applies to international shipments that the TOO is approving the address change for + if shipment.PrimeEstimatedWeight != nil && + shipment.MarketCode == models.MarketCodeInternational && + tooApprovalStatus == models.ShipmentAddressUpdateStatusApproved { + portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), shipment.ID) + if err != nil { + return err + } + // if we don't have the port data, then we won't worry about pricing + if portZip != nil && portType != nil { + var pickupZip string + var destZip string + // if the port type is POEFSC this means the shipment is CONUS -> OCONUS (pickup -> port) + // if the port type is PODFSC this means the shipment is OCONUS -> CONUS (port -> destination) + if *portType == models.ReServiceCodePOEFSC.String() { + pickupZip = shipment.PickupAddress.PostalCode + destZip = *portZip + } else if *portType == models.ReServiceCodePODFSC.String() { + pickupZip = *portZip + destZip = shipment.DestinationAddress.PostalCode + } + // we need to get the mileage from DTOD first, the db proc will consume that + mileage, err := f.planner.ZipTransitDistance(appCtx, pickupZip, destZip, true, true) + if err != nil { + return err + } + + // update the service item pricing if relevant fields have changed + err = models.UpdateEstimatedPricingForShipmentBasicServiceItems(appCtx.DB(), &shipment, &mileage) + if err != nil { + return err + } + } else { + // if we don't have the port data, that's okay - we can update the other service items except for PODFSC/POEFSC + err = models.UpdateEstimatedPricingForShipmentBasicServiceItems(appCtx.DB(), &shipment, nil) + if err != nil { + return err + } + } + } + _, err = f.moveRouter.ApproveOrRequestApproval(appCtx, shipment.MoveTaskOrder) if err != nil { return err diff --git a/pkg/services/shipment_address_update/shipment_address_update_requester_test.go b/pkg/services/shipment_address_update/shipment_address_update_requester_test.go index 863e1851d21..4330b13f9eb 100644 --- a/pkg/services/shipment_address_update/shipment_address_update_requester_test.go +++ b/pkg/services/shipment_address_update/shipment_address_update_requester_test.go @@ -2,6 +2,7 @@ package shipmentaddressupdate import ( "fmt" + "slices" "time" "github.com/stretchr/testify/mock" @@ -70,11 +71,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres mock.AnythingOfType("*appcontext.appContext"), "90210", "94535", + false, + false, ).Return(2500, nil).Twice() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "94535", "94535", + false, + false, ).Return(2500, nil).Once() move := setupTestData() shipment := factory.BuildMTOShipmentWithMove(&move, suite.DB(), nil, nil) @@ -123,8 +128,10 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres suite.Run("Failed distance calculation should error", func() { mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - mock.AnythingOfType("90210"), - mock.AnythingOfType("94535"), + mock.AnythingOfType("string"), + mock.AnythingOfType("string"), + false, + false, ).Return(0, fmt.Errorf("error calculating distance 2")).Once() testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ @@ -141,6 +148,28 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres State: "CA", PostalCode: "90210", } + // building DDASIT service item to get dest SIT checks + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + SITDestinationOriginalAddressID: shipment.DestinationAddressID, + }, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeDDASIT, + }, + }, + }, nil) update, err := addressUpdateRequester.RequestShipmentDeliveryAddressUpdate(suite.AppContextForTest(), shipment.ID, newAddress, "we really need to change the address", etag.GenerateEtag(shipment.UpdatedAt)) suite.Error(err) suite.Nil(update) @@ -242,11 +271,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres mock.AnythingOfType("*appcontext.appContext"), "90210", "94535", + false, + false, ).Return(2500, nil).Times(4) mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "94535", "94535", + false, + false, ).Return(2500, nil).Twice() update, err := addressUpdateRequester.RequestShipmentDeliveryAddressUpdate(suite.AppContextForTest(), shipment.ID, newAddress, "we really need to change the address", etag.GenerateEtag(shipment.UpdatedAt)) @@ -272,11 +305,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres mock.AnythingOfType("*appcontext.appContext"), "89523", "89503", + false, + false, ).Return(2500, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "89523", "90210", + false, + false, ).Return(2500, nil).Once() newAddress := models.Address{ StreetAddress1: "123 Any St", @@ -317,11 +354,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres mock.AnythingOfType("*appcontext.appContext"), "89523", "89503", + false, + false, ).Return(2500, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "89523", "90210", + false, + false, ).Return(2500, nil).Once() move := setupTestData() shipment := factory.BuildMTOShipmentWithMove(&move, suite.DB(), []factory.Customization{ @@ -362,11 +403,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres mock.AnythingOfType("*appcontext.appContext"), "90210", "94535", + false, + false, ).Return(0, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "90210", "89503", + false, + false, ).Return(200, nil).Once() testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ @@ -489,11 +534,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("string"), "87108", + false, + false, ).Return(500, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("string"), "87053", + false, + false, ).Return(501, nil).Once() suite.NotEmpty(move.MTOShipments) update, err := addressUpdateRequester.RequestShipmentDeliveryAddressUpdate(suite.AppContextForTest(), shipment.ID, newAddress, "we really need to change the address", etag.GenerateEtag(shipment.UpdatedAt)) @@ -547,16 +596,22 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres mock.AnythingOfType("*appcontext.appContext"), "94535", "94535", + false, + false, ).Return(0, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "94523", "90210", + false, + false, ).Return(500, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "94535", "90210", + false, + false, ).Return(501, nil).Once() // request the update @@ -582,6 +637,8 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestTOOApprovedShipmentAddressUp mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) addressUpdateRequester := NewShipmentAddressUpdateRequester(mockPlanner, addressCreator, moveRouter) @@ -786,6 +843,445 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestTOOApprovedShipmentAddressUp suite.Equal(updatedShipment.MarketCode, models.MarketCodeInternational) }) + + suite.Run("Successfully update estiamted pricing on service items when address update is approved by TOO", func() { + ghcDomesticTransitTime := models.GHCDomesticTransitTime{ + MaxDaysTransitTime: 12, + WeightLbsLower: 0, + WeightLbsUpper: 10000, + DistanceMilesLower: 0, + DistanceMilesUpper: 10000, + } + _, _ = suite.DB().ValidateAndCreate(&ghcDomesticTransitTime) + + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "50314") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, + }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + now := time.Now() + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusApproved, + PrimeEstimatedWeight: models.PoundPointer(4000), + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + ScheduledPickupDate: &now, + RequestedPickupDate: &now, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeISLH, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: models.CentPointer(1000), + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: models.CentPointer(1000), + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHUPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: models.CentPointer(1000), + }, + }, + }, nil) + // POEFSC needs a port location + portLocation := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "PDX", + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodePOEFSC, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: models.CentPointer(1000), + }, + }, + { + Model: portLocation, + LinkOnly: true, + Type: &factory.PortLocations.PortOfEmbarkation, + }, + }, nil) + + mockPlanner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + false, + ).Return(300, nil) + + mockPlanner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + true, + true, + ).Return(300, nil) + + newDestUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99703") + suite.FatalNoError(err) + factory.BuildShipmentAddressUpdate(suite.DB(), []factory.Customization{ + { + Model: shipment, + LinkOnly: true, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: models.Address{ + StreetAddress1: "Cold Ave.", + City: "Fairbanks", + State: "AK", + PostalCode: "99703", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &newDestUSPRC.ID, + }, + Type: &factory.Addresses.NewAddress, + }, + }, []factory.Trait{factory.GetTraitShipmentAddressUpdateRequested}) + + officeRemarks := "Changing to another OCONUS address" + update, err := addressUpdateRequester.ReviewShipmentAddressChange(suite.AppContextForTest(), shipment.ID, "APPROVED", officeRemarks) + + suite.NoError(err) + suite.NotNil(update) + suite.Equal(models.ShipmentAddressUpdateStatusApproved, update.Status) + + // checking out the service items + var serviceItems []models.MTOServiceItem + err = suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", shipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err) + + expectedReserviceCodes := []models.ReServiceCode{ + models.ReServiceCodePOEFSC, + models.ReServiceCodeISLH, + models.ReServiceCodeIHPK, + models.ReServiceCodeIHUPK, + } + + initialPrice := 1000 + suite.Equal(4, len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + actualReServiceCode := serviceItems[i].ReService.Code + suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode)) + // pricing should not be nil + suite.NotNil(serviceItems[i].PricingEstimate) + // initially we set them all to 1000 and they should all be changed + suite.NotEqual(serviceItems[i].PricingEstimate, &initialPrice) + } + }) + suite.Run("On approval - successfully update estimated pricing on all basic iHHG service items except for POEFSC when port isn't set", func() { + ghcDomesticTransitTime := models.GHCDomesticTransitTime{ + MaxDaysTransitTime: 12, + WeightLbsLower: 0, + WeightLbsUpper: 10000, + DistanceMilesLower: 0, + DistanceMilesUpper: 10000, + } + _, _ = suite.DB().ValidateAndCreate(&ghcDomesticTransitTime) + + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "50314") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, + }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + now := time.Now() + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusApproved, + PrimeEstimatedWeight: models.PoundPointer(4000), + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + ScheduledPickupDate: &now, + RequestedPickupDate: &now, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeISLH, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + }, + }, + }, nil) + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeIHUPK, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + PricingEstimate: models.CentPointer(1000), + }, + }, + }, nil) + // no port data + factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodePOEFSC, + }, + }, + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + }, + }, + }, nil) + + mockPlanner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + false, + ).Return(300, nil) + + newDestUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99703") + suite.FatalNoError(err) + factory.BuildShipmentAddressUpdate(suite.DB(), []factory.Customization{ + { + Model: shipment, + LinkOnly: true, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: models.Address{ + StreetAddress1: "Cold Ave.", + City: "Fairbanks", + State: "AK", + PostalCode: "99703", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &newDestUSPRC.ID, + }, + Type: &factory.Addresses.NewAddress, + }, + }, []factory.Trait{factory.GetTraitShipmentAddressUpdateRequested}) + + officeRemarks := "Changing to another OCONUS address" + update, err := addressUpdateRequester.ReviewShipmentAddressChange(suite.AppContextForTest(), shipment.ID, "APPROVED", officeRemarks) + + suite.NoError(err) + suite.NotNil(update) + suite.Equal(models.ShipmentAddressUpdateStatusApproved, update.Status) + + // checking out the service items + var serviceItems []models.MTOServiceItem + err = suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", shipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err) + + suite.Equal(4, len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + if serviceItems[i].ReService.Code != models.ReServiceCodePOEFSC { + suite.NotNil(serviceItems[i].PricingEstimate) + } else if serviceItems[i].ReService.Code == models.ReServiceCodePOEFSC { + suite.Nil(serviceItems[i].PricingEstimate) + } + } + }) } func (suite *ShipmentAddressUpdateServiceSuite) TestTOOApprovedShipmentAddressUpdateRequestChangedPricing() { @@ -842,11 +1338,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestTOOApprovedShipmentAddressUp mock.AnythingOfType("*appcontext.appContext"), "89523", "89503", + false, + false, ).Return(2500, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "89523", "90210", + false, + false, ).Return(2500, nil).Once() move := setupTestData() shipment := factory.BuildMTOShipmentWithMove(&move, suite.DB(), []factory.Customization{ @@ -908,11 +1408,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestTOOApprovedShipmentAddressUp mock.AnythingOfType("*appcontext.appContext"), "89523", "89503", + false, + false, ).Return(2500, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "89523", "90210", + false, + false, ).Return(2500, nil).Once() move := setupTestData() shipment := factory.BuildMTOShipmentWithMove(&move, suite.DB(), []factory.Customization{ @@ -984,8 +1488,17 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestTOOApprovedShipmentAddressUp } mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "90210", "94535", + false, + false, + ).Return(2500, nil).Once() + mockPlanner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), "94535", + "94535", + false, + false, ).Return(2500, nil).Once() addressChange, _ := addressUpdateRequester.RequestShipmentDeliveryAddressUpdate(suite.AppContextForTest(), shipment.ID, newAddress, "we really need to change the address", etag.GenerateEtag(shipment.UpdatedAt)) @@ -1007,11 +1520,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestTOOApprovedShipmentAddressUp mock.AnythingOfType("*appcontext.appContext"), "89523", "89503", + false, + false, ).Return(2500, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "89523", "90210", + false, + false, ).Return(2500, nil).Once() move := setupTestData() shipment := factory.BuildMTOShipmentWithMove(&move, suite.DB(), []factory.Customization{ @@ -1068,11 +1585,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestTOOApprovedShipmentAddressUp mock.AnythingOfType("*appcontext.appContext"), "89523", "89503", + false, + false, ).Return(2500, nil).Once() mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "89523", "90210", + false, + false, ).Return(2500, nil).Once() newAddress := models.Address{ StreetAddress1: "123 Any St", @@ -1122,12 +1643,15 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestTOOApprovedShipmentAddressUp suite.Equal(autoRejectionRemark, *shorthaul[0].RejectionReason) suite.Equal(linehaul[0].Status, models.MTOServiceItemStatusApproved) }) + suite.Run("Successfully update shipment and its service items without error", func() { mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "94535", "94535", - ).Return(30, nil).Once() + false, + false, + ).Return(30, nil) move := setupTestData() shipment := factory.BuildMTOShipmentWithMove(&move, suite.DB(), nil, nil) diff --git a/pkg/services/sit_extension/sit_extension_denier.go b/pkg/services/sit_extension/sit_extension_denier.go index 4acb6cbc197..cc8fc66d2a8 100644 --- a/pkg/services/sit_extension/sit_extension_denier.go +++ b/pkg/services/sit_extension/sit_extension_denier.go @@ -33,6 +33,8 @@ func NewSITExtensionDenier(moveRouter services.MoveRouter) services.SITExtension mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) return &sitExtensionDenier{moveRouter, mtoserviceitem.NewMTOServiceItemUpdater(planner, query.NewQueryBuilder(), moveRouter, mtoshipment.NewMTOShipmentFetcher(), address.NewAddressCreator(), portlocation.NewPortLocationFetcher())} } diff --git a/pkg/testdatagen/make_pricing_data.go b/pkg/testdatagen/make_pricing_data.go index 02a855a8c93..61ebfb7231d 100644 --- a/pkg/testdatagen/make_pricing_data.go +++ b/pkg/testdatagen/make_pricing_data.go @@ -20,7 +20,7 @@ func SetupServiceAreaRateArea(db *pop.Connection, assertions Assertions) (models mergeModels(&contractYear, assertions.ReContractYear) - contractYear = MakeReContractYear(db, Assertions{ReContractYear: contractYear}) + contractYear = FetchOrMakeReContractYear(db, Assertions{ReContractYear: contractYear}) serviceArea := models.ReDomesticServiceArea{ Contract: contractYear.Contract, @@ -29,7 +29,7 @@ func SetupServiceAreaRateArea(db *pop.Connection, assertions Assertions) (models mergeModels(&serviceArea, assertions.ReDomesticServiceArea) - serviceArea = MakeReDomesticServiceArea(db, + serviceArea = FetchOrMakeReDomesticServiceArea(db, Assertions{ ReDomesticServiceArea: serviceArea, }) @@ -64,7 +64,7 @@ func SetupServiceAreaRateArea(db *pop.Connection, assertions Assertions) (models mergeModels(&reZip3, assertions.ReZip3) - reZip3 = MakeReZip3(db, Assertions{ + reZip3 = FetchOrMakeReZip3(db, Assertions{ ReZip3: reZip3, }) diff --git a/pkg/testdatagen/make_re_domestic_service_area.go b/pkg/testdatagen/make_re_domestic_service_area.go index 411dee468ca..5f57b4cd3dc 100644 --- a/pkg/testdatagen/make_re_domestic_service_area.go +++ b/pkg/testdatagen/make_re_domestic_service_area.go @@ -54,10 +54,16 @@ func FetchOrMakeReDomesticServiceArea(db *pop.Connection, assertions Assertions) } var reDomesticServiceArea models.ReDomesticServiceArea - err := db.Where("re_domestic_service_areas.contract_id = ? AND re_domestic_service_areas.service_area = ?", contractID, assertions.ReDomesticServiceArea.ServiceArea).First(&reDomesticServiceArea) - - if err != nil && err != sql.ErrNoRows { - log.Panic(err) + if assertions.ReDomesticServiceArea.ServiceArea != "" { + err := db.Where("re_domestic_service_areas.service_area = ?", assertions.ReDomesticServiceArea.ServiceArea).First(&reDomesticServiceArea) + if err != nil && err != sql.ErrNoRows { + log.Panic(err) + } + } else { + err := db.Where("re_domestic_service_areas.contract_id = ? AND re_domestic_service_areas.service_area = ?", contractID, assertions.ReDomesticServiceArea.ServiceArea).First(&reDomesticServiceArea) + if err != nil && err != sql.ErrNoRows { + log.Panic(err) + } } if reDomesticServiceArea.ID == uuid.Nil { diff --git a/pkg/testdatagen/scenario/shared.go b/pkg/testdatagen/scenario/shared.go index 8d04108fd6b..15e8836bc1d 100644 --- a/pkg/testdatagen/scenario/shared.go +++ b/pkg/testdatagen/scenario/shared.go @@ -4211,6 +4211,8 @@ func createHHGWithOriginSITServiceItems( mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) queryBuilder := query.NewQueryBuilder() @@ -4238,7 +4240,7 @@ func createHHGWithOriginSITServiceItems( // called for zip 3 domestic linehaul service item planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "90210", "30813").Return(2361, nil) + "90210", "30813", false, false).Return(2361, nil) shipmentUpdater := mtoshipment.NewMTOShipmentStatusUpdater(queryBuilder, serviceItemCreator, planner) _, updateErr := shipmentUpdater.UpdateMTOShipmentStatus(appCtx, shipment.ID, models.MTOShipmentStatusApproved, nil, nil, etag.GenerateEtag(shipment.UpdatedAt)) @@ -4289,6 +4291,8 @@ func createHHGWithOriginSITServiceItems( mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) serviceItemUpdator := mtoserviceitem.NewMTOServiceItemUpdater(planner, queryBuilder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) @@ -4480,6 +4484,8 @@ func createHHGWithDestinationSITServiceItems(appCtx appcontext.AppContext, prime mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) serviceItemCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -4507,7 +4513,7 @@ func createHHGWithDestinationSITServiceItems(appCtx appcontext.AppContext, prime // called for zip 3 domestic linehaul service item planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "90210", "30813").Return(2361, nil) + "90210", "30813", false, false).Return(2361, nil) shipmentUpdater := mtoshipment.NewMTOShipmentStatusUpdater(queryBuilder, serviceItemCreator, planner) _, updateErr := shipmentUpdater.UpdateMTOShipmentStatus(appCtx, shipment.ID, models.MTOShipmentStatusApproved, nil, nil, etag.GenerateEtag(shipment.UpdatedAt)) @@ -4553,6 +4559,8 @@ func createHHGWithDestinationSITServiceItems(appCtx appcontext.AppContext, prime mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) serviceItemUpdator := mtoserviceitem.NewMTOServiceItemUpdater(planner, queryBuilder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) @@ -4889,7 +4897,7 @@ func createHHGWithPaymentServiceItems( queryBuilder := query.NewQueryBuilder() planner := &routemocks.Planner{} - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything).Return(123, nil).Once() + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, false, false).Return(123, nil).Once() serviceItemCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -4914,33 +4922,33 @@ func createHHGWithPaymentServiceItems( logger.Fatal("Error approving move") } // called using the addresses with origin zip of 90210 and destination zip of 94535 - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything).Return(348, nil).Times(2) + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, false, false).Return(348, nil).Times(2) // called using the addresses with origin zip of 90210 and destination zip of 90211 - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything).Return(3, nil).Times(5) + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, false, false).Return(3, nil).Times(5) // called for zip 3 domestic linehaul service item planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "94535", "94535").Return(348, nil).Times(2) + "94535", "94535", false).Return(348, nil).Times(2) // called for zip 5 domestic linehaul service item - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "94535", "94535").Return(348, nil).Once() + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "94535", "94535", false, false).Return(348, nil).Times(2) // called for domestic shorthaul service item planner.On("Zip5TransitDistance", mock.AnythingOfType("*appcontext.appContext"), "90210", "90211").Return(3, nil).Times(7) // called for domestic shorthaul service item - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "90210", "90211").Return(348, nil).Times(10) + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "90210", "90211", false, false).Return(348, nil).Times(10) // called for domestic origin SIT pickup service item - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "90210", "94535").Return(348, nil).Once() + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "90210", "94535", false, false).Return(348, nil).Once() // called for domestic destination SIT delivery service item - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "94535", "90210").Return(348, nil).Times(2) + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "94535", "90210", false, false).Return(348, nil).Times(2) // called for DLH, DSH, FSC service item estimated price calculations - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything).Return(400, nil).Times(3) + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, false, false).Return(400, nil).Times(3) for _, shipment := range []models.MTOShipment{longhaulShipment, shorthaulShipment, shipmentWithOriginalWeight, shipmentWithOriginalAndReweighWeight, shipmentWithOriginalAndReweighWeightReweihBolded, shipmentWithOriginalReweighAndAdjustedWeight, shipmentWithOriginalAndAdjustedWeight} { shipmentUpdater := mtoshipment.NewMTOShipmentStatusUpdater(queryBuilder, serviceItemCreator, planner) @@ -5035,6 +5043,8 @@ func createHHGWithPaymentServiceItems( mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(400, nil) serviceItemUpdater := mtoserviceitem.NewMTOServiceItemUpdater(planner, queryBuilder, moveRouter, shipmentFetcher, addressCreator, portLocationFetcher) @@ -5522,6 +5532,8 @@ func createHHGMoveWithPaymentRequest(appCtx appcontext.AppContext, userUploader mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, + false, + false, ).Return(910, nil) paymentRequestCreator := paymentrequest.NewPaymentRequestCreator( diff --git a/pkg/testdatagen/testharness/dispatch.go b/pkg/testdatagen/testharness/dispatch.go index 58f7c202899..e9cf4c27d5f 100644 --- a/pkg/testdatagen/testharness/dispatch.go +++ b/pkg/testdatagen/testharness/dispatch.go @@ -20,6 +20,9 @@ var actionDispatcher = map[string]actionFunc{ "DefaultAdminUser": func(appCtx appcontext.AppContext) testHarnessResponse { return factory.BuildDefaultAdminUser(appCtx.DB()) }, + "SuperAdminUser": func(appCtx appcontext.AppContext) testHarnessResponse { + return factory.BuildDefaultSuperAdminUser(appCtx.DB()) + }, "DefaultMove": func(appCtx appcontext.AppContext) testHarnessResponse { return factory.BuildMove(appCtx.DB(), nil, nil) }, @@ -248,6 +251,18 @@ var actionDispatcher = map[string]actionFunc{ "BoatHaulAwayMoveNeedsTOOApproval": func(appCtx appcontext.AppContext) testHarnessResponse { return MakeBoatHaulAwayMoveNeedsTOOApproval(appCtx) }, + "OfficeUserWithCustomer": func(appCtx appcontext.AppContext) testHarnessResponse { + return MakeOfficeUserWithCustomer(appCtx) + }, + "OfficeUserWithContractingOfficer": func(appCtx appcontext.AppContext) testHarnessResponse { + return MakeOfficeUserWithContractingOfficer(appCtx) + }, + "OfficeUserWithPrimeSimulator": func(appCtx appcontext.AppContext) testHarnessResponse { + return MakeOfficeUserWithPrimeSimulator(appCtx) + }, + "OfficeUserWithGSR": func(appCtx appcontext.AppContext) testHarnessResponse { + return MakeOfficeUserWithGSR(appCtx) + }, } func Actions() []string { diff --git a/pkg/testdatagen/testharness/make_move.go b/pkg/testdatagen/testharness/make_move.go index fd337bb029f..ddd96329c38 100644 --- a/pkg/testdatagen/testharness/make_move.go +++ b/pkg/testdatagen/testharness/make_move.go @@ -3823,7 +3823,7 @@ func MakeHHGMoveWithApprovedNTSShipmentsForTOO(appCtx appcontext.AppContext) mod planner := &routemocks.Planner{} // mock any and all planner calls - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything).Return(2361, nil) + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, false, false).Return(2361, nil) queryBuilder := query.NewQueryBuilder() serviceItemCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) @@ -3927,7 +3927,7 @@ func MakeHHGMoveWithApprovedNTSRShipmentsForTOO(appCtx appcontext.AppContext) mo planner := &routemocks.Planner{} // mock any and all planner calls - planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything).Return(2361, nil) + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, false, false).Return(2361, nil) queryBuilder := query.NewQueryBuilder() serviceItemCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) diff --git a/pkg/testdatagen/testharness/make_office_user.go b/pkg/testdatagen/testharness/make_office_user.go index ec7a03c34c2..64001d91b65 100644 --- a/pkg/testdatagen/testharness/make_office_user.go +++ b/pkg/testdatagen/testharness/make_office_user.go @@ -62,3 +62,183 @@ func MakeOfficeUserWithTOOAndTIO(appCtx appcontext.AppContext) models.User { return user } + +func MakeOfficeUserWithCustomer(appCtx appcontext.AppContext) models.User { + customerRole := roles.Role{} + err := appCtx.DB().Where("role_type = $1", roles.RoleTypeCustomer).First(&customerRole) + if err != nil { + log.Panic(fmt.Errorf("failed to find RoleTypeCustomer in the DB: %w", err)) + } + + email := strings.ToLower(fmt.Sprintf("fred_office_%s@example.com", + testdatagen.MakeRandomString(5))) + + user := factory.BuildUser(appCtx.DB(), []factory.Customization{ + { + Model: models.User{ + OktaEmail: email, + Active: true, + Roles: []roles.Role{customerRole}, + }, + }, + }, nil) + approvedStatus := models.OfficeUserStatusAPPROVED + factory.BuildOfficeUserWithRoles(appCtx.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: email, + Active: true, + UserID: &user.ID, + Status: &approvedStatus, + }, + }, + { + Model: user, + LinkOnly: true, + }, + }, []roles.RoleType{roles.RoleTypeCustomer}) + + factory.BuildServiceMember(appCtx.DB(), []factory.Customization{ + { + Model: user, + LinkOnly: true, + }, + }, nil) + + return user +} + +func MakeOfficeUserWithContractingOfficer(appCtx appcontext.AppContext) models.User { + contractingOfficerRole := roles.Role{} + err := appCtx.DB().Where("role_type = $1", roles.RoleTypeContractingOfficer).First(&contractingOfficerRole) + if err != nil { + log.Panic(fmt.Errorf("failed to find RoleTypeContractingOfficer in the DB: %w", err)) + } + + email := strings.ToLower(fmt.Sprintf("fred_office_%s@example.com", + testdatagen.MakeRandomString(5))) + + user := factory.BuildUser(appCtx.DB(), []factory.Customization{ + { + Model: models.User{ + OktaEmail: email, + Active: true, + Roles: []roles.Role{contractingOfficerRole}, + }, + }, + }, nil) + approvedStatus := models.OfficeUserStatusAPPROVED + factory.BuildOfficeUserWithRoles(appCtx.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: email, + Active: true, + UserID: &user.ID, + Status: &approvedStatus, + }, + }, + { + Model: user, + LinkOnly: true, + }, + }, []roles.RoleType{roles.RoleTypeContractingOfficer}) + + factory.BuildServiceMember(appCtx.DB(), []factory.Customization{ + { + Model: user, + LinkOnly: true, + }, + }, nil) + + return user +} + +func MakeOfficeUserWithPrimeSimulator(appCtx appcontext.AppContext) models.User { + primeSimulatorRole := roles.Role{} + err := appCtx.DB().Where("role_type = $1", roles.RoleTypePrimeSimulator).First(&primeSimulatorRole) + if err != nil { + log.Panic(fmt.Errorf("failed to find RoleTypePrimeSimulator in the DB: %w", err)) + } + + email := strings.ToLower(fmt.Sprintf("fred_office_%s@example.com", + testdatagen.MakeRandomString(5))) + + user := factory.BuildUser(appCtx.DB(), []factory.Customization{ + { + Model: models.User{ + OktaEmail: email, + Active: true, + Roles: []roles.Role{primeSimulatorRole}, + }, + }, + }, nil) + approvedStatus := models.OfficeUserStatusAPPROVED + factory.BuildOfficeUserWithRoles(appCtx.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: email, + Active: true, + UserID: &user.ID, + Status: &approvedStatus, + }, + }, + { + Model: user, + LinkOnly: true, + }, + }, []roles.RoleType{roles.RoleTypePrimeSimulator}) + + factory.BuildServiceMember(appCtx.DB(), []factory.Customization{ + { + Model: user, + LinkOnly: true, + }, + }, nil) + + return user +} + +func MakeOfficeUserWithGSR(appCtx appcontext.AppContext) models.User { + gsrRole := roles.Role{} + err := appCtx.DB().Where("role_type = $1", roles.RoleTypeGSR).First(&gsrRole) + if err != nil { + log.Panic(fmt.Errorf("failed to find RoleTypeGSR in the DB: %w", err)) + } + + email := strings.ToLower(fmt.Sprintf("fred_office_%s@example.com", + testdatagen.MakeRandomString(5))) + + user := factory.BuildUser(appCtx.DB(), []factory.Customization{ + { + Model: models.User{ + OktaEmail: email, + Active: true, + Roles: []roles.Role{gsrRole}, + }, + }, + }, nil) + approvedStatus := models.OfficeUserStatusAPPROVED + factory.BuildOfficeUserWithRoles(appCtx.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: email, + Active: true, + UserID: &user.ID, + Status: &approvedStatus, + }, + }, + { + Model: user, + LinkOnly: true, + }, + }, []roles.RoleType{roles.RoleTypeGSR}) + + factory.BuildServiceMember(appCtx.DB(), []factory.Customization{ + { + Model: user, + LinkOnly: true, + }, + }, nil) + + return user +} diff --git a/playwright/tests/admin/officeUsers.spec.js b/playwright/tests/admin/officeUsers.spec.js index cbf1aa7fbab..fd7646a2604 100644 --- a/playwright/tests/admin/officeUsers.spec.js +++ b/playwright/tests/admin/officeUsers.spec.js @@ -83,8 +83,8 @@ test.describe('Office User Create Page', () => { await phone.focus(); await phone.fill('222-555-1234'); - await page.getByText('Services Counselor').click(); - await page.getByText('Supervisor').click(); + await page.getByLabel('Services Counselor').click(); + await page.getByLabel('Supervisor').click(); // The autocomplete form results in multiple matching elements, so // pick the input element @@ -227,4 +227,115 @@ test.describe('Office Users Edit Page', () => { await expect(page.locator(`tr:has(:text("${email}")) >> td.column-firstName`)).toHaveText('NewFirst'); await expect(page.locator(`tr:has(:text("${email}")) >> td.column-lastName`)).toHaveText('NewLast'); }); + + test('prevents safety move priv selection with Customer role', async ({ page, adminPage }) => { + const officeUser = await adminPage.testHarness.buildOfficeUserWithCustomer(); + const email = officeUser.okta_email; + + // create a new admin user to edit + // using an existing one may stop on a concurrent playwright session + const adminUser = await adminPage.testHarness.buildDefaultSuperAdminUser(); + await adminPage.signInAsExistingAdminUser(adminUser.user_id); + + expect(page.url()).toContain('/system/requested-office-users'); + await page.getByRole('menuitem', { name: 'Office Users', exact: true }).click(); + expect(page.url()).toContain('/system/office-users'); + await searchForOfficeUser(page, email); + await page.getByText(email).click(); + await adminPage.waitForPage.adminPage(); + + await page.getByRole('link', { name: 'Edit' }).click(); + await adminPage.waitForPage.adminPage(); + + const safetyMoveCheckbox = page.getByLabel('Safety Moves'); + const customerCheckbox = page.getByLabel('Customer', { exact: true }); + + await expect(customerCheckbox).toBeChecked(); + await safetyMoveCheckbox.click(); + await expect(safetyMoveCheckbox).not.toBeChecked(); + }); + + test('prevents safety move priv selection with Contracting Officer role', async ({ page, adminPage }) => { + const officeUser = await adminPage.testHarness.buildOfficeUserWithContractingOfficer(); + const email = officeUser.okta_email; + + // create a new admin user to edit + // using an existing one may stop on a concurrent playwright session + const adminUser = await adminPage.testHarness.buildDefaultSuperAdminUser(); + await adminPage.signInAsExistingAdminUser(adminUser.user_id); + + expect(page.url()).toContain('/system/requested-office-users'); + await page.getByRole('menuitem', { name: 'Office Users', exact: true }).click(); + expect(page.url()).toContain('/system/office-users'); + await searchForOfficeUser(page, email); + await page.getByText(email).click(); + await adminPage.waitForPage.adminPage(); + + await page.getByRole('link', { name: 'Edit' }).click(); + await adminPage.waitForPage.adminPage(); + + const safetyMoveCheckbox = page.getByLabel('Safety Moves'); + const customerCheckbox = page.getByLabel('Contracting Officer', { exact: true }); + + await expect(customerCheckbox).toBeChecked(); + await safetyMoveCheckbox.click(); + await expect(safetyMoveCheckbox).not.toBeChecked(); + }); + + test('prevents safety move priv selection with Prime Simulator role', async ({ page, adminPage }) => { + const officeUser = await adminPage.testHarness.buildOfficeUserWithPrimeSimulator(); + const email = officeUser.okta_email; + + // create a new admin user to edit + // using an existing one may stop on a concurrent playwright session + const adminUser = await adminPage.testHarness.buildDefaultSuperAdminUser(); + await adminPage.signInAsExistingAdminUser(adminUser.user_id); + + expect(page.url()).toContain('/system/requested-office-users'); + await page.getByRole('menuitem', { name: 'Office Users', exact: true }).click(); + expect(page.url()).toContain('/system/office-users'); + await searchForOfficeUser(page, email); + await page.getByText(email).click(); + await adminPage.waitForPage.adminPage(); + + await page.getByRole('link', { name: 'Edit' }).click(); + await adminPage.waitForPage.adminPage(); + + const safetyMoveCheckbox = page.getByLabel('Safety Moves'); + const customerCheckbox = page.getByLabel('Prime Simulator', { exact: true }); + + await expect(customerCheckbox).toBeChecked(); + await safetyMoveCheckbox.click(); + await expect(safetyMoveCheckbox).not.toBeChecked(); + }); + + test('prevents safety move priv selection with Government Surveillance Representative role', async ({ + page, + adminPage, + }) => { + const officeUser = await adminPage.testHarness.buildOfficeUserWithGSR(); + const email = officeUser.okta_email; + + // create a new admin user to edit + // using an existing one may stop on a concurrent playwright session + const adminUser = await adminPage.testHarness.buildDefaultSuperAdminUser(); + await adminPage.signInAsExistingAdminUser(adminUser.user_id); + + expect(page.url()).toContain('/system/requested-office-users'); + await page.getByRole('menuitem', { name: 'Office Users', exact: true }).click(); + expect(page.url()).toContain('/system/office-users'); + await searchForOfficeUser(page, email); + await page.getByText(email).click(); + await adminPage.waitForPage.adminPage(); + + await page.getByRole('link', { name: 'Edit' }).click(); + await adminPage.waitForPage.adminPage(); + + const safetyMoveCheckbox = page.getByLabel('Safety Moves'); + const customerCheckbox = page.getByLabel('Government Surveillance Representative', { exact: true }); + + await expect(customerCheckbox).toBeChecked(); + await safetyMoveCheckbox.click(); + await expect(safetyMoveCheckbox).not.toBeChecked(); + }); }); diff --git a/playwright/tests/my/mymove/orders.spec.js b/playwright/tests/my/mymove/orders.spec.js index fd3a0cab544..7c418ee006d 100644 --- a/playwright/tests/my/mymove/orders.spec.js +++ b/playwright/tests/my/mymove/orders.spec.js @@ -107,10 +107,8 @@ test.describe('(MultiMove) Orders', () => { await expect(page.getByLabel('Current duty location')).toBeEmpty(); await customerPage.selectDutyLocation('Marine Corps AS Yuma, AZ 85369', 'origin_duty_location'); + await page.getByRole('combobox', { name: 'Counseling Office' }).selectOption({ label: 'PPPO DMO Camp Pendleton' }); await page.getByRole('combobox', { name: 'Pay grade' }).selectOption({ label: 'E-7' }); - await page - .getByRole('combobox', { name: 'Counseling Office' }) - .selectOption({ label: 'PPPO DMO Camp Pendleton - USMC' }); await customerPage.navigateForward(); await customerPage.waitForPage.ordersUpload(); diff --git a/playwright/tests/utils/admin/adminTest.js b/playwright/tests/utils/admin/adminTest.js index e8a06190135..0c83f368917 100644 --- a/playwright/tests/utils/admin/adminTest.js +++ b/playwright/tests/utils/admin/adminTest.js @@ -39,6 +39,16 @@ class AdminPage extends BaseTestPage { await this.waitForPage.adminPage(); } + /** + * Create a new admin user and sign in as them + * @param {string} userId + * @returns {Promise} + */ + async signInAsExistingAdminUser(userId) { + await this.signInAsUserWithId(userId); + await this.waitForPage.adminPage(); + } + /** * @param {import('aria-query').ARIARole} role * @param {Array} labels diff --git a/playwright/tests/utils/testharness.js b/playwright/tests/utils/testharness.js index 385224d3571..a01dac3461e 100644 --- a/playwright/tests/utils/testharness.js +++ b/playwright/tests/utils/testharness.js @@ -17,6 +17,12 @@ export class TestHarness { * @property {string} okta_email */ + /** + * @typedef {Object} Admin + * @property {string} id + * @property {string} user_id + */ + /** * @typedef {Object} Move * @property {string} id @@ -77,6 +83,13 @@ export class TestHarness { return this.buildDefault('DefaultAdminUser'); } + /** + * @returns {Promise} + */ + async buildDefaultSuperAdminUser() { + return this.buildDefault('SuperAdminUser'); + } + /** * build office user with TOO and TIO roles * @returns {Promise} @@ -622,5 +635,37 @@ export class TestHarness { async buildBoatHaulAwayMoveNeedsTOOApproval() { return this.buildDefault('BoatHaulAwayMoveNeedsTOOApproval'); } + + /** + * build office user with Customer role + * @returns {Promise} + */ + async buildOfficeUserWithCustomer() { + return this.buildDefault('OfficeUserWithCustomer'); + } + + /** + * build office user with Contracting Officer role + * @returns {Promise} + */ + async buildOfficeUserWithContractingOfficer() { + return this.buildDefault('OfficeUserWithContractingOfficer'); + } + + /** + * build office user with Prime Simulator role + * @returns {Promise} + */ + async buildOfficeUserWithPrimeSimulator() { + return this.buildDefault('OfficeUserWithPrimeSimulator'); + } + + /** + * build office user with GSR role + * @returns {Promise} + */ + async buildOfficeUserWithGSR() { + return this.buildDefault('OfficeUserWithGSR'); + } } export default TestHarness; diff --git a/scripts/db-truncate b/scripts/db-truncate index a0f15bc3450..341412b4ab0 100755 --- a/scripts/db-truncate +++ b/scripts/db-truncate @@ -9,7 +9,13 @@ 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', 'ghc_diesel_fuel_prices', + 're_zip3s','zip3_distances', 're_contracts', 're_domestic_service_areas', + 're_intl_prices', 're_intl_other_prices', 're_domestic_linehaul_prices', + 're_domestic_service_area_prices', 're_domestic_other_prices')) LOOP EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE'; END LOOP; END \$\$; diff --git a/src/components/Customer/Home/Helper/Helper.stories.jsx b/src/components/Customer/Home/Helper/Helper.stories.jsx index 3e23865065c..383509723f0 100644 --- a/src/components/Customer/Home/Helper/Helper.stories.jsx +++ b/src/components/Customer/Home/Helper/Helper.stories.jsx @@ -54,9 +54,11 @@ export const NeedsShipment = () => ; export const NeedsSubmitMove = () => ; export const SubmittedMove = () => ; export const AmendedOrders = () => ; + +const defaultProps = { orderId: '12345' }; export const ApprovedMove = () => ( - + ); export const PPMCloseoutSubmitted = () => ; diff --git a/src/components/Office/ServiceItemDetails/ServiceItemDetails.jsx b/src/components/Office/ServiceItemDetails/ServiceItemDetails.jsx index c6be9ec77af..689a790ca64 100644 --- a/src/components/Office/ServiceItemDetails/ServiceItemDetails.jsx +++ b/src/components/Office/ServiceItemDetails/ServiceItemDetails.jsx @@ -487,7 +487,12 @@ const ServiceItemDetails = ({ id, code, details, serviceRequestDocs, shipment, s case 'DOP': case 'DDP': case 'DPK': - case 'DUPK': { + case 'DUPK': + case 'ISLH': + case 'IHPK': + case 'IHUPK': + case 'POEFSC': + case 'PODFSC': { detailSection = (
diff --git a/src/components/PrimeUI/CreatePaymentRequestForm/CreatePaymentRequestForm.jsx b/src/components/PrimeUI/CreatePaymentRequestForm/CreatePaymentRequestForm.jsx index 3a3edafb556..d5b3f283829 100644 --- a/src/components/PrimeUI/CreatePaymentRequestForm/CreatePaymentRequestForm.jsx +++ b/src/components/PrimeUI/CreatePaymentRequestForm/CreatePaymentRequestForm.jsx @@ -18,6 +18,7 @@ import ServiceItem from 'components/PrimeUI/ServiceItem/ServiceItem'; import Shipment from 'components/PrimeUI/Shipment/Shipment'; import { DatePickerInput } from 'components/form/fields'; import TextField from 'components/form/fields/TextField/TextField'; +import { SERVICE_ITEM_CODES } from 'constants/serviceItems'; const CreatePaymentRequestForm = ({ initialValues, @@ -128,22 +129,27 @@ const CreatePaymentRequestForm = ({ /> )} - {(mtoServiceItem.reServiceCode === 'DLH' || - mtoServiceItem.reServiceCode === 'DSH' || - mtoServiceItem.reServiceCode === 'FSC' || - mtoServiceItem.reServiceCode === 'DUPK' || - mtoServiceItem.reServiceCode === 'DNPK' || - mtoServiceItem.reServiceCode === 'DOFSIT' || - mtoServiceItem.reServiceCode === 'DOPSIT' || - mtoServiceItem.reServiceCode === 'DOSHUT' || - mtoServiceItem.reServiceCode === 'DDFSIT' || - mtoServiceItem.reServiceCode === 'DDDSIT' || - mtoServiceItem.reServiceCode === 'DOP' || - mtoServiceItem.reServiceCode === 'DDP' || - mtoServiceItem.reServiceCode === 'DPK' || - mtoServiceItem.reServiceCode === 'DDSFSC' || - mtoServiceItem.reServiceCode === 'DOSFSC' || - mtoServiceItem.reServiceCode === 'DDSHUT') && ( + {(mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DLH || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DSH || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.FSC || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DUPK || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DNPK || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DOFSIT || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DOPSIT || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DOSHUT || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DDFSIT || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DDDSIT || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DOP || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DDP || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DPK || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DDSFSC || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DOSFSC || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.DDSHUT || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.IHPK || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.IHUPK || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.ISLH || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.POEFSC || + mtoServiceItem.reServiceCode === SERVICE_ITEM_CODES.PODFSC) && ( { { id: '6', reServiceCode: 'DDFSIT', reServiceName: 'Domestic destination 1st day SIT' }, ], 2: [{ id: '5', reServiceCode: 'FSC' }], + 3: [ + { id: '7', reServiceCode: 'IHPK' }, + { id: '8', reServiceCode: 'IHUPK' }, + { id: '8', reServiceCode: 'ISLH' }, + { id: '8', reServiceCode: 'POEFSC' }, + ], }; it('renders the form', async () => { diff --git a/src/constants/MoveHistory/Database/BooleanFields.js b/src/constants/MoveHistory/Database/BooleanFields.js index 91654232fd0..32a4cc5f7c7 100644 --- a/src/constants/MoveHistory/Database/BooleanFields.js +++ b/src/constants/MoveHistory/Database/BooleanFields.js @@ -16,4 +16,8 @@ export default { phone_is_preferred: 'phone_is_preferred', uses_external_vendor: 'uses_external_vendor', diversion: 'diversion', + has_secondary_pickup_address: 'has_secondary_pickup_address', + has_secondary_delivery_address: 'has_secondary_delivery_address', + has_tertiary_pickup_address: 'has_tertiary_pickup_address', + has_tertiary_delivery_address: 'has_tertiary_delivery_address', }; diff --git a/src/constants/MoveHistory/Database/FieldMappings.js b/src/constants/MoveHistory/Database/FieldMappings.js index 737df5b08ae..5b9ece8b735 100644 --- a/src/constants/MoveHistory/Database/FieldMappings.js +++ b/src/constants/MoveHistory/Database/FieldMappings.js @@ -44,10 +44,14 @@ export default { shipment_type: 'Shipment type', pickup_address: 'Pickup Address', secondary_pickup_address: 'Second Pickup Address', + has_secondary_pickup_address: 'Secondary Pickup Address', tertiary_pickup_address: 'Third Pickup Address', + has_tertiary_pickup_address: 'Third Pickup Address', destination_address: 'Delivery Address', secondary_destination_address: 'Second Delivery Address', + has_secondary_delivery_address: 'Secondary Delivery Address', tertiary_destination_address: 'Third Delivery Address', + has_tertiary_destination_address: 'Third Delivery Address', receiving_agent: 'Receiving agent', releasing_agent: 'Releasing agent', tio_remarks: 'Max billable weight remark', diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipment/approveShipmentUpdateAllowances.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipment/approveShipmentUpdateAllowances.jsx index d0809da3847..41acd4af475 100644 --- a/src/constants/MoveHistory/EventTemplates/ApproveShipment/approveShipmentUpdateAllowances.jsx +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipment/approveShipmentUpdateAllowances.jsx @@ -4,11 +4,12 @@ import a from 'constants/MoveHistory/Database/Actions'; import t from 'constants/MoveHistory/Database/Tables'; import o from 'constants/MoveHistory/UIDisplay/Operations'; import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; +import { formatMoveHistoryMaxBillableWeight } from 'utils/formatters'; export default { action: a.UPDATE, eventName: o.approveShipment, tableName: t.entitlements, - getEventNameDisplay: () => 'Updated allowances', - getDetails: (historyRecord) => , + getEventNameDisplay: () => 'Updated shipment', + getDetails: (historyRecord) => , }; diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipment/approveShipmentUpdateAllowances.test.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipment/approveShipmentUpdateAllowances.test.jsx index d15bc3d6005..1be3f2ffa5d 100644 --- a/src/constants/MoveHistory/EventTemplates/ApproveShipment/approveShipmentUpdateAllowances.test.jsx +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipment/approveShipmentUpdateAllowances.test.jsx @@ -18,7 +18,7 @@ describe('when given an Approved shipment, Updated allowances history record', ( it('displays the proper value in the details field', () => { const template = getTemplate(historyRecord); render(template.getDetails(historyRecord)); - expect(screen.getByText('Authorized weight')).toBeInTheDocument(); + expect(screen.getByText('Max billable weight')).toBeInTheDocument(); expect(screen.getByText(': 13,230 lbs')).toBeInTheDocument(); }); }); diff --git a/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeight.jsx b/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeight.jsx index a1c924f74c7..8e4e7047c95 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeight.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeight.jsx @@ -4,22 +4,12 @@ import o from 'constants/MoveHistory/UIDisplay/Operations'; import a from 'constants/MoveHistory/Database/Actions'; import t from 'constants/MoveHistory/Database/Tables'; import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; - -// To-do: Remove set authorized_weight as max_billable_weight once max_billable_weight is its own value -const formatChangedValues = (historyRecord) => { - const { changedValues } = historyRecord; - const newChangedValues = { ...changedValues }; - if (changedValues.authorized_weight) { - newChangedValues.max_billable_weight = changedValues.authorized_weight; - delete newChangedValues.authorized_weight; - } - return { ...historyRecord, changedValues: newChangedValues }; -}; +import { formatMoveHistoryMaxBillableWeight } from 'utils/formatters'; export default { action: a.UPDATE, eventName: o.updateBillableWeight, tableName: t.entitlements, getEventNameDisplay: () => 'Updated move', - getDetails: (historyRecord) => , + getDetails: (historyRecord) => , }; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeightAsTIO.jsx b/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeightAsTIO.jsx index 1a947c5e50b..d187a2d602c 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeightAsTIO.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeightAsTIO.jsx @@ -4,22 +4,12 @@ import o from 'constants/MoveHistory/UIDisplay/Operations'; import a from 'constants/MoveHistory/Database/Actions'; import t from 'constants/MoveHistory/Database/Tables'; import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; - -// To-do: Remove setting authorized_weight as max_billable_weight once max_billable_weight is its own value -const formatChangedValues = (historyRecord) => { - const { changedValues } = historyRecord; - const newChangedValues = { ...changedValues }; - if (changedValues.authorized_weight) { - newChangedValues.max_billable_weight = changedValues.authorized_weight; - delete newChangedValues.authorized_weight; - } - return { ...historyRecord, changedValues: newChangedValues }; -}; +import { formatMoveHistoryMaxBillableWeight } from 'utils/formatters'; export default { action: a.UPDATE, eventName: o.updateBillableWeightAsTIO, tableName: t.entitlements, getEventNameDisplay: () => 'Updated move', - getDetails: (historyRecord) => , + getDetails: (historyRecord) => , }; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeightRemarksAsTIO.jsx b/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeightRemarksAsTIO.jsx index ade3ec1db94..2531ef3bd59 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeightRemarksAsTIO.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateBillableWeight/updateBillableWeightRemarksAsTIO.jsx @@ -4,22 +4,12 @@ import o from 'constants/MoveHistory/UIDisplay/Operations'; import a from 'constants/MoveHistory/Database/Actions'; import t from 'constants/MoveHistory/Database/Tables'; import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; - -// To-do: Remove setting authorized_weight as max_billable_weight once max_billable_weight is its own value -const formatChangedValues = (historyRecord) => { - const { changedValues } = historyRecord; - const newChangedValues = { ...changedValues }; - if (changedValues.authorized_weight) { - newChangedValues.max_billable_weight = changedValues.authorized_weight; - delete newChangedValues.authorized_weight; - } - return { ...historyRecord, changedValues: newChangedValues }; -}; +import { formatMoveHistoryMaxBillableWeight } from 'utils/formatters'; export default { action: a.UPDATE, eventName: o.updateBillableWeightAsTIO, tableName: t.moves, getEventNameDisplay: () => 'Updated move', - getDetails: (historyRecord) => , + getDetails: (historyRecord) => , }; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateAllowances/updateAllowanceUpdateMTOShipment.jsx b/src/constants/MoveHistory/EventTemplates/UpdateMTOShipment/updateMTOShipmentUpdateAllowance.jsx similarity index 71% rename from src/constants/MoveHistory/EventTemplates/UpdateAllowances/updateAllowanceUpdateMTOShipment.jsx rename to src/constants/MoveHistory/EventTemplates/UpdateMTOShipment/updateMTOShipmentUpdateAllowance.jsx index 5c82947cc9e..09a9e4322c3 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateAllowances/updateAllowanceUpdateMTOShipment.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateMTOShipment/updateMTOShipmentUpdateAllowance.jsx @@ -4,11 +4,12 @@ import o from 'constants/MoveHistory/UIDisplay/Operations'; import a from 'constants/MoveHistory/Database/Actions'; import t from 'constants/MoveHistory/Database/Tables'; import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; +import { formatMoveHistoryMaxBillableWeight } from 'utils/formatters'; export default { action: a.UPDATE, eventName: o.updateMTOShipment, tableName: t.entitlements, - getEventNameDisplay: () => 'Updated allowances', - getDetails: (historyRecord) => , + getEventNameDisplay: () => 'Updated shipment', + getDetails: (historyRecord) => , }; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateAllowances/updateAllowanceUpdateMTOShipment.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdateMTOShipment/updateMTOShipmentUpdateAllowance.test.jsx similarity index 66% rename from src/constants/MoveHistory/EventTemplates/UpdateAllowances/updateAllowanceUpdateMTOShipment.test.jsx rename to src/constants/MoveHistory/EventTemplates/UpdateMTOShipment/updateMTOShipmentUpdateAllowance.test.jsx index 4c7d3bd6fe2..4ba5a5d7a71 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateAllowances/updateAllowanceUpdateMTOShipment.test.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateMTOShipment/updateMTOShipmentUpdateAllowance.test.jsx @@ -1,9 +1,9 @@ import { render, screen } from '@testing-library/react'; import getTemplate from 'constants/MoveHistory/TemplateManager'; -import e from 'constants/MoveHistory/EventTemplates/UpdateAllowances/updateAllowanceUpdateMTOShipment'; +import e from 'constants/MoveHistory/EventTemplates/UpdateMTOShipment/updateMTOShipmentUpdateAllowance'; -describe('when given a update allowance, update MTO shipment history record', () => { +describe('when given an update to the allowance due to MTOShipment update, update MTO shipment history record', () => { const historyRecord = { action: 'UPDATE', eventName: 'updateMTOShipment', @@ -18,7 +18,7 @@ describe('when given a update allowance, update MTO shipment history record', () changedValues: { authorized_weight: 1650 }, }; - it('correctly matches the update allowance, update MTO shipment event', () => { + it('correctly matches the update to the allowance, update MTO shipment event', () => { const template = getTemplate(historyRecord); expect(template).toMatchObject(e); }); @@ -26,7 +26,7 @@ describe('when given a update allowance, update MTO shipment history record', () it('displays the proper update MTO shipment record', () => { const template = getTemplate(historyRecord); render(template.getDetails(historyRecord)); - expect(screen.getByText('Authorized weight')).toBeInTheDocument(); + expect(screen.getByText('Max billable weight')).toBeInTheDocument(); expect(screen.getByText(': 1,650 lbs')).toBeInTheDocument(); }); }); diff --git a/src/constants/MoveHistory/EventTemplates/index.js b/src/constants/MoveHistory/EventTemplates/index.js index 42ac00d51c7..afcbd6b7060 100644 --- a/src/constants/MoveHistory/EventTemplates/index.js +++ b/src/constants/MoveHistory/EventTemplates/index.js @@ -92,7 +92,7 @@ export { default as updateServiceItemStatusUpdateMove } from './UpdateServiceIte export { default as approveShipmentUpdateMove } from './ApproveShipment/approveShipmentUpdateMove'; export { default as createSITExtension } from './CreateSITExtension/createSITExtension'; export { default as updateAllowanceUpdateOrder } from './UpdateAllowances/updateAllowanceUpdateOrder'; -export { default as updateAllowanceUpdateMTOShipment } from './UpdateAllowances/updateAllowanceUpdateMTOShipment'; +export { default as updateMTOShipmentUpdateAllowance } from './UpdateMTOShipment/updateMTOShipmentUpdateAllowance'; export { default as approveShipmentUpdateAllowances } from './ApproveShipment/approveShipmentUpdateAllowances'; export { default as updateOrderUpdateAllowances } from './UpdateOrders/updateOrderUpdateAllowances'; export { default as patchMTOShipment } from './UpdateMTOShipment/patchMTOShipment'; diff --git a/src/constants/serviceItems.js b/src/constants/serviceItems.js index 7a1a0f76502..01dad1c3eb0 100644 --- a/src/constants/serviceItems.js +++ b/src/constants/serviceItems.js @@ -147,6 +147,9 @@ const SERVICE_ITEM_CODES = { DDSFSC: 'DDSFSC', POEFSC: 'POEFSC', PODFSC: 'PODFSC', + IHPK: 'IHPK', + IHUPK: 'IHUPK', + ISLH: 'ISLH', }; const SERVICE_ITEMS_ALLOWED_WEIGHT_BILLED_PARAM = [ diff --git a/src/pages/MyMove/Home/HomeHelpers.jsx b/src/pages/MyMove/Home/HomeHelpers.jsx index 7a73196a168..206b16c8381 100644 --- a/src/pages/MyMove/Home/HomeHelpers.jsx +++ b/src/pages/MyMove/Home/HomeHelpers.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Link } from 'react-router-dom'; +import { generatePath, Link } from 'react-router-dom'; import { Link as ExternalLink } from '@trussworks/react-uswds'; import styles from './Home.module.scss'; @@ -96,35 +96,41 @@ export const HelperSubmittedMove = () => ( ); -export const HelperApprovedMove = () => ( - -
-

Talk to your counselor or to your movers to make any changes to your move.

-
-
-

- For PPM shipments -

-

- When you are done moving your things, select Upload PPM Documents to document your PPM, - calculate your final incentive, and create a payment request packet. You will upload weight tickets, receipts, - and other documentation that a counselor will review. -

-
-
-

- If you receive new orders while your move is underway -

-
    -
  • Talk to your counselor
  • -
  • Talk to your movers
  • -
  • - Upload a copy of your new orders -
  • -
-
-
-); +export const HelperApprovedMove = ({ orderId }) => { + const path = generatePath(customerRoutes.ORDERS_AMEND_PATH, { + orderId, + }); + + return ( + +
+

Talk to your counselor or to your movers to make any changes to your move.

+
+
+

+ For PPM shipments +

+

+ When you are done moving your things, select Upload PPM Documents to document your PPM, + calculate your final incentive, and create a payment request packet. You will upload weight tickets, receipts, + and other documentation that a counselor will review. +

+
+
+

+ If you receive new orders while your move is underway +

+
    +
  • Talk to your counselor
  • +
  • Talk to your movers
  • +
  • + Upload a copy of your new orders +
  • +
+
+
+ ); +}; export const HelperAmendedOrders = () => ( diff --git a/src/pages/MyMove/Home/MoveHome.jsx b/src/pages/MyMove/Home/MoveHome.jsx index 9af82b0bbcf..69afb201a28 100644 --- a/src/pages/MyMove/Home/MoveHome.jsx +++ b/src/pages/MyMove/Home/MoveHome.jsx @@ -417,7 +417,7 @@ const MoveHome = ({ serviceMemberMoves, isProfileComplete, serviceMember, signed if (!hasSubmittedMove()) return ; if (hasSubmittedPPMCloseout()) return ; if (hasUnapprovedAmendedOrders()) return ; - if (isMoveApproved()) return ; + if (isMoveApproved()) return ; return ; }; diff --git a/src/pages/MyMove/Home/index.jsx b/src/pages/MyMove/Home/index.jsx index 830c1b17cae..33f7d5bfb34 100644 --- a/src/pages/MyMove/Home/index.jsx +++ b/src/pages/MyMove/Home/index.jsx @@ -241,12 +241,14 @@ export class Home extends Component { }; renderHelper = () => { + const { orders } = this.props; if (!this.hasOrders) return ; if (!this.hasAnyShipments) return ; if (!this.hasSubmittedMove) return ; if (this.hasSubmittedPPMCloseout) return ; if (this.hasUnapprovedAmendedOrders) return ; - if (this.isMoveApproved) return ; + if (this.isMoveApproved) return ; + return ; }; diff --git a/src/pages/MyMove/Home/index.stories.jsx b/src/pages/MyMove/Home/index.stories.jsx index e70e0e04a6b..6e67f7d84c7 100644 --- a/src/pages/MyMove/Home/index.stories.jsx +++ b/src/pages/MyMove/Home/index.stories.jsx @@ -153,6 +153,15 @@ const propsForApprovedPPMShipment = { status: MOVE_STATUSES.APPROVED, submitted_at: '2020-12-24', }, + orders: { + id: '12345', + origin_duty_location: { + name: 'NAS Norfolk', + }, + new_duty_location: { + name: 'NAS Jacksonville', + }, + }, }; const propsForCloseoutCompletePPMShipment = { diff --git a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.test.jsx b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.test.jsx index 84587db4032..568902243d6 100644 --- a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.test.jsx +++ b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.test.jsx @@ -643,7 +643,7 @@ describe('successful submission of form', () => { await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith(moveDetailsURL); }); - }); + }, 50000); it('successful submission of form when updating a shipments actual weight but not estimated weight', async () => { usePrimeSimulatorGetMove.mockReturnValue(readyReturnValueWithOneHHG); diff --git a/src/scenes/SystemAdmin/shared/RolesPrivilegesCheckboxes.jsx b/src/scenes/SystemAdmin/shared/RolesPrivilegesCheckboxes.jsx index 8298127ad7a..1cc554ed70d 100644 --- a/src/scenes/SystemAdmin/shared/RolesPrivilegesCheckboxes.jsx +++ b/src/scenes/SystemAdmin/shared/RolesPrivilegesCheckboxes.jsx @@ -36,11 +36,8 @@ const RolesPrivilegesCheckboxInput = (props) => { }; const parseRolesCheckboxInput = (input) => { - if ( - privilegesSelected.includes(elevatedPrivilegeTypes.SUPERVISOR) || - privilegesSelected.includes(elevatedPrivilegeTypes.SAFETY) - ) { - var index; + let index; + if (privilegesSelected.includes(elevatedPrivilegeTypes.SUPERVISOR)) { if (input.includes(roleTypes.CUSTOMER)) { index = input.indexOf(roleTypes.CUSTOMER); if (index !== -1) { @@ -55,14 +52,34 @@ const RolesPrivilegesCheckboxInput = (props) => { } } - if (!isHeadquartersRoleFF && input.includes(roleTypes.HQ)) { - if (input.includes(roleTypes.HQ)) { - index = input.indexOf(roleTypes.HQ); + if (privilegesSelected.includes(elevatedPrivilegeTypes.SAFETY)) { + if (input.includes(roleTypes.CUSTOMER)) { + index = input.indexOf(roleTypes.CUSTOMER); + if (index !== -1) { + input.splice(index, 1); + } + } + if (input.includes(roleTypes.CONTRACTING_OFFICER)) { + index = input.indexOf(roleTypes.CONTRACTING_OFFICER); if (index !== -1) { input.splice(index, 1); } } - } else if (isHeadquartersRoleFF && privilegesSelected.includes(elevatedPrivilegeTypes.SAFETY)) { + if (input.includes(roleTypes.PRIME_SIMULATOR)) { + index = input.indexOf(roleTypes.PRIME_SIMULATOR); + if (index !== -1) { + input.splice(index, 1); + } + } + if (input.includes(roleTypes.GSR)) { + index = input.indexOf(roleTypes.GSR); + if (index !== -1) { + input.splice(index, 1); + } + } + } + + if (!isHeadquartersRoleFF && input.includes(roleTypes.HQ)) { if (input.includes(roleTypes.HQ)) { index = input.indexOf(roleTypes.HQ); if (index !== -1) { @@ -93,24 +110,21 @@ const RolesPrivilegesCheckboxInput = (props) => { }; const parsePrivilegesCheckboxInput = (input) => { + var index; if (rolesSelected.includes(roleTypes.CUSTOMER) || rolesSelected.includes(roleTypes.CONTRACTING_OFFICER)) { - var index; if (input.includes(elevatedPrivilegeTypes.SUPERVISOR)) { index = input.indexOf(elevatedPrivilegeTypes.SUPERVISOR); if (index !== -1) { input.splice(index, 1); } } - - if (input.includes(elevatedPrivilegeTypes.SAFETY)) { - index = input.indexOf(elevatedPrivilegeTypes.SAFETY); - if (index !== -1) { - input.splice(index, 1); - } - } } - - if (isHeadquartersRoleFF && rolesSelected.includes(roleTypes.HQ)) { + if ( + rolesSelected.includes(roleTypes.CUSTOMER) || + rolesSelected.includes(roleTypes.CONTRACTING_OFFICER) || + rolesSelected.includes(roleTypes.PRIME_SIMULATOR) || + rolesSelected.includes(roleTypes.GSR) + ) { if (input.includes(elevatedPrivilegeTypes.SAFETY)) { index = input.indexOf(elevatedPrivilegeTypes.SAFETY); if (index !== -1) { @@ -156,6 +170,10 @@ const RolesPrivilegesCheckboxInput = (props) => { Privileges cannot be selected with Customer or Contracting Officer roles. + + The Safety Moves privilege can only be selected for the following roles: Task Ordering Officer, Task Invoicing + Officer, Services Counselor, Quality Assurance Evaluator, Customer Service Representative, and Headquarters. + ); }; diff --git a/src/setupProxy.js b/src/setupProxy.js index 1f34bb64ffc..7d92e2f9aea 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -11,9 +11,9 @@ module.exports = (app) => { app.use('/testharness', createProxyMiddleware({ target: 'http://milmovelocal:8080/testharness' })); app.use('/storage', createProxyMiddleware({ target: 'http://milmovelocal:8080/storage' })); app.use('/devlocal-auth', createProxyMiddleware({ target: 'http://milmovelocal:8080/devlocal-auth' })); - app.use('/auth/**', createProxyMiddleware({ target: 'http://milmovelocal:8080/auth/**' })); + app.use('/auth', createProxyMiddleware({ target: 'http://milmovelocal:8080/auth' })); app.use('/logout', createProxyMiddleware({ target: 'http://milmovelocal:8080/logout' })); app.use('/downloads', createProxyMiddleware({ target: 'http://milmovelocal:8080/downloads' })); - app.use('/debug/**', createProxyMiddleware({ target: 'http://milmovelocal:8080/debug/**' })); - app.use('/client/**', createProxyMiddleware({ target: 'http://milmovelocal:8080/client/**' })); + app.use('/debug', createProxyMiddleware({ target: 'http://milmovelocal:8080/debug' })); + app.use('/client', createProxyMiddleware({ target: 'http://milmovelocal:8080/client' })); }; diff --git a/src/utils/formatters.js b/src/utils/formatters.js index b0eee711141..1470ad756ad 100644 --- a/src/utils/formatters.js +++ b/src/utils/formatters.js @@ -292,6 +292,16 @@ export const formatMoveHistoryAgent = (agent) => { return formattedAgentValues; }; +export const formatMoveHistoryMaxBillableWeight = (historyRecord) => { + const { changedValues } = historyRecord; + const newChangedValues = { ...changedValues }; + if (changedValues.authorized_weight) { + newChangedValues.max_billable_weight = changedValues.authorized_weight; + delete newChangedValues.authorized_weight; + } + return { ...historyRecord, changedValues: newChangedValues }; +}; + export const dropdownInputOptions = (options) => { return Object.entries(options).map(([key, value]) => ({ key, value })); }; 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-def/definitions/prime/v3/MTOShipmentWithoutServiceItems.yaml b/swagger-def/definitions/prime/v3/MTOShipmentWithoutServiceItems.yaml index 634d48690d3..2ac4cd3806b 100644 --- a/swagger-def/definitions/prime/v3/MTOShipmentWithoutServiceItems.yaml +++ b/swagger-def/definitions/prime/v3/MTOShipmentWithoutServiceItems.yaml @@ -249,3 +249,7 @@ properties: $ref: '../../Port.yaml' portOfEmbarkation: $ref: '../../Port.yaml' + originRateArea: + $ref: 'RateArea.yaml' + destinationRateArea: + $ref: 'RateArea.yaml' diff --git a/swagger-def/definitions/prime/v3/PPMShipment.yaml b/swagger-def/definitions/prime/v3/PPMShipment.yaml index 8923694af0b..f8ea0a396cb 100644 --- a/swagger-def/definitions/prime/v3/PPMShipment.yaml +++ b/swagger-def/definitions/prime/v3/PPMShipment.yaml @@ -197,6 +197,10 @@ properties: format: cents x-nullable: true x-omitempty: false + originRateArea: + $ref: 'RateArea.yaml' + destinationRateArea: + $ref: 'RateArea.yaml' isActualExpenseReimbursement: description: Used for PPM shipments only. Denotes if this shipment uses the Actual Expense Reimbursement method. type: boolean diff --git a/swagger-def/definitions/prime/v3/RateArea.yaml b/swagger-def/definitions/prime/v3/RateArea.yaml new file mode 100644 index 00000000000..749e16fd675 --- /dev/null +++ b/swagger-def/definitions/prime/v3/RateArea.yaml @@ -0,0 +1,19 @@ +type: object +description: Rate area info for OCONUS postal code +properties: + id: + example: 1f2270c7-7166-40ae-981e-b200ebdf3054 + format: uuid + type: string + rateAreaId: + description: Rate area code + example: US8101000 + type: string + rateAreaName: + description: Rate area name + example: Alaska (Zone) I + type: string +required: + - id + - rateAreaId + - rateAreaName diff --git a/swagger-def/prime.yaml b/swagger-def/prime.yaml index 5fcb6cdd5e3..61f19f25a93 100644 --- a/swagger-def/prime.yaml +++ b/swagger-def/prime.yaml @@ -952,7 +952,17 @@ paths: **NOTE**: In order to create a payment request for most service items, the shipment *must* be updated with the `PrimeActualWeight` value via [updateMTOShipment](#operation/updateMTOShipment). - **FSC - Fuel Surcharge** service items require `ActualPickupDate` to be updated on the shipment. + If `WeightBilled` is not provided then the full shipment weight (`PrimeActualWeight`) will be considered in the calculation. + + **NOTE**: Diversions have a unique calcuation for payment requests without a `WeightBilled` parameter. + + If you created a payment request for a diversion and `WeightBilled` is not provided, then the following will be used in the calculation: + - The lowest shipment weight (`PrimeActualWeight`) found in the diverted shipment chain. + - The lowest reweigh weight found in the diverted shipment chain. + + The diverted shipment chain is created by referencing the `diversion` boolean, `divertedFromShipmentId` UUID, and matching destination to pickup addresses. + If the chain cannot be established it will fall back to the `PrimeActualWeight` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations. + The lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found. A service item can be on several payment requests in the case of partial payment requests and payments. @@ -979,19 +989,102 @@ paths: } ``` - SIT Service Items & Accepted Payment Request Parameters: + Domestic Basic Service Items & Accepted Payment Request Parameters: --- - If `WeightBilled` is not provided then the full shipment weight (`PrimeActualWeight`) will be considered in the calculation. - **NOTE**: Diversions have a unique calcuation for payment requests without a `WeightBilled` parameter. + **DLH - Domestic Linehaul** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` - If you created a payment request for a diversion and `WeightBilled` is not provided, then the following will be used in the calculation: - - The lowest shipment weight (`PrimeActualWeight`) found in the diverted shipment chain. - - The lowest reweigh weight found in the diverted shipment chain. + **DSH - Domestic Shorthaul** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` - The diverted shipment chain is created by referencing the `diversion` boolean, `divertedFromShipmentId` UUID, and matching destination to pickup addresses. - If the chain cannot be established it will fall back to the `PrimeActualWeight` of the current shipment. This is utilized because diverted shipments are all one single shipment, but going to different locations. - The lowest weight found is the true shipment weight, and thus we search the chain of shipments for the lowest weight found. + **FSC - Fuel Surcharge** + **NOTE**: FSC requires `ActualPickupDate` to be updated on the shipment. + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **DUPK - Domestic Unpacking** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **DPK - Domestic Packing** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **DNPK - Domestic NTS Packing** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **DPK - Domestic Packing** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **DOP - Domestic Origin Price** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **DDP - Domestic Destination Price** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + Domestic SIT Service Items & Accepted Payment Request Parameters: + --- **DOFSIT - Domestic origin 1st day SIT** ```json @@ -1092,6 +1185,64 @@ paths: ] ``` --- + + International Basic Service Items & Accepted Payment Request Parameters: + --- + Just like domestic shipments & service items, if `WeightBilled` is not provided then the full shipment weight (`PrimeActualWeight`) will be considered in the calculation. + **NOTE**: `POEFSC` & `PODFSC` service items must have a port associated on the service item in order to successfully add it to a payment request. To update the port of a service item, you must use the (#operation/updateMTOServiceItem) endpoint. + + **ISLH - International Shipping & Linehaul** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **IHPK - International HHG Pack** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **IHUPK - International HHG Unpack** + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **POEFSC - International Port of Embarkation Fuel Surcharge** + **NOTE**: POEFSC requires `ActualPickupDate` to be updated on the shipment & `POELocation` on the service item. + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + **PODFSC - International Port of Debarkation Fuel Surcharge** + **NOTE**: PODFSC requires `ActualPickupDate` to be updated on the shipment & `PODLocation` on the service item. + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + --- operationId: createPaymentRequest tags: - paymentRequest diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index ab568b434c8..2456656b1c6 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -8301,6 +8301,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 84097cd100a..e6026ff442e 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 83ef52b06c7..bfc37501b8b 100644 --- a/swagger/prime.yaml +++ b/swagger/prime.yaml @@ -1214,8 +1214,34 @@ paths: [updateMTOShipment](#operation/updateMTOShipment). - **FSC - Fuel Surcharge** service items require `ActualPickupDate` to be - updated on the shipment. + If `WeightBilled` is not provided then the full shipment weight + (`PrimeActualWeight`) will be considered in the calculation. + + + **NOTE**: Diversions have a unique calcuation for payment requests + without a `WeightBilled` parameter. + + + If you created a payment request for a diversion and `WeightBilled` is + not provided, then the following will be used in the calculation: + + - The lowest shipment weight (`PrimeActualWeight`) found in the diverted + shipment chain. + + - The lowest reweigh weight found in the diverted shipment chain. + + + The diverted shipment chain is created by referencing the `diversion` + boolean, `divertedFromShipmentId` UUID, and matching destination to + pickup addresses. + + If the chain cannot be established it will fall back to the + `PrimeActualWeight` of the current shipment. This is utilized because + diverted shipments are all one single shipment, but going to different + locations. + + The lowest weight found is the true shipment weight, and thus we search + the chain of shipments for the lowest weight found. A service item can be on several payment requests in the case of partial @@ -1250,38 +1276,124 @@ paths: ``` - SIT Service Items & Accepted Payment Request Parameters: + Domestic Basic Service Items & Accepted Payment Request Parameters: --- - If `WeightBilled` is not provided then the full shipment weight - (`PrimeActualWeight`) will be considered in the calculation. + **DLH - Domestic Linehaul** - **NOTE**: Diversions have a unique calcuation for payment requests - without a `WeightBilled` parameter. + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` - If you created a payment request for a diversion and `WeightBilled` is - not provided, then the following will be used in the calculation: + **DSH - Domestic Shorthaul** - - The lowest shipment weight (`PrimeActualWeight`) found in the diverted - shipment chain. + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` - - The lowest reweigh weight found in the diverted shipment chain. + **FSC - Fuel Surcharge** - The diverted shipment chain is created by referencing the `diversion` - boolean, `divertedFromShipmentId` UUID, and matching destination to - pickup addresses. + **NOTE**: FSC requires `ActualPickupDate` to be updated on the shipment. - If the chain cannot be established it will fall back to the - `PrimeActualWeight` of the current shipment. This is utilized because - diverted shipments are all one single shipment, but going to different - locations. + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` - The lowest weight found is the true shipment weight, and thus we search - the chain of shipments for the lowest weight found. + + **DUPK - Domestic Unpacking** + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + **DPK - Domestic Packing** + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + **DNPK - Domestic NTS Packing** + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + **DPK - Domestic Packing** + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + **DOP - Domestic Origin Price** + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + **DDP - Domestic Destination Price** + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + Domestic SIT Service Items & Accepted Payment Request Parameters: + + --- **DOFSIT - Domestic origin 1st day SIT** @@ -1410,6 +1522,85 @@ paths: ``` --- + + + International Basic Service Items & Accepted Payment Request Parameters: + + --- + + Just like domestic shipments & service items, if `WeightBilled` is not + provided then the full shipment weight (`PrimeActualWeight`) will be + considered in the calculation. + + **NOTE**: `POEFSC` & `PODFSC` service items must have a port associated + on the service item in order to successfully add it to a payment + request. To update the port of a service item, you must use the + (#operation/updateMTOServiceItem) endpoint. + + + **ISLH - International Shipping & Linehaul** + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + **IHPK - International HHG Pack** + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + **IHUPK - International HHG Unpack** + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + **POEFSC - International Port of Embarkation Fuel Surcharge** + **NOTE**: POEFSC requires `ActualPickupDate` to be updated on the shipment & `POELocation` on the service item. + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + + **PODFSC - International Port of Debarkation Fuel Surcharge** + + **NOTE**: PODFSC requires `ActualPickupDate` to be updated on the + shipment & `PODLocation` on the service item. + + ```json + "params": [ + { + "key": "WeightBilled", + "value": "integer" + } + ] + ``` + + --- operationId: createPaymentRequest tags: - paymentRequest @@ -2955,6 +3146,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 00c4e8d169b..815a6e87ad5 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 cb7c5a5d10a..fcb9b1a0d40 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 @@ -2667,6 +2671,26 @@ definitions: enum: - ORIGIN - DESTINATION + RateArea: + type: object + description: Rate area info for OCONUS postal code + properties: + id: + example: 1f2270c7-7166-40ae-981e-b200ebdf3054 + format: uuid + type: string + rateAreaId: + description: Rate area code + example: US8101000 + type: string + rateAreaName: + description: Rate area name + example: Alaska (Zone) I + type: string + required: + - id + - rateAreaId + - rateAreaName PPMShipment: description: >- A personally procured move is a type of shipment that a service member @@ -2897,6 +2921,10 @@ definitions: format: cents x-nullable: true x-omitempty: false + originRateArea: + $ref: '#/definitions/RateArea' + destinationRateArea: + $ref: '#/definitions/RateArea' isActualExpenseReimbursement: description: >- Used for PPM shipments only. Denotes if this shipment uses the Actual @@ -3501,6 +3529,10 @@ definitions: $ref: '#/definitions/Port' portOfEmbarkation: $ref: '#/definitions/Port' + originRateArea: + $ref: '#/definitions/RateArea' + destinationRateArea: + $ref: '#/definitions/RateArea' MTOShipmentsWithoutServiceObjects: description: A list of shipments without their associated service items. items: