diff --git a/api/src/pcapi/core/finance/api.py b/api/src/pcapi/core/finance/api.py index fae59a52802..618ff6d868e 100644 --- a/api/src/pcapi/core/finance/api.py +++ b/api/src/pcapi/core/finance/api.py @@ -3099,7 +3099,7 @@ def _has_celebrated_birthday_since_credit_or_registration(user: users_models.Use if user.deposit and user.deposit.dateCreated and (user.deposit.dateCreated.date() < latest_birthday_date): return True - first_registration_datetime = subscription_api.get_first_registration_date( + first_registration_datetime = subscription_api.get_first_registration_date_with_eligibility( user, user.validatedBirthDate, users_models.EligibilityType.UNDERAGE ) if first_registration_datetime is None: @@ -3157,7 +3157,7 @@ def _get_known_age_at_deposit(user: users_models.User) -> int | None: if known_birthday_at_deposit is None: return None - first_registration_date = subscription_api.get_first_registration_date( + first_registration_date = subscription_api.get_first_registration_date_with_eligibility( user, known_birthday_at_deposit, users_models.EligibilityType.UNDERAGE ) if first_registration_date is not None: diff --git a/api/src/pcapi/core/fraud/common/models.py b/api/src/pcapi/core/fraud/common/models.py index 0e02a043457..9f3881de5a4 100644 --- a/api/src/pcapi/core/fraud/common/models.py +++ b/api/src/pcapi/core/fraud/common/models.py @@ -60,7 +60,7 @@ 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 = eligibility_api.get_eligibility_at_date( + eligibility_at_registration = eligibility_api.get_extended_eligibility_at_date( birth_date, registration_datetime, department ) diff --git a/api/src/pcapi/core/subscription/api.py b/api/src/pcapi/core/subscription/api.py index d42f84a6b82..524c92fa303 100644 --- a/api/src/pcapi/core/subscription/api.py +++ b/api/src/pcapi/core/subscription/api.py @@ -45,7 +45,7 @@ def _get_age_at_first_registration(user: users_models.User, eligibility: users_m if not user.birth_date: return None - first_registration_date = get_first_registration_date(user, user.birth_date, eligibility) + first_registration_date = get_first_registration_date_with_eligibility(user, user.birth_date, eligibility) if not first_registration_date: return user.age @@ -506,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 eligibility_api.get_eligibility_at_date( + and not eligibility_api.get_extended_eligibility_at_date( user.birth_date, identity_fraud_check.get_min_date_between_creation_and_registration(), user.departementCode ) ) @@ -747,7 +747,30 @@ def update_user_birth_date_if_not_beneficiary(user: users_models.User, birth_dat pcapi_repository.repository.save(user) -def get_first_registration_date( +def get_first_registration_date_with_age( + user: users_models.User, + birth_date: datetime.date | None, +) -> datetime.datetime | None: + fraud_checks = user.beneficiaryFraudChecks + if not fraud_checks or not birth_date: + return None + + def _is_user_eligible_at_fraud_check(fraud_check: fraud_models.BeneficiaryFraudCheck) -> bool: + age_at_fraud_check = users_utils.get_age_at_date( + birth_date, fraud_check.get_min_date_between_creation_and_registration(), user.departementCode + ) + return 15 <= age_at_fraud_check <= 18 + + registration_dates_when_eligible = [ + fraud_check.get_min_date_between_creation_and_registration() + for fraud_check in fraud_checks + if _is_user_eligible_at_fraud_check(fraud_check) + ] + + return min(registration_dates_when_eligible) if registration_dates_when_eligible else None + + +def get_first_registration_date_with_eligibility( user: users_models.User, birth_date: datetime.date | None, eligibility: users_models.EligibilityType, diff --git a/api/src/pcapi/core/users/eligibility_api.py b/api/src/pcapi/core/users/eligibility_api.py index b251ae4a983..b9c757c96a1 100644 --- a/api/src/pcapi/core/users/eligibility_api.py +++ b/api/src/pcapi/core/users/eligibility_api.py @@ -3,9 +3,13 @@ from dateutil.relativedelta import relativedelta from pcapi.core.subscription import api as subscription_api +from pcapi.core.users import constants from pcapi.core.users import models as users_models from pcapi.core.users import utils as users_utils -from pcapi.core.users import constants +from pcapi.models.feature import FeatureToggle + + +CREDIT_V3_DECREE_DATETIME = datetime.datetime(2025, 3, 3) class EligibilityError(Exception): @@ -21,10 +25,12 @@ def decide_eligibility( 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 + if FeatureToggle.WIP_ENABLE_CREDIT_V3.is_active(): + return decide_v3_credit_eligibility(user, birth_date, registration_datetime) + user_age_today = users_utils.get_age_from_birth_date(birth_date, user.departementCode) if user_age_today < 15: return None @@ -33,28 +39,70 @@ def decide_eligibility( if user_age_today == 18: return users_models.EligibilityType.AGE18 - eligibility_today = get_eligibility_at_date(birth_date, datetime.datetime.utcnow(), user.departementCode) + eligibility_today = get_extended_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) + get_extended_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( + earliest_identity_check_date = subscription_api.get_first_registration_date_with_eligibility( 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 get_extended_eligibility_at_date(birth_date, earliest_identity_check_date, user.departementCode) return eligibility_at_registration -def get_eligibility_at_date( +def decide_v3_credit_eligibility( + user: users_models.User, + birth_date: datetime.date, + registration_datetime: datetime.datetime | None, +) -> users_models.EligibilityType | None: + """ + An user eligibility determines what type of deposit can be granted. + Returns the first eligibility found using: + 1. the age at registration_datetime + 2. the age at first registration, looking into the user's fraud checks + 3. the current age. + + This function assumes that the user.beneficiaryFraudChecks relation is already loaded. + """ + user_age = users_utils.get_age_from_birth_date(birth_date, user.departementCode) + if user_age < 15 or user_age > 20: + return None + + eligibility: users_models.EligibilityType | None = None + + if registration_datetime: + eligibility = get_extended_eligibility_at_date(birth_date, registration_datetime, user.departementCode) + if eligibility: + return eligibility + + earliest_identity_check_date = subscription_api.get_first_registration_date_with_age(user, birth_date) + if earliest_identity_check_date: + eligibility = get_extended_eligibility_at_date(birth_date, earliest_identity_check_date, user.departementCode) + if eligibility: + return eligibility + + if 17 <= user_age <= 18: + eligibility = users_models.EligibilityType.AGE17_18 + + return eligibility + + +def get_extended_eligibility_at_date( date_of_birth: datetime.date | None, specified_datetime: datetime.datetime, department_code: str | None = None ) -> users_models.EligibilityType | None: + """ + Decide eligibility by computing the age at the specified datetime. + Extended eligibility means that this function considers a 19 year old eligible as long as the specified datetime + is contained between the eligibility datetime start and end. + """ eligibility_start = get_eligibility_start_datetime(date_of_birth) eligibility_end = get_eligibility_end_datetime(date_of_birth) @@ -65,11 +113,15 @@ def get_eligibility_at_date( 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 + if specified_datetime < CREDIT_V3_DECREE_DATETIME or not FeatureToggle.WIP_ENABLE_CREDIT_V3.is_active(): + 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 + + if FeatureToggle.WIP_ENABLE_CREDIT_V3.is_active() and age >= 17: + return users_models.EligibilityType.AGE17_18 return None @@ -108,11 +160,29 @@ def is_eligibility_activable(user: users_models.User, eligibility: users_models. def is_eligible_for_beneficiary_upgrade( user: users_models.User, eligibility: users_models.EligibilityType | None ) -> bool: + if FeatureToggle.WIP_ENABLE_CREDIT_V3.is_active(): + return is_eligible_for_next_recredit_activation_steps(user) 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_eligible_for_next_recredit_activation_steps(user: users_models.User) -> bool: + """ + Returns whether a user can undertake the activation steps to unlock the next deposit recredit + """ + if not user.age: + return False + + if not user.is_beneficiary: + return 17 <= user.age + + if not user.has_beneficiary_role: + return 18 <= user.age < 21 + + return False + + def is_user_age_compatible_with_eligibility( user_age: int | None, eligibility: users_models.EligibilityType | None ) -> bool: diff --git a/api/src/pcapi/core/users/models.py b/api/src/pcapi/core/users/models.py index e364b0cec28..5c6fe07c5a8 100644 --- a/api/src/pcapi/core/users/models.py +++ b/api/src/pcapi/core/users/models.py @@ -68,6 +68,8 @@ class UserRole(enum.Enum): class EligibilityType(enum.Enum): + AGE17_18 = "age-17-18" + # legacy eligibilities that are present in the database UNDERAGE = "underage" AGE18 = "age-18" diff --git a/api/src/pcapi/routes/backoffice/accounts/blueprint.py b/api/src/pcapi/routes/backoffice/accounts/blueprint.py index c196c545091..5ac04595d59 100644 --- a/api/src/pcapi/routes/backoffice/accounts/blueprint.py +++ b/api/src/pcapi/routes/backoffice/accounts/blueprint.py @@ -333,11 +333,11 @@ def render_public_account_details( history = get_public_account_history(user) duplicate_user_id = None eligibility_history = get_eligibility_history(user) - user_current_eligibility = eligibility_api.get_eligibility_at_date(user.birth_date, datetime.datetime.utcnow()) + user_current_eligibility = eligibility_api.get_extended_eligibility_at_date( + user.birth_date, datetime.datetime.utcnow() + ) - if ( - user_current_eligibility is not None and user_current_eligibility.value in eligibility_history - ): # get_eligibility_at_date might return None + if user_current_eligibility is not None and user_current_eligibility.value in eligibility_history: subscription_items = [ item for item in eligibility_history[user_current_eligibility.value].subscriptionItems diff --git a/api/tests/core/bookings/test_api.py b/api/tests/core/bookings/test_api.py index 8ee106cd46a..9014c1db1ec 100644 --- a/api/tests/core/bookings/test_api.py +++ b/api/tests/core/bookings/test_api.py @@ -1895,8 +1895,8 @@ def test_num_queries(self): educational_factories.CollectiveBookingFactory(collectiveStock__startDatetime=event_date) educational_factories.CollectiveBookingFactory(collectiveStock__startDatetime=event_date) - queries = 1 # select feature flag - queries += 1 # select individual bookings + # queries = 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports + queries = 1 # select individual bookings queries += 1 # select individual booking user achievements # fmt: off queries += 2 * ( diff --git a/api/tests/core/bookings/test_repository.py b/api/tests/core/bookings/test_repository.py index 88fef161081..6cd337e184b 100644 --- a/api/tests/core/bookings/test_repository.py +++ b/api/tests/core/bookings/test_repository.py @@ -1371,7 +1371,6 @@ def _validate_csv_row( @pytest.mark.features(WIP_ENABLE_OFFER_ADDRESS=True, WIP_USE_OFFERER_ADDRESS_AS_DATA_SOURCE=True) def should_return_validated_bookings_for_offer(self): - beneficiary = users_factories.BeneficiaryGrant18Factory( email="beneficiary@example.com", firstName="Ron", lastName="Weasley", postalCode="97300" ) @@ -1402,7 +1401,7 @@ def should_return_validated_bookings_for_offer(self): bookings_factories.BookingFactory(stock=stock_2) queries = 0 - queries += 1 # Get feature + # queries += 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports queries += 1 # Get bookings offer_id = offer.id @@ -1459,7 +1458,7 @@ def should_return_validated_bookings_for_offer_with_old_cancelled_booking(self): bookings_factories.BookingFactory(stock=stock_2) queries = 0 - queries += 1 # Get feature + # queries += 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports queries += 1 # Get bookings offer_id = offer.id @@ -1684,7 +1683,6 @@ def _validate_csv_row( assert data_dict["Duo"] == duo def should_return_validated_bookings_for_offer(self): - beneficiary = users_factories.BeneficiaryGrant18Factory( email="beneficiary@example.com", firstName="Ron", lastName="Weasley", postalCode="97300" ) @@ -1715,7 +1713,7 @@ def should_return_validated_bookings_for_offer(self): bookings_factories.BookingFactory(stock=stock_2) queries = 0 - queries += 1 # Get feature + # queries += 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports queries += 1 # Get bookings offer_id = offer.id @@ -1737,7 +1735,6 @@ def should_return_validated_bookings_for_offer(self): ) def should_return_validated_bookings_for_offer_with_offerer_address_as_data_source(self): - beneficiary = users_factories.BeneficiaryGrant18Factory( email="beneficiary@example.com", firstName="Ron", lastName="Weasley", postalCode="97300" ) @@ -1768,7 +1765,7 @@ def should_return_validated_bookings_for_offer_with_offerer_address_as_data_sour bookings_factories.BookingFactory(stock=stock_2) queries = 0 - queries += 1 # Get feature + # queries += 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports queries += 1 # Get bookings offer_id = offer.id @@ -1790,7 +1787,6 @@ def should_return_validated_bookings_for_offer_with_offerer_address_as_data_sour ) def should_return_validated_bookings_for_offer_with_old_cancelled_booking(self): - beneficiary = users_factories.BeneficiaryGrant18Factory( email="beneficiary@example.com", firstName="Ron", lastName="Weasley", postalCode="97300" ) @@ -1824,7 +1820,7 @@ def should_return_validated_bookings_for_offer_with_old_cancelled_booking(self): bookings_factories.BookingFactory(stock=stock_2) queries = 0 - queries += 1 # Get feature + # queries += 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports queries += 1 # Get bookings offer_id = offer.id @@ -2073,7 +2069,6 @@ def _validate_excel_row( assert sheet.cell(row=row, column=18).value == duo def should_return_validated_bookings_for_offer_with_offerer_address_as_data_source(self): - beneficiary = users_factories.BeneficiaryGrant18Factory( email="beneficiary@example.com", firstName="Ron", lastName="Weasley", postalCode="97300" ) @@ -2105,7 +2100,7 @@ def should_return_validated_bookings_for_offer_with_offerer_address_as_data_sour queries = 0 queries += 1 # select offer - queries += 1 # select feature + # queries += 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports queries += 1 # select booking with assert_num_queries(queries): @@ -2423,7 +2418,6 @@ def _validate_excel_row( assert sheet.cell(row=row, column=18).value == duo def should_return_validated_bookings_for_offer(self): - beneficiary = users_factories.BeneficiaryGrant18Factory( email="beneficiary@example.com", firstName="Ron", lastName="Weasley", postalCode="97300" ) @@ -2455,7 +2449,7 @@ def should_return_validated_bookings_for_offer(self): queries = 0 queries += 1 # select offer - queries += 1 # select feature + # queries += 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports queries += 1 # select booking with assert_num_queries(queries): diff --git a/api/tests/core/external/external_pro_test.py b/api/tests/core/external/external_pro_test.py index a1d61ef9bcf..2ac609b1b06 100644 --- a/api/tests/core/external/external_pro_test.py +++ b/api/tests/core/external/external_pro_test.py @@ -268,6 +268,8 @@ def test_update_external_pro_user_attributes( ) num_queries = EXPECTED_PRO_ATTR_NUM_QUERIES + if create_booking: + num_queries -= 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports with assert_num_queries(num_queries): attributes = get_pro_attributes(email) diff --git a/api/tests/core/external/external_users_test.py b/api/tests/core/external/external_users_test.py index 9ac354e1c63..8f932a4b750 100644 --- a/api/tests/core/external/external_users_test.py +++ b/api/tests/core/external/external_users_test.py @@ -205,6 +205,7 @@ def test_get_user_attributes_beneficiary_with_v1_deposit(): ) +@pytest.mark.features(WIP_ENABLE_CREDIT_V3=False) def test_get_user_attributes_ex_beneficiary_because_of_expiration(): with time_machine.travel(datetime.utcnow() - relativedelta(years=2, days=2)): user = BeneficiaryGrant18Factory( @@ -370,6 +371,7 @@ def test_get_user_attributes_ex_underage_beneficiary_who_did_not_claim_credit_18 assert attributes.deposits_count == 1 +@pytest.mark.features(WIP_ENABLE_CREDIT_V3=False) def test_get_user_attributes_ex_underage_beneficiary_who_did_not_claim_credit_18_on_time(): # At 17 years old with time_machine.travel(datetime.utcnow() - relativedelta(years=2)): diff --git a/api/tests/core/external/user_automations_test.py b/api/tests/core/external/user_automations_test.py index d195274b486..a0647d4abc6 100644 --- a/api/tests/core/external/user_automations_test.py +++ b/api/tests/core/external/user_automations_test.py @@ -392,6 +392,7 @@ def test_get_ex_underage_beneficiaries_who_can_no_longer_recredit(self): @patch("pcapi.core.external.attributes.api.update_batch_user") @patch("pcapi.core.external.attributes.api.update_sendinblue_user") + @pytest.mark.features(WIP_ENABLE_CREDIT_V3=False) def test_users_whose_credit_expired_today_automation_underage(self, mock_update_sendinblue, mock_update_batch): with time_machine.travel("2033-09-10 15:00:00"): user = users_factories.UnderageBeneficiaryFactory( diff --git a/api/tests/core/finance/test_api.py b/api/tests/core/finance/test_api.py index 0123e6ad20f..fd614ccd3bd 100644 --- a/api/tests/core/finance/test_api.py +++ b/api/tests/core/finance/test_api.py @@ -3297,8 +3297,8 @@ def test_with_free_pricings(self, _generate_invoice_html, _store_invoice_pdf): class GenerateInvoiceTest: EXPECTED_NUM_QUERIES = ( - 1 # select feature flag WIP_ENABLE_NEW_FINANCE_WORKFLOW - + 1 # lock reimbursement point + # 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports + 1 # lock reimbursement point + 1 # select cashflows, pricings, pricing_lines, and custom_reimbursement_rules + 1 # select and lock ReferenceScheme + 1 # update ReferenceScheme diff --git a/api/tests/core/finance/test_api_legacy.py b/api/tests/core/finance/test_api_legacy.py index 7295431b13e..e464d67223d 100644 --- a/api/tests/core/finance/test_api_legacy.py +++ b/api/tests/core/finance/test_api_legacy.py @@ -1933,8 +1933,8 @@ def test_with_free_pricings(self, _generate_invoice_html, _store_invoice_pdf): class GenerateInvoiceTest: EXPECTED_NUM_QUERIES = ( - 1 # select feature flag WIP_ENABLE_NEW_FINANCE_WORKFLOW - + 1 # lock reimbursement point + # 1 # feature flags are already cached by BeneficiaryGrant18Factory.beneficiaryImports + 1 # lock reimbursement point + 1 # select cashflows, pricings, pricing_lines, and custom_reimbursement_rules + 1 # select and lock ReferenceScheme + 1 # update ReferenceScheme diff --git a/api/tests/core/mails/transactional/bookings/new_booking_to_pro_test.py b/api/tests/core/mails/transactional/bookings/new_booking_to_pro_test.py index 24bd18126ff..4cb050a62c1 100644 --- a/api/tests/core/mails/transactional/bookings/new_booking_to_pro_test.py +++ b/api/tests/core/mails/transactional/bookings/new_booking_to_pro_test.py @@ -370,10 +370,10 @@ def test_when_venue_with_validated_bank_account(self): _ = booking.stock.offer.venue.offererAddress.address _ = booking.user # 1 - SELECT venue with bank account - # 1 - SELECT feature (WIP_USE_OFFERER_ADDRESS_AS_DATA_SOURCE) + # 0 - SELECT feature (already cached by BeneficiaryGrant18Factory.beneficiaryImports) # 1 - SELECT external booking (might be preloaded ?) # 1 - SELECT activation code (might be preloaded ?) - with assert_num_queries(4): + with assert_num_queries(3): email_data = get_new_booking_to_pro_email_data(booking) assert not email_data.params["NEEDS_BANK_INFORMATION_REMINDER"] @@ -398,10 +398,10 @@ def test_when_venue_is_caledonian(self): _ = booking.stock.offer.venue.offererAddress.address _ = booking.user # 1 - SELECT venue with bank account - # 1 - SELECT feature (WIP_USE_OFFERER_ADDRESS_AS_DATA_SOURCE) + # 0 - SELECT feature (already cached by BeneficiaryGrant18Factory.beneficiaryImports) # 1 - SELECT external booking (might be preloaded ?) # 1 - SELECT activation code (might be preloaded ?) - with assert_num_queries(4): + with assert_num_queries(3): email_data = get_new_booking_to_pro_email_data(booking) assert email_data.params["FORMATTED_PRICE"] == "1193 F" diff --git a/api/tests/core/subscription/dms/test_api.py b/api/tests/core/subscription/dms/test_api.py index b46afb402b5..7aabba5f47a 100644 --- a/api/tests/core/subscription/dms/test_api.py +++ b/api/tests/core/subscription/dms/test_api.py @@ -139,6 +139,7 @@ def test_handle_dms_application_updates_birth_date(self): state=dms_models.GraphQLApplicationStates.accepted, email=beneficiary.email, birth_date=sixteen_years_ago, + construction_datetime=datetime.datetime.utcnow().isoformat(), first_name="little", last_name="sister", ) diff --git a/api/tests/core/subscription/test_api.py b/api/tests/core/subscription/test_api.py index e1e354f4da6..725950cc774 100644 --- a/api/tests/core/subscription/test_api.py +++ b/api/tests/core/subscription/test_api.py @@ -1039,7 +1039,9 @@ class GetFirstRegistrationDateTest: def test_get_first_registration_date_no_check(self): user = users_factories.UserFactory() assert ( - subscription_api.get_first_registration_date(user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE) + subscription_api.get_first_registration_date_with_eligibility( + user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE + ) is None ) @@ -1069,7 +1071,9 @@ def test_get_first_registration_date_underage(self): eligibilityType=users_models.EligibilityType.UNDERAGE, ) assert ( - subscription_api.get_first_registration_date(user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE) + subscription_api.get_first_registration_date_with_eligibility( + user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE + ) == d1 ) @@ -1086,7 +1090,7 @@ def test_get_first_registration_date_underage_with_timezone(self, age): eligibilityType=users_models.EligibilityType.UNDERAGE, ) - first_registration_date = subscription_api.get_first_registration_date( + first_registration_date = subscription_api.get_first_registration_date_with_eligibility( user, user.birth_date, users_models.EligibilityType.UNDERAGE ) @@ -1105,7 +1109,7 @@ def test_get_first_registration_date_18_with_timezone(self, age): eligibilityType=users_models.EligibilityType.AGE18, ) - first_registration_date = subscription_api.get_first_registration_date( + first_registration_date = subscription_api.get_first_registration_date_with_eligibility( user, user.birth_date, users_models.EligibilityType.AGE18 ) @@ -1130,7 +1134,9 @@ def test_get_first_registration_date_age_18(self): eligibilityType=users_models.EligibilityType.AGE18, ) assert ( - subscription_api.get_first_registration_date(user, user.dateOfBirth, users_models.EligibilityType.AGE18) + subscription_api.get_first_registration_date_with_eligibility( + user, user.dateOfBirth, users_models.EligibilityType.AGE18 + ) == d2 ) @@ -1154,7 +1160,9 @@ def test_with_uneligible_age_try(self): ) assert ( - subscription_api.get_first_registration_date(user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE) + subscription_api.get_first_registration_date_with_eligibility( + user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE + ) == d2 ) @@ -1178,7 +1186,9 @@ def test_with_registration_before_opening_try(self): ) assert ( - subscription_api.get_first_registration_date(user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE) + subscription_api.get_first_registration_date_with_eligibility( + user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE + ) == d2 ) @@ -1194,7 +1204,9 @@ def test_without_eligible_try(self): ) assert ( - subscription_api.get_first_registration_date(user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE) + subscription_api.get_first_registration_date_with_eligibility( + user, user.dateOfBirth, users_models.EligibilityType.UNDERAGE + ) is None ) diff --git a/api/tests/core/subscription/ubble/end_to_end/test_subscription_via_ubble.py b/api/tests/core/subscription/ubble/end_to_end/test_subscription_via_ubble.py index f89466d7263..8ff882fbfe1 100644 --- a/api/tests/core/subscription/ubble/end_to_end/test_subscription_via_ubble.py +++ b/api/tests/core/subscription/ubble/end_to_end/test_subscription_via_ubble.py @@ -19,6 +19,7 @@ from pcapi.core.subscription import models as subscription_models from pcapi.core.users import factories as users_factories from pcapi.core.users import models as users_models +from pcapi.utils.date import DATE_ISO_FORMAT from pcapi.validation.routes import ubble as ubble_routes import tests @@ -45,14 +46,18 @@ def test_beneficiary_activation_with_ubble_mocked_response(self, client, ubble_c fraud_factories.ProfileCompletionFraudCheckFactory( user=user, resultContent__first_name="Catherine", resultContent__last_name="Destivelle" ) - eighteen_years_ago = datetime.datetime.utcnow() - relativedelta(years=18, months=1) + registration_date = datetime.datetime.strptime( + fixtures.ID_VERIFICATION_APPROVED_RESPONSE["created_on"], DATE_ISO_FORMAT + ) + eighteen_years_before_registration = registration_date - relativedelta(years=18, months=1) fraud_factories.BeneficiaryFraudCheckFactory( user=user, type=fraud_models.FraudCheckType.UBBLE, status=fraud_models.FraudCheckStatus.STARTED, thirdPartyId="", resultContent=fraud_models.UbbleContent( - birth_date=eighteen_years_ago.date(), external_applicant_id="eaplt_61313A10000000000000000000" + birth_date=eighteen_years_before_registration.date(), + external_applicant_id="eaplt_61313A10000000000000000000", ).dict(exclude_none=True), ) diff --git a/api/tests/core/subscription/ubble/test_api.py b/api/tests/core/subscription/ubble/test_api.py index 524e2071394..fc05e4d192e 100644 --- a/api/tests/core/subscription/ubble/test_api.py +++ b/api/tests/core/subscription/ubble/test_api.py @@ -31,6 +31,7 @@ from pcapi.notifications import push as push_notifications import pcapi.notifications.push.testing as push_testing from pcapi.utils import requests as requests_utils +from pcapi.utils.date import DATE_ISO_FORMAT from pcapi.utils.string import u_nbsp import tests @@ -340,7 +341,7 @@ def test_concurrent_requests_leave_fraud_check_ok(self, requests_mock): ) requests_mock.get( f"{settings.UBBLE_API_URL}/v2/identity-verifications/{fraud_check.thirdPartyId}", - json=build_ubble_identification_v2_response(birth_date=datetime.date.today() - relativedelta(years=18)), + json=build_ubble_identification_v2_response(age_at_registration=18), ) ubble_subscription_api.update_ubble_workflow(fraud_check) @@ -386,15 +387,16 @@ def test_ubble_workflow_updates_birth_date_on_eligibility_upgrade(self, requests eligibilityType=users_models.EligibilityType.UNDERAGE, ) - eighteen_years_and_a_month_ago = datetime.date.today() - relativedelta(years=18, months=1) + registration_date = datetime.datetime.strptime(UBBLE_IDENTIFICATION_V2_RESPONSE["created_on"], DATE_ISO_FORMAT) + eighteen_years_and_a_month_ago = registration_date - relativedelta(years=18, months=1) requests_mock.get( f"{settings.UBBLE_API_URL}/v2/identity-verifications/{fraud_check.thirdPartyId}", - json=build_ubble_identification_v2_response(birth_date=eighteen_years_and_a_month_ago), + json=build_ubble_identification_v2_response(birth_date=eighteen_years_and_a_month_ago.date()), ) ubble_subscription_api.update_ubble_workflow(fraud_check) - assert user.validatedBirthDate == eighteen_years_and_a_month_ago + assert user.validatedBirthDate == eighteen_years_and_a_month_ago.date() assert user.has_beneficiary_role def test_ubble_workflow_with_eligibility_change_17_18(self, requests_mock): @@ -409,10 +411,9 @@ def test_ubble_workflow_with_eligibility_change_17_18(self, requests_mock): ) original_third_party_id = fraud_check.thirdPartyId - eighteen_years_ago = datetime.date.today() - relativedelta(years=18, months=1) requests_mock.get( f"{settings.UBBLE_API_URL}/v2/identity-verifications/{fraud_check.thirdPartyId}", - json=build_ubble_identification_v2_response(birth_date=eighteen_years_ago), + json=build_ubble_identification_v2_response(age_at_registration=18), ) ubble_subscription_api.update_ubble_workflow(fraud_check) @@ -456,17 +457,18 @@ def test_ubble_workflow_with_eligibility_change_18_19(self, requests_mock): assert ko_fraud_check.eligibilityType == users_models.EligibilityType.AGE18 assert fraud_check.reasonCodes == [fraud_models.FraudReasonCode.AGE_TOO_OLD] - def test_ubble_workflow_with_eligibility_change_18_19_with_first_attempt_at_18(self, requests_mock): - nineteen_years_ago = datetime.date.today() - relativedelta(years=19, months=1) - user = users_factories.UserFactory(dateOfBirth=nineteen_years_ago) - last_year = datetime.datetime.utcnow() - relativedelta(years=1) + @pytest.mark.parametrize("age", [19, 20]) + def test_ubble_workflow_with_eligibility_change_with_first_attempt_at_18(self, requests_mock, age): + years_ago = datetime.date.today() - relativedelta(years=age, months=1) + user = users_factories.UserFactory(dateOfBirth=years_ago) + year_when_user_was_eighteen = datetime.datetime.utcnow() - relativedelta(years=age - 18) fraud_factories.BeneficiaryFraudCheckFactory( type=fraud_models.FraudCheckType.UBBLE, status=fraud_models.FraudCheckStatus.KO, user=user, thirdPartyId="idv_qwerty1234", eligibilityType=users_models.EligibilityType.AGE18, - dateCreated=last_year, + dateCreated=year_when_user_was_eighteen, ) fraud_check = fraud_factories.BeneficiaryFraudCheckFactory( type=fraud_models.FraudCheckType.UBBLE, @@ -478,7 +480,7 @@ def test_ubble_workflow_with_eligibility_change_18_19_with_first_attempt_at_18(s ) requests_mock.get( f"{settings.UBBLE_API_URL}/v2/identity-verifications/{fraud_check.thirdPartyId}", - json=build_ubble_identification_v2_response(birth_date=nineteen_years_ago), + json=build_ubble_identification_v2_response(age_at_registration=age), ) ubble_subscription_api.update_ubble_workflow(fraud_check) @@ -488,17 +490,17 @@ def test_ubble_workflow_with_eligibility_change_18_19_with_first_attempt_at_18(s assert ok_fraud_check.status == fraud_models.FraudCheckStatus.OK assert ok_fraud_check.eligibilityType == users_models.EligibilityType.AGE18 - def test_ubble_workflow_with_eligibility_change_18_20_with_first_attempt_at_18(self, requests_mock): - twenty_years_ago = datetime.date.today() - relativedelta(years=20, months=1) - user = users_factories.UserFactory(dateOfBirth=twenty_years_ago) - two_years_ago = datetime.datetime.utcnow() - relativedelta(years=2) + def test_ubble_workflow_with_eligibility_change_at_21_with_first_attempt_at_18(self, requests_mock): + twenty_one_years_ago = datetime.date.today() - relativedelta(years=21, months=1) + user = users_factories.UserFactory(dateOfBirth=twenty_one_years_ago) + year_when_user_was_eighteen = datetime.datetime.utcnow() - relativedelta(years=3) fraud_factories.BeneficiaryFraudCheckFactory( type=fraud_models.FraudCheckType.UBBLE, status=fraud_models.FraudCheckStatus.KO, user=user, thirdPartyId="idv_qwerty1234", eligibilityType=users_models.EligibilityType.AGE18, - dateCreated=two_years_ago, + dateCreated=year_when_user_was_eighteen, ) fraud_check = fraud_factories.BeneficiaryFraudCheckFactory( type=fraud_models.FraudCheckType.UBBLE, @@ -510,9 +512,7 @@ def test_ubble_workflow_with_eligibility_change_18_20_with_first_attempt_at_18(s ) requests_mock.get( f"{settings.UBBLE_API_URL}/v2/identity-verifications/{fraud_check.thirdPartyId}", - json=build_ubble_identification_v2_response( - birth_date=twenty_years_ago, created_on=datetime.datetime.utcnow() - ), + json=build_ubble_identification_v2_response(age_at_registration=21), ) ubble_subscription_api.update_ubble_workflow(fraud_check) @@ -521,7 +521,7 @@ def test_ubble_workflow_with_eligibility_change_18_20_with_first_attempt_at_18(s assert ko_fraud_check.type == fraud_models.FraudCheckType.UBBLE assert ko_fraud_check.status == fraud_models.FraudCheckStatus.KO assert ko_fraud_check.eligibilityType == users_models.EligibilityType.AGE18 - assert ko_fraud_check.reason == "L'utilisateur a dépassé l'âge maximum (20 ans)" + assert ko_fraud_check.reason == "L'utilisateur a dépassé l'âge maximum (21 ans)" assert fraud_check.reasonCodes == [fraud_models.FraudReasonCode.AGE_TOO_OLD] @@ -532,6 +532,7 @@ def build_ubble_identification_v2_response( documents: list[dict] | None = None, birth_date: datetime.date | None = None, created_on: datetime.datetime | None = None, + age_at_registration: int | None = None, ) -> dict: identification_response = copy.deepcopy(UBBLE_IDENTIFICATION_V2_RESPONSE) if status is not None: @@ -546,6 +547,10 @@ def build_ubble_identification_v2_response( identification_response["documents"][0]["birth_date"] = birth_date.isoformat() if created_on is not None: identification_response["created_on"] = created_on.isoformat() + "Z" + if age_at_registration is not None and birth_date is None: + registration_date = datetime.datetime.strptime(identification_response["created_on"], DATE_ISO_FORMAT) + years_before_registration = registration_date - relativedelta(years=age_at_registration, months=1) + identification_response["documents"][0]["birth_date"] = years_before_registration.date().isoformat() return identification_response @@ -896,7 +901,7 @@ def test_ubble_workflow_started_at_19_with_previous_attempt_at_18(self, ubble_mo assert fraud_check.thirdPartyId == fraud_check.thirdPartyId @time_machine.travel("2020-05-05") - def test_ubble_workflow_started_at_20_with_previous_attempt_at_18(self, ubble_mocker, db_session): + def test_ubble_workflow_started_at_21_with_previous_attempt_at_18(self, ubble_mocker, db_session): # Given user = users_factories.UserFactory(dateOfBirth=datetime.datetime(year=2000, month=5, day=1)) # User started a ubble workflow at 18 years old and was rejected @@ -907,13 +912,13 @@ def test_ubble_workflow_started_at_20_with_previous_attempt_at_18(self, ubble_mo eligibilityType=users_models.EligibilityType.AGE18, dateCreated=datetime.datetime(year=2018, month=5, day=4), ) - # User started a new ubble workflow at 19 years old + # User started a new ubble workflow at 21 years old fraud_check = fraud_factories.BeneficiaryFraudCheckFactory( type=fraud_models.FraudCheckType.UBBLE, status=fraud_models.FraudCheckStatus.OK, user=user, eligibilityType=users_models.EligibilityType.AGE18, - dateCreated=datetime.datetime(year=2020, month=5, day=4), + dateCreated=datetime.datetime(year=2021, month=5, day=4), ) # When @@ -922,7 +927,7 @@ def test_ubble_workflow_started_at_20_with_previous_attempt_at_18(self, ubble_mo data__attributes__identification_id=str(fraud_check.thirdPartyId), included=[ UbbleIdentificationIncludedDocumentsFactory( - attributes__birth_date=datetime.datetime(year=2000, month=5, day=1).date().isoformat() + attributes__birth_date=datetime.datetime(year=1999, month=5, day=1).date().isoformat() ), ], ) @@ -939,7 +944,7 @@ def test_ubble_workflow_started_at_20_with_previous_attempt_at_18(self, ubble_mo assert fraud_check.status == fraud_models.FraudCheckStatus.KO assert fraud_check.eligibilityType == users_models.EligibilityType.AGE18 assert fraud_check.thirdPartyId == fraud_check.thirdPartyId - assert fraud_check.reason == "L'utilisateur a dépassé l'âge maximum (20 ans)" + assert fraud_check.reason == "L'utilisateur a dépassé l'âge maximum (21 ans)" assert fraud_check.reasonCodes == [fraud_models.FraudReasonCode.AGE_TOO_OLD] diff --git a/api/tests/core/users/test_eligibility.py b/api/tests/core/users/test_eligibility.py index bfb67cd4be9..47560411fe3 100644 --- a/api/tests/core/users/test_eligibility.py +++ b/api/tests/core/users/test_eligibility.py @@ -1,4 +1,5 @@ -import datetime +from datetime import date +from datetime import datetime from dateutil.relativedelta import relativedelta import pytest @@ -12,9 +13,184 @@ @pytest.mark.usefixtures("db_session") +@pytest.mark.features(WIP_ENABLE_CREDIT_V3=True) +class DecideV3CreditEligibilityTest: + @pytest.mark.parametrize("age", [17, 18]) + def test_eligible_age(self, age): + birth_date = date.today() - relativedelta(years=age, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + + eligibility = eligibility_api.decide_eligibility(user, birth_date, None) + + assert eligibility == users_models.EligibilityType.AGE17_18 + + @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 = date.today() + march_2nd_seventeen_years_ago = today - relativedelta(years=17, days=-1) + new_caledonian_user = users_factories.UserFactory( + dateOfBirth=march_2nd_seventeen_years_ago, departementCode="988" + ) + + eligibility = eligibility_api.decide_eligibility(new_caledonian_user, march_2nd_seventeen_years_ago, None) + + assert eligibility == users_models.EligibilityType.AGE17_18 + + @pytest.mark.parametrize("age", [14, 15, 16, 19, 20, 21]) + def test_not_eligible_age(self, age): + birth_date = date.today() - relativedelta(years=age, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + + eligibility = eligibility_api.decide_eligibility(user, birth_date, None) + + assert eligibility is None + + @time_machine.travel(eligibility_api.CREDIT_V3_DECREE_DATETIME + relativedelta(years=2)) + @pytest.mark.parametrize("age", [19, 20]) + def test_old_user_still_eligible_with_early_first_registration(self, age): + birth_date = date.today() - relativedelta(years=age, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + date_when_user_was_eighteen = birth_date + relativedelta(years=18, months=1) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.UBBLE, + eligibilityType=users_models.EligibilityType.AGE17_18, + dateCreated=date_when_user_was_eighteen, + ) + + eligibility = eligibility_api.decide_eligibility(user, birth_date, None) + + assert eligibility == users_models.EligibilityType.AGE17_18 + + @time_machine.travel(eligibility_api.CREDIT_V3_DECREE_DATETIME + relativedelta(years=2)) + @pytest.mark.parametrize("age", [19, 20]) + def test_old_user_still_eligible_with_early_registration_datetime(self, age): + birth_date = date.today() - relativedelta(years=age, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + date_when_user_was_eighteen = birth_date + relativedelta(years=18, months=1) + + datetime_when_user_was_eighteen = datetime.combine(date_when_user_was_eighteen, datetime.min.time()) + eligibility = eligibility_api.decide_eligibility(user, birth_date, datetime_when_user_was_eighteen) + + assert eligibility == users_models.EligibilityType.AGE17_18 + + @pytest.mark.parametrize("age", [19, 20]) + def test_old_user_not_eligible_with_first_registration_before_fourteen(self, age): + birth_date = date.today() - relativedelta(years=age, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + date_when_user_was_fourteen = birth_date + relativedelta(years=14, months=1) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.UBBLE, + eligibilityType=users_models.EligibilityType.AGE17_18, + dateCreated=date_when_user_was_fourteen, + ) + + eligibility = eligibility_api.decide_eligibility(user, birth_date, None) + + assert eligibility is None + + @pytest.mark.parametrize("age", [19, 20]) + def test_old_user_not_eligible_with_registration_datetime_before_fourteen(self, age): + birth_date = date.today() - relativedelta(years=age, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + date_when_user_was_fourteen = birth_date + relativedelta(years=14, months=1) + + datetime_when_user_was_fourteen = datetime.combine(date_when_user_was_fourteen, datetime.min.time()) + eligibility = eligibility_api.decide_eligibility(user, birth_date, datetime_when_user_was_fourteen) + + assert eligibility is None + + @time_machine.travel(eligibility_api.CREDIT_V3_DECREE_DATETIME) + @pytest.mark.parametrize("age", [15, 16, 17]) + def test_user_underage_eligibility_when_registered_before_decree(self, age): + birth_date = date.today() - relativedelta(years=age, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.UBBLE, + eligibilityType=users_models.EligibilityType.UNDERAGE, + dateCreated=eligibility_api.CREDIT_V3_DECREE_DATETIME - relativedelta(days=1), + ) + + eligibility = eligibility_api.decide_eligibility(user, birth_date, None) + + assert eligibility == users_models.EligibilityType.UNDERAGE + + @time_machine.travel(eligibility_api.CREDIT_V3_DECREE_DATETIME) + @pytest.mark.parametrize("age", [15, 16, 17]) + def test_user_underage_eligibility_with_registration_datetime_before_decree(self, age): + birth_date = date.today() - relativedelta(years=age, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.UBBLE, + eligibilityType=users_models.EligibilityType.UNDERAGE, + dateCreated=eligibility_api.CREDIT_V3_DECREE_DATETIME - relativedelta(days=1), + ) + + yesterday = datetime.utcnow() - relativedelta(days=1) + eligibility = eligibility_api.decide_eligibility(user, birth_date, yesterday) + + assert eligibility == users_models.EligibilityType.UNDERAGE + + @time_machine.travel(eligibility_api.CREDIT_V3_DECREE_DATETIME) + def test_user_eighteen_eligibility_when_registered_before_decree(self): + birth_date = date.today() - relativedelta(years=18, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.UBBLE, + eligibilityType=users_models.EligibilityType.AGE18, + dateCreated=eligibility_api.CREDIT_V3_DECREE_DATETIME - relativedelta(days=1), + ) + + eligibility = eligibility_api.decide_eligibility(user, birth_date, None) + + assert eligibility == users_models.EligibilityType.AGE18 + + @time_machine.travel(eligibility_api.CREDIT_V3_DECREE_DATETIME) + def test_user_eighteen_eligibility_with_registration_datetime_before_decree(self): + birth_date = date.today() - relativedelta(years=18, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + + yesterday = datetime.utcnow() - relativedelta(days=1) + eligibility = eligibility_api.decide_eligibility(user, birth_date, yesterday) + + assert eligibility == users_models.EligibilityType.AGE18 + + @time_machine.travel(eligibility_api.CREDIT_V3_DECREE_DATETIME) + def test_user_21_ineligibility_when_registered_before_decree(self): + birth_date = date.today() - relativedelta(years=21, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + year_when_user_was_eighteen = datetime.utcnow() - relativedelta(years=user.age - 18) + fraud_factories.BeneficiaryFraudCheckFactory( + user=user, + type=fraud_models.FraudCheckType.UBBLE, + eligibilityType=users_models.EligibilityType.AGE18, + dateCreated=year_when_user_was_eighteen, + ) + + eligibility = eligibility_api.decide_eligibility(user, birth_date, year_when_user_was_eighteen) + + assert eligibility is None + + @time_machine.travel(eligibility_api.CREDIT_V3_DECREE_DATETIME) + def test_user_21_ineligibility_with_registration_date_before_decree(self): + birth_date = date.today() - relativedelta(years=21, months=1) + user = users_factories.UserFactory(dateOfBirth=birth_date) + + year_when_user_was_eighteen = datetime.utcnow() - relativedelta(years=user.age - 18) + eligibility = eligibility_api.decide_eligibility(user, birth_date, year_when_user_was_eighteen) + + assert eligibility is None + + +@pytest.mark.usefixtures("db_session") +@pytest.mark.features(WIP_ENABLE_CREDIT_V3=False) class DecideEligibilityTest: def test_19yo_is_eligible_if_application_at_18_yo(self): - today = datetime.date.today() + today = date.today() birth_date = today - relativedelta(years=19, days=1) user = users_factories.UserFactory() fraud_factories.BeneficiaryFraudCheckFactory( @@ -25,7 +201,7 @@ def test_19yo_is_eligible_if_application_at_18_yo(self): ) dms_content = fraud_factories.DMSContentFactory( - registration_datetime=datetime.datetime.combine(today, datetime.datetime.min.time()), birth_date=birth_date + registration_datetime=datetime.combine(today, datetime.min.time()), birth_date=birth_date ) fraud_factories.BeneficiaryFraudCheckFactory( user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content @@ -37,12 +213,12 @@ def test_19yo_is_eligible_if_application_at_18_yo(self): assert result == users_models.EligibilityType.AGE18 def test_19yo_not_eligible(self): - today = datetime.date.today() + today = 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 + registration_datetime=datetime.combine(today, datetime.min.time()), birth_date=birth_date ) fraud_factories.BeneficiaryFraudCheckFactory( user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content @@ -54,7 +230,7 @@ def test_19yo_not_eligible(self): assert result is None def test_19yo_ex_underage_not_eligible(self): - today = datetime.date.today() + today = date.today() birth_date = today - relativedelta(years=19, days=1) user = users_factories.UserFactory() fraud_factories.BeneficiaryFraudCheckFactory( @@ -64,7 +240,7 @@ def test_19yo_ex_underage_not_eligible(self): eligibilityType=users_models.EligibilityType.UNDERAGE, ) dms_content = fraud_factories.DMSContentFactory( - registration_datetime=datetime.datetime.combine(today, datetime.datetime.min.time()), birth_date=birth_date + registration_datetime=datetime.combine(today, datetime.min.time()), birth_date=birth_date ) fraud_factories.BeneficiaryFraudCheckFactory( user=user, type=fraud_models.FraudCheckType.DMS, resultContent=dms_content @@ -76,7 +252,7 @@ def test_19yo_ex_underage_not_eligible(self): assert result is None def test_18yo_eligible(self): - today = datetime.date.today() + today = date.today() birth_date = today - relativedelta(years=19, days=1) user = users_factories.UserFactory() fraud_factories.BeneficiaryFraudCheckFactory( @@ -86,9 +262,7 @@ def test_18yo_eligible(self): 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() - ), + registration_datetime=datetime.combine(today - relativedelta(years=1, days=-1), datetime.min.time()), birth_date=birth_date, ) fraud_factories.BeneficiaryFraudCheckFactory( @@ -101,13 +275,11 @@ def test_18yo_eligible(self): assert result == users_models.EligibilityType.AGE18 def test_18yo_underage_eligible(self): - today = datetime.date.today() + today = 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() - ), + registration_datetime=datetime.combine(today - relativedelta(years=1, days=-1), datetime.min.time()), birth_date=birth_date, ) fraud_factories.BeneficiaryFraudCheckFactory( @@ -123,9 +295,9 @@ def test_18yo_underage_eligible(self): 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)) + user = users_factories.UserFactory(dateOfBirth=datetime.utcnow() - relativedelta(years=age)) birth_date = user.dateOfBirth - registration_datetime = datetime.datetime.today() + registration_datetime = datetime.today() assert ( eligibility_api.decide_eligibility(user, birth_date, registration_datetime) @@ -134,7 +306,7 @@ def test_decide_eligibility_for_underage_users(self): @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() + today = 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" @@ -147,16 +319,15 @@ def test_decide_underage_eligibility_with_timezone(self): @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) + birth_date = datetime.utcnow() - relativedelta(years=18) user = users_factories.UserFactory() assert ( - eligibility_api.decide_eligibility(user, birth_date, datetime.datetime.today()) - == users_models.EligibilityType.AGE18 + eligibility_api.decide_eligibility(user, birth_date, 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)) + eligibility_api.decide_eligibility(user, birth_date, datetime.utcnow() - relativedelta(years=1)) == users_models.EligibilityType.AGE18 ) @@ -165,16 +336,16 @@ def test_decide_eligibility_for_18_yo_users_is_always_age_18(self): "first_registration_datetime,expected_eligibility", [ (None, None), - (datetime.datetime(year=2022, month=1, day=15), None), + (datetime(year=2022, month=1, day=15), None), ( - datetime.datetime(year=2021, month=12, day=1), + 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) + birth_date = datetime(year=2003, month=1, day=1) user = users_factories.UserFactory() if first_registration_datetime: @@ -186,10 +357,10 @@ def test_decide_eligibility_for_19_yo_users(self, first_registration_datetime, e ), ) - assert eligibility_api.decide_eligibility(user, birth_date, datetime.datetime.today()) == expected_eligibility + assert eligibility_api.decide_eligibility(user, birth_date, datetime.today()) == expected_eligibility def test_decide_eligibility_for_19_yo_users_with_no_registration_datetime(self): - today = datetime.date.today() + today = date.today() birth_date = today - relativedelta(years=19, days=1) user = users_factories.UserFactory() fraud_factories.BeneficiaryFraudCheckFactory( @@ -205,14 +376,16 @@ def test_decide_eligibility_for_19_yo_users_with_no_registration_datetime(self): assert eligibility_api.decide_eligibility(user, birth_date, None) == users_models.EligibilityType.AGE18 -class GetEligibilityTest: +class GetExtendedEligibilityTest: def test_get_eligibility_at_date_timezones_tolerance(self): - date_of_birth = datetime.datetime(2000, 2, 1, 0, 0) + date_of_birth = 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 = date_of_birth + relativedelta(years=19, hours=8) + eligibility = eligibility_api.get_extended_eligibility_at_date(date_of_birth, specified_date) + + assert eligibility == users_models.EligibilityType.AGE18 + + specified_date = date_of_birth + relativedelta(years=19, hours=12) + eligibility = eligibility_api.get_extended_eligibility_at_date(date_of_birth, specified_date) - specified_date = datetime.datetime(2019, 2, 1, 12, 0) - assert eligibility_api.get_eligibility_at_date(date_of_birth, specified_date) is None + assert eligibility is None diff --git a/api/tests/routes/backoffice/offerers_test.py b/api/tests/routes/backoffice/offerers_test.py index fbe0d9d1a2c..9cbfe96e2eb 100644 --- a/api/tests/routes/backoffice/offerers_test.py +++ b/api/tests/routes/backoffice/offerers_test.py @@ -910,7 +910,9 @@ def test_get_data( ): db.session.refresh(offerer) - with assert_num_queries(self.expected_num_queries): + with assert_num_queries( + self.expected_num_queries - 1 + ): # ff already cached by BeneficiaryGrant18Factory.beneficiaryImports stats = offerer_blueprint.get_stats_data(offerer) assert stats["active"]["individual"] == 2 @@ -931,7 +933,9 @@ def test_individual_offers_only( ): db.session.refresh(offerer) - with assert_num_queries(self.expected_num_queries): + with assert_num_queries( + self.expected_num_queries - 1 + ): # ff already cached by BeneficiaryGrant18Factory.beneficiaryImports stats = offerer_blueprint.get_stats_data(offerer) assert stats["active"]["individual"] == 2 diff --git a/api/tests/routes/native/openapi_test.py b/api/tests/routes/native/openapi_test.py index 9446ee039e9..c14618aae52 100644 --- a/api/tests/routes/native/openapi_test.py +++ b/api/tests/routes/native/openapi_test.py @@ -799,7 +799,7 @@ def test_public_api(client): }, "EligibilityType": { "description": "An enumeration.", - "enum": ["underage", "age-18"], + "enum": ["age-17-18", "underage", "age-18"], "title": "EligibilityType", }, "EmailChangeConfirmationResponse": { diff --git a/api/tests/routes/native/v1/account_test.py b/api/tests/routes/native/v1/account_test.py index 8d8140569eb..bf691ed97b9 100644 --- a/api/tests/routes/native/v1/account_test.py +++ b/api/tests/routes/native/v1/account_test.py @@ -182,7 +182,7 @@ def test_status_contains_subscription_status_when_eligible(self, client): def test_get_user_not_beneficiary(self, client, app): users_factories.UserFactory(email=self.identifier) - expected_num_queries = 5 # user + booking + deposit + beneficiary_fraud_review + achievement + expected_num_queries = 7 # user + achievement + booking + deposit + ff + fraud_checks + fraud_review client.with_token(email=self.identifier) @@ -195,7 +195,7 @@ def test_get_user_not_beneficiary(self, client, app): def test_get_user_profile_empty_first_name(self, client, app): users_factories.UserFactory(email=self.identifier, firstName="") - expected_num_queries = 5 # user + booking + deposit + beneficiary_fraud_review + achievement + expected_num_queries = 7 # user + achievement + booking + deposit + ff + fraud_checks + fraud_review client.with_token(email=self.identifier) with assert_num_queries(expected_num_queries): @@ -210,7 +210,7 @@ def test_get_user_profile_empty_first_name(self, client, app): def test_get_user_profile_legacy_activity(self, client): users_factories.UserFactory(email=self.identifier, activity="activity not in enum") - expected_num_queries = 5 # user + booking + deposit + beneficiary_fraud_review + achievement + expected_num_queries = 7 # user + achievement + booking + deposit + ff + fraud_checks + fraud_review with assert_num_queries(expected_num_queries): response = client.with_token(email=self.identifier).get("/native/v1/me") @@ -218,17 +218,18 @@ def test_get_user_profile_legacy_activity(self, client): assert "activity" not in response.json def test_get_user_profile_recredit_amount_to_show(self, client, app): - with time_machine.travel("2020-01-01"): + with time_machine.travel("2018-01-01"): users_factories.UnderageBeneficiaryFactory(email=self.identifier) - with time_machine.travel("2021-01-02"): + with time_machine.travel("2019-01-02"): finance_api.recredit_underage_users() expected_num_queries = 1 # user - expected_num_queries += 1 # booking(from _get_booked_offers) - expected_num_queries += 1 # booking (from get_domains_credit) + expected_num_queries += 1 # achievements + expected_num_queries += 1 # bookings (from _get_booked_offers) + expected_num_queries += 1 # bookings (from get_domains_credit) + expected_num_queries += 1 # ff (from decide_eligibility) expected_num_queries += 1 # beneficiary fraud checks - expected_num_queries += 1 # achievement client.with_token(email=self.identifier) with assert_num_queries(expected_num_queries): @@ -325,7 +326,7 @@ def test_user_should_need_to_fill_cultural_survey( def test_not_eligible_user_should_not_need_to_fill_cultural_survey(self, client): user = users_factories.UserFactory(age=4) - expected_num_queries = 5 # user + booking + deposit + beneficiary_fraud_review + achievement + expected_num_queries = 7 # user + achievement + booking + deposit + ff + fraud_check + beneficiary_fraud_review client.with_token(user.email) with assert_num_queries(expected_num_queries): @@ -404,7 +405,9 @@ def test_user_without_password(self, client): user = sso.user user.password = None - expected_num_queries = 6 # user(update) + user + beneficiary_fraud_review + deposit + booking + achievement + expected_num_queries = ( + 8 # user(update) + user + achievements + bookings + deposit + ff + fraud checks + fraud review + ) with assert_num_queries(expected_num_queries): response = client.with_token(user.email).get("/native/v1/me") assert response.status_code == 200, response.json @@ -414,7 +417,7 @@ def test_user_without_password(self, client): def test_currency_pacific_franc(self, client): user = users_factories.UserFactory(departementCode="988", postalCode="98818") - expected_num_queries = 6 # user(update) + user + beneficiary_fraud_review + deposit + booking + achievement + expected_num_queries = 8 # user*2 + achievements + bookings + deposit + ff + fraud checks + fraud reviews with assert_num_queries(expected_num_queries): response = client.with_token(user.email).get("/native/v1/me") @@ -478,6 +481,7 @@ def test_account_creation(self, mocked_check_recaptcha_token_is_valid, client, a expected_num_queries += 1 # bookings expected_num_queries += 1 # favorites expected_num_queries += 1 # deposit + expected_num_queries += 1 # fraud check expected_num_queries += 1 # action history with assert_num_queries(expected_num_queries): response = client.post("/native/v1/account", json=data) @@ -1416,7 +1420,7 @@ def test_end_to_end_update_email(self, client): # Ensure the access token is valid access_token = response.json["accessToken"] - expected_num_queries = 5 # user + booking + deposit + beneficiary_fraud_review + achievement + expected_num_queries = 6 # user + achievement + booking + deposit + ff + beneficiary_fraud_review client.auth_header = {"Authorization": f"Bearer {access_token}"} with assert_num_queries(expected_num_queries): diff --git a/api/tests/routes/native/v1/authentication_test.py b/api/tests/routes/native/v1/authentication_test.py index 91d2fce2d1f..d60750a8724 100644 --- a/api/tests/routes/native/v1/authentication_test.py +++ b/api/tests/routes/native/v1/authentication_test.py @@ -1155,7 +1155,10 @@ def test_validate_email_dms_orphan(self, execute_query, client): fraud_factories.OrphanDmsApplicationFactory(email=email, application_id=application_number) execute_query.return_value = make_single_application( - application_number, dms_models.GraphQLApplicationStates.accepted, email=email + application_number, + dms_models.GraphQLApplicationStates.accepted, + email=email, + construction_datetime=datetime.utcnow().isoformat(), ) response = client.post("/native/v1/validate_email", json={"email_validation_token": token}) diff --git a/api/tests/routes/native/v1/banner_test.py b/api/tests/routes/native/v1/banner_test.py index 1f93561bae5..09e212355df 100644 --- a/api/tests/routes/native/v1/banner_test.py +++ b/api/tests/routes/native/v1/banner_test.py @@ -50,7 +50,7 @@ def should_be_allowed_to_get_banner_when_active(self, client): user = users_factories.UserFactory() client.with_token(email=user.email) - with assert_num_queries(self.expected_num_queries_without_subscription_check): + with assert_num_queries(self.expected_num_queries_without_subscription_check + 1): # credit v3 FF response = client.get("/native/v1/banner") assert response.status_code == 200 @@ -58,7 +58,7 @@ def should_return_geolocation_banner_when_not_geolocated(self, client): user = users_factories.UserFactory() client.with_token(email=user.email) - with assert_num_queries(self.expected_num_queries_without_subscription_check): + with assert_num_queries(self.expected_num_queries_without_subscription_check + 1): # credit v3 FF response = client.get("/native/v1/banner?isGeolocated=false") assert response.status_code == 200 @@ -82,7 +82,7 @@ def should_return_activation_banner_when_user_has_profile_to_complete(self, clie ) client.with_token(email=user.email) - with assert_num_queries(self.expected_num_queries_without_subscription_check): + with assert_num_queries(self.expected_num_queries_without_subscription_check + 1): # credit v3 FF response = client.get("/native/v1/banner?isGeolocated=false") assert response.status_code == 200 @@ -98,7 +98,7 @@ def should_return_activation_banner_when_user_has_identity_check_to_complete(sel ) client.with_token(email=user.email) - with assert_num_queries(self.expected_num_queries_with_subscription_check): + with assert_num_queries(self.expected_num_queries_without_subscription_check + 1): # credit v3 FF response = client.get("/native/v1/banner?isGeolocated=false") assert response.status_code == 200 @@ -127,7 +127,7 @@ def should_not_return_any_banner_when_beneficiary_and_geolocated(self, client): user = users_factories.BeneficiaryGrant18Factory() client.with_token(email=user.email) - with assert_num_queries(self.expected_num_queries_without_subscription_check): + with assert_num_queries(self.expected_num_queries_without_subscription_check + 1): # credit v3 FF response = client.get("/native/v1/banner?isGeolocated=true") assert response.status_code == 200 @@ -139,8 +139,8 @@ def should_return_activation_banner_with_20_euros_when_15_year_old(self, client) client.with_token(email=user.email) - # authenticated user + joined user - with assert_num_queries(2): + # authenticated user + joined user + credit v3 FF + with assert_num_queries(3): response = client.get("/native/v1/banner?isGeolocated=false") assert response.status_code == 200 diff --git a/api/tests/routes/native/v1/subscription_test.py b/api/tests/routes/native/v1/subscription_test.py index 399a1265ad6..0438d9d1260 100644 --- a/api/tests/routes/native/v1/subscription_test.py +++ b/api/tests/routes/native/v1/subscription_test.py @@ -70,6 +70,7 @@ def test_next_subscription_test_profile_completion(self, client): ENABLE_EDUCONNECT_AUTHENTICATION=False, ENABLE_UBBLE=False, ENABLE_DMS_LINK_ON_MAINTENANCE_PAGE_FOR_UNDERAGE=True, + WIP_ENABLE_CREDIT_V3=False, ) @time_machine.travel("2022-09-08 11:54:22") def test_next_subscription_maintenance_page(self, client): @@ -427,6 +428,7 @@ def test_underage_ubble_ok_turned_18(self, client): ENABLE_DMS_LINK_ON_MAINTENANCE_PAGE_FOR_AGE_18=False, ENABLE_DMS_LINK_ON_MAINTENANCE_PAGE_FOR_UNDERAGE=False, ENABLE_EDUCONNECT_AUTHENTICATION=False, + WIP_ENABLE_CREDIT_V3=False, ) @pytest.mark.parametrize("age", [15, 16, 17, 18]) @time_machine.travel("2022-09-08 12:45:13") @@ -849,6 +851,7 @@ def test_next_subscription_test_profile_completion(self, client): ENABLE_EDUCONNECT_AUTHENTICATION=False, ENABLE_UBBLE=False, ENABLE_DMS_LINK_ON_MAINTENANCE_PAGE_FOR_UNDERAGE=True, + WIP_ENABLE_CREDIT_V3=False, ) @time_machine.travel("2022-09-08 11:54:22") def test_next_subscription_maintenance_page(self, client): @@ -1210,6 +1213,7 @@ def test_underage_ubble_ok_turned_18(self, client): ENABLE_DMS_LINK_ON_MAINTENANCE_PAGE_FOR_AGE_18=False, ENABLE_DMS_LINK_ON_MAINTENANCE_PAGE_FOR_UNDERAGE=False, ENABLE_EDUCONNECT_AUTHENTICATION=False, + WIP_ENABLE_CREDIT_V3=False, ) @pytest.mark.parametrize("age", [15, 16, 17, 18]) @time_machine.travel("2022-09-08 12:45:13") @@ -1651,7 +1655,7 @@ class ActivityTypesTest: def test_get_activity_types(self, client, age): user = users_factories.BaseUserFactory(age=age) client.with_token(user.email) - with assert_num_queries(1): # user + with assert_num_queries(2): # user + credit v3 FF response = client.get("/native/v1/subscription/activity_types") assert response.status_code == 200 diff --git a/api/tests/scripts/beneficiary/import_dms_accepted_applications_test.py b/api/tests/scripts/beneficiary/import_dms_accepted_applications_test.py index 824b8ee8f24..c587698dafa 100644 --- a/api/tests/scripts/beneficiary/import_dms_accepted_applications_test.py +++ b/api/tests/scripts/beneficiary/import_dms_accepted_applications_test.py @@ -405,7 +405,11 @@ def test_import_duplicated_user(self, get_applications_with_details): get_applications_with_details.return_value = [ fixture.make_parsed_graphql_application( - application_number=123, state="accepte", email=user.email, birth_date=self.BENEFICIARY_BIRTH_DATE + application_number=123, + state="accepte", + email=user.email, + birth_date=self.BENEFICIARY_BIRTH_DATE, + construction_datetime=datetime.utcnow().isoformat(), ) ] import_all_updated_dms_applications(6712558)