From b3555a7807ac8bfd177235ad2f429219295595fb Mon Sep 17 00:00:00 2001 From: Emmy D'Anello Date: Mon, 19 Feb 2024 00:17:14 +0100 Subject: [PATCH] Create Hello Asso checkout intents Signed-off-by: Emmy D'Anello --- registration/models.py | 46 +++++++++- .../templates/registration/payment_form.html | 2 +- registration/urls.py | 5 +- registration/views.py | 20 +++++ tfjm/helloasso.py | 86 +++++++++++++++++++ tfjm/settings.py | 4 +- 6 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 tfjm/helloasso.py diff --git a/registration/models.py b/registration/models.py index 407e5aa..cc0ef80 100644 --- a/registration/models.py +++ b/registration/models.py @@ -1,13 +1,13 @@ # Copyright (C) 2020 by Animath # SPDX-License-Identifier: GPL-3.0-or-later -from datetime import date +from datetime import date, datetime from django.contrib.sites.models import Site from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.template import loader -from django.urls import reverse_lazy +from django.urls import reverse_lazy, reverse from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.encoding import force_bytes @@ -16,6 +16,8 @@ from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from polymorphic.models import PolymorphicModel + +from tfjm import helloasso from tfjm.tokens import email_validation_token @@ -564,6 +566,46 @@ class Payment(models.Model): default=False, ) + def get_checkout_intent(self): + if self.checkout_intent_id is None: + return None + return helloasso.get_checkout_intent(self.checkout_intent_id) + + def create_checkout_intent(self): + checkout_intent = self.get_checkout_intent() + if checkout_intent is not None: + return checkout_intent + + from participation.models import Tournament + tournament = self.registrations.first().team.participation.tournament \ + if not self.final else Tournament.final_tournament() + year = datetime.now().year + base_site = "https://" + Site.objects.first().domain + checkout_intent = helloasso.create_checkout_intent( + amount=100 * self.amount, + name=f"Participation au TFJM² {year} - {tournament.name}", + back_url=base_site + reverse('registration:update_payment', args=(self.id,)), + error_url=base_site + reverse('registration:update_payment', args=(self.id,)), + return_url=base_site + reverse('registration:update_payment', args=(self.id,)), + contains_donation=False, + metadata=dict( + users=[ + dict(user_id=registration.user.id, + first_name=registration.user.first_name, + last_name=registration.user.last_name, + email=registration.user.email,) + for registration in self.registrations.all() + ], + payment_id=self.id, + final=self.final, + tournament_id=tournament.id, + ) + ) + self.checkout_intent_id = checkout_intent["id"] + self.save() + + return checkout_intent + def get_absolute_url(self): return reverse_lazy("registration:update_payment", args=(self.pk,)) diff --git a/registration/templates/registration/payment_form.html b/registration/templates/registration/payment_form.html index 6272c2a..327bb2f 100644 --- a/registration/templates/registration/payment_form.html +++ b/registration/templates/registration/payment_form.html @@ -86,7 +86,7 @@ payer pour vous. diff --git a/registration/urls.py b/registration/urls.py index 68ceeb4..463d2c5 100644 --- a/registration/urls.py +++ b/registration/urls.py @@ -4,8 +4,8 @@ from django.urls import path from .views import AddOrganizerView, AdultPhotoAuthorizationTemplateView, ChildPhotoAuthorizationTemplateView, \ - InstructionsTemplateView, MyAccountDetailView, ParentalAuthorizationTemplateView, PaymentUpdateView, \ - PaymentUpdateGroupView, \ + InstructionsTemplateView, MyAccountDetailView, ParentalAuthorizationTemplateView, \ + PaymentUpdateGroupView, PaymentUpdateView, PaymenRedirectHelloAssoView, \ ResetAdminView, SignupView, UserDetailView, UserImpersonateView, UserListView, UserResendValidationEmailView, \ UserUpdateView, UserUploadHealthSheetView, UserUploadParentalAuthorizationView, UserUploadPhotoAuthorizationView, \ UserUploadVaccineSheetView, UserValidateView, UserValidationEmailSentView @@ -40,6 +40,7 @@ urlpatterns = [ path("update-payment//", PaymentUpdateView.as_view(), name="update_payment"), path("update-payment//toggle-group-mode/", PaymentUpdateGroupView.as_view(), name="update_payment_group_mode"), + path("update-payment//hello-asso/", PaymenRedirectHelloAssoView.as_view(), name="payment_hello_asso"), path("user//impersonate/", UserImpersonateView.as_view(), name="user_impersonate"), path("user/list/", UserListView.as_view(), name="user_list"), path("reset-admin/", ResetAdminView.as_view(), name="reset_admin"), diff --git a/registration/views.py b/registration/views.py index 5ea96df..304de01 100644 --- a/registration/views.py +++ b/registration/views.py @@ -506,6 +506,7 @@ class PaymentUpdateGroupView(LoginRequiredMixin, DetailView): payment.grouped = False tournament = first_reg.team.participation.tournament if not payment.final else Tournament.final_tournament() payment.amount = tournament.price + payment.checkout_intent_id = None payment.save() for registration in registrations[1:]: p = Payment.objects.create(type=payment.type, @@ -525,11 +526,30 @@ class PaymentUpdateGroupView(LoginRequiredMixin, DetailView): payment.registrations.add(student) payment.amount = tournament.price * reg.team.students.count() payment.grouped = True + payment.checkout_intent_id = None payment.save() return redirect(reverse_lazy("registration:update_payment", args=(payment.pk,))) +class PaymenRedirectHelloAssoView(LoginRequiredMixin, DetailView): + model = Payment + + def dispatch(self, request, *args, **kwargs): + if not self.request.user.is_authenticated or \ + not self.request.user.registration.is_admin \ + and (self.request.user.registration not in self.get_object().registrations.all() + or self.get_object().valid is not False): + return self.handle_no_permission() + return super().dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + payment = self.get_object() + checkout_intent = payment.create_checkout_intent() + + return redirect(checkout_intent["redirectUrl"]) + + class PhotoAuthorizationView(LoginRequiredMixin, View): """ Display the sent photo authorization. diff --git a/tfjm/helloasso.py b/tfjm/helloasso.py new file mode 100644 index 0000000..768fe88 --- /dev/null +++ b/tfjm/helloasso.py @@ -0,0 +1,86 @@ +# Copyright (C) 2024 by Animath +# SPDX-License-Identifier: GPL-3.0-or-later + +from datetime import datetime, timedelta + +from django.conf import settings +import requests + + +_access_token = None +_refresh_token = None +_expires_at = None + + +def get_hello_asso_access_token(): + global _access_token, _refresh_token, _expires_at + + now = datetime.now() + if _access_token is None: + response = requests.post( + "https://api.helloasso.com/oauth2/token", + data={ + "grant_type": "client_credentials", + "client_id": settings.HELLOASSO_CLIENT_ID, + "client_secret": settings.HELLOASSO_CLIENT_SECRET, + }, + ) + response.raise_for_status() + data = response.json() + _access_token = data["access_token"] + _refresh_token = data["refresh_token"] + _expires_at = now + timedelta(seconds=data["expires_in"]) + elif now >= _expires_at: + response = requests.post( + "https://api.helloasso.com/oauth2/token", + data={ + "grant_type": "refresh_token", + "refresh_token": _refresh_token, + }, + ) + response.raise_for_status() + data = response.json() + _access_token = data["access_token"] + _refresh_token = data["refresh_token"] + _expires_at = now + timedelta(seconds=data["expires_in"]) + + return _access_token + + +def get_checkout_intent(checkout_id): + token = get_hello_asso_access_token() + response = requests.get( + f"https://api.helloasso.com/v5/organizations/animath/checkout-intents/{checkout_id}", + headers={"Authorization": f"Bearer {token}"}, + ) + if response.status_code == 404: + return None + response.raise_for_status() + + checkout_intent = response.json() + if requests.head(checkout_intent["redirectUrl"]).status_code == 404: + return None + + return checkout_intent + + +def create_checkout_intent(amount, name, back_url, error_url, return_url, contains_donation=False, metadata=None): + token = get_hello_asso_access_token() + metadata = metadata or {} + response = requests.post( + "https://api.helloasso.com/v5/organizations/animath/checkout-intents/", + headers={"Authorization": f"Bearer {token}"}, + json={ + "totalAmount": amount, + "initialAmount": amount, + "itemName": name, + "backUrl": back_url, + "errorUrl": error_url, + "returnUrl": return_url, + "containsDonation": contains_donation, + "metadata": metadata, + }, + ) + print(response.text) + response.raise_for_status() + return response.json() diff --git a/tfjm/settings.py b/tfjm/settings.py index a817b37..130f551 100644 --- a/tfjm/settings.py +++ b/tfjm/settings.py @@ -238,7 +238,9 @@ else: PHONENUMBER_DB_FORMAT = 'NATIONAL' PHONENUMBER_DEFAULT_REGION = 'FR' -GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") +# Hello Asso API creds +HELLOASSO_CLIENT_ID = os.getenv('HELLOASSO_CLIENT_ID', 'CHANGE_ME_IN_ENV_SETTINGS') +HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS') # Custom parameters PROBLEMS = [