Skip to content

Commit

Permalink
(PC-34274)[API] feat: add age 17 to 18 eligibility
Browse files Browse the repository at this point in the history
  • Loading branch information
dnguyen1-pass committed Jan 29, 2025
1 parent fc1d572 commit 6fbaf8a
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 77 deletions.
4 changes: 2 additions & 2 deletions api/src/pcapi/core/finance/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion api/src/pcapi/core/fraud/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
29 changes: 26 additions & 3 deletions api/src/pcapi/core/subscription/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
)
)
Expand Down Expand Up @@ -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,
Expand Down
91 changes: 79 additions & 12 deletions api/src/pcapi/core/users/eligibility_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -33,28 +39,67 @@ 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.
"""
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

user_age = users_utils.get_age_from_birth_date(birth_date, user.departementCode)
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)

Expand All @@ -65,11 +110,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

Expand Down Expand Up @@ -108,11 +157,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:
Expand Down
2 changes: 2 additions & 0 deletions api/src/pcapi/core/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
8 changes: 4 additions & 4 deletions api/src/pcapi/routes/backoffice/accounts/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 20 additions & 8 deletions api/tests/core/subscription/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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
)

Expand All @@ -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
)

Expand All @@ -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
)

Expand All @@ -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
)

Expand All @@ -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
)

Expand All @@ -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
)

Expand All @@ -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
)

Expand Down
Loading

0 comments on commit 6fbaf8a

Please sign in to comment.