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)
enable_online_payment_refund = models.BooleanField(default=False)
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):
for team_registration in self.team_registrations.all():
@ -1724,12 +1725,13 @@ class Tournament(BaseModel):
return None
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
else:
return False
def is_refund_possible(self):
return True
if self.enable_online_payment_refund:
time = timezone.now()
if self.refund_date_limit:
@ -1755,6 +1757,11 @@ class Tournament(BaseModel):
else:
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:
def __init__(self, name, matches, formatted_schedule, round_id=None):
self.name = name

@ -2,6 +2,7 @@ from django.core.mail import EmailMessage
from enum import Enum
from ..models.player_registration import RegistrationStatus
from ..models.tournament import TeamSortingType
import django.utils.timezone
class TeamEmailType(Enum):
REGISTERED = "registered"
@ -521,3 +522,67 @@ class TournamentEmailService:
]
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.urls import reverse
from django.contrib import messages
import stripe
from ..models import TeamRegistration, PlayerRegistration, Tournament
@ -26,6 +25,19 @@ class PaymentService:
stripe.api_key = self.stripe_api_key
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
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})
),
'cancel_url': cancel_url,
'payment_intent_data': {
'application_fee_amount': platform_amount,
'transfer_data': {
'destination': stripe_account_id,
},
},
'metadata': {
'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
@ -79,6 +97,7 @@ class PaymentService:
checkout_session_params['customer_email'] = customer_email
# Create the checkout session
try:
checkout_session = stripe.checkout.Session.create(**checkout_session_params)
# Store checkout session ID and source page in session
@ -87,6 +106,12 @@ class PaymentService:
self.request.session.modified = True
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):
"""
@ -191,3 +216,53 @@ class PaymentService:
player_reg.save()
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.utils import timezone
from ..models import PlayerRegistration, UnregisteredTeam, UnregisteredPlayer
from ..models import PlayerRegistration, UnregisteredTeam, UnregisteredPlayer, PlayerPaymentType
from ..utils.licence_validator import LicenseValidator
from ..services.payment_service import PaymentService
from ..services.email_service import TournamentEmailService
class TournamentUnregistrationService:
def __init__(self, request, tournament):
@ -9,6 +11,7 @@ class TournamentUnregistrationService:
self.tournament = tournament
self.player_registration = None
self.other_player = None
self.team_registration = None
def can_unregister(self):
if not self.tournament.is_unregistration_possible():
@ -28,11 +31,50 @@ class TournamentUnregistrationService:
"La désincription a échouée. Veuillez contacter le juge-arbitre.")
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._delete_registered_team()
self._cleanup_session()
messages.success(self.request, "Votre désinscription a été effectuée.")
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):
# Create unregistered team record
team_registration = self.player_registration.team_registration
@ -65,8 +107,8 @@ class TournamentUnregistrationService:
).first()
if self.player_registration:
team_registration = self.player_registration.team_registration
self.other_player = team_registration.get_other_player(self.player_registration)
self.team_registration = self.player_registration.team_registration
self.other_player = self.team_registration.get_other_player(self.player_registration)
return True
return False

@ -115,14 +115,17 @@
{% if tournament.is_unregistration_possible %}
<div class="topmargin20">
{% if tournament.is_refund_possible and team.is_paid %}
<p>
Votre inscription sera remboursé automatiquement.
<p class="alert alert-info">
<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>
{% endif %}
<a href="{% url 'unregister_tournament' tournament.id %}"
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 %}
Se retirer de la liste d'attente
{% else %}
@ -130,15 +133,8 @@
{% endif %}
</a>
</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 %}
</div>
{% endif %}
<h1 class="club padding10 topmargin20">{{ tournament.display_name }} {{ tournament.get_federal_age_category_display}}</h1>

Loading…
Cancel
Save