From 94d1f48ac7461d15033b10f58b4dc2740c7fd7c8 Mon Sep 17 00:00:00 2001 From: Tadhg O'Higgins <2626258+tadhg-ohiggins@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:33:24 -0700 Subject: [PATCH] YAUF (#2446) * YAUF. * Add test for oddly-shaped results. --- backend/api/serializers.py | 106 ++++++++++++++++++++++++-------- backend/api/test_serializers.py | 54 ++++++++++++++++ 2 files changed, 133 insertions(+), 27 deletions(-) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index fc37e3b43f..c3244b8d15 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -72,40 +72,92 @@ def validate_user_provided_organization_type(self, value): class UEISerializer(serializers.Serializer): + """ + Does a UEI request against the SAM.gov API and returns a flattened shape + containing only the fields we're interested in. + + The below operations are nested and mixed among functions, rather than done + serially, but the approximate order of operations is: + + + Assemble the parameters to pass to the API. + Mostly in api.uei.get_uei_info_from_sam_gov. + + Make the API request. + api.uei.call_sam_api + + Check for high-level errors. + api.uei.get_uei_info_from_sam_gov + + Extract the JSON for the individual record out of the response and check + for some other errors. + api.uei.parse_sam_uei_json + + For a specific class of error, retry the API call with different + parameters. + api.uei.get_uei_info_from_sam_gov + api.uei.call_sam_api + api.uei.parse_sam_uei_json + + If we don't have errors by that point, flatten the data. + api.serializsers.UEISerializer.validate_auditee_uei + + If we don't encounter errors at that point, return the flattened data. + api.serializsers.UEISerializer.validate_auditee_uei + + """ + auditee_uei = serializers.CharField() def validate_auditee_uei(self, value): + """ + Flattens the UEI response info and returns this shape: + + { + "auditee_uei": …, + "auditee_name": …, + "auditee_fiscal_year_end_date": …, + "auditee_address_line_1": …, + "auditee_city": …, + "auditee_state": …, + "auditee_zip": …, + } + + Will provide default error-message-like values (such as “No address in SAM.gov) + if the keys are missing, but if the SAM.gov fields are present but empty, we + return the empty strings. + + """ sam_response = get_uei_info_from_sam_gov(value) if sam_response.get("errors"): raise serializers.ValidationError(sam_response.get("errors")) - return json.dumps( - { - "auditee_uei": value, - "auditee_name": sam_response.get("response") - .get("entityRegistration") - .get("legalBusinessName"), - "auditee_fiscal_year_end_date": sam_response.get("response") - .get("coreData") - .get("entityInformation") - .get("fiscalYearEndCloseDate"), - "auditee_address_line_1": sam_response.get("response") - .get("coreData") - .get("mailingAddress") - .get("addressLine1"), - "auditee_city": sam_response.get("response") - .get("coreData") - .get("mailingAddress") - .get("city"), - "auditee_state": sam_response.get("response") - .get("coreData") - .get("mailingAddress") - .get("stateOrProvinceCode"), - "auditee_zip": sam_response.get("response") - .get("coreData") - .get("mailingAddress") - .get("zipCode"), + + entity_registration = sam_response.get("response")["entityRegistration"] + core = sam_response.get("response")["coreData"] + + basic_data = { + "auditee_uei": value, + "auditee_name": entity_registration.get("legalBusinessName"), + } + addr_key = "mailingAddress" if "mailingAddress" in core else "physicalAddress" + + mailing_data = { + "auditee_address_line_1": "No address in SAM.gov.", + "auditee_city": "No address in SAM.gov.", + "auditee_state": "No address in SAM.gov.", + "auditee_zip": "No address in SAM.gov.", + } + + if addr_key in core: + mailing_data = { + "auditee_address_line_1": core.get(addr_key).get("addressLine1"), + "auditee_city": core.get(addr_key).get("city"), + "auditee_state": core.get(addr_key).get("stateOrProvinceCode"), + "auditee_zip": core.get(addr_key).get("zipCode"), } - ) + + # 2023-10-10: Entities with a samRegistered value of No may be missing + # some fields from coreData entirely. + entity_information = core.get("entityInformation", {}) + extra_data = { + "auditee_fiscal_year_end_date": entity_information.get( + "fiscalYearEndCloseDate", "No fiscal year end date in SAM.gov." + ), + } + return json.dumps(basic_data | mailing_data | extra_data) class AuditeeInfoSerializer(serializers.Serializer): diff --git a/backend/api/test_serializers.py b/backend/api/test_serializers.py index 318e784933..cd646f35b9 100644 --- a/backend/api/test_serializers.py +++ b/backend/api/test_serializers.py @@ -95,6 +95,60 @@ def test_invalid_uei_payload(self): # Invalid self.assertFalse(UEISerializer(data=invalid).is_valid()) + def test_quirky_uei_payload(self): + """ + It turns out that some entries can be missing fields that we thought would + always be present. + + """ + quirky = { + "totalRecords": 1, + "entityData": [ + { + "entityRegistration": { + "samRegistered": "No", + "ueiSAM": "ZQGGHJH74DW7", + "cageCode": None, + "legalBusinessName": "Some organization", + "registrationStatus": "ID Assigned", + "evsSource": "X&Y", + "ueiStatus": "Active", + "ueiExpirationDate": None, + "ueiCreationDate": "2023-04-01", + "publicDisplayFlag": "Y", + "dnbOpenData": None, + }, + "coreData": { + "physicalAddress": { + "addressLine1": "SOME RD", + "addressLine2": None, + "city": "SOME CITY", + "stateOrProvinceCode": "AL", + "zipCode": "36659", + "zipCodePlus4": "3903", + "countryCode": "USA", + } + }, + } + ], + "links": { + "selfLink": "https://api.sam.gov/entity-information/v3/entities?api_key=REPLACE_WITH_API_KEY&ueiSAM=ZQGGHJH74DW7&samRegistered=No&page=0&size=10" + }, + } + empty = {"totalRecords": 0, "entityData": []} + data = {"auditee_uei": "ZQGGHJH74DW7"} + with patch("api.uei.SESSION.get") as mock_get: + mock_get.return_value.status_code = 200 + # First time, it should return zero results and retry: + mock_get.return_value.json.return_value = empty + self.assertFalse(UEISerializer(data=data).is_valid()) + self.assertEqual(mock_get.call_count, 2) + # Second time, it should return the samRegistered No result and only + # call the get method once: + mock_get.return_value.json.return_value = quirky + self.assertTrue(UEISerializer(data=data).is_valid()) + self.assertEqual(mock_get.call_count, 3) + class AuditeeInfoStepTests(SimpleTestCase): def test_valid_auditee_info_with_uei(self):