From ca118ba95fdd1082f9c9b473d28ebbadef0b8aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Robert?= Date: Tue, 4 Aug 2026 00:59:08 +0200 Subject: [PATCH 1/2] feat: change MyPayment CGU to V3 --- app/core/mypayment/types_mypayment.py | 2 +- assets/mypayment-terms-of-service.txt | 69 ++++++++++++++++++++------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/app/core/mypayment/types_mypayment.py b/app/core/mypayment/types_mypayment.py index 9703d5fe12..c25cf00d7f 100644 --- a/app/core/mypayment/types_mypayment.py +++ b/app/core/mypayment/types_mypayment.py @@ -1,6 +1,6 @@ from enum import StrEnum -LATEST_TOS = 2 +LATEST_TOS = 3 QRCODE_EXPIRATION = 5 # minutes REQUEST_EXPIRATION = 8 # minutes RETENTION_DURATION = 10 * 365 # 10 years in days diff --git a/assets/mypayment-terms-of-service.txt b/assets/mypayment-terms-of-service.txt index 1214fd3d74..e6a209e644 100644 --- a/assets/mypayment-terms-of-service.txt +++ b/assets/mypayment-terms-of-service.txt @@ -1,30 +1,63 @@ -Version 2 +Version 3 # CGU {payment_name} -## Objet des CGU -Les présentes Conditions Générales d’Utilisation (CGU) ont pour objet de définir les modalités et conditions d’utilisation de la solution de paiement dématérialisée {payment_name}, éditée par {entity_name}, destinée aux élèves et étudiants de {school_name}. +## 1. Objet des CGU -## Présentation de {payment_name} -{payment_name} est une solution de paiement virtuel et dématérialisé utilisable au sein de {school_name} exclusivement, avec les associations participantes. Elle repose sur une monnaie virtuelle appelée , qui n’a aucune valeur légale et ne constitue pas une monnaie électronique au sens du Code monétaire et financier. Un (1) euro correspond à un (1) . +Les présentes Conditions Générales d’Utilisation (CGU) ont pour objet de définir les modalités et conditions d’utilisation de la solution de paiement dématérialisée {payment_name}, éditée par {entity_name}, exclusivement destinée aux élèves et étudiants de {school_name}. -## Fonctionnement du portefeuille virtuel -Lors de son inscription, l’utilisateur se voit attribuer un portefeuille numérique personnel plafonné. Il s’engage à ne pas tenter de dépasser ce plafond par quelque moyen que ce soit. En validant son inscription, l’utilisateur reconnaît renoncer expressément à son droit de rétractation, conformément à l’article L221-28 du Code de la Consommation. -La monnaie virtuelle ne peut pas être convertie en euros ou en toute autre devise à cours légal. Elle est non remboursable, non réversible, et ne peut pas être cédée ou transférée à d’autres utilisateurs. Un portefeuille ne peut jamais être en solde négatif. +## 2. Présentation du service et Cadre Réglementaire + +{payment_name} est un instrument de paiement reposant sur une valeur monétaire stockée sous forme électronique (monnaie électronique), appelée MyCoin (ʍ). Un (1) euro correspond à un (1) MyCoin. + +Conformément aux articles L. 521-3 et L. 525-5 du Code monétaire et financier, ce service est fourni sous le régime de l’exemption de réseau limité. L'utilisation de MyCoin est strictement limitée à l’enceinte de {school_name} et aux événements organisés par les associations partenaires ayant conclu un mandat d’encaissement avec {bank_account_holder}. + +## 3. Fonctionnement du portefeuille virtuel et Plafonds + +Lors de son inscription, l’utilisateur se voit attribuer un portefeuille numérique personnel. Pour respecter les seuils de vigilance relatifs à la lutte contre le blanchiment et le financement du terrorisme (LCB-FT), le portefeuille est soumis aux limites suivantes : + +- Le solde du compte ne peut excéder 150 ʍ. +- Un portefeuille ne peut jamais être en solde négatif. +- La monnaie virtuelle ne peut pas être cédée ou transférée à d’autres utilisateurs. + +## 4. Recharge du portefeuille via HelloAsso + +L’utilisateur peut recharger son portefeuille par carte bancaire via la plateforme HelloAsso. Les fonds seront versés directement sur le compte bancaire de l’association {bank_account_holder}, et un équivalent en est crédité sur le portefeuille numérique de l’utilisateur. -## Recharge du portefeuille via HelloAsso -L’utilisateur peut recharger son portefeuille par carte bancaire via la plateforme HelloAsso. Les fonds seront versés directement sur le compte bancaire de l’association {entity_name}, et un équivalent en est crédité sur le portefeuille numérique de l’utilisateur. HelloAsso est une plateforme française de paiement pour les associations, fonctionnant sur un modèle solidaire, garantissant que 100 % de votre paiement sera versé à l’association choisie. Elle se finance uniquement par des 'contribution volontaire' et par défaut, HelloAsso proposera à l’utilisateur de faire une 'contribution volontaire', qui peut librement refuser sans que cela empêche la transaction. Si vous choisissez de faire cette contribution, celle-ci soutiendra l’aide qu’HelloAsso apporte aux associations et seul HelloAsso en bénéficiera. -## Utilisation des fonds et paiements -Le paiement avec {payment_name} est uniquement possible auprès des associations partenaires. L’utilisateur doit préalablement activer son appareil par un lien unique reçu par email. Chaque paiement est validé par une vérification biométrique effectuée localement par l’appareil de l’utilisateur. +Conformément à l'article L221-28 du Code de la consommation, les paiements en ligne pour des prestations de services de billetterie ou de consommation immédiate sont fermes et définitifs. Toutefois, le stockage de la valeur sur le portefeuille {payment_name} reste remboursable selon les conditions de l’Article 5. + +## 5. Droit au remboursement + +Conformément à la réglementation applicable à la monnaie électronique, l’utilisateur dispose d’un droit au remboursement de son solde à tout moment, à sa valeur nominale. + +L'utilisateur peut à tout moment demander le remboursement de son solde via l'application. Le remboursement sera effectué par virement bancaire sur le compte bancaire de l’utilisateur, dans un délai maximum de 30 jours. + +Les frais bancaires éventuels liés au virement seront à la charge de l’utilisateur. +Cette demande de remboursement équivaut à la fermeture définitive du compte {payment_name} de l'utilisateur. + +## 6. Utilisation des fonds et Interdictions + +Le paiement avec {payment_name} est uniquement possible auprès des associations partenaires pour l’acquisition de biens et services liés à la vie du campus. + +- Interdiction de transfert (P2P) : La cession ou le transfert de MyCoins entre utilisateurs est strictement interdit afin de garantir l'étanchéité du circuit de paiement. +- Interdiction de retrait : La conversion du crédit MyCoin en espèces (cash-out) est interdite, hors procédure de remboursement exceptionnel définie à l'Article 5. + +## 7. Sécurité, responsabilité et données personnelles + +L’utilisateur est responsable de la sécurité de son accès. Chaque paiement est validé par une vérification biométrique locale effectuée par l’appareil de l’utilisateur. Aucune donnée biométrique n'est collectée, transmise ou stockée par {payment_name} ou {entity_name}. +{payment_name} respecte le RGPD. Les données collectées (email, historique de transactions, identifiant d'appareil) sont strictement nécessaires au service. + +Dans le cadre du remboursement, l'IBAN fourni lors de la demande est utilisé exclusivement pour l'exécution du virement et n'est pas conservé au-delà du traitement comptable. + +Aucune autre donnée bancaire n’est conservée par l’association {bank_account_holder}, cette gestion étant déléguée aux prestataires de services de paiement agréés de HelloAsso. + +{payment_name} fait partie de la plateforme {application_name}. En utilisant le service, l’utilisateur reconnait et accepte les conditions générales d’utilisations et la politique de confidentialité de {application_name} + +## 8. Modifications des CGU -## Sécurité, responsabilité et données personnelles -L’utilisateur est responsable de la sécurité de son appareil et de son accès à {payment_name}. Toute utilisation frauduleuse, perte ou vol doit être signalée sans délai. Le compte pourra alors être suspendu temporairement. Il est interdit de prêter ou partager son compte. -{payment_name} respecte le Règlement Général sur la Protection des Données (RGPD). Les données collectées sont strictement nécessaires au fonctionnement du service : identifiants, email, historique de transactions, identifiant de l’appareil. Aucune donnée bancaire n’est collectée ni conservée par {payment_name}. -{payment_name} fait partie de la plateforme {application_name}, en utilisant le service, l’utilisateur reconnait et accepte les conditions générales d’utilisations et la politique de confidentialité de {application_name} +{entity_name} se réserve le droit de modifier les présentes CGU à tout moment. L’utilisateur sera notifié de toute modification majeure. -## Modifications des CGU -{entity_name} se réserve le droit de modifier les présentes CGU à tout moment. L’utilisation du service après notification est conditionné par l’acceptation des nouvelles CGU. Pour toute question, demande d’assistance ou réclamation, l’utilisateur peut contacter : {entity_email}. From d06cfef2805166ac1f0546d39988547f0c101ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Robert?= Date: Tue, 4 Aug 2026 12:52:27 +0200 Subject: [PATCH 2/2] fix: add dynamic payment data in TOS --- app/core/core_endpoints/endpoints_core.py | 11 +++++-- app/core/mypayment/endpoints_mypayment.py | 7 +++-- app/core/mypayment/utils_mypayment.py | 35 +++++++++++++++++++++++ assets/mypayment-terms-of-service.txt | 2 +- tests/core/test_core.py | 12 ++++++++ tests/core/test_mypayment.py | 3 ++ 6 files changed, 64 insertions(+), 6 deletions(-) diff --git a/app/core/core_endpoints/endpoints_core.py b/app/core/core_endpoints/endpoints_core.py index c1ebf7299f..6f4d25ea8a 100644 --- a/app/core/core_endpoints/endpoints_core.py +++ b/app/core/core_endpoints/endpoints_core.py @@ -1,10 +1,13 @@ from anyio import Path from fastapi import APIRouter, Depends, Request from fastapi.responses import FileResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.core.core_endpoints import schemas_core +from app.core.mypayment.utils_mypayment import patch_payment_identity_in_text from app.core.utils.config import Settings from app.dependencies import ( + get_db, get_settings, ) from app.types.module import CoreModule @@ -78,13 +81,17 @@ async def read_terms_and_conditions(settings: Settings = Depends(get_settings)): response_model=str, status_code=200, ) -async def read_mypayment_tos(settings: Settings = Depends(get_settings)): +async def read_mypayment_tos( + settings: Settings = Depends(get_settings), + db: AsyncSession = Depends(get_db), +): """ Return MyPayment latest ToS """ - return patch_identity_in_text( + return await patch_payment_identity_in_text( await Path("assets/mypayment-terms-of-service.txt").read_text(encoding="utf-8"), settings, + db, ) diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index 0fe6758f66..dc91718e31 100644 --- a/app/core/mypayment/endpoints_mypayment.py +++ b/app/core/mypayment/endpoints_mypayment.py @@ -75,6 +75,7 @@ apply_transaction, call_mypayment_callback, is_user_latest_tos_signed, + patch_payment_identity_in_text, validate_transfer_callback, verify_signature, ) @@ -108,7 +109,6 @@ generate_pdf_from_template, get_core_data, get_file_from_data, - patch_identity_in_text, set_core_data, ) @@ -1418,9 +1418,10 @@ async def get_user_tos( return schemas_mypayment.TOSSignatureResponse( accepted_tos_version=existing_user_payment.accepted_tos_version, latest_tos_version=LATEST_TOS, - tos_content=patch_identity_in_text( + tos_content=await patch_payment_identity_in_text( await Path("assets/mypayment-terms-of-service.txt").read_text(), - settings=settings, + settings, + db, ), max_wallet_balance=settings.MYPAYMENT_MAXIMUM_WALLET_BALANCE, ) diff --git a/app/core/mypayment/utils_mypayment.py b/app/core/mypayment/utils_mypayment.py index d9e3358a07..7d6daeadc5 100644 --- a/app/core/mypayment/utils_mypayment.py +++ b/app/core/mypayment/utils_mypayment.py @@ -13,6 +13,7 @@ from app.core.checkout.types_checkout import HelloAssoConfigName from app.core.checkout.utils_checkout import CHECKOUT_EXPIRATION from app.core.mypayment import cruds_mypayment, models_mypayment, schemas_mypayment +from app.core.mypayment.coredata_mypayment import MyPaymentBankAccountHolder from app.core.mypayment.exceptions_mypayment import ( InvalidCheckoutToolError, InvalidRequestTypeError, @@ -42,7 +43,9 @@ from app.core.users import schemas_users from app.core.utils.config import Settings from app.module import all_modules +from app.types.exceptions import CoreDataNotFoundError from app.utils.communication.notifications import NotificationTool +from app.utils.tools import get_core_data, patch_identity_in_text hyperion_security_logger = logging.getLogger("hyperion.security") hyperion_mypayment_logger = logging.getLogger("hyperion.mypayment") @@ -440,3 +443,35 @@ async def ensure_user_can_manage_events( 403, detail="User is not authorized to manage store's events", ) + + +async def patch_payment_identity_in_text( + text: str, + settings: Settings, + db: AsyncSession, +) -> str: + """ + Replace placeholders in the text with the corresponding values from the settings. + """ + try: + bank_account_holder = await get_core_data( + MyPaymentBankAccountHolder, + db=db, + ) + structure = await cruds_mypayment.get_structure_by_id( + db=db, + structure_id=bank_account_holder.holder_structure_id, + ) + except CoreDataNotFoundError: + structure = None + text = patch_identity_in_text( + text, + settings=settings, + ) + return text.replace( + "{mypayment_max_balance}", + "%.2f" % (settings.MYPAYMENT_MAXIMUM_WALLET_BALANCE / 100.0), + ).replace( + "{bank_account_holder}", + structure.name if structure else "N/A", + ) diff --git a/assets/mypayment-terms-of-service.txt b/assets/mypayment-terms-of-service.txt index e6a209e644..92ce145b84 100644 --- a/assets/mypayment-terms-of-service.txt +++ b/assets/mypayment-terms-of-service.txt @@ -16,7 +16,7 @@ Conformément aux articles L. 521-3 et L. 525-5 du Code monétaire et financier, Lors de son inscription, l’utilisateur se voit attribuer un portefeuille numérique personnel. Pour respecter les seuils de vigilance relatifs à la lutte contre le blanchiment et le financement du terrorisme (LCB-FT), le portefeuille est soumis aux limites suivantes : -- Le solde du compte ne peut excéder 150 ʍ. +- Le solde du compte ne peut excéder {mypayment_max_balance} ʍ. - Un portefeuille ne peut jamais être en solde négatif. - La monnaie virtuelle ne peut pas être cédée ou transférée à d’autres utilisateurs. diff --git a/tests/core/test_core.py b/tests/core/test_core.py index cc4bd14a04..7a7920ebd5 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -15,6 +15,8 @@ def test_get_privacy(client: TestClient) -> None: "/privacy", ) assert response.status_code == 200 + assert "{" not in response.text # Ensure no placeholders are present + assert "}" not in response.text def test_get_terms_and_conditions(client: TestClient) -> None: @@ -22,6 +24,8 @@ def test_get_terms_and_conditions(client: TestClient) -> None: "/terms-and-conditions", ) assert response.status_code == 200 + assert "{" not in response.text # Ensure no placeholders are present + assert "}" not in response.text def test_get_mypayment_tos(client: TestClient) -> None: @@ -29,6 +33,8 @@ def test_get_mypayment_tos(client: TestClient) -> None: "/mypayment-terms-of-service", ) assert response.status_code == 200 + assert "{" not in response.text # Ensure no placeholders are present + assert "}" not in response.text def test_get_support(client: TestClient) -> None: @@ -36,6 +42,8 @@ def test_get_support(client: TestClient) -> None: "/support", ) assert response.status_code == 200 + assert "{" not in response.text # Ensure no placeholders are present + assert "}" not in response.text def test_get_security_txt(client: TestClient) -> None: @@ -43,6 +51,8 @@ def test_get_security_txt(client: TestClient) -> None: "/security.txt", ) assert response.status_code == 200 + assert "{" not in response.text # Ensure no placeholders are present + assert "}" not in response.text def test_get_wellknown_security_txt(client: TestClient) -> None: @@ -50,6 +60,8 @@ def test_get_wellknown_security_txt(client: TestClient) -> None: "/.well-known/security.txt", ) assert response.status_code == 200 + assert "{" not in response.text # Ensure no placeholders are present + assert "}" not in response.text def test_get_favicon(client: TestClient) -> None: diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index e3c7eea6b1..1c86ee8e3a 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -2012,6 +2012,9 @@ async def test_get_user_tos(client: TestClient): assert response.status_code == 200 assert response.json()["latest_tos_version"] == LATEST_TOS assert response.json()["accepted_tos_version"] == LATEST_TOS + # Check that the TOS content does not contain any placeholders + assert "{" not in response.json()["tos_content"] + assert "}" not in response.json()["tos_content"] async def test_register_new_user(client: TestClient):