Skip to content

Commit

Permalink
(PC-34162)[API] feat: credit v3 - create new deposit when v3 active
Browse files Browse the repository at this point in the history
  • Loading branch information
cnormant-pass committed Jan 30, 2025
1 parent 87e583f commit e2e2058
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 17 deletions.
70 changes: 66 additions & 4 deletions api/src/pcapi/core/finance/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import pcapi.core.educational.models as educational_models
from pcapi.core.external import batch as push_notifications
import pcapi.core.external.attributes.api as external_attributes_api
from pcapi.core.finance.enum import DepositType
from pcapi.core.fraud import models as fraud_models
from pcapi.core.history import api as history_api
import pcapi.core.history.models as history_models
Expand Down Expand Up @@ -2950,6 +2951,27 @@ def edit_reimbursement_rule(
return rule


def compute_deposit_expiration_date(beneficiary: users_models.User, deposit_type: DepositType) -> datetime.datetime:
if deposit_type == DepositType.GRANT_17_18:
# If the feature is active, we use the new deposit expiration date computation (1 day before the 21st birthday)
return compute_deposit_expiration_date_v3(beneficiary)

if deposit_type == DepositType.GRANT_15_17:
return compute_underage_deposit_expiration_datetime(beneficiary.birth_date)

# deposit_type == DepositType.GRANT_18
expiration_date = datetime.datetime.utcnow().date() + relativedelta(years=conf.GRANT_18_VALIDITY_IN_YEARS)
expiration_datetime = datetime.datetime.combine(expiration_date, datetime.time.max)
return expiration_datetime


def compute_deposit_expiration_date_v3(beneficiary: users_models.User) -> datetime.datetime:
if not beneficiary.birth_date:
raise ValueError(f"Beneficiary {beneficiary.id} has no birth date")

return beneficiary.birth_date + relativedelta(years=21)


def compute_underage_deposit_expiration_datetime(birth_date: datetime.date | None) -> datetime.datetime:
if not birth_date:
raise exceptions.UserNotGrantable("User has no validated birth date")
Expand All @@ -2967,17 +2989,15 @@ def get_granted_deposit(

return models.GrantedDeposit(
amount=conf.GRANTED_DEPOSIT_AMOUNTS_FOR_UNDERAGE_BY_AGE[age_at_registration],
expiration_date=compute_underage_deposit_expiration_datetime(beneficiary.validatedBirthDate),
expiration_date=compute_deposit_expiration_date(beneficiary, deposit_type=DepositType.GRANT_15_17),
type=models.DepositType.GRANT_15_17,
version=1,
)

if eligibility == users_models.EligibilityType.AGE18:
expiration_date = datetime.datetime.utcnow().date() + relativedelta(years=conf.GRANT_18_VALIDITY_IN_YEARS)
expiration_datetime = datetime.datetime.combine(expiration_date, datetime.time.max)
return models.GrantedDeposit(
amount=conf.GRANTED_DEPOSIT_AMOUNTS_FOR_18_BY_VERSION[2],
expiration_date=expiration_datetime,
expiration_date=compute_deposit_expiration_date(beneficiary, deposit_type=DepositType.GRANT_18),
type=models.DepositType.GRANT_18,
version=2,
)
Expand Down Expand Up @@ -3029,6 +3049,48 @@ def create_deposit(
deposit_source: str,
eligibility: users_models.EligibilityType,
age_at_registration: int | None = None,
) -> models.Deposit:
"""Create a new deposit for the user if there is no deposit yet."""
if feature.FeatureToggle.WIP_ENABLE_CREDIT_V3.is_active():
return create_deposit_v3(beneficiary, deposit_source, eligibility, age_at_registration)

return create_deposit_v2(beneficiary, deposit_source, eligibility, age_at_registration)


def create_deposit_v3(
beneficiary: users_models.User,
deposit_source: str,
eligibility: users_models.EligibilityType,
age_at_registration: int | None,
) -> models.Deposit:
"""Create a new deposit for the user if there is no deposit yet."""
if eligibility in [users_models.EligibilityType.AGE18, users_models.EligibilityType.UNDERAGE]:
return create_deposit_v2(beneficiary, deposit_source, eligibility, age_at_registration)

if repository.deposit_exists_for_beneficiary_and_type(beneficiary, DepositType.GRANT_17_18):
raise exceptions.DepositTypeAlreadyGrantedException(DepositType.GRANT_17_18)

if beneficiary.has_active_deposit:
raise exceptions.UserHasAlreadyActiveDeposit()

deposit = models.Deposit(
version=1,
type=DepositType.GRANT_17_18,
amount=decimal.Decimal(0),
source=deposit_source,
user=beneficiary,
expirationDate=compute_deposit_expiration_date(beneficiary, DepositType.GRANT_17_18),
)
db.session.add(deposit)
db.session.flush()
return deposit


def create_deposit_v2(
beneficiary: users_models.User,
deposit_source: str,
eligibility: users_models.EligibilityType,
age_at_registration: int | None = None,
) -> models.Deposit:
"""Create a new deposit for the user if there is no deposit yet."""
granted_deposit = get_granted_deposit(
Expand Down
2 changes: 2 additions & 0 deletions api/src/pcapi/core/finance/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@


class DepositType(enum.Enum):
GRANT_17_18 = "GRANT_17_18"
# legacy deposit types that are present in the database
GRANT_15_17 = "GRANT_15_17"
GRANT_18 = "GRANT_18"
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
76 changes: 65 additions & 11 deletions api/tests/core/finance/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4074,6 +4074,7 @@ def test_validation(self):
api.edit_reimbursement_rule(rule, end_date=end)


@pytest.mark.features(WIP_ENABLE_CREDIT_V3=False)
class CreateDepositTest:
@time_machine.travel("2021-02-05 09:00:00")
@pytest.mark.parametrize("age,expected_amount", [(15, Decimal(20)), (16, Decimal(30)), (17, Decimal(30))])
Expand Down Expand Up @@ -4341,6 +4342,61 @@ def test_cannot_create_twice_a_deposit_of_same_type(self):
assert error.value.errors["user"] == ['Cet utilisateur a déjà été crédité de la subvention "GRANT_18".']


@pytest.mark.features(WIP_ENABLE_CREDIT_V3=True)
class CreateDepositV3Test:
@time_machine.travel("2025-03-03")
def test_compute_deposit_expiration_date_v3(self):
user = users_models.User(validatedBirthDate=datetime.date(2007, 1, 1))
assert user.age == 18

expiration_date = api.compute_deposit_expiration_date_v3(user)

assert expiration_date == datetime.date(2028, 1, 1)

@time_machine.travel("2025-03-03")
def test_create_deposit_v3(self):
user_v3 = users_factories.HonorStatementValidatedUserFactory(validatedBirthDate=datetime.date(2007, 1, 1))
assert user_v3.age == 18

deposit_v3 = api.create_deposit(user_v3, "created by test", users_models.EligibilityType.AGE17_18)
assert deposit_v3.expirationDate == datetime.date(2028, 1, 1)
assert deposit_v3.type == models.DepositType.GRANT_17_18
assert deposit_v3.amount == 0

@time_machine.travel("2025-03-03")
def test_create_deposit_age_18_even_when_ff_credit_v3_is_active(self):
user = users_factories.HonorStatementValidatedUserFactory(validatedBirthDate=datetime.date(2007, 1, 1))
assert user.age == 18

deposit = api.create_deposit(user, "created by test", users_models.EligibilityType.AGE18)

assert deposit.type == models.DepositType.GRANT_18
assert deposit.amount == 300

@time_machine.travel("2025-03-03")
def test_17yo_becomes_17_18_when_ff_credit_v3_is_active(self):
user = users_factories.HonorStatementValidatedUserFactory(validatedBirthDate=datetime.date(2008, 1, 1))
assert user.age == 17

deposit = api.create_deposit(
user, "created by test", users_models.EligibilityType.AGE17_18, age_at_registration=user.age
)

assert deposit.type == models.DepositType.GRANT_17_18

@time_machine.travel("2025-03-03")
def test_less_than_17_yo_keeps_underage_when_ff_credit_v3_is_active(self):
user = users_factories.HonorStatementValidatedUserFactory(validatedBirthDate=datetime.date(2009, 1, 1))
assert user.age == 16

deposit = api.create_deposit(
user, "created by test", users_models.EligibilityType.UNDERAGE, age_at_registration=user.age
)

assert deposit.type == models.DepositType.GRANT_15_17
assert deposit.amount == 30


class UserRecreditTest:
@time_machine.travel("2021-07-01")
@pytest.mark.parametrize(
Expand Down Expand Up @@ -4888,17 +4944,15 @@ def test_notify_user_on_recredit(self):
api.recredit_underage_users()
assert user.deposit.amount == 50

assert push_testing.requests[-1] == {
"can_be_asynchronously_retried": True,
"event_name": "recredited_account",
"event_payload": {
"deposit_amount": 50,
"deposit_type": "GRANT_15_17",
"deposits_count": 1,
"deposit_expiration_date": "2022-12-01T00:00:00",
},
"user_id": user.id,
}
push_data = push_testing.requests[-1]
assert push_data["can_be_asynchronously_retried"] is True
assert push_data["event_name"] == "recredited_account"
assert push_data["user_id"] == user.id
payload = push_data["event_payload"]
assert payload["deposit_amount"] == 50
assert payload["deposit_type"] == "GRANT_15_17"
assert payload["deposits_count"] == 1
assert payload["deposit_expiration_date"] == user.deposit.expirationDate.isoformat()


class ValidateFinanceIncidentTest:
Expand Down
4 changes: 2 additions & 2 deletions api/tests/routes/native/openapi_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ def test_public_api(client):
},
"DepositType": {
"description": "An enumeration.",
"enum": ["GRANT_15_17", "GRANT_18"],
"enum": ["GRANT_17_18", "GRANT_15_17", "GRANT_18"],
"title": "DepositType",
},
"DomainsCredit": {
Expand Down Expand Up @@ -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": {
Expand Down

0 comments on commit e2e2058

Please sign in to comment.