diff --git a/usaspending_api/common/query_with_filters.py b/usaspending_api/common/query_with_filters.py index f8fc6653d8..4812a40046 100644 --- a/usaspending_api/common/query_with_filters.py +++ b/usaspending_api/common/query_with_filters.py @@ -473,9 +473,8 @@ class _RecipientLocations(_Filter): @classmethod def generate_elasticsearch_query(cls, filter_values: List[dict], query_type: QueryType, **options) -> ES_Q: recipient_locations_query = [] - + field_prefix = "sub_" if query_type == QueryType.SUBAWARDS else "" for filter_value in filter_values: - location_query = [] county = filter_value.get("county") state = filter_value.get("state") country = filter_value.get("country") @@ -491,21 +490,19 @@ def generate_elasticsearch_query(cls, filter_values: List[dict], query_type: Que "zip5": filter_value.get("zip"), } - for location_key, location_value in location_lookup.items(): - if (state is None or country != "USA" or county is not None) and ( - district_current is not None or district_original is not None - ): - raise InvalidParameterException(INCOMPATIBLE_DISTRICT_LOCATION_PARAMETERS) - if location_value is not None: - location_value = location_value.upper() - if query_type == QueryType.SUBAWARDS: - location_query.append( - ES_Q("match", **{f"sub_recipient_location_{location_key}": location_value}) - ) - else: - location_query.append(ES_Q("match", **{f"recipient_location_{location_key}": location_value})) - - recipient_locations_query.append(ES_Q("bool", must=location_query)) + if (state is None or country != "USA" or county is not None) and ( + district_current is not None or district_original is not None + ): + raise InvalidParameterException(INCOMPATIBLE_DISTRICT_LOCATION_PARAMETERS) + + location_query = [ + ES_Q("match", **{f"{field_prefix}recipient_location_{location_key}": location_value.upper()}) + for location_key, location_value in location_lookup.items() + if location_value is not None + ] + + if location_query: + recipient_locations_query.append(ES_Q("bool", must=location_query)) return ES_Q("bool", should=recipient_locations_query, minimum_should_match=1) @@ -550,8 +547,8 @@ class _PlaceOfPerformanceLocations(_Filter): @classmethod def generate_elasticsearch_query(cls, filter_values: List[dict], query_type: QueryType, **options) -> ES_Q: pop_locations_query = [] + field_prefix = "sub_" if query_type == QueryType.SUBAWARDS else "" for filter_value in filter_values: - location_query = [] county = filter_value.get("county") state = filter_value.get("state") country = filter_value.get("country") @@ -564,25 +561,19 @@ def generate_elasticsearch_query(cls, filter_values: List[dict], query_type: Que "congressional_code_current": district_current, "congressional_code": district_original, "city_name__keyword": filter_value.get("city"), + "zip5": filter_value.get("zip"), } - if query_type == QueryType.SUBAWARDS: - location_lookup["zip"] = filter_value.get("zip") - else: - location_lookup["zip5"] = filter_value.get("zip") + _PlaceOfPerformanceLocations._validate_district(state, country, county, district_current, district_original) - for location_key, location_value in location_lookup.items(): - _PlaceOfPerformanceLocations._validate_district( - state, country, county, district_current, district_original - ) + location_query = [ + ES_Q("match", **{f"{field_prefix}pop_{location_key}": location_value.upper()}) + for location_key, location_value in location_lookup.items() + if location_value is not None + ] - if location_value is not None: - location_value = location_value.upper() - if query_type == QueryType.SUBAWARDS: - location_query.append(ES_Q("match", **{f"sub_pop_{location_key}": location_value})) - else: - location_query.append(ES_Q("match", **{f"pop_{location_key}": location_value})) - pop_locations_query.append(ES_Q("bool", must=location_query)) + if location_query: + pop_locations_query.append(ES_Q("bool", must=location_query)) return ES_Q("bool", should=pop_locations_query, minimum_should_match=1) diff --git a/usaspending_api/database_scripts/etl/subaward_es_view.sql b/usaspending_api/database_scripts/etl/subaward_es_view.sql index d77e594b17..f566a7f35b 100644 --- a/usaspending_api/database_scripts/etl/subaward_es_view.sql +++ b/usaspending_api/database_scripts/etl/subaward_es_view.sql @@ -87,6 +87,7 @@ SELECT s.sub_place_of_perform_state_code AS sub_pop_state_code, s.sub_place_of_perform_state_name AS sub_pop_state_name, s.sub_place_of_perform_county_code AS sub_pop_county_code, + s.sub_place_of_perform_zip5 AS sub_pop_zip5, s.sub_place_of_performance_zip AS sub_pop_zip, s.sub_place_of_perform_congressio AS sub_pop_congressional_code, s.sub_place_of_performance_congressional_current AS sub_pop_congressional_code_current, diff --git a/usaspending_api/etl/es_subaward_template.json b/usaspending_api/etl/es_subaward_template.json index 821e80fc23..2b882b0b92 100644 --- a/usaspending_api/etl/es_subaward_template.json +++ b/usaspending_api/etl/es_subaward_template.json @@ -358,7 +358,12 @@ "type": "integer" }, "subaward_number": { - "type": "text" + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } }, "subaward_amount": { "type": "scaled_float", @@ -533,6 +538,14 @@ } } }, + "sub_pop_zip5": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, "sub_pop_congressional_code": { "type": "keyword" }, diff --git a/usaspending_api/search/tests/integration/test_spending_by_award.py b/usaspending_api/search/tests/integration/test_spending_by_award.py index 33996fc3ef..e91248606c 100644 --- a/usaspending_api/search/tests/integration/test_spending_by_award.py +++ b/usaspending_api/search/tests/integration/test_spending_by_award.py @@ -234,6 +234,36 @@ def award_data_fixture(db): ) +@pytest.fixture +def subaward_data(db): + baker.make( + "search.SubawardSearch", + broker_subaward_id=1, + sub_action_date="2023-01-01", + prime_award_group="grant", + prime_award_type="07", + subaward_number=99999, + action_date="2023-01-01", + sub_legal_entity_zip5="12345", + sub_legal_entity_country_code="USA", + sub_place_of_perform_zip5="23456", + sub_place_of_perform_country_co="USA", + ) + baker.make( + "search.SubawardSearch", + broker_subaward_id=2, + sub_action_date="2023-01-01", + prime_award_group="procurement", + prime_award_type="08", + subaward_number=99998, + action_date="2023-01-01", + sub_legal_entity_zip5="54321", + sub_legal_entity_country_code="USA", + sub_place_of_perform_zip5="65432", + sub_place_of_perform_country_co="USA", + ) + + @pytest.mark.django_db def test_spending_by_award_subaward_success( client, monkeypatch, elasticsearch_subaward_index, spending_by_award_test_data @@ -3738,3 +3768,52 @@ def test_covid_and_iija_values(client, monkeypatch, elasticsearch_award_index, a ] assert resp.status_code == status.HTTP_200_OK assert resp.data["results"] == expected_result + + +def test_spending_by_subaward_place_of_perf_zip_filter( + client, monkeypatch, elasticsearch_subaward_index, subaward_data +): + setup_elasticsearch_test(monkeypatch, elasticsearch_subaward_index) + + test_payload = { + "spending_level": "subawards", + "fields": ["Sub-Award ID"], + "filters": { + "award_type_codes": ["07", "08"], + "place_of_performance_locations": [{"country": "USA", "zip": "65432"}], + }, + "sort": "Sub-Award ID", + "order": "desc", + } + + resp = client.post( + "/api/v2/search/spending_by_award/", content_type="application/json", data=json.dumps(test_payload) + ) + + assert resp.status_code == status.HTTP_200_OK + results = resp.json().get("results") + assert len(results) == 1 + assert results[0]["Sub-Award ID"] == "99998" + + +def test_spending_by_subaward_recipient_location_zip_filter( + client, monkeypatch, elasticsearch_subaward_index, subaward_data +): + setup_elasticsearch_test(monkeypatch, elasticsearch_subaward_index) + + test_payload = { + "spending_level": "subawards", + "fields": ["Sub-Award ID"], + "filters": {"award_type_codes": ["07", "08"], "recipient_locations": [{"country": "USA", "zip": "12345"}]}, + "sort": "Sub-Award ID", + "order": "desc", + } + + resp = client.post( + "/api/v2/search/spending_by_award/", content_type="application/json", data=json.dumps(test_payload) + ) + + assert resp.status_code == status.HTTP_200_OK + results = resp.json().get("results") + assert len(results) == 1 + assert results[0]["Sub-Award ID"] == "99999" diff --git a/usaspending_api/search/v2/views/spending_by_award.py b/usaspending_api/search/v2/views/spending_by_award.py index 9652cbb02e..5e0e5517d8 100644 --- a/usaspending_api/search/v2/views/spending_by_award.py +++ b/usaspending_api/search/v2/views/spending_by_award.py @@ -275,8 +275,10 @@ def add_award_generated_id_field(self, records): def get_elastic_sort_by_fields(self): match self.pagination["sort_key"]: - case "Award ID" | "Sub-Award ID": + case "Award ID": sort_by_fields = ["display_award_id"] + case "Sub-Award ID": + sort_by_fields = ["subaward_number.keyword"] case "NAICS": sort_by_fields = ( [contracts_mapping["sub_naics_code"], contracts_mapping["naics_description"]] @@ -669,7 +671,7 @@ def calculate_complex_fields(self, row, hit): zip5 = hit.get("sub_pop_zip") case _: zip4 = None - zip5 = None + zip5 = hit.get("sub_pop_zip5") else: zip4 = None zip5 = None