From fc1d5723574f11076d7e64ebb3f49209a6b8350f Mon Sep 17 00:00:00 2001 From: Dan Nguyen <186835528+dnguyen1-pass@users.noreply.github.com> Date: Fri, 24 Jan 2025 18:23:15 +0100 Subject: [PATCH] (PC-34274)[API] refactor: centralize user eligibility functions --- api/src/pcapi/core/fraud/api.py | 47 +--- api/src/pcapi/core/fraud/common/models.py | 6 +- .../users/ubble/reminder_emails.py | 4 +- api/src/pcapi/core/subscription/api.py | 29 +-- api/src/pcapi/core/subscription/dms/api.py | 5 +- api/src/pcapi/core/users/api.py | 59 ----- api/src/pcapi/core/users/eligibility_api.py | 123 ++++++++++ api/src/pcapi/core/users/models.py | 4 +- .../routes/backoffice/accounts/blueprint.py | 3 +- .../routes/native/v1/serialization/account.py | 7 +- api/tests/core/fraud/test_api.py | 194 ---------------- api/tests/core/subscription/test_api.py | 2 +- api/tests/core/users/test_api.py | 11 - api/tests/core/users/test_eligibility.py | 218 ++++++++++++++++++ 14 files changed, 375 insertions(+), 337 deletions(-) create mode 100644 api/src/pcapi/core/users/eligibility_api.py create mode 100644 api/tests/core/users/test_eligibility.py diff --git a/api/src/pcapi/core/fraud/api.py b/api/src/pcapi/core/fraud/api.py index 5e0b7db0dc9..acb974b8e6b 100644 --- a/api/src/pcapi/core/fraud/api.py +++ b/api/src/pcapi/core/fraud/api.py @@ -15,6 +15,7 @@ from pcapi.core.subscription import repository as subscription_repository from pcapi.core.users import api as users_api from pcapi.core.users import constants +from pcapi.core.users import eligibility_api from pcapi.core.users import models as users_models from pcapi.core.users import utils as users_utils from pcapi.models import db @@ -509,7 +510,7 @@ def has_user_pending_identity_check(user: users_models.User) -> bool: def has_user_performed_identity_check(user: users_models.User) -> bool: - if user.is_beneficiary and not users_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility): + if user.is_beneficiary and not eligibility_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility): return True user_subscription_state = subscription_api.get_user_subscription_state(user) @@ -562,46 +563,6 @@ def has_performed_honor_statement(user: users_models.User, eligibility_type: use ) -def decide_eligibility( - user: users_models.User, - birth_date: datetime.date | None, - registration_datetime: datetime.datetime | None, -) -> users_models.EligibilityType | None: - """Returns the applicable eligibility of the user. - It may be the current eligibility of the user if the age is between 15 and 18, or it may be the eligibility AGE18 - if the user is over 19 and had previously tried to register when the age was 18. - """ - if birth_date is None: - return None - - user_age_today = users_utils.get_age_from_birth_date(birth_date, user.departementCode) - if user_age_today < 15: - return None - if user_age_today < 18: - return users_models.EligibilityType.UNDERAGE - if user_age_today == 18: - return users_models.EligibilityType.AGE18 - - eligibility_today = users_api.get_eligibility_at_date(birth_date, datetime.datetime.utcnow(), user.departementCode) - - if eligibility_today == users_models.EligibilityType.AGE18: - return users_models.EligibilityType.AGE18 - - eligibility_at_registration = ( - users_api.get_eligibility_at_date(birth_date, registration_datetime, user.departementCode) - if registration_datetime - else None - ) - if eligibility_at_registration is None and eligibility_today is None and user_age_today == 19: - earliest_identity_check_date = subscription_api.get_first_registration_date( - user, birth_date, users_models.EligibilityType.AGE18 - ) - if earliest_identity_check_date: - return users_api.get_eligibility_at_date(birth_date, earliest_identity_check_date, user.departementCode) - - return eligibility_at_registration - - def handle_ok_manual_review( user: users_models.User, _review: models.BeneficiaryFraudReview, @@ -627,7 +588,9 @@ def handle_ok_manual_review( users_api.update_user_information_from_external_source(user, source_data) if eligibility is None: - eligibility = decide_eligibility(user, source_data.get_birth_date(), source_data.get_registration_datetime()) + eligibility = eligibility_api.decide_eligibility( + user, source_data.get_birth_date(), source_data.get_registration_datetime() + ) if not eligibility: raise EligibilityError("Aucune éligibilité trouvée. Veuillez renseigner une éligibilité.") diff --git a/api/src/pcapi/core/fraud/common/models.py b/api/src/pcapi/core/fraud/common/models.py index 84659f5b3cc..0e02a043457 100644 --- a/api/src/pcapi/core/fraud/common/models.py +++ b/api/src/pcapi/core/fraud/common/models.py @@ -50,7 +50,7 @@ def get_registration_datetime(self) -> datetime.datetime | None: return None def get_eligibility_type_at_registration(self) -> users_models.EligibilityType | None: - from pcapi.core.users import api as users_api + from pcapi.core.users import eligibility_api registration_datetime = self.get_registration_datetime() # pylint: disable=assignment-from-none birth_date = self.get_birth_date() @@ -60,6 +60,8 @@ def get_eligibility_type_at_registration(self) -> users_models.EligibilityType | postal_code = self.get_postal_code() # pylint: disable=assignment-from-none department = postal_code_utils.PostalCode(postal_code).get_departement_code() if postal_code else None - eligibility_at_registration = users_api.get_eligibility_at_date(birth_date, registration_datetime, department) + eligibility_at_registration = eligibility_api.get_eligibility_at_date( + birth_date, registration_datetime, department + ) return eligibility_at_registration diff --git a/api/src/pcapi/core/mails/transactional/users/ubble/reminder_emails.py b/api/src/pcapi/core/mails/transactional/users/ubble/reminder_emails.py index 9d231dc3df6..ecdae9fff00 100755 --- a/api/src/pcapi/core/mails/transactional/users/ubble/reminder_emails.py +++ b/api/src/pcapi/core/mails/transactional/users/ubble/reminder_emails.py @@ -11,7 +11,7 @@ import pcapi.core.subscription.api as subscription_api import pcapi.core.subscription.models as subscription_models import pcapi.core.subscription.ubble.api as ubble_subscription -import pcapi.core.users.api as users_api +from pcapi.core.users import eligibility_api import pcapi.core.users.models as users_models @@ -60,7 +60,7 @@ def _find_users_to_remind( users_with_reasons: list[tuple[users_models.User, fraud_models.FraudReasonCode]] = [] for user in users: if not ( - users_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility) + eligibility_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility) and subscription_api.get_user_subscription_state(user).fraud_status == subscription_models.SubscriptionItemStatus.TODO ): diff --git a/api/src/pcapi/core/subscription/api.py b/api/src/pcapi/core/subscription/api.py index e6787bbe338..d42f84a6b82 100644 --- a/api/src/pcapi/core/subscription/api.py +++ b/api/src/pcapi/core/subscription/api.py @@ -23,6 +23,7 @@ from pcapi.core.subscription.ubble import api as ubble_subscription_api from pcapi.core.users import api as users_api from pcapi.core.users import constants as users_constants +from pcapi.core.users import eligibility_api from pcapi.core.users import models as users_models from pcapi.core.users import utils as users_utils from pcapi.core.users import young_status as young_status_module @@ -140,21 +141,13 @@ def get_declared_names(user: users_models.User) -> tuple[str, str] | None: return None -def is_eligibility_activable(user: users_models.User, eligibility: users_models.EligibilityType | None) -> bool: - return ( - user.eligibility == eligibility - and users_api.is_eligible_for_beneficiary_upgrade(user, eligibility) - and users_api.is_user_age_compatible_with_eligibility(user.age, eligibility) - ) - - def get_email_validation_subscription_item( user: users_models.User, eligibility: users_models.EligibilityType | None ) -> models.SubscriptionItem: if user.isEmailValidated: status = models.SubscriptionItemStatus.OK else: - if is_eligibility_activable(user, eligibility): + if eligibility_api.is_eligibility_activable(user, eligibility): status = models.SubscriptionItemStatus.TODO else: status = models.SubscriptionItemStatus.VOID @@ -174,7 +167,7 @@ def get_phone_validation_subscription_item( status = models.SubscriptionItemStatus.SKIPPED elif fraud_repository.has_failed_phone_validation(user): status = models.SubscriptionItemStatus.KO - elif is_eligibility_activable(user, eligibility): + elif eligibility_api.is_eligibility_activable(user, eligibility): has_user_filled_phone = user.phoneNumber is not None if not FeatureToggle.ENABLE_PHONE_VALIDATION.is_active() and has_user_filled_phone: status = models.SubscriptionItemStatus.OK @@ -191,7 +184,7 @@ def get_profile_completion_subscription_item( ) -> models.SubscriptionItem: if has_completed_profile_for_given_eligibility(user, eligibility): status = models.SubscriptionItemStatus.OK - elif is_eligibility_activable(user, eligibility): + elif eligibility_api.is_eligibility_activable(user, eligibility): status = models.SubscriptionItemStatus.TODO else: status = models.SubscriptionItemStatus.VOID @@ -221,7 +214,7 @@ def get_identity_check_fraud_status( return models.SubscriptionItemStatus.VOID if not fraud_check: - if is_eligibility_activable(user, eligibility): + if eligibility_api.is_eligibility_activable(user, eligibility): return models.SubscriptionItemStatus.TODO return models.SubscriptionItemStatus.VOID @@ -329,7 +322,7 @@ def get_honor_statement_subscription_item( if fraud_api.has_performed_honor_statement(user, eligibility): # type: ignore[arg-type] status = models.SubscriptionItemStatus.OK else: - if is_eligibility_activable(user, eligibility): + if eligibility_api.is_eligibility_activable(user, eligibility): status = models.SubscriptionItemStatus.TODO else: status = models.SubscriptionItemStatus.VOID @@ -349,7 +342,7 @@ def get_user_subscription_state(user: users_models.User) -> subscription_models. ) # Early return if user is beneficiary - if user.is_beneficiary and not users_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility): + if user.is_beneficiary and not eligibility_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility): if user.has_active_deposit: return subscription_models.UserSubscriptionState( fraud_status=models.SubscriptionItemStatus.OK, @@ -513,7 +506,7 @@ def requires_manual_review_before_activation( return ( identity_fraud_check.type == fraud_models.FraudCheckType.DMS and identity_fraud_check.status == fraud_models.FraudCheckStatus.OK - and not users_api.get_eligibility_at_date( + and not eligibility_api.get_eligibility_at_date( user.birth_date, identity_fraud_check.get_min_date_between_creation_and_registration(), user.departementCode ) ) @@ -698,7 +691,7 @@ def _update_fraud_check_eligibility_with_history( def get_id_provider_detected_eligibility( user: users_models.User, identity_content: common_fraud_models.IdentityCheckContent ) -> users_models.EligibilityType | None: - return fraud_api.decide_eligibility( + return eligibility_api.decide_eligibility( user, identity_content.get_birth_date(), identity_content.get_registration_datetime() ) @@ -748,7 +741,7 @@ def update_user_birth_date_if_not_beneficiary(user: users_models.User, birth_dat if ( birth_date and user.validatedBirthDate != birth_date - and (users_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility) or not user.validatedBirthDate) + and (eligibility_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility) or not user.validatedBirthDate) ): user.validatedBirthDate = birth_date pcapi_repository.repository.save(user) @@ -767,7 +760,7 @@ def get_first_registration_date( fraud_check.get_min_date_between_creation_and_registration() for fraud_check in fraud_checks if fraud_check.eligibilityType == eligibility - and users_api.is_user_age_compatible_with_eligibility( + and eligibility_api.is_user_age_compatible_with_eligibility( users_utils.get_age_at_date( birth_date, fraud_check.get_min_date_between_creation_and_registration(), user.departementCode ), diff --git a/api/src/pcapi/core/subscription/dms/api.py b/api/src/pcapi/core/subscription/dms/api.py index b6efe3b0c11..34752eaa19c 100644 --- a/api/src/pcapi/core/subscription/dms/api.py +++ b/api/src/pcapi/core/subscription/dms/api.py @@ -16,6 +16,7 @@ from pcapi.core.subscription import models as subscription_models from pcapi.core.subscription.dms import dms_internal_mailing from pcapi.core.subscription.dms import messages +from pcapi.core.users import eligibility_api from pcapi.core.users import models as users_models from pcapi.core.users.repository import find_user_by_email from pcapi.domain.demarches_simplifiees import update_demarches_simplifiees_text_annotations @@ -65,7 +66,7 @@ def _update_fraud_check_with_new_content( fraud_check.reason = None fraud_check.reasonCodes = [] fraud_check.resultContent = new_content.dict() - new_eligibility = fraud_api.decide_eligibility( + new_eligibility = eligibility_api.decide_eligibility( fraud_check.user, new_content.get_birth_date(), new_content.get_registration_datetime() ) if new_eligibility != fraud_check.eligibilityType: @@ -172,7 +173,7 @@ def handle_dms_application( fraud_check = fraud_dms_api.get_fraud_check(user, application_number) if fraud_check is None: eligibility_type = ( - fraud_api.decide_eligibility( + eligibility_api.decide_eligibility( user, application_content.get_birth_date(), application_content.get_registration_datetime() ) if application_content diff --git a/api/src/pcapi/core/users/api.py b/api/src/pcapi/core/users/api.py index 3d9f0f32887..5049b705f4c 100644 --- a/api/src/pcapi/core/users/api.py +++ b/api/src/pcapi/core/users/api.py @@ -1024,65 +1024,6 @@ def reset_recredit_amount_to_show(user: models.User) -> None: repository.save(user) -def get_eligibility_end_datetime( - date_of_birth: datetime.date | datetime.datetime | None, -) -> datetime.datetime | None: - if not date_of_birth: - return None - - return datetime.datetime.combine(date_of_birth, datetime.time(0, 0)) + relativedelta( - years=constants.ELIGIBILITY_AGE_18 + 1, hour=11 - ) - - -def get_eligibility_start_datetime( - date_of_birth: datetime.date | datetime.datetime | None, -) -> datetime.datetime | None: - if not date_of_birth: - return None - - date_of_birth = datetime.datetime.combine(date_of_birth, datetime.time(0, 0)) - fifteenth_birthday = date_of_birth + relativedelta(years=constants.ELIGIBILITY_UNDERAGE_RANGE[0]) - - return fifteenth_birthday - - -def get_eligibility_at_date( - date_of_birth: datetime.date | None, specified_datetime: datetime.datetime, department_code: str | None = None -) -> models.EligibilityType | None: - eligibility_start = get_eligibility_start_datetime(date_of_birth) - eligibility_end = get_eligibility_end_datetime(date_of_birth) - - if not date_of_birth or not (eligibility_start <= specified_datetime < eligibility_end): # type: ignore[operator] - return None - - age = users_utils.get_age_at_date(date_of_birth, specified_datetime, department_code) - if not age: - return None - - if age in constants.ELIGIBILITY_UNDERAGE_RANGE: - return models.EligibilityType.UNDERAGE - # If the user is older than 18 in UTC timezone, we consider them eligible until they reach eligibility_end - if constants.ELIGIBILITY_AGE_18 <= age and specified_datetime < eligibility_end: # type: ignore[operator] - return models.EligibilityType.AGE18 - - return None - - -def is_eligible_for_beneficiary_upgrade(user: models.User, eligibility: models.EligibilityType | None) -> bool: - return (eligibility == models.EligibilityType.UNDERAGE and not user.is_beneficiary) or ( - eligibility == models.EligibilityType.AGE18 and not user.has_beneficiary_role - ) - - -def is_user_age_compatible_with_eligibility(user_age: int | None, eligibility: models.EligibilityType | None) -> bool: - if eligibility == models.EligibilityType.UNDERAGE: - return user_age in constants.ELIGIBILITY_UNDERAGE_RANGE - if eligibility == models.EligibilityType.AGE18: - return user_age is not None and user_age >= constants.ELIGIBILITY_AGE_18 - return False - - def _filter_user_accounts(accounts: BaseQuery, search_term: str) -> BaseQuery: filters = [] name_term = None diff --git a/api/src/pcapi/core/users/eligibility_api.py b/api/src/pcapi/core/users/eligibility_api.py new file mode 100644 index 00000000000..b251ae4a983 --- /dev/null +++ b/api/src/pcapi/core/users/eligibility_api.py @@ -0,0 +1,123 @@ +import datetime + +from dateutil.relativedelta import relativedelta + +from pcapi.core.subscription import api as subscription_api +from pcapi.core.users import models as users_models +from pcapi.core.users import utils as users_utils +from pcapi.core.users import constants + + +class EligibilityError(Exception): + pass + + +def decide_eligibility( + user: users_models.User, + birth_date: datetime.date | None, + registration_datetime: datetime.datetime | None, +) -> users_models.EligibilityType | None: + """Returns the applicable eligibility of the user. + It may be the current eligibility of the user if the age is between 15 and 18, or it may be the eligibility AGE18 + if the user is over 19 and had previously tried to register when the age was 18. + """ + + if birth_date is None: + return None + + user_age_today = users_utils.get_age_from_birth_date(birth_date, user.departementCode) + if user_age_today < 15: + return None + if user_age_today < 18: + return users_models.EligibilityType.UNDERAGE + if user_age_today == 18: + return users_models.EligibilityType.AGE18 + + eligibility_today = get_eligibility_at_date(birth_date, datetime.datetime.utcnow(), user.departementCode) + if eligibility_today == users_models.EligibilityType.AGE18: + return users_models.EligibilityType.AGE18 + + eligibility_at_registration = ( + get_eligibility_at_date(birth_date, registration_datetime, user.departementCode) + if registration_datetime + else None + ) + if eligibility_at_registration is None and eligibility_today is None and user_age_today == 19: + earliest_identity_check_date = subscription_api.get_first_registration_date( + user, birth_date, users_models.EligibilityType.AGE18 + ) + if earliest_identity_check_date: + return get_eligibility_at_date(birth_date, earliest_identity_check_date, user.departementCode) + + return eligibility_at_registration + + +def get_eligibility_at_date( + date_of_birth: datetime.date | None, specified_datetime: datetime.datetime, department_code: str | None = None +) -> users_models.EligibilityType | None: + eligibility_start = get_eligibility_start_datetime(date_of_birth) + eligibility_end = get_eligibility_end_datetime(date_of_birth) + + if not date_of_birth or not (eligibility_start <= specified_datetime < eligibility_end): # type: ignore[operator] + return None + + age = users_utils.get_age_at_date(date_of_birth, specified_datetime, department_code) + if not age: + return None + + if age in constants.ELIGIBILITY_UNDERAGE_RANGE: + return users_models.EligibilityType.UNDERAGE + # If the user is older than 18 in UTC timezone, we consider them eligible until they reach eligibility_end + if constants.ELIGIBILITY_AGE_18 <= age and specified_datetime < eligibility_end: # type: ignore[operator] + return users_models.EligibilityType.AGE18 + + return None + + +def get_eligibility_start_datetime( + date_of_birth: datetime.date | datetime.datetime | None, +) -> datetime.datetime | None: + if not date_of_birth: + return None + + date_of_birth = datetime.datetime.combine(date_of_birth, datetime.time(0, 0)) + fifteenth_birthday = date_of_birth + relativedelta(years=constants.ELIGIBILITY_UNDERAGE_RANGE[0]) + + return fifteenth_birthday + + +def get_eligibility_end_datetime( + date_of_birth: datetime.date | datetime.datetime | None, +) -> datetime.datetime | None: + if not date_of_birth: + return None + + return datetime.datetime.combine(date_of_birth, datetime.time(0, 0)) + relativedelta( + years=constants.ELIGIBILITY_AGE_18 + 1, hour=11 + ) + + +def is_eligibility_activable(user: users_models.User, eligibility: users_models.EligibilityType | None) -> bool: + return ( + user.eligibility == eligibility + and is_eligible_for_beneficiary_upgrade(user, eligibility) + and is_user_age_compatible_with_eligibility(user.age, eligibility) + ) + + +def is_eligible_for_beneficiary_upgrade( + user: users_models.User, eligibility: users_models.EligibilityType | None +) -> bool: + return (eligibility == users_models.EligibilityType.UNDERAGE and not user.is_beneficiary) or ( + eligibility == users_models.EligibilityType.AGE18 and not user.has_beneficiary_role + ) + + +def is_user_age_compatible_with_eligibility( + user_age: int | None, eligibility: users_models.EligibilityType | None +) -> bool: + if eligibility == users_models.EligibilityType.UNDERAGE: + return user_age in constants.ELIGIBILITY_UNDERAGE_RANGE + if eligibility == users_models.EligibilityType.AGE18: + return user_age is not None and user_age >= constants.ELIGIBILITY_AGE_18 + return False diff --git a/api/src/pcapi/core/users/models.py b/api/src/pcapi/core/users/models.py index 207b1edb4e0..e364b0cec28 100644 --- a/api/src/pcapi/core/users/models.py +++ b/api/src/pcapi/core/users/models.py @@ -389,9 +389,9 @@ def deposit_version(self) -> int | None: @property def eligibility(self) -> EligibilityType | None: - from pcapi.core.fraud import api as fraud_api + from pcapi.core.users import eligibility_api - return fraud_api.decide_eligibility(self, self.birth_date, datetime.utcnow()) + return eligibility_api.decide_eligibility(self, self.birth_date, datetime.utcnow()) @hybrid_property def full_name(self) -> str: diff --git a/api/src/pcapi/routes/backoffice/accounts/blueprint.py b/api/src/pcapi/routes/backoffice/accounts/blueprint.py index c5be6465606..c196c545091 100644 --- a/api/src/pcapi/routes/backoffice/accounts/blueprint.py +++ b/api/src/pcapi/routes/backoffice/accounts/blueprint.py @@ -38,6 +38,7 @@ from pcapi.core.subscription.phone_validation import exceptions as phone_validation_exceptions from pcapi.core.users import api as users_api from pcapi.core.users import constants as users_constants +from pcapi.core.users import eligibility_api from pcapi.core.users import exceptions as users_exceptions from pcapi.core.users import models as users_models from pcapi.core.users import utils as users_utils @@ -332,7 +333,7 @@ def render_public_account_details( history = get_public_account_history(user) duplicate_user_id = None eligibility_history = get_eligibility_history(user) - user_current_eligibility = users_api.get_eligibility_at_date(user.birth_date, datetime.datetime.utcnow()) + user_current_eligibility = eligibility_api.get_eligibility_at_date(user.birth_date, datetime.datetime.utcnow()) if ( user_current_eligibility is not None and user_current_eligibility.value in eligibility_history diff --git a/api/src/pcapi/routes/native/v1/serialization/account.py b/api/src/pcapi/routes/native/v1/serialization/account.py index 4846827f588..9acf5e17c97 100644 --- a/api/src/pcapi/routes/native/v1/serialization/account.py +++ b/api/src/pcapi/routes/native/v1/serialization/account.py @@ -19,6 +19,7 @@ from pcapi.core.subscription import profile_options from pcapi.core.users import api as users_api from pcapi.core.users import constants as users_constants +from pcapi.core.users import eligibility_api from pcapi.core.users import young_status import pcapi.core.users.models as users_models from pcapi.core.users.utils import decode_jwt_token @@ -149,9 +150,9 @@ def get(self, key: str, default: typing.Any | None = None) -> typing.Any: if key == "domainsCredit": return users_api.get_domains_credit(user) if key == "eligibilityEndDatetime": - return users_api.get_eligibility_end_datetime(user.birth_date) + return eligibility_api.get_eligibility_end_datetime(user.birth_date) if key == "eligibilityStartDatetime": - return users_api.get_eligibility_start_datetime(user.birth_date) + return eligibility_api.get_eligibility_start_datetime(user.birth_date) if key == "firstDepositActivationDate": return user.first_deposit_activation_date if key == "firstName": @@ -161,7 +162,7 @@ def get(self, key: str, default: typing.Any | None = None) -> typing.Any: if key == "isBeneficiary": return user.is_beneficiary if key == "isEligibleForBeneficiaryUpgrade": - return users_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility) + return eligibility_api.is_eligible_for_beneficiary_upgrade(user, user.eligibility) if key == "needsToFillCulturalSurvey": return user.needsToFillCulturalSurvey and user.is_eligible and _is_cultural_survey_active() if key == "requiresIdCheck": diff --git a/api/tests/core/fraud/test_api.py b/api/tests/core/fraud/test_api.py index 107429fc841..d1cd5fd167d 100644 --- a/api/tests/core/fraud/test_api.py +++ b/api/tests/core/fraud/test_api.py @@ -655,200 +655,6 @@ def test_user_not_eligible_anymore_but_has_performed(self): assert fraud_api.has_user_performed_identity_check(user) -@pytest.mark.usefixtures("db_session") -class DecideEligibilityTest: - def test_19yo_is_eligible_if_application_at_18_yo(self): - today = datetime.date.today() - birth_date = today - relativedelta(years=19, days=1) - user = users_factories.UserFactory() - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, - type=fraud_models.FraudCheckType.UBBLE, - eligibilityType=users_models.EligibilityType.AGE18, - dateCreated=today - relativedelta(years=1), - ) - - dms_content = fraud_factories.DMSContentFactory( - registration_datetime=datetime.datetime.combine(today, datetime.datetime.min.time()), birth_date=birth_date - ) - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content - ) - - result = fraud_api.decide_eligibility( - user, dms_content.get_birth_date(), dms_content.get_registration_datetime() - ) - assert result == users_models.EligibilityType.AGE18 - - def test_19yo_not_eligible(self): - today = datetime.date.today() - birth_date = today - relativedelta(years=19, days=1) - user = users_factories.UserFactory() - - dms_content = fraud_factories.DMSContentFactory( - registration_datetime=datetime.datetime.combine(today, datetime.datetime.min.time()), birth_date=birth_date - ) - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content - ) - - result = fraud_api.decide_eligibility( - user, dms_content.get_birth_date(), dms_content.get_registration_datetime() - ) - assert result is None - - def test_19yo_ex_underage_not_eligible(self): - today = datetime.date.today() - birth_date = today - relativedelta(years=19, days=1) - user = users_factories.UserFactory() - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, - type=fraud_models.FraudCheckType.EDUCONNECT, - dateCreated=today - relativedelta(years=3, days=1), - eligibilityType=users_models.EligibilityType.UNDERAGE, - ) - dms_content = fraud_factories.DMSContentFactory( - registration_datetime=datetime.datetime.combine(today, datetime.datetime.min.time()), birth_date=birth_date - ) - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content - ) - - result = fraud_api.decide_eligibility( - user, dms_content.get_birth_date(), dms_content.get_registration_datetime() - ) - assert result is None - - def test_18yo_eligible(self): - today = datetime.date.today() - birth_date = today - relativedelta(years=19, days=1) - user = users_factories.UserFactory() - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, - type=fraud_models.FraudCheckType.UBBLE, - eligibilityType=users_models.EligibilityType.AGE18, - dateCreated=today - relativedelta(years=1), - ) - dms_content = fraud_factories.DMSContentFactory( - registration_datetime=datetime.datetime.combine( - today - relativedelta(years=1, days=-1), datetime.datetime.min.time() - ), - birth_date=birth_date, - ) - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content - ) - - result = fraud_api.decide_eligibility( - user, dms_content.get_birth_date(), dms_content.get_registration_datetime() - ) - assert result == users_models.EligibilityType.AGE18 - - def test_18yo_underage_eligible(self): - today = datetime.date.today() - birth_date = today - relativedelta(years=18, days=1) - user = users_factories.UserFactory() - dms_content = fraud_factories.DMSContentFactory( - registration_datetime=datetime.datetime.combine( - today - relativedelta(years=1, days=-1), datetime.datetime.min.time() - ), - birth_date=birth_date, - ) - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content - ) - - result = fraud_api.decide_eligibility( - user, dms_content.get_birth_date(), dms_content.get_registration_datetime() - ) - assert result == users_models.EligibilityType.AGE18 - - @time_machine.travel("2022-03-01") - def test_decide_eligibility_for_underage_users(self): - # All 15-17 users are eligible after 2022-01-01 - for age in range(15, 18): - user = users_factories.UserFactory(dateOfBirth=datetime.datetime.utcnow() - relativedelta(years=age)) - birth_date = user.dateOfBirth - registration_datetime = datetime.datetime.today() - - assert ( - fraud_api.decide_eligibility(user, birth_date, registration_datetime) - == users_models.EligibilityType.UNDERAGE - ) - - @time_machine.travel("2022-03-01 13:45:00") # 2022-03-02 00:45 in Noumea timezone - def test_decide_underage_eligibility_with_timezone(self): - today = datetime.date.today() - march_2nd_fifteen_years_ago = today - relativedelta(years=15, days=-1) - new_caledonian_user = users_factories.UserFactory( - dateOfBirth=march_2nd_fifteen_years_ago, departementCode="988" - ) - - eligibility = fraud_api.decide_eligibility(new_caledonian_user, march_2nd_fifteen_years_ago, today) - - assert eligibility == users_models.EligibilityType.UNDERAGE - - @time_machine.travel("2022-01-01") - def test_decide_eligibility_for_18_yo_users_is_always_age_18(self): - # 18 users are always eligible - birth_date = datetime.datetime.utcnow() - relativedelta(years=18) - user = users_factories.UserFactory() - - assert ( - fraud_api.decide_eligibility(user, birth_date, datetime.datetime.today()) - == users_models.EligibilityType.AGE18 - ) - assert fraud_api.decide_eligibility(user, birth_date, None) == users_models.EligibilityType.AGE18 - assert ( - fraud_api.decide_eligibility(user, birth_date, datetime.datetime.utcnow() - relativedelta(years=1)) - == users_models.EligibilityType.AGE18 - ) - - @time_machine.travel("2022-07-01") - @pytest.mark.parametrize( - "first_registration_datetime,expected_eligibility", - [ - (None, None), - (datetime.datetime(year=2022, month=1, day=15), None), - ( - datetime.datetime(year=2021, month=12, day=1), - users_models.EligibilityType.AGE18, - ), - ], - ) - # 19yo users are eligible if they have started registration before turning 19 - def test_decide_eligibility_for_19_yo_users(self, first_registration_datetime, expected_eligibility): - birth_date = datetime.datetime(year=2003, month=1, day=1) - user = users_factories.UserFactory() - - if first_registration_datetime: - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, - type=fraud_models.FraudCheckType.DMS, - resultContent=fraud_factories.DMSContentFactory( - registration_datetime=first_registration_datetime, birth_date=birth_date - ), - ) - - assert fraud_api.decide_eligibility(user, birth_date, datetime.datetime.today()) == expected_eligibility - - def test_decide_eligibility_for_19_yo_users_with_no_registration_datetime(self): - today = datetime.date.today() - birth_date = today - relativedelta(years=19, days=1) - user = users_factories.UserFactory() - fraud_factories.BeneficiaryFraudCheckFactory( - user=user, - type=fraud_models.FraudCheckType.DMS, - resultContent=fraud_factories.DMSContentFactory( - registration_datetime=None, - birth_date=birth_date, - ), - dateCreated=today - relativedelta(days=3), - ) - - assert fraud_api.decide_eligibility(user, birth_date, None) == users_models.EligibilityType.AGE18 - - @pytest.mark.usefixtures("db_session") class DuplicateBeneficiaryEmailTest: diff --git a/api/tests/core/subscription/test_api.py b/api/tests/core/subscription/test_api.py index 7d84747956a..e1e354f4da6 100644 --- a/api/tests/core/subscription/test_api.py +++ b/api/tests/core/subscription/test_api.py @@ -555,7 +555,7 @@ def test_subscription_is_possible_if_flag_is_false(self): @pytest.mark.usefixtures("db_session") -class CommonSubscritpionTest: +class CommonSubscriptionTest: def test_handle_eligibility_difference_between_declaration_and_identity_provider_no_difference(self): user = users_factories.UserFactory() fraud_check = fraud_factories.BeneficiaryFraudCheckFactory( diff --git a/api/tests/core/users/test_api.py b/api/tests/core/users/test_api.py index d407fc3c669..cfc5a5dfbe3 100644 --- a/api/tests/core/users/test_api.py +++ b/api/tests/core/users/test_api.py @@ -1308,17 +1308,6 @@ def test_no_update(self): assert len(sendinblue_testing.sendinblue_requests) == 0 -class GetEligibilityTest: - def test_get_eligibility_at_date_timezones_tolerance(self): - date_of_birth = datetime.datetime(2000, 2, 1, 0, 0) - - specified_date = datetime.datetime(2019, 2, 1, 8, 0) - assert users_api.get_eligibility_at_date(date_of_birth, specified_date) == users_models.EligibilityType.AGE18 - - specified_date = datetime.datetime(2019, 2, 1, 12, 0) - assert users_api.get_eligibility_at_date(date_of_birth, specified_date) is None - - class UserEmailValidationTest: def test_validate_pro_user_email_from_pro_ff_on(self): user_offerer = offerers_factories.UserOffererFactory(user__isEmailValidated=False) diff --git a/api/tests/core/users/test_eligibility.py b/api/tests/core/users/test_eligibility.py new file mode 100644 index 00000000000..bfb67cd4be9 --- /dev/null +++ b/api/tests/core/users/test_eligibility.py @@ -0,0 +1,218 @@ +import datetime + +from dateutil.relativedelta import relativedelta +import pytest +import time_machine + +from pcapi.core.fraud import factories as fraud_factories +from pcapi.core.fraud import models as fraud_models +from pcapi.core.users import eligibility_api +from pcapi.core.users import factories as users_factories +from pcapi.core.users import models as users_models + + +@pytest.mark.usefixtures("db_session") +class DecideEligibilityTest: + def test_19yo_is_eligible_if_application_at_18_yo(self): + today = datetime.date.today() + birth_date = today - relativedelta(years=19, days=1) + user = users_factories.UserFactory() + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.UBBLE, + eligibilityType=users_models.EligibilityType.AGE18, + dateCreated=today - relativedelta(years=1), + ) + + dms_content = fraud_factories.DMSContentFactory( + registration_datetime=datetime.datetime.combine(today, datetime.datetime.min.time()), birth_date=birth_date + ) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content + ) + + result = eligibility_api.decide_eligibility( + user, dms_content.get_birth_date(), dms_content.get_registration_datetime() + ) + assert result == users_models.EligibilityType.AGE18 + + def test_19yo_not_eligible(self): + today = datetime.date.today() + birth_date = today - relativedelta(years=19, days=1) + user = users_factories.UserFactory() + + dms_content = fraud_factories.DMSContentFactory( + registration_datetime=datetime.datetime.combine(today, datetime.datetime.min.time()), birth_date=birth_date + ) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content + ) + + result = eligibility_api.decide_eligibility( + user, dms_content.get_birth_date(), dms_content.get_registration_datetime() + ) + assert result is None + + def test_19yo_ex_underage_not_eligible(self): + today = datetime.date.today() + birth_date = today - relativedelta(years=19, days=1) + user = users_factories.UserFactory() + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.EDUCONNECT, + dateCreated=today - relativedelta(years=3, days=1), + eligibilityType=users_models.EligibilityType.UNDERAGE, + ) + dms_content = fraud_factories.DMSContentFactory( + registration_datetime=datetime.datetime.combine(today, datetime.datetime.min.time()), birth_date=birth_date + ) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content + ) + + result = eligibility_api.decide_eligibility( + user, dms_content.get_birth_date(), dms_content.get_registration_datetime() + ) + assert result is None + + def test_18yo_eligible(self): + today = datetime.date.today() + birth_date = today - relativedelta(years=19, days=1) + user = users_factories.UserFactory() + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.UBBLE, + eligibilityType=users_models.EligibilityType.AGE18, + dateCreated=today - relativedelta(years=1), + ) + dms_content = fraud_factories.DMSContentFactory( + registration_datetime=datetime.datetime.combine( + today - relativedelta(years=1, days=-1), datetime.datetime.min.time() + ), + birth_date=birth_date, + ) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content + ) + + result = eligibility_api.decide_eligibility( + user, dms_content.get_birth_date(), dms_content.get_registration_datetime() + ) + assert result == users_models.EligibilityType.AGE18 + + def test_18yo_underage_eligible(self): + today = datetime.date.today() + birth_date = today - relativedelta(years=18, days=1) + user = users_factories.UserFactory() + dms_content = fraud_factories.DMSContentFactory( + registration_datetime=datetime.datetime.combine( + today - relativedelta(years=1, days=-1), datetime.datetime.min.time() + ), + birth_date=birth_date, + ) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content + ) + + result = eligibility_api.decide_eligibility( + user, dms_content.get_birth_date(), dms_content.get_registration_datetime() + ) + assert result == users_models.EligibilityType.AGE18 + + @time_machine.travel("2022-03-01") + def test_decide_eligibility_for_underage_users(self): + # All 15-17 users are eligible after 2022-01-01 + for age in range(15, 18): + user = users_factories.UserFactory(dateOfBirth=datetime.datetime.utcnow() - relativedelta(years=age)) + birth_date = user.dateOfBirth + registration_datetime = datetime.datetime.today() + + assert ( + eligibility_api.decide_eligibility(user, birth_date, registration_datetime) + == users_models.EligibilityType.UNDERAGE + ) + + @time_machine.travel("2022-03-01 13:45:00") # 2022-03-02 00:45 in Noumea timezone + def test_decide_underage_eligibility_with_timezone(self): + today = datetime.date.today() + march_2nd_fifteen_years_ago = today - relativedelta(years=15, days=-1) + new_caledonian_user = users_factories.UserFactory( + dateOfBirth=march_2nd_fifteen_years_ago, departementCode="988" + ) + + eligibility = eligibility_api.decide_eligibility(new_caledonian_user, march_2nd_fifteen_years_ago, today) + + assert eligibility == users_models.EligibilityType.UNDERAGE + + @time_machine.travel("2022-01-01") + def test_decide_eligibility_for_18_yo_users_is_always_age_18(self): + # 18 users are always eligible + birth_date = datetime.datetime.utcnow() - relativedelta(years=18) + user = users_factories.UserFactory() + + assert ( + eligibility_api.decide_eligibility(user, birth_date, datetime.datetime.today()) + == users_models.EligibilityType.AGE18 + ) + assert eligibility_api.decide_eligibility(user, birth_date, None) == users_models.EligibilityType.AGE18 + assert ( + eligibility_api.decide_eligibility(user, birth_date, datetime.datetime.utcnow() - relativedelta(years=1)) + == users_models.EligibilityType.AGE18 + ) + + @time_machine.travel("2022-07-01") + @pytest.mark.parametrize( + "first_registration_datetime,expected_eligibility", + [ + (None, None), + (datetime.datetime(year=2022, month=1, day=15), None), + ( + datetime.datetime(year=2021, month=12, day=1), + users_models.EligibilityType.AGE18, + ), + ], + ) + # 19yo users are eligible if they have started registration before turning 19 + def test_decide_eligibility_for_19_yo_users(self, first_registration_datetime, expected_eligibility): + birth_date = datetime.datetime(year=2003, month=1, day=1) + user = users_factories.UserFactory() + + if first_registration_datetime: + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.DMS, + resultContent=fraud_factories.DMSContentFactory( + registration_datetime=first_registration_datetime, birth_date=birth_date + ), + ) + + assert eligibility_api.decide_eligibility(user, birth_date, datetime.datetime.today()) == expected_eligibility + + def test_decide_eligibility_for_19_yo_users_with_no_registration_datetime(self): + today = datetime.date.today() + birth_date = today - relativedelta(years=19, days=1) + user = users_factories.UserFactory() + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.DMS, + resultContent=fraud_factories.DMSContentFactory( + registration_datetime=None, + birth_date=birth_date, + ), + dateCreated=today - relativedelta(days=3), + ) + + assert eligibility_api.decide_eligibility(user, birth_date, None) == users_models.EligibilityType.AGE18 + + +class GetEligibilityTest: + def test_get_eligibility_at_date_timezones_tolerance(self): + date_of_birth = datetime.datetime(2000, 2, 1, 0, 0) + + specified_date = datetime.datetime(2019, 2, 1, 8, 0) + assert ( + eligibility_api.get_eligibility_at_date(date_of_birth, specified_date) == users_models.EligibilityType.AGE18 + ) + + specified_date = datetime.datetime(2019, 2, 1, 12, 0) + assert eligibility_api.get_eligibility_at_date(date_of_birth, specified_date) is None