From 4953183a7a2dd6df87baa07374089b1921429617 Mon Sep 17 00:00:00 2001 From: Adam King Date: Fri, 27 Sep 2024 14:48:33 -0500 Subject: [PATCH] #2009 - IIR - Update VAPRO Contact Lookup to Filter by Valid Phone Numbers (#2010) --- .talismanrc | 48 +++--- app/va/va_profile/va_profile_client.py | 104 +++++++++--- app/va/va_profile/va_profile_types.py | 8 +- .../dev/vaec-celery-task-definition.json | 4 + .../perf/vaec-celery-task-definition.json | 4 + .../prod/vaec-celery-task-definition.json | 4 + .../staging/vaec-celery-task-definition.json | 4 + tests/app/va/va_profile/mock_response.json | 18 ++- .../va/va_profile/test_va_profile_client.py | 23 --- .../test_va_profile_client_for_profile_v3.py | 148 +++++++++++++++--- 10 files changed, 276 insertions(+), 89 deletions(-) diff --git a/.talismanrc b/.talismanrc index 80752db77a..cfcf39cf69 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,26 +1,26 @@ fileignoreconfig: -- filename: README.md - checksum: b2cbbb8508af49abccae8b35b317f7ce09215f3508430ed31440add78f450e5a -- filename: app/celery/contact_information_tasks.py - checksum: 80d0acf88bafb1358583016b9e143f4523ef1160d6eacdc9754ca68859b90eae -- filename: app/notifications/process_notifications.py - checksum: ae4e31c6eb56d91ec80ae09d13baf4558cf461c65f08893b93fee43f036a17a7 -- filename: app/service/rest.py - checksum: b42aefd1ae0e6ea76e75db4cf14d425facd0941943b17f7ba2e41f850ad1ec23 -- filename: app/template/rest.py - checksum: 1e5bdac8bc694d50f8f656dec127dd036b7b1b5b6156e3282d3411956c71ba0b -- filename: lambda_functions/pinpoint_callback/pinpoint_callback_lambda.py - checksum: 7bd4900e14b1fa789bbb2568b8a8d7a400e3c8350ba32fb44cc0b5b66a2df037 -- filename: lambda_functions/ses_callback/ses_callback_lambda.py - checksum: b20c36921290a9609f158784e2a3278c36190887e6054ea548004a67675fd79b -- filename: poetry.lock - checksum: 34d12acdf749363555c31add4e7e7afa9e2a27afd792bd98c85f331b87bd7112 -- filename: scripts/trigger_task.py - checksum: 0e9d244dbe285de23fc84bb643407963dacf7d25a3358373f01f6272fb217778 -- filename: tests/app/celery/test_process_ga4_measurement_task.py - checksum: d33a6911258922f4bd3d149c90c2ee16c021a8e59e462594e4b1cd972902d689 -- filename: tests/app/conftest.py - checksum: a80aa727586db82ed1b50bdb81ddfe1379e649a9dfc1ece2c36047486b41b83d -- filename: tests/app/notifications/test_process_notifications_for_profile_v3.py - checksum: 4e15e63d349635131173ffdd7aebcd547621db08de877ef926d3a41fde72d065 + - filename: README.md + checksum: b2cbbb8508af49abccae8b35b317f7ce09215f3508430ed31440add78f450e5a + - filename: app/celery/contact_information_tasks.py + checksum: 80d0acf88bafb1358583016b9e143f4523ef1160d6eacdc9754ca68859b90eae + - filename: app/notifications/process_notifications.py + checksum: ae4e31c6eb56d91ec80ae09d13baf4558cf461c65f08893b93fee43f036a17a7 + - filename: app/service/rest.py + checksum: b42aefd1ae0e6ea76e75db4cf14d425facd0941943b17f7ba2e41f850ad1ec23 + - filename: app/template/rest.py + checksum: 1e5bdac8bc694d50f8f656dec127dd036b7b1b5b6156e3282d3411956c71ba0b + - filename: lambda_functions/pinpoint_callback/pinpoint_callback_lambda.py + checksum: 7bd4900e14b1fa789bbb2568b8a8d7a400e3c8350ba32fb44cc0b5b66a2df037 + - filename: lambda_functions/ses_callback/ses_callback_lambda.py + checksum: b20c36921290a9609f158784e2a3278c36190887e6054ea548004a67675fd79b + - filename: poetry.lock + checksum: 34d12acdf749363555c31add4e7e7afa9e2a27afd792bd98c85f331b87bd7112 + - filename: scripts/trigger_task.py + checksum: 0e9d244dbe285de23fc84bb643407963dacf7d25a3358373f01f6272fb217778 + - filename: tests/app/celery/test_process_ga4_measurement_task.py + checksum: d33a6911258922f4bd3d149c90c2ee16c021a8e59e462594e4b1cd972902d689 + - filename: tests/app/conftest.py + checksum: a80aa727586db82ed1b50bdb81ddfe1379e649a9dfc1ece2c36047486b41b83d + - filename: tests/app/notifications/test_process_notifications_for_profile_v3.py + checksum: 4e15e63d349635131173ffdd7aebcd547621db08de877ef926d3a41fde72d065 version: "1.0" diff --git a/app/va/va_profile/va_profile_client.py b/app/va/va_profile/va_profile_client.py index 1e87a0b694..f80c7e9207 100644 --- a/app/va/va_profile/va_profile_client.py +++ b/app/va/va_profile/va_profile_client.py @@ -3,12 +3,13 @@ from dataclasses import dataclass from enum import Enum from http.client import responses -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import iso8601 import requests +from app.feature_flags import FeatureFlag, is_feature_enabled from app.va.identifier import OIDS, IdentifierType, transform_to_fhir_format from app.va.va_profile import NoContactInfoException, VAProfileNonRetryableException, VAProfileRetryableException from app.va.va_profile.exceptions import ( @@ -26,6 +27,12 @@ 'sms': 'Text', } +VALID_PHONE_TYPES_FOR_SMS_DELIVERY = [ + 0, # "MOBILE" + 2, # "VOIP" + 5, # "PREPAID" +] + class CommunicationChannel(Enum): EMAIL = ('Email', 2) @@ -158,6 +165,77 @@ def get_email(self, va_profile_id: RecipientIdentifier) -> str: self.statsd_client.incr('clients.va-profile.get-email.failure') self._raise_no_contact_info_exception(self.EMAIL_BIO_TYPE, va_profile_id, contact_info.get(self.TX_AUDIT_ID)) + def has_valid_mobile_telephone_classification(self, telephone: Telephone) -> bool: + """ + Args: + telephone (Telephone): telephone entry from ContactInformation object retrieved from Vet360 API endpoint + + Returns: + bool - if AWS-classified telephone is a valid sms recipient (if nonexistent, return True) + """ + classification = telephone.get('classification', {}) + classification_code = classification.get('classificationCode', None) + if classification_code is not None: + if classification_code not in VALID_PHONE_TYPES_FOR_SMS_DELIVERY: + self.logger.debug( + 'V3 Profile -- Phone classification code of %s is not a valid SMS recipient (VA Profile ID: %s)', + classification_code, + telephone['vaProfileId'], + ) + return False + + self.logger.debug( + 'V3 Profile -- Phone classification code of %s is a valid SMS recipient (VA Profile ID: %s)', + classification_code, + telephone['vaProfileId'], + ) + return True + + # fall back, if no phone number classification is present + self.logger.debug( + 'V3 Profile -- No telephone classification present, assuming the number is a valid SMS recipient (VA Profile ID: %s)', + telephone['vaProfileId'], + ) + return True + + def get_mobile_telephone_from_contact_info(self, contact_info: ContactInformation) -> Optional[str]: + """ + Find the most recently created mobile phone number from a veteran's Vet360 contact information + + Args: + contact_info (ContactInformation): Contact Information object retrieved from Vet360 API endpoint + + Returns: + string representation of the most recently created mobile phone number, or None + """ + telephones: list[Telephone] = contact_info.get(self.PHONE_BIO_TYPE, []) + + sorted_telephones = sorted( + [phone for phone in telephones if phone['phoneType'] == PhoneNumberType.MOBILE.value], + key=lambda phone: iso8601.parse_date(phone['createDate']), + reverse=True, + ) + + if sorted_telephones: + is_mobile = True + if is_feature_enabled(FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS): + self.logger.debug( + 'V3 Profile -- VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS enabled. Checking telephone classification info.' + ) + is_mobile = self.has_valid_mobile_telephone_classification(sorted_telephones[0]) + else: + self.logger.debug( + 'V3 Profile -- VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS is not enabled. Will not check classification info.' + ) + if ( + is_mobile + and sorted_telephones[0].get('countryCode') + and sorted_telephones[0].get('areaCode') + and sorted_telephones[0].get('phoneNumber') + ): + self.statsd_client.incr('clients.va-profile.get-telephone.success') + return f"+{sorted_telephones[0]['countryCode']}{sorted_telephones[0]['areaCode']}{sorted_telephones[0]['phoneNumber']}" + def get_telephone_with_permission( self, va_profile_id: RecipientIdentifier, @@ -189,24 +267,14 @@ def get_telephone_with_permission( contact_info: ContactInformation = profile.get('contactInformation', {}) - telephones: list[Telephone] = contact_info.get(self.PHONE_BIO_TYPE, []) - sorted_telephones = sorted( - [phone for phone in telephones if phone['phoneType'] == PhoneNumberType.MOBILE.value], - key=lambda phone: iso8601.parse_date(phone['createDate']), - reverse=True, - ) - if sorted_telephones: - if ( - sorted_telephones[0].get('countryCode') - and sorted_telephones[0].get('areaCode') - and sorted_telephones[0].get('phoneNumber') - ): - self.statsd_client.incr('clients.va-profile.get-telephone.success') - telephone_result = f"+{sorted_telephones[0]['countryCode']}{sorted_telephones[0]['areaCode']}{sorted_telephones[0]['phoneNumber']}" - return VAProfileResult(telephone_result, communication_allowed, permission_message) + telephone = self.get_mobile_telephone_from_contact_info(contact_info) + if not telephone: + self.statsd_client.incr('clients.va-profile.get-telephone.failure') + self._raise_no_contact_info_exception( + self.PHONE_BIO_TYPE, va_profile_id, contact_info.get(self.TX_AUDIT_ID) + ) - self.statsd_client.incr('clients.va-profile.get-telephone.failure') - self._raise_no_contact_info_exception(self.PHONE_BIO_TYPE, va_profile_id, contact_info.get(self.TX_AUDIT_ID)) + return VAProfileResult(telephone, communication_allowed, permission_message) def get_email_with_permission( self, diff --git a/app/va/va_profile/va_profile_types.py b/app/va/va_profile/va_profile_types.py index cb3919cd10..7ef7331e0e 100644 --- a/app/va/va_profile/va_profile_types.py +++ b/app/va/va_profile/va_profile_types.py @@ -1,4 +1,4 @@ -from typing import TypedDict, List +from typing import TypedDict, List, Optional class Country(TypedDict): @@ -44,6 +44,11 @@ class Address(TypedDict): geocodeDate: str +class Classification(TypedDict): + classificationCode: int + classificationName: str + + class Telephone(TypedDict): createDate: str updateDate: str @@ -60,6 +65,7 @@ class Telephone(TypedDict): countryCode: str areaCode: str phoneNumber: str + classification: Optional[Classification] class Email(TypedDict): diff --git a/cd/application-deployment/dev/vaec-celery-task-definition.json b/cd/application-deployment/dev/vaec-celery-task-definition.json index 7506d57220..1a8e7a2e2f 100644 --- a/cd/application-deployment/dev/vaec-celery-task-definition.json +++ b/cd/application-deployment/dev/vaec-celery-task-definition.json @@ -232,6 +232,10 @@ { "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", "value": "True" + }, + { + "name": "VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS", + "value": "True" } ], "secrets": [ diff --git a/cd/application-deployment/perf/vaec-celery-task-definition.json b/cd/application-deployment/perf/vaec-celery-task-definition.json index 300d760d01..47e1424d75 100644 --- a/cd/application-deployment/perf/vaec-celery-task-definition.json +++ b/cd/application-deployment/perf/vaec-celery-task-definition.json @@ -192,6 +192,10 @@ { "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", "value": "True" + }, + { + "name": "VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS", + "value": "False" } ], "secrets": [ diff --git a/cd/application-deployment/prod/vaec-celery-task-definition.json b/cd/application-deployment/prod/vaec-celery-task-definition.json index 99707f8b5a..fc842cf835 100644 --- a/cd/application-deployment/prod/vaec-celery-task-definition.json +++ b/cd/application-deployment/prod/vaec-celery-task-definition.json @@ -196,6 +196,10 @@ { "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", "value": "False" + }, + { + "name": "VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS", + "value": "False" } ], "secrets": [ diff --git a/cd/application-deployment/staging/vaec-celery-task-definition.json b/cd/application-deployment/staging/vaec-celery-task-definition.json index 02aae8eb99..9a6fba05ec 100644 --- a/cd/application-deployment/staging/vaec-celery-task-definition.json +++ b/cd/application-deployment/staging/vaec-celery-task-definition.json @@ -212,6 +212,10 @@ { "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", "value": "True" + }, + { + "name": "VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS", + "value": "False" } ], "secrets": [ diff --git a/tests/app/va/va_profile/mock_response.json b/tests/app/va/va_profile/mock_response.json index 7f90526c28..aa949dcde6 100644 --- a/tests/app/va/va_profile/mock_response.json +++ b/tests/app/va/va_profile/mock_response.json @@ -114,7 +114,11 @@ "phoneType": "MOBILE", "countryCode": "1", "areaCode": "582", - "phoneNumber": "3028742" + "phoneNumber": "3028742", + "classification": { + "classificationCode": 0, + "classificationName": "MOBILE" + } }, { "createDate": "2022-06-09T15:12:10Z", @@ -131,7 +135,11 @@ "phoneType": "HOME", "countryCode": "1", "areaCode": "582", - "phoneNumber": "2015400" + "phoneNumber": "2015400", + "classification": { + "classificationCode": 1, + "classificationName": "LANDLINE" + } }, { "createDate": "2022-06-09T15:11:50Z", @@ -148,7 +156,11 @@ "phoneType": "MOBILE", "countryCode": "1", "areaCode": "582", - "phoneNumber": "3028743" + "phoneNumber": "3028743", + "classification": { + "classificationCode": 0, + "classificationName": "MOBILE" + } } ], "emails": [ diff --git a/tests/app/va/va_profile/test_va_profile_client.py b/tests/app/va/va_profile/test_va_profile_client.py index 0f4a43d803..9eb7278576 100644 --- a/tests/app/va/va_profile/test_va_profile_client.py +++ b/tests/app/va/va_profile/test_va_profile_client.py @@ -6,7 +6,6 @@ from app.models import EMAIL_TYPE, RecipientIdentifier from app.va.identifier import IdentifierType, transform_to_fhir_format, OIDS -from app.va.va_profile import VAProfileClient from app.va.va_profile.exceptions import ( CommunicationItemNotFoundException, NoContactInfoException, @@ -19,28 +18,6 @@ MOCK_VA_PROFILE_URL = 'http://mock.vaprofile.va.gov' -@pytest.fixture(scope='function') -def mock_va_profile_client(mocker, notify_api): - with notify_api.app_context(): - mock_logger = mocker.Mock() - mock_ssl_key_path = 'some_key.pem' - mock_ssl_cert_path = 'some_cert.pem' - mock_statsd_client = mocker.Mock() - mock_va_profile_token = mocker.Mock() - - client = VAProfileClient() - client.init_app( - logger=mock_logger, - va_profile_url=MOCK_VA_PROFILE_URL, - ssl_cert_path=mock_ssl_cert_path, - ssl_key_path=mock_ssl_key_path, - va_profile_token=mock_va_profile_token, - statsd_client=mock_statsd_client, - ) - - return client - - @pytest.fixture(scope='function') def mock_response(): with open('tests/app/va/va_profile/mock_response.json', 'r') as f: diff --git a/tests/app/va/va_profile/test_va_profile_client_for_profile_v3.py b/tests/app/va/va_profile/test_va_profile_client_for_profile_v3.py index 57164cd6f1..8ee4aa383a 100644 --- a/tests/app/va/va_profile/test_va_profile_client_for_profile_v3.py +++ b/tests/app/va/va_profile/test_va_profile_client_for_profile_v3.py @@ -1,5 +1,7 @@ import json +import random from urllib import parse +from datetime import datetime, timedelta import pytest import requests @@ -16,10 +18,36 @@ VAProfileNonRetryableException, VAProfileRetryableException, ) -from app.va.va_profile.va_profile_client import CommunicationChannel +from app.va.va_profile.va_profile_client import CommunicationChannel, VALID_PHONE_TYPES_FOR_SMS_DELIVERY +from app.va.va_profile.va_profile_types import Telephone -from tests.app.factories.feature_flag import mock_feature_flag from tests.app.conftest import MOCK_VA_PROFILE_URL +from tests.app.factories.feature_flag import mock_feature_flag + + +def telephone_entry( + create_date=datetime.today(), phone_type='MOBILE', classification_code='0', name_for_debugging='test mobile phone' +): + area_code = random.randint(100, 999) + phone_number = random.randint(100000, 9999999) + return { + 'createDate': f'{create_date}', + 'updateDate': f'{create_date}', + 'txAuditId': '6706b496-d727-401f-8df7-d6fc9adef0e7', + 'sourceSystem': name_for_debugging, + 'sourceDate': '2022-06-09T15:11:58Z', + 'originatingSourceSystem': 'VETSGOV', + 'sourceSystemUser': '1012833438V267437', + 'effectiveStartDate': '2022-06-09T15:11:58Z', + 'vaProfileId': 1550370, + 'telephoneId': 293410, + 'internationalIndicator': False, + 'phoneType': phone_type, + 'countryCode': '1', + 'areaCode': f'{area_code}', + 'phoneNumber': f'{phone_number}', + 'classification': {'classificationCode': classification_code}, + } @pytest.fixture(scope='function') @@ -49,7 +77,7 @@ def url(oid, id_with_aaid): class TestVAProfileClient: - def test_ut_get_email_calls_endpoint_and_returns_email_address( + def test_get_email_calls_endpoint_and_returns_email_address( self, rmock, mock_va_profile_client, @@ -73,7 +101,7 @@ def test_ut_get_email_calls_endpoint_and_returns_email_address( assert email == mock_response['profile']['contactInformation']['emails'][0]['emailAddressText'] assert rmock.called - def test_ut_get_email_raises_NoContactInfoException_if_no_emails_exist( + def test_get_email_raises_NoContactInfoException_if_no_emails_exist( self, rmock, mock_va_profile_client, @@ -95,7 +123,7 @@ def test_ut_get_email_raises_NoContactInfoException_if_no_emails_exist( sample_notification(gen_type=EMAIL_TYPE), ) - def test_ut_get_profile_calls_correct_url( + def test_get_profile_calls_correct_url( self, rmock, mock_va_profile_client, @@ -121,7 +149,7 @@ def test_ut_get_profile_calls_correct_url( assert rmock.request_history[0].url == expected_url - def test_ut_get_email_raises_exception_when_failed_request( + def test_get_email_raises_exception_when_failed_request( self, rmock, mock_va_profile_client, @@ -152,7 +180,7 @@ def test_ut_get_email_raises_exception_when_failed_request( recipient_identifier, sample_notification(gen_type=EMAIL_TYPE) ) - def test_ut_get_telephone_calls_endpoint_and_returns_phone_number( + def test_get_telephone_calls_endpoint_and_returns_phone_number( self, rmock, mock_va_profile_client, @@ -173,9 +201,43 @@ def test_ut_get_telephone_calls_endpoint_and_returns_phone_number( assert telephone is not None assert rmock.called + @pytest.mark.parametrize( + 'classification_code, expected', + [ + *[(code, True) for code in VALID_PHONE_TYPES_FOR_SMS_DELIVERY], + (None, True), # if no classification code exists, fall back to True + (1, False), # LANDLINE + (3, False), # INVALID + (4, False), # OTHER + ], + ) + def test_has_valid_telephone_classification(self, mock_va_profile_client, classification_code, expected): + telephone_instance: Telephone = { + 'createDate': '2023-10-01', + 'updateDate': '2023-10-02', + 'txAuditId': 'TX123456', + 'sourceSystem': 'SystemA', + 'sourceDate': '2023-10-01', + 'originatingSourceSystem': 'SystemB', + 'sourceSystemUser': 'User123', + 'effectiveStartDate': '2023-10-01', + 'vaProfileId': 12345, + 'telephoneId': 67890, + 'internationalIndicator': False, + 'phoneType': 'Mobile', + 'countryCode': '1', + 'areaCode': '123', + 'phoneNumber': '4567890', + 'classification': {'classificationCode': classification_code, 'classificationName': 'SOME NAME'}, + } + if classification_code is None: + telephone_instance.pop('classification') + + assert mock_va_profile_client.has_valid_mobile_telephone_classification(telephone_instance) is expected + class TestVAProfileClientExceptionHandling: - def test_ut_get_telephone_raises_NoContactInfoException_if_no_telephones_exist( + def test_get_telephone_raises_NoContactInfoException_if_no_telephones_exist( self, rmock, mock_va_profile_client, @@ -194,7 +256,7 @@ def test_ut_get_telephone_raises_NoContactInfoException_if_no_telephones_exist( with pytest.raises(NoContactInfoException): mock_va_profile_client.get_telephone_with_permission(recipient_identifier, sample_notification()) - def test_ut_get_telephone_raises_NoContactInfoException_if_no_mobile_telephones_exist( + def test_get_telephone_raises_NoContactInfoException_if_no_mobile_telephones_exist( self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url, mocker, sample_notification ): mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') @@ -209,7 +271,53 @@ def test_ut_get_telephone_raises_NoContactInfoException_if_no_mobile_telephones_ with pytest.raises(NoContactInfoException): mock_va_profile_client.get_telephone_with_permission(recipient_identifier, sample_notification()) - def test_ut_handle_exceptions_retryable_exception(self, mock_va_profile_client, mocker): + def test_get_telephone_raises_NoContactInfoException_if_number_classified_as_not_mobile( + self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url, mocker, sample_notification + ): + mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') + + telephones = mock_response['profile']['contactInformation']['telephones'] + for telephone in telephones: + telephone['classification'] = {'classificationCode': 1} # LANDLINE classification + mock_response['profile']['contactInformation']['telephones'] = telephones + rmock.post(url, json=mock_response, status_code=200) + + with pytest.raises(NoContactInfoException): + mock_va_profile_client.get_telephone_with_permission(recipient_identifier, sample_notification()) + + def test_get_telephone_with_permission_prefers_user_specified_mobile_phone( + self, rmock, mock_va_profile_client, mock_response, mocker, url, recipient_identifier, sample_notification + ): + # A veteran has configured a mobile telephone to receive notifications. They add an additional mobile + # phone, and save it as their "home" phone. Even though it is technically a mobile device and is newer, + # we should send notifications to the device specified by the user + mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') + + today = datetime.today() + yesterday_morning = (datetime.today() - timedelta(days=1)).replace(hour=6) + yesterday_evening = yesterday_morning.replace(hour=20) + home_phone_created_today = telephone_entry(today, 'HOME', 1, 'home phone created today') + mobile_phone_created_yesterday_morning = telephone_entry( + yesterday_morning, 'MOBILE', 0, 'mobile phone created yesterday morning' + ) + home_phone_with_mobile_classification_created_yesterday_evening = telephone_entry( + yesterday_evening, 'HOME', 0, 'home phone with mobile classification created yesterday evening' + ) + contact_info = mock_response['profile']['contactInformation'] + contact_info['telephones'] = [ + home_phone_created_today, + mobile_phone_created_yesterday_morning, + home_phone_with_mobile_classification_created_yesterday_evening, + ] + + rmock.post(url, json=mock_response, status_code=200) + result = mock_va_profile_client.get_telephone_with_permission(recipient_identifier, sample_notification()) + assert ( + result.recipient + == f"+{mobile_phone_created_yesterday_morning['countryCode']}{mobile_phone_created_yesterday_morning['areaCode']}{mobile_phone_created_yesterday_morning['phoneNumber']}" + ) + + def test_handle_exceptions_retryable_exception(self, mock_va_profile_client, mocker): mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') @@ -217,7 +325,7 @@ def test_ut_handle_exceptions_retryable_exception(self, mock_va_profile_client, with pytest.raises(VAProfileRetryableException): mock_va_profile_client._handle_exceptions('some_va_profile_id', requests.RequestException()) - def test_ut_handle_exceptions_id_not_found_exception(self, mock_va_profile_client, mocker): + def test_handle_exceptions_id_not_found_exception(self, mock_va_profile_client, mocker): mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') @@ -228,7 +336,7 @@ def test_ut_handle_exceptions_id_not_found_exception(self, mock_va_profile_clien with pytest.raises(VAProfileIDNotFoundException): mock_va_profile_client._handle_exceptions('some_va_profile_id', error) - def test_ut_handle_exceptions_non_retryable_exception(self, mock_va_profile_client, mocker): + def test_handle_exceptions_non_retryable_exception(self, mock_va_profile_client, mocker): mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') @@ -239,7 +347,7 @@ def test_ut_handle_exceptions_non_retryable_exception(self, mock_va_profile_clie with pytest.raises(VAProfileNonRetryableException): mock_va_profile_client._handle_exceptions('some_va_profile_id', error) - def test_ut_handle_exceptions_timeout_exception(self, mock_va_profile_client, mocker): + def test_handle_exceptions_timeout_exception(self, mock_va_profile_client, mocker): mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') @@ -250,7 +358,7 @@ def test_ut_handle_exceptions_timeout_exception(self, mock_va_profile_client, mo mock_va_profile_client._handle_exceptions('some_va_profile_id', requests.Timeout()) @pytest.mark.parametrize('status', [429, 500]) - def test_ut_client_raises_retryable_exception( + def test_client_raises_retryable_exception( self, rmock, mock_va_profile_client, @@ -272,7 +380,7 @@ def test_ut_client_raises_retryable_exception( with pytest.raises(VAProfileRetryableException): mock_va_profile_client.get_email_with_permission(recipient_identifier, sample_notification()) - def test_ut_client_raises_retryable_exception_when_request_exception_is_thrown( + def test_client_raises_retryable_exception_when_request_exception_is_thrown( self, mock_va_profile_client, recipient_identifier, @@ -296,7 +404,7 @@ def test_ut_client_raises_retryable_exception_when_request_exception_is_thrown( class TestCommunicationPermissions: @pytest.mark.parametrize('expected', [True, False]) - def test_ut_get_is_communication_allowed_returns_whether_permissions_granted_for_sms_communication( + def test_get_is_communication_allowed_returns_whether_permissions_granted_for_sms_communication( self, rmock, mock_va_profile_client, @@ -320,7 +428,7 @@ def test_ut_get_is_communication_allowed_returns_whether_permissions_granted_for assert allowed is expected @pytest.mark.parametrize('expected', [True, False]) - def test_ut_get_is_communication_allowed_returns_whether_permissions_granted_for_email_communication( + def test_get_is_communication_allowed_returns_whether_permissions_granted_for_email_communication( self, rmock, mock_va_profile_client, @@ -415,7 +523,7 @@ class TestSendEmailStatus: 'provider': 'ses', # email provider } - def test_ut_send_va_profile_email_status_sent_successfully(self, rmock, mock_va_profile_client, mocker): + def test_send_va_profile_email_status_sent_successfully(self, rmock, mock_va_profile_client, mocker): mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') @@ -428,7 +536,7 @@ def test_ut_send_va_profile_email_status_sent_successfully(self, rmock, mock_va_ expected_url = f'{MOCK_VA_PROFILE_URL}/contact-information-vanotify/notify/status' assert rmock.request_history[0].url == expected_url - def test_ut_send_va_profile_email_status_timeout(self, rmock, mock_va_profile_client, mocker): + def test_send_va_profile_email_status_timeout(self, rmock, mock_va_profile_client, mocker): mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') @@ -442,7 +550,7 @@ def test_ut_send_va_profile_email_status_timeout(self, rmock, mock_va_profile_cl expected_url = f'{MOCK_VA_PROFILE_URL}/contact-information-vanotify/notify/status' assert rmock.request_history[0].url == expected_url - def test_ut_send_va_profile_email_status_throws_exception(self, rmock, mock_va_profile_client, mocker): + def test_send_va_profile_email_status_throws_exception(self, rmock, mock_va_profile_client, mocker): mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True')