diff --git a/app/core/myeclpay/endpoints_myeclpay.py b/app/core/myeclpay/endpoints_myeclpay.py index 30f6e30749..8ceca54c1e 100644 --- a/app/core/myeclpay/endpoints_myeclpay.py +++ b/app/core/myeclpay/endpoints_myeclpay.py @@ -1097,6 +1097,7 @@ async def register_user( accepted_tos_version=0, db=db, ) + await db.flush() hyperion_myeclpay_logger.info( wallet_id, @@ -1685,7 +1686,7 @@ async def init_ha_transfer( ) raise HTTPException( status_code=400, - detail="Redirect URL is not trusted by hyperion", + detail="Redirect URL is not trusted by Hyperion", ) if transfer_info.amount < 100: @@ -1701,7 +1702,7 @@ async def init_ha_transfer( if user_payment is None: raise HTTPException( status_code=404, - detail="User is not registered for MyECL Pay", + detail="User is not registered for MyECLPay", ) if not is_user_latest_tos_signed(user_payment): @@ -1725,29 +1726,23 @@ async def init_ha_transfer( detail="Wallet balance would exceed the maximum allowed balance", ) - user_schema = schemas_users.CoreUser( - account_type=user.account_type, - school_id=user.school_id, - email=user.email, - birthday=user.birthday, - promo=user.promo, - floor=user.floor, - phone=user.phone, - created_on=user.created_on, - groups=[], - id=user.id, - name=user.name, - firstname=user.firstname, - nickname=user.nickname, - ) - checkout = await payment_tool.init_checkout( - module="myeclpay", - checkout_amount=transfer_info.amount, - checkout_name="Recharge MyECL Pay", - redirection_uri=f"{settings.CLIENT_URL}myeclpay/transfer/redirect?url={transfer_info.redirect_url}", - payer_user=user_schema, - db=db, - ) + try: + checkout = await payment_tool.init_checkout( + module="myeclpay", + checkout_amount=transfer_info.amount, + checkout_name="Recharge MyECL Pay", + redirection_uri=f"{settings.CLIENT_URL}myeclpay/transfer/redirect?url={transfer_info.redirect_url}", + payer_user=schemas_payment.PayerUser( + firstname=user.firstname, + name=user.name, + email=user.email, + birthday=user.birthday, + ), + db=db, + ) + except Exception: + raise HTTPException(status_code=502, detail="Cannot init the checkout") + hyperion_error_logger.info(f"Competition: Logging Checkout id {checkout.id}") await cruds_myeclpay.create_transfer( db=db, @@ -1762,7 +1757,6 @@ async def init_ha_transfer( confirmed=False, ), ) - return schemas_payment.PaymentUrl( url=checkout.payment_url, ) diff --git a/app/core/payment/payment_tool.py b/app/core/payment/payment_tool.py index a2f4a95e72..6b8ab20f96 100644 --- a/app/core/payment/payment_tool.py +++ b/app/core/payment/payment_tool.py @@ -19,7 +19,6 @@ from app.core.payment import cruds_payment, models_payment, schemas_payment from app.core.payment.types_payment import HelloAssoConfig -from app.core.users import schemas_users from app.core.utils import security from app.types.exceptions import ( MissingHelloAssoCheckoutIdError, @@ -130,7 +129,7 @@ async def init_checkout( checkout_amount: int, checkout_name: str, db: AsyncSession, - payer_user: schemas_users.CoreUser | None = None, + payer_user: schemas_payment.PayerUser, redirection_uri: str | None = None, ) -> schemas_payment.Checkout: """ @@ -152,27 +151,24 @@ async def init_checkout( This method use HelloAsso API. It may raise exceptions if HA checkout initialization fails. Exceptions can be imported from `helloasso_python` package. """ - configuration = self.get_hello_asso_configuration() redirection_uri = redirection_uri or self._redirection_uri if not redirection_uri: raise UnsetRedirectionUriError - # We want to ensure that any error is logged, even if modules tries to try/except this method - # Thus we catch any exception and log it, then reraise it + exception_start = f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name} for payer {payer_user.firstname} {payer_user.name}" try: - payer: HelloAssoApiV5ModelsCartsCheckoutPayer | None = None - if payer_user: - payer = HelloAssoApiV5ModelsCartsCheckoutPayer( - firstName=payer_user.firstname, - lastName=payer_user.name, - email=payer_user.email, - dateOfBirth=payer_user.birthday, - ) + # We want to ensure that any error is logged + # Thus we catch any exception and log it, then reraise it checkout_model_id = uuid.uuid4() secret = security.generate_token(nbytes=12) - + payer = HelloAssoApiV5ModelsCartsCheckoutPayer( + firstName=payer_user.firstname, + lastName=payer_user.name, + email=payer_user.email, + dateOfBirth=payer_user.birthday, + ) init_checkout_body = HelloAssoApiV5ModelsCartsInitCheckoutBody( total_amount=checkout_amount, initial_amount=checkout_amount, @@ -189,6 +185,7 @@ async def init_checkout( ) response: HelloAssoApiV5ModelsCartsInitCheckoutResponse + configuration = self.get_hello_asso_configuration() with ApiClient(configuration) as api_client: checkout_api = CheckoutApi(api_client) try: @@ -196,62 +193,54 @@ async def init_checkout( self._helloasso_slug, init_checkout_body, ) - except (UnauthorizedException, BadRequestException): - # We know that HelloAsso may refuse some payer infos, like using the firstname "test" - # Even when prefilling the payer infos,the user will be able to edit them on the payment page, - # so we can safely retry without the payer infos - if not payer_user: - hyperion_error_logger.exception( - f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name} (no payer info provided).", - ) - else: - payer_user_name = f"{payer_user.firstname} {payer_user.name}" - hyperion_error_logger.warning( - f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. Retrying without payer infos for {payer_user_name}", - ) - - init_checkout_body.payer = None - try: - response = checkout_api.organizations_organization_slug_checkout_intents_post( - self._helloasso_slug, - init_checkout_body, - ) - except UnauthorizedException: - # HelloAsso returned a 401 unauthorized again - hyperion_error_logger.exception( - f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}, with and without payer {payer_user_name} infos", - ) - - if response and response.id: - checkout_model = models_payment.Checkout( - id=checkout_model_id, - module=module, - name=checkout_name, - amount=checkout_amount, - hello_asso_checkout_id=response.id, - secret=secret, - ) - - await cruds_payment.create_checkout(db=db, checkout=checkout_model) - - return schemas_payment.Checkout( - id=checkout_model_id, - payment_url=response.redirect_url, - ) - hyperion_error_logger.error( - f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. No checkout id returned", + except BadRequestException: + # In this case only, we retry + hyperion_error_logger.warning( + f"{exception_start}: retrying without payer infos", + ) + init_checkout_body.payer = None + response = checkout_api.organizations_organization_slug_checkout_intents_post( + self._helloasso_slug, + init_checkout_body, + ) + if response.id is None: + raise MissingHelloAssoCheckoutIdError() # noqa: TRY301 + + checkout_model = models_payment.Checkout( + id=checkout_model_id, + module=module, + name=checkout_name, + amount=checkout_amount, + hello_asso_checkout_id=response.id, + secret=secret, + ) + await cruds_payment.create_checkout(db=db, checkout=checkout_model) + return schemas_payment.Checkout( + id=checkout_model_id, + payment_url=response.redirect_url, ) - raise MissingHelloAssoCheckoutIdError() # noqa: TRY301 - except Exception: - # Different from a 401 unauthorized - payer_user_name = "" - if payer_user: - payer_user_name = f"{payer_user.firstname} {payer_user.name}" + except UnauthorizedException as e: hyperion_error_logger.exception( - f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name} with payer {payer_user_name} infos", + f"{exception_start}: unauthorized for headers {e.headers}.", ) raise + except BadRequestException as e: + # We know that HelloAsso may refuse some payer infos, + # e.g. >18 years old, valid email, and firstname and name without some characters and patterns. + # See https://dev.helloasso.com/docs/int%C3%A9grer-le-paiement-sur-votre-site#contr%C3%B4le-des-champs + hyperion_error_logger.exception( + f"{exception_start}: bad request for reason {e.data or e.body}.", + ) + raise + except MissingHelloAssoCheckoutIdError: + hyperion_error_logger.exception( + f"{exception_start}: no checkout id returned.", + ) + raise + except Exception: + hyperion_error_logger.exception(f"{exception_start}: unknown exception.") + raise async def get_checkout( self, diff --git a/app/core/payment/schemas_payment.py b/app/core/payment/schemas_payment.py index ab40eadf65..13f4ae4737 100644 --- a/app/core/payment/schemas_payment.py +++ b/app/core/payment/schemas_payment.py @@ -1,4 +1,5 @@ import uuid +from datetime import date from pydantic import BaseModel, computed_field @@ -38,3 +39,10 @@ class HelloAssoCheckoutMetadata(BaseModel): class PaymentUrl(BaseModel): url: str + + +class PayerUser(BaseModel): + firstname: str + name: str + email: str + birthday: date | None = None diff --git a/app/modules/cdr/endpoints_cdr.py b/app/modules/cdr/endpoints_cdr.py index fa486eb9e7..dd751d094a 100644 --- a/app/modules/cdr/endpoints_cdr.py +++ b/app/modules/cdr/endpoints_cdr.py @@ -15,10 +15,11 @@ from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession -from app.core.groups import cruds_groups, schemas_groups +from app.core.groups import cruds_groups from app.core.groups.groups_type import GroupType from app.core.memberships import cruds_memberships, schemas_memberships from app.core.payment.payment_tool import PaymentTool +from app.core.payment.schemas_payment import PayerUser from app.core.payment.types_payment import HelloAssoConfigName from app.core.users import cruds_users, models_users, schemas_users from app.core.users.cruds_users import get_user_by_id, get_users @@ -2619,36 +2620,24 @@ async def get_payment_url( status_code=403, detail="Please give an amount in cents, greater than 1€.", ) - user_schema = schemas_users.CoreUser( - account_type=user.account_type, - school_id=user.school_id, - email=user.email, - birthday=user.birthday, - promo=user.promo, - floor=user.floor, - phone=user.phone, - created_on=user.created_on, - groups=[ - schemas_groups.CoreGroupSimple( - id=group.id, - name=group.name, - description=group.description, - ) - for group in user.groups - ], - id=user.id, - name=user.name, - firstname=user.firstname, - nickname=user.nickname, - ) - checkout = await payment_tool.init_checkout( - module=module.root, - checkout_amount=amount, - checkout_name="Chaine de rentrée", - payer_user=user_schema, - db=db, - ) + + try: + checkout = await payment_tool.init_checkout( + module=module.root, + checkout_amount=amount, + checkout_name="Chaine de rentrée", + payer_user=PayerUser( + firstname=user.firstname, + name=user.name, + email=user.email, + birthday=user.birthday, + ), + db=db, + ) + except Exception: + raise HTTPException(status_code=502, detail="Cannot init the checkout") hyperion_error_logger.info(f"CDR: Logging Checkout id {checkout.id}") + cruds_cdr.create_checkout( db=db, checkout=models_cdr.Checkout( @@ -2657,7 +2646,6 @@ async def get_payment_url( checkout_id=checkout.id, ), ) - return schemas_cdr.PaymentUrl( url=checkout.payment_url, ) diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index 9b866fd244..e455225c93 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -9,8 +9,9 @@ from app.core.groups.groups_type import AccountType, GroupType from app.core.payment.payment_tool import PaymentTool +from app.core.payment.schemas_payment import PayerUser from app.core.payment.types_payment import HelloAssoConfigName -from app.core.users import models_users, schemas_users +from app.core.users import models_users from app.dependencies import ( get_db, get_payment_tool, @@ -900,16 +901,23 @@ async def get_payment_url( if not participant: raise HTTPException(status_code=403, detail="You are not a participant.") price, checkout_name = calculate_raid_payment(participant, raid_prices) - user_dict = user.__dict__ - user_dict.pop("school", None) - checkout = await payment_tool.init_checkout( - module=module.root, - checkout_amount=price, - checkout_name=checkout_name, - payer_user=schemas_users.CoreUser(**user_dict), - db=db, - ) + try: + checkout = await payment_tool.init_checkout( + module=module.root, + checkout_amount=price, + checkout_name=checkout_name, + payer_user=PayerUser( + firstname=user.firstname, + name=user.name, + email=user.email, + birthday=user.birthday, + ), + db=db, + ) + except Exception: + raise HTTPException(status_code=502, detail="Cannot init the checkout") hyperion_error_logger.info(f"RAID: Logging Checkout id {checkout.id}") + await cruds_raid.create_participant_checkout( models_raid.RaidParticipantCheckout( id=str(uuid.uuid4()), diff --git a/app/modules/sport_competition/endpoints_sport_competition.py b/app/modules/sport_competition/endpoints_sport_competition.py index 8426ef41e2..21796d5945 100644 --- a/app/modules/sport_competition/endpoints_sport_competition.py +++ b/app/modules/sport_competition/endpoints_sport_competition.py @@ -8,6 +8,7 @@ from app.core.groups.groups_type import GroupType, get_account_types_except_externals from app.core.payment.payment_tool import PaymentTool +from app.core.payment.schemas_payment import PayerUser from app.core.payment.types_payment import HelloAssoConfigName from app.core.schools import cruds_schools from app.core.schools.schools_type import SchoolType @@ -3688,24 +3689,24 @@ async def get_payment_url( status_code=403, detail="Please give an amount in cents, greater than 1€.", ) - user_schema = schemas_users.CoreUser( - account_type=user.account_type, - school_id=user.school_id, - id=user.id, - email=user.email, - name=user.name, - firstname=user.firstname, - created_on=user.created_on, - groups=[], - ) - checkout = await payment_tool.init_checkout( - module=module.root, - checkout_amount=amount, - checkout_name=f"Challenge {edition.name}", - payer_user=user_schema, - db=db, - ) + + try: + checkout = await payment_tool.init_checkout( + module=module.root, + checkout_amount=amount, + checkout_name=f"Challenge {edition.name}", + payer_user=PayerUser( + firstname=user.firstname, + name=user.name, + email=user.email, + birthday=user.birthday, + ), + db=db, + ) + except Exception: + raise HTTPException(status_code=502, detail="Cannot init the checkout") hyperion_error_logger.info(f"Competition: Logging Checkout id {checkout.id}") + cruds_sport_competition.add_checkout( db=db, checkout=schemas_sport_competition.Checkout( @@ -3715,7 +3716,6 @@ async def get_payment_url( checkout_id=checkout.id, ), ) - return schemas_sport_competition.PaymentUrl( url=checkout.payment_url, ) diff --git a/tests/commons.py b/tests/commons.py index 5b664defad..abc69b2969 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -20,7 +20,7 @@ from app.core.payment.payment_tool import PaymentTool from app.core.payment.types_payment import HelloAssoConfig, HelloAssoConfigName from app.core.schools.schools_type import SchoolType -from app.core.users import cruds_users, models_users, schemas_users +from app.core.users import cruds_users, models_users from app.core.utils import security from app.core.utils.config import Settings from app.types import core_data @@ -305,7 +305,7 @@ async def init_checkout( checkout_amount: int, checkout_name: str, db: AsyncSession, - payer_user: schemas_users.CoreUser | None = None, + payer_user: schemas_payment.PayerUser, redirection_uri: str | None = None, ) -> schemas_payment.Checkout: exist = await cruds_payment.get_checkout_by_id(mocked_checkout_id, db) diff --git a/tests/test_myeclpay.py b/tests/test_myeclpay.py index 99f11199db..66fdf82018 100644 --- a/tests/test_myeclpay.py +++ b/tests/test_myeclpay.py @@ -1789,7 +1789,7 @@ def test_transfer_with_redirect_url_not_trusted(client: TestClient): ) assert response.status_code == 400 - assert response.json()["detail"] == "Redirect URL is not trusted by hyperion" + assert response.json()["detail"] == "Redirect URL is not trusted by Hyperion" def test_transfer_with_unregistered_user(client: TestClient): @@ -1804,7 +1804,7 @@ def test_transfer_with_unregistered_user(client: TestClient): ) assert response.status_code == 404 - assert response.json()["detail"] == "User is not registered for MyECL Pay" + assert response.json()["detail"] == "User is not registered for MyECLPay" def test_transfer_with_too_small_amount(client: TestClient): diff --git a/tests/test_payment.py b/tests/test_payment.py index 88cd84264a..f7f4552d52 100644 --- a/tests/test_payment.py +++ b/tests/test_payment.py @@ -4,7 +4,7 @@ import pytest import pytest_asyncio from fastapi.testclient import TestClient -from helloasso_python.exceptions import UnauthorizedException +from helloasso_python.exceptions import BadRequestException from helloasso_python.models.hello_asso_api_v5_models_carts_init_checkout_body import ( HelloAssoApiV5ModelsCartsInitCheckoutBody, ) @@ -18,8 +18,6 @@ from app.core.payment import cruds_payment, models_payment, schemas_payment from app.core.payment.payment_tool import PaymentTool from app.core.payment.types_payment import HelloAssoConfig, HelloAssoConfigName -from app.core.schools import schemas_schools -from app.core.users import schemas_users from app.types.module import Module from tests.commons import ( MockedPaymentTool, @@ -35,7 +33,7 @@ existing_checkout_payment: models_payment.CheckoutPayment checkout: models_payment.Checkout -user_schema: schemas_users.CoreUser +payer_user: schemas_payment.PayerUser TEST_MODULE_ROOT = "tests" @@ -74,19 +72,14 @@ async def init_objects() -> None: ) await add_object_to_db(checkout) - global user_schema - user = await create_user_with_groups( - groups=[], + user = await create_user_with_groups(groups=[]) + global payer_user + payer_user = schemas_payment.PayerUser( + firstname=user.firstname, + name=user.name, + email=user.email, + birthday=user.birthday, ) - school = schemas_schools.CoreSchool( - id=user.school.id, - name=user.school.name, - email_regex=user.school.email_regex, - ) - user_dict = user.__dict__ - user_dict.pop("school") - - user_schema = schemas_users.CoreUser(**user_dict, school=school) # Test endpoints # @@ -451,7 +444,7 @@ async def test_payment_tool_init_checkout( checkout_name="test", redirection_uri="redirect", db=db, - payer_user=user_schema, + payer_user=payer_user, ) assert returned_checkout.payment_url == redirect_url @@ -497,7 +490,7 @@ def init_a_checkout_side_effect( if init_checkout_body.payer is not None: r = Response() r.status_code = 400 - raise UnauthorizedException + raise BadRequestException return HelloAssoApiV5ModelsCartsInitCheckoutResponse( id=7, redirect_url=redirect_url, @@ -526,7 +519,7 @@ def init_a_checkout_side_effect( checkout_name="test", redirection_uri="redirect", db=db, - payer_user=user_schema, + payer_user=payer_user, ) assert returned_checkout.payment_url == redirect_url @@ -592,7 +585,7 @@ async def test_payment_tool_init_checkout_fail( checkout_name="test", redirection_uri="redirect", db=db, - payer_user=user_schema, + payer_user=payer_user, ) mocked_hyperion_security_logger.assert_called()