timetoconfirm
Raz 7 months ago
parent 28e39be609
commit f2fcd83cc5
  1. 18
      tournaments/migrations/0121_tournament_stripe_account_id.py
  2. 9
      tournaments/models/tournament.py
  3. 65
      tournaments/services/email_service.py
  4. 79
      tournaments/services/payment_service.py
  5. 48
      tournaments/services/tournament_unregistration.py
  6. 18
      tournaments/templates/tournaments/tournament_info.html

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-04-09 20:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0120_tournament_enable_online_payment_refund_and_more'),
]
operations = [
migrations.AddField(
model_name='tournament',
name='stripe_account_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

@ -83,6 +83,7 @@ class Tournament(BaseModel):
online_payment_is_mandatory = models.BooleanField(default=False) online_payment_is_mandatory = models.BooleanField(default=False)
enable_online_payment_refund = models.BooleanField(default=False) enable_online_payment_refund = models.BooleanField(default=False)
refund_date_limit = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil refund_date_limit = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil
stripe_account_id = models.CharField(max_length=255, blank=True, null=True)
def delete_dependencies(self): def delete_dependencies(self):
for team_registration in self.team_registrations.all(): for team_registration in self.team_registrations.all():
@ -1724,12 +1725,13 @@ class Tournament(BaseModel):
return None return None
def should_request_payment(self): def should_request_payment(self):
if self.entry_fee is not None and self.entry_fee > 0 and self.enable_online_payment: if self.entry_fee is not None and self.entry_fee > 0 and self.enable_online_payment and self.stripe_account_id is not None:
return True return True
else: else:
return False return False
def is_refund_possible(self): def is_refund_possible(self):
return True
if self.enable_online_payment_refund: if self.enable_online_payment_refund:
time = timezone.now() time = timezone.now()
if self.refund_date_limit: if self.refund_date_limit:
@ -1755,6 +1757,11 @@ class Tournament(BaseModel):
else: else:
return 0 return 0
def effective_commission_rate(self):
"""Get the commission rate for this tournament, falling back to the umpire default if not set"""
return 1.00 # Fallback default
class MatchGroup: class MatchGroup:
def __init__(self, name, matches, formatted_schedule, round_id=None): def __init__(self, name, matches, formatted_schedule, round_id=None):
self.name = name self.name = name

@ -2,6 +2,7 @@ from django.core.mail import EmailMessage
from enum import Enum from enum import Enum
from ..models.player_registration import RegistrationStatus from ..models.player_registration import RegistrationStatus
from ..models.tournament import TeamSortingType from ..models.tournament import TeamSortingType
import django.utils.timezone
class TeamEmailType(Enum): class TeamEmailType(Enum):
REGISTERED = "registered" REGISTERED = "registered"
@ -521,3 +522,67 @@ class TournamentEmailService:
] ]
return "\n".join(payment_info) return "\n".join(payment_info)
@staticmethod
def send_refund_confirmation(tournament, team_registration, refund_details):
"""
Send a refund confirmation email to team members
Args:
tournament: The tournament
team_registration: The team registration
refund_details: The refund details from Stripe
"""
player_registrations = team_registration.player_registrations.all()
refund_amount = None
if refund_details and 'amount' in refund_details:
# Convert cents to euros
refund_amount = refund_details['amount'] / 100
if refund_amount is None:
refund_amount = tournament.team_fee()
for player in player_registrations:
if not player.email or not player.registered_online:
continue
tournament_details_str = tournament.build_tournament_details_str()
other_player = team_registration.get_other_player(player) if len(player_registrations) > 1 else None
body_parts = [
"Bonjour,\n\n",
f"Votre remboursement pour le tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été traité avec succès."
]
# Add information about the other player if available
if other_player:
body_parts.append(
f"\n\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire du remboursement."
)
# Add refund details
body_parts.append(
f"\n\nMontant remboursé : {refund_amount}€ par joueur"
)
refund_date = timezone.now().strftime("%d/%m/%Y")
body_parts.append(
f"\nDate du remboursement : {refund_date}"
)
absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
link_text = "informations sur le tournoi"
absolute_url = f'<a href="{absolute_url}">{link_text}</a>'
body_parts.append(f"\n\nVoir les {absolute_url}")
body_parts.extend([
f"\n\n{TournamentEmailService._format_umpire_contact(tournament)}",
"\n\nCeci est un e-mail automatique, veuillez ne pas y répondre."
])
email_body = "".join(body_parts)
email_subject = TournamentEmailService.email_subject(tournament, "Confirmation de remboursement")
TournamentEmailService._send_email(player.email, email_subject, email_body)

@ -1,7 +1,6 @@
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.contrib import messages
import stripe import stripe
from ..models import TeamRegistration, PlayerRegistration, Tournament from ..models import TeamRegistration, PlayerRegistration, Tournament
@ -26,6 +25,19 @@ class PaymentService:
stripe.api_key = self.stripe_api_key stripe.api_key = self.stripe_api_key
tournament = get_object_or_404(Tournament, id=tournament_id) tournament = get_object_or_404(Tournament, id=tournament_id)
# Check if payments are enabled for this tournament
if not tournament.should_request_payment():
raise Exception("Les paiements ne sont pas activés pour ce tournoi.")
# Get the umpire's Stripe account ID
stripe_account_id = tournament.stripe_account_id
if not stripe_account_id:
raise Exception("L'arbitre n'a pas configuré son compte Stripe.")
# Calculate commission
commission_rate = tournament.effective_commission_rate() / 100
platform_amount = int((team_fee * commission_rate) * 100) # Commission in cents
# Get user email if authenticated # Get user email if authenticated
customer_email = self.request.user.email if self.request.user.is_authenticated else None customer_email = self.request.user.email if self.request.user.is_authenticated else None
@ -60,6 +72,12 @@ class PaymentService:
reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id}) reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id})
), ),
'cancel_url': cancel_url, 'cancel_url': cancel_url,
'payment_intent_data': {
'application_fee_amount': platform_amount,
'transfer_data': {
'destination': stripe_account_id,
},
},
'metadata': { 'metadata': {
'tournament_id': str(tournament_id), # Convert UUID to string 'tournament_id': str(tournament_id), # Convert UUID to string
'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None, # Convert UUID to string 'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None, # Convert UUID to string
@ -79,6 +97,7 @@ class PaymentService:
checkout_session_params['customer_email'] = customer_email checkout_session_params['customer_email'] = customer_email
# Create the checkout session # Create the checkout session
try:
checkout_session = stripe.checkout.Session.create(**checkout_session_params) checkout_session = stripe.checkout.Session.create(**checkout_session_params)
# Store checkout session ID and source page in session # Store checkout session ID and source page in session
@ -87,6 +106,12 @@ class PaymentService:
self.request.session.modified = True self.request.session.modified = True
return checkout_session return checkout_session
except stripe.error.StripeError as e:
# Handle specific Stripe errors more gracefully
if 'destination' in str(e):
raise Exception("Erreur avec le compte Stripe de l'arbitre. Contactez l'administrateur.")
else:
raise Exception(f"Erreur Stripe: {str(e)}")
def process_successful_payment(self, tournament_id, checkout_session): def process_successful_payment(self, tournament_id, checkout_session):
""" """
@ -191,3 +216,53 @@ class PaymentService:
player_reg.save() player_reg.save()
return True return True
def process_refund(self, team_registration_id):
"""
Process a refund for a tournament registration as part of unregistration
Returns a tuple (success, message)
"""
stripe.api_key = self.stripe_api_key
try:
# Get the team registration
team_registration = get_object_or_404(TeamRegistration, id=team_registration_id)
tournament = team_registration.tournament
# Check if refund is possible for this tournament
if not tournament.is_refund_possible():
return False, "Les remboursements ne sont plus possibles pour ce tournoi.", None
# Get payment ID from player registrations
player_registrations = PlayerRegistration.objects.filter(team_registration=team_registration)
payment_id = None
for player_reg in player_registrations:
# Find the first valid payment ID
if player_reg.payment_id and player_reg.payment_type == PlayerPaymentType.CREDIT_CARD:
payment_id = player_reg.payment_id
break
if not payment_id:
return False, "Aucun paiement trouvé pour cette équipe.", None
# Get the Stripe payment intent
payment_intent = stripe.PaymentIntent.retrieve(payment_id)
if payment_intent.status != 'succeeded':
return False, "Le paiement n'a pas été complété, il ne peut pas être remboursé.", None
# Process the refund
refund = stripe.Refund.create(
payment_intent=payment_id,
refund_application_fee=True,
reverse_transfer=True
)
# Return success with refund object
return True, "Votre inscription a été remboursée automatiquement.", refund
except stripe.error.StripeError as e:
return False, f"Erreur de remboursement Stripe: {str(e)}", None
except Exception as e:
return False, f"Erreur lors du remboursement: {str(e)}", None

@ -1,7 +1,9 @@
from django.contrib import messages from django.contrib import messages
from django.utils import timezone from django.utils import timezone
from ..models import PlayerRegistration, UnregisteredTeam, UnregisteredPlayer from ..models import PlayerRegistration, UnregisteredTeam, UnregisteredPlayer, PlayerPaymentType
from ..utils.licence_validator import LicenseValidator from ..utils.licence_validator import LicenseValidator
from ..services.payment_service import PaymentService
from ..services.email_service import TournamentEmailService
class TournamentUnregistrationService: class TournamentUnregistrationService:
def __init__(self, request, tournament): def __init__(self, request, tournament):
@ -9,6 +11,7 @@ class TournamentUnregistrationService:
self.tournament = tournament self.tournament = tournament
self.player_registration = None self.player_registration = None
self.other_player = None self.other_player = None
self.team_registration = None
def can_unregister(self): def can_unregister(self):
if not self.tournament.is_unregistration_possible(): if not self.tournament.is_unregistration_possible():
@ -28,11 +31,50 @@ class TournamentUnregistrationService:
"La désincription a échouée. Veuillez contacter le juge-arbitre.") "La désincription a échouée. Veuillez contacter le juge-arbitre.")
return False return False
# Check if refund is possible and needed
if self.tournament.is_refund_possible() and self._team_has_paid():
refund_processed, message, refund_details = self._process_refund()
if refund_processed:
# Refund successful, continue with unregistration process
messages.success(self.request, message)
TournamentEmailService.send_refund_confirmation(self.tournament, self.player_registration.team_registration, refund_details)
else:
# Refund failed, show error but continue with normal unregistration
messages.error(self.request, message)
# Proceed with unregistration
self._unregister_team() self._unregister_team()
self._delete_registered_team() self._delete_registered_team()
self._cleanup_session() self._cleanup_session()
messages.success(self.request, "Votre désinscription a été effectuée.")
return True return True
def _team_has_paid(self):
"""Check if team has paid for registration"""
if not self.team_registration:
print("Team registration not found")
return False
# Check if any player registration has a payment ID
player_registrations = PlayerRegistration.objects.filter(team_registration=self.team_registration)
for player_reg in player_registrations:
if player_reg.payment_id and player_reg.payment_type == PlayerPaymentType.CREDIT_CARD:
print("Player has paid")
return True
print("No player has paid")
return False
def _process_refund(self):
"""Process refund for paid registration"""
try:
payment_service = PaymentService(self.request)
return payment_service.process_refund(self.team_registration.id)
except Exception as e:
return False, f"Erreur lors du remboursement: {str(e)}", None
def _unregister_team(self): def _unregister_team(self):
# Create unregistered team record # Create unregistered team record
team_registration = self.player_registration.team_registration team_registration = self.player_registration.team_registration
@ -65,8 +107,8 @@ class TournamentUnregistrationService:
).first() ).first()
if self.player_registration: if self.player_registration:
team_registration = self.player_registration.team_registration self.team_registration = self.player_registration.team_registration
self.other_player = team_registration.get_other_player(self.player_registration) self.other_player = self.team_registration.get_other_player(self.player_registration)
return True return True
return False return False

@ -115,14 +115,17 @@
{% if tournament.is_unregistration_possible %} {% if tournament.is_unregistration_possible %}
<div class="topmargin20"> <div class="topmargin20">
{% if tournament.is_refund_possible and team.is_paid %} {% if tournament.is_refund_possible and team.is_paid %}
<p> <p class="alert alert-info">
Votre inscription sera remboursé automatiquement. <span class="icon">💰</span> Si vous vous désinscrivez, votre inscription sera remboursée automatiquement sur votre carte bancaire.
{% if tournament.refund_date_limit %}
<br><small>Remboursement possible jusqu'au {{ tournament.refund_date_limit|date:"d/m/Y à H:i" }}</small>
{% endif %}
</p> </p>
{% endif %} {% endif %}
<a href="{% url 'unregister_tournament' tournament.id %}" <a href="{% url 'unregister_tournament' tournament.id %}"
class="rounded-button destructive-button" class="rounded-button destructive-button"
onclick="return confirm('Êtes-vous sûr de vouloir vous désinscrire ?');"> onclick="return confirm('Êtes-vous sûr de vouloir vous désinscrire ?{% if tournament.is_refund_possible and team.is_paid %} Votre inscription sera remboursée automatiquement.{% endif %}');">
{% if team.is_in_waiting_list >= 0 %} {% if team.is_in_waiting_list >= 0 %}
Se retirer de la liste d'attente Se retirer de la liste d'attente
{% else %} {% else %}
@ -130,15 +133,8 @@
{% endif %} {% endif %}
</a> </a>
</div> </div>
<!-- {% if is_captain %}
{% else %}
<p>
<div>Vous n'êtes pas le capitaine de l'équipe, la désinscription en ligne n'est pas disponible. Veuillez contacter le JAP ou votre partenaire.</div>
</p>
{% endif %}
-->
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<h1 class="club padding10 topmargin20">{{ tournament.display_name }} {{ tournament.get_federal_age_category_display}}</h1> <h1 class="club padding10 topmargin20">{{ tournament.display_name }} {{ tournament.get_federal_age_category_display}}</h1>

Loading…
Cancel
Save