1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-21 01:58:23 +02:00

Allow anonymous users to perform a payment using a special auth token

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello
2024-02-21 22:44:50 +01:00
parent 8d08b18d08
commit b16b6e422f
5 changed files with 189 additions and 59 deletions

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.1 on 2024-02-20 22:48
import registration.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registration", "0011_remove_payment_registration_and_more"),
]
operations = [
migrations.AddField(
model_name="payment",
name="token",
field=models.CharField(
default=registration.models.get_random_token,
help_text="A token to authorize external users to make this payment.",
max_length=32,
verbose_name="token",
),
),
migrations.AlterField(
model_name="payment",
name="type",
field=models.CharField(
blank=True,
choices=[
("", "No payment"),
("helloasso", "Credit card"),
("scholarship", "Scholarship"),
("bank_transfer", "Bank transfer"),
("other", "Other (please indicate)"),
("free", "The tournament is free"),
],
default="",
max_length=16,
verbose_name="type",
),
),
]

View File

@ -506,6 +506,10 @@ def get_receipt_filename(instance, filename):
return f"authorization/receipt/receipt_{instance.id}"
def get_random_token():
return get_random_string(32)
class Payment(models.Model):
registrations = models.ManyToManyField(
ParticipantRegistration,
@ -526,6 +530,13 @@ class Payment(models.Model):
default=0,
)
token = models.CharField(
verbose_name=_("token"),
max_length=32,
default=get_random_token,
help_text=_("A token to authorize external users to make this payment."),
)
final = models.BooleanField(
verbose_name=_("for final tournament"),
default=False,
@ -585,13 +596,13 @@ class Payment(models.Model):
return Tournament.final_tournament()
return self.registrations.first().team.participation.tournament
def get_checkout_intent(self):
def get_checkout_intent(self, none_if_link_disabled=False):
if self.checkout_intent_id is None:
return None
return helloasso.get_checkout_intent(self.checkout_intent_id)
return helloasso.get_checkout_intent(self.checkout_intent_id, none_if_link_disabled=none_if_link_disabled)
def create_checkout_intent(self):
checkout_intent = self.get_checkout_intent()
checkout_intent = self.get_checkout_intent(none_if_link_disabled=True)
if checkout_intent is not None:
return checkout_intent

View File

@ -79,17 +79,39 @@
<div class="card-body">
<div class="tab-content" id="payment-form">
<div class="tab-pane fade show active" id="credit-card" role="tabpanel" aria-labelledby="credit-card-tab">
Le paiement par carte bancaire s'effectue via Hello Asso. Pour cela, vous pouvez cliquer sur
le bouton ci-dessous, qui vous redirigera vers la page de paiement sécurisée de Hello Asso.
La validation du paiement sera ensuite faite automatiquement, sous quelques minutes.
Si un tiers doit payer pour vous (parents, lycée,…), vous pouvez lui transmettre le lien pour
payer pour vous.
<p>
Le paiement par carte bancaire s'effectue via Hello Asso. Pour cela, vous pouvez cliquer sur
le bouton ci-dessous, qui vous redirigera vers la page de paiement sécurisée de Hello Asso.
La validation du paiement sera ensuite faite automatiquement, sous quelques minutes.
</p>
<div class="text-center">
<a href="{% url "registration:payment_hello_asso" pk=payment.pk %}" class="btn btn-primary">
<i class="fas fa-credit-card"></i> Aller sur la page Hello Asso
</a>
</div>
<p>
Si un tiers doit payer pour vous (parents, lycée,…), vous pouvez lui transmettre le lien pour
payer pour vous :
</p>
<div class="text-center border border-1 my-3 p-2 border-danger bg-body-tertiary shadow-lg rounded">
{% url "registration:payment_hello_asso" pk=payment.pk as payment_url %}
{{ request.scheme }}://{{ request.site.domain }}{{ payment_url }}?token={{ payment.token }}
<a id="copyIcon" href="#"
data-bs-title="Copié !"
onclick="event.preventDefault();copyToClipboard('{{ request.scheme }}://{{ request.site.domain }}{{ payment_url }}?token={{ payment.token }}')">
<i class="fas fa-copy"></i> Copier
</a>
</div>
<p>
Si tel est le cas et si une facture est nécessaire, merci de contacter les organisateur⋅ices
du tournoi en transmettant le nom de l'équipe, le nombre de participant⋅es, le nom de
l'établissement payeur, l'adresse mail de l'établissement et/ou l'adresse mail du ou de la
gestionnaire de l'établissement.
</p>
</div>
<div class="tab-pane fade" id="bank-transfer" role="tabpanel" aria-labelledby="bank-transfer-tab">
@ -138,5 +160,28 @@
elem => elem.addEventListener(
'click', () => document.location.hash = '#' + elem.getAttribute('aria-controls')))
})
function copyToClipboard(text) {
const copyIcon = document.getElementById('copyIcon')
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
const tooltip = bootstrap.Tooltip.getOrCreateInstance(copyIcon)
tooltip.setContent('Copied!')
tooltip.show()
}
)
} else {
const input = document.createElement('input')
input.value = text
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
const tooltip = bootstrap.Tooltip.getOrCreateInstance(copyIcon)
tooltip.enable()
tooltip.show()
setTimeout(() => {tooltip.disable(); tooltip.hide()}, 2000)
}
}
</script>
{% endblock %}

View File

@ -7,7 +7,7 @@ from tempfile import mkdtemp
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.exceptions import PermissionDenied, ValidationError
@ -535,21 +535,41 @@ class PaymentUpdateGroupView(LoginRequiredMixin, DetailView):
return redirect(reverse_lazy("registration:update_payment", args=(payment.pk,)))
class PaymenRedirectHelloAssoView(LoginRequiredMixin, DetailView):
class PaymenRedirectHelloAssoView(AccessMixin, 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):
payment = self.get_object()
# An external user has the link for the payment
token = request.GET.get('token', "")
if token and token == payment.token:
return super().dispatch(request, *args, **kwargs)
if not request.user.is_authenticated:
return self.handle_no_permission()
if not request.user.registration.is_admin:
if request.user.registration.is_volunteer \
and payment.tournament not in request.user.registration.organized_tournaments.all():
return self.handle_no_permission()
if request.user.registration.is_student \
and request.user.registration not in payment.registrations.all():
return self.handle_no_permission()
if request.user.registration.is_coach \
and request.user.registration.team != payment.team:
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()
if payment.valid is not False:
raise PermissionDenied(_("The payment is already valid or pending validation."))
checkout_intent = payment.create_checkout_intent()
return redirect(checkout_intent["redirectUrl"])
@ -558,9 +578,10 @@ class PaymentHelloAssoReturnView(DetailView):
def get(self, request, *args, **kwargs):
checkout_id = request.GET.get("checkoutIntentId")
payment = Payment.objects.get(checkout_intent_id=checkout_id).exclude(valid=True)
if payment != self.get_object():
messages.error(request, _("The payment is not found or is already validated."))
payment = self.get_object()
payment_qs = Payment.objects.exclude(valid=True).filter(checkout_intent_id=checkout_id).filter(pk=payment.pk)
if not payment_qs.exists():
messages.error(request, _("The payment is not found or is already validated."), "danger")
return redirect("index")
team = payment.team
@ -580,18 +601,18 @@ class PaymentHelloAssoReturnView(DetailView):
return_type = request.GET.get("type")
if return_type == "error":
messages.error(request, format_lazy(_("An error occurred during the payment: {error}"),
error=request.GET.get("error")))
error=request.GET.get("error")), "danger")
return error_response
elif return_type == "return":
code = request.GET.get("code")
if code == "refused":
messages.error(request, _("The payment has been refused."))
messages.error(request, _("The payment has been refused."), "danger")
return error_response
elif code != "success":
messages.error(request, format_lazy(_("The return code is unknown: {code}"), code=code))
elif code != "succeeded":
messages.error(request, format_lazy(_("The return code is unknown: {code}"), code=code), "danger")
return error_response
else:
messages.error(request, format_lazy(_("The return type is unknown: {type}"), type=return_type))
messages.error(request, format_lazy(_("The return type is unknown: {type}"), type=return_type), "danger")
return error_response
checkout_intent = payment.get_checkout_intent()
@ -608,7 +629,7 @@ class PaymentHelloAssoReturnView(DetailView):
"and will be automatically done. "
"If it is not the case, please contact us."))
if request.user.registration in payment.registrations.all():
if not request.user.is_anonymous and request.user.registration in payment.registrations.all():
success_response = redirect("registration:user_detail", args=(request.user.pk,))
elif right_to_see:
success_response = redirect("participation:team_detail", args=(team.pk,))