Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PC-34274)[API] feat: add age 17 to 18 eligibility #16072

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
47 changes: 5 additions & 42 deletions api/src/pcapi/core/fraud/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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é.")

Expand Down
6 changes: 4 additions & 2 deletions api/src/pcapi/core/fraud/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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_extended_eligibility_at_date(
birth_date, registration_datetime, department
)

return eligibility_at_registration
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
):
Expand Down
56 changes: 36 additions & 20 deletions api/src/pcapi/core/subscription/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,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 @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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_extended_eligibility_at_date(
user.birth_date, identity_fraud_check.get_min_date_between_creation_and_registration(), user.departementCode
)
)
Expand Down Expand Up @@ -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()
)

Expand Down Expand Up @@ -748,13 +741,36 @@ 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)


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 All @@ -767,7 +783,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
),
Expand Down
5 changes: 3 additions & 2 deletions api/src/pcapi/core/subscription/dms/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
59 changes: 0 additions & 59 deletions api/src/pcapi/core/users/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading