add payment for registration

timetoconfirm
Raz 7 months ago
parent a321c4c154
commit 42090b5ab7
  1. 23
      tournaments/migrations/0117_playerregistration_payment_id_and_more.py
  2. 22
      tournaments/migrations/0118_remove_playerregistration_payment_status_and_more.py
  3. 10
      tournaments/models/player_registration.py
  4. 51
      tournaments/models/team_registration.py
  5. 6
      tournaments/models/tournament.py
  6. 38
      tournaments/services/email_service.py
  7. 21
      tournaments/services/registration_cart.py
  8. 33
      tournaments/templates/register_tournament.html
  9. 31
      tournaments/templates/tournaments/tournament_info.html
  10. 2
      tournaments/urls.py
  11. 277
      tournaments/views.py

@ -0,0 +1,23 @@
# Generated by Django 5.1 on 2025-04-09 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0116_playerregistration_payment_status_and_more'),
]
operations = [
migrations.AddField(
model_name='playerregistration',
name='payment_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='tournament',
name='reserved_spots',
field=models.IntegerField(default=0),
),
]

@ -0,0 +1,22 @@
# Generated by Django 5.1 on 2025-04-09 13:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0117_playerregistration_payment_id_and_more'),
]
operations = [
migrations.RemoveField(
model_name='playerregistration',
name='payment_status',
),
migrations.AlterField(
model_name='playerregistration',
name='registration_status',
field=models.CharField(choices=[('WAITING', 'Waiting'), ('PENDING', 'Pending'), ('CONFIRMED', 'Confirmed'), ('CANCELED', 'Canceled')], default='WAITING', max_length=20),
),
]

@ -7,7 +7,6 @@ class RegistrationStatus(models.TextChoices):
WAITING = 'WAITING', 'Waiting'
PENDING = 'PENDING', 'Pending'
CONFIRMED = 'CONFIRMED', 'Confirmed'
PAID = 'PAID', 'Paid'
CANCELED = 'CANCELED', 'Canceled'
@ -47,11 +46,7 @@ class PlayerRegistration(SideStoreModel):
registered_online = models.BooleanField(default=False)
time_to_confirm = models.DateTimeField(null=True, blank=True)
registration_status = models.CharField(max_length=20, choices=RegistrationStatus.choices, default=RegistrationStatus.WAITING)
payment_status = models.CharField(max_length=20, default='UNPAID', choices=[
('UNPAID', 'Unpaid'),
('PAID', 'Paid'),
('FAILED', 'Failed'),
])
payment_id = models.CharField(max_length=255, blank=True, null=True)
def delete_dependencies(self):
pass
@ -172,3 +167,6 @@ class PlayerRegistration(SideStoreModel):
status['short_label'] = 'inscrit'
return status
def has_paid(self):
return self.payment_type is not None

@ -340,3 +340,54 @@ class TeamRegistration(SideStoreModel):
# Save confirmation date
self.confirmation_date = now
self.save()
# Add to TeamRegistration class in team_registration.py
def get_payment_status(self):
"""
Gets the payment status for this team.
Returns:
- 'PAID': If all players in the team have paid
- 'UNPAID': If no player has paid
- 'MIXED': If some players have paid and others haven't (unusual case)
"""
# Get all player registrations for this team
player_registrations = self.player_registrations.all()
# If we have no players, return None
if not player_registrations.exists():
return None
# Check payment status for each player
payment_statuses = [player.has_paid() for player in player_registrations]
print(f"Payment statuses: {payment_statuses}")
print(all(payment_statuses))
# If all players have paid
if all(payment_statuses):
return 'PAID'
# If no players have paid
if not any(payment_statuses):
return 'UNPAID'
# If some players have paid and others haven't (unusual case)
return 'MIXED'
def is_payment_required(self):
"""Check if payment is required for this team"""
return self.tournament.should_request_payment() and self.is_in_waiting_list() < 0
def is_paid(self):
"""Check if this team has paid"""
status = self.get_payment_status()
return status == 'PAID'
def get_remaining_fee(self):
"""Get the remaining fee for this team"""
status = self.get_payment_status()
if status == 'PAID':
return 0
elif status == 'UNPAID':
return self.tournament.entry_fee * 2
elif status == 'MIXED':
return self.tournament.entry_fee

@ -78,6 +78,7 @@ class Tournament(BaseModel):
hide_umpire_mail = models.BooleanField(default=False)
hide_umpire_phone = models.BooleanField(default=True)
disable_ranking_federal_ruling = models.BooleanField(default=False)
reserved_spots = models.IntegerField(default=0)
def delete_dependencies(self):
for team_registration in self.team_registrations.all():
@ -1699,6 +1700,11 @@ class Tournament(BaseModel):
team_registration__walk_out=False
).exists()
def should_request_payment(self):
return True
def team_fee(self):
return self.entry_fee * 2
class MatchGroup:
def __init__(self, name, matches, formatted_schedule, round_id=None):

@ -90,7 +90,15 @@ class TournamentEmailService:
f"\nDate d'inscription: {inscription_date}",
f"\nÉquipe inscrite: {captain.name()} et {other_player.name()}",
f"\nLe tournoi commencera le {tournament.formatted_start_date()} au club {tournament.event.club.name}",
f"\nVoir les {absolute_url}",
f"\nVoir les {absolute_url}"
])
# Add payment information if applicable
if tournament.should_request_payment:
payment_info = TournamentEmailService._build_payment_info(tournament, captain.team_registration)
body_parts.append(payment_info)
body_parts.extend([
"\nPour toute question, veuillez contacter votre juge-arbitre. Si vous n'êtes pas à l'origine de cette inscription, merci de le contacter rapidement.",
f"\n{TournamentEmailService._format_umpire_contact(tournament)}",
"\nCeci est un e-mail automatique, veuillez ne pas y répondre.",
@ -485,3 +493,31 @@ class TournamentEmailService:
print("TournamentEmailService.notify_team 1p", team)
# If there's only one player, just send them the notification
TournamentEmailService.notify(players[0], None, tournament, message_type)
@staticmethod
def _build_payment_info(tournament, team_registration):
"""
Build payment information section for emails
"""
if not tournament.should_request_payment:
return ""
# Check payment status
payment_status = team_registration.get_payment_status()
if payment_status == 'PAID':
return "\n\n✅ Le paiement de votre inscription a bien été reçu."
# If the team is on the waiting list, don't mention payment
if team_registration.is_in_waiting_list() >= 0:
return ""
# For unpaid teams, add payment instructions
payment_info = [
"\n\n Paiement des frais d'inscription requis",
f"Les frais d'inscription de {tournament.entry_fee}€ doivent être payés pour confirmer votre participation.",
"Vous pouvez effectuer le paiement en vous connectant à votre compte Padel Club.",
f"Lien pour payer: https://padelclub.app/tournament/{tournament.id}/info"
]
return "\n".join(payment_info)

@ -46,8 +46,24 @@ def initialize_registration_cart(request, tournament_id):
# Clear any existing cart
clear_registration_cart(request)
try:
tournament = Tournament.objects.get(id=tournament_id)
except Tournament.DoesNotExist:
return False, "Tournoi introuvable."
tournament.reserved_spots = max(0, tournament.reserved_spots - 1)
waiting_list_position = tournament.get_waiting_list_position()
if waiting_list_position >= 0:
tournament.reserved_spots = 0
else:
tournament.reserved_spots += 1
tournament.save()
# Set up the new cart
request.session['registration_cart_id'] = str(uuid.uuid4())
request.session['waiting_list_position'] = waiting_list_position
request.session['registration_tournament_id'] = tournament_id
request.session['registration_cart_players'] = []
reset_cart_expiry(request)
@ -71,6 +87,7 @@ def get_registration_cart_data(request):
cart_data = {
'cart_id': get_or_create_registration_cart_id(request),
'tournament_id': request.session.get('registration_tournament_id'),
'waiting_list_position': request.session.get('waiting_list_position'),
'players': request.session.get('registration_cart_players', []),
'expiry': get_cart_expiry(request),
'mobile_number': request.session.get('registration_mobile_number',
@ -287,7 +304,7 @@ def checkout_registration_cart(request):
birthdate=str(player_data.get('birth_year', '')),
captain=(idx == 0), # First player is captain
registered_online=True,
registration_status='CONFIRMED' if tournament.get_waiting_list_position() < 0 else 'WAITING'
registration_status='CONFIRMED' if request.session['waiting_list_position'] < 0 else 'WAITING'
)
# Update user phone if provided
@ -297,6 +314,8 @@ def checkout_registration_cart(request):
# Clear the cart
clear_registration_cart(request)
tournament.reserved_spots = max(0, tournament.reserved_spots - 1)
tournament.save()
return True, team_registration

@ -29,6 +29,7 @@
{% if not registration_successful %}
<div class="info-box">
<p>Votre session d'inscription est active. Complétez le formulaire pour confirmer votre participation.</p>
<p>DEBUG reserved_spots: {{ tournament.reserved_spots }}</p>
</div>
{% endif %}
@ -164,23 +165,29 @@
<div class="margin10">
</div>
<div class="semibold margin10">
{% if tournament.get_waiting_list_position == 1 %}
Tournoi complet, {{ tournament.get_waiting_list_position }} équipe en liste d'attente actuellement.
{% elif tournament.get_waiting_list_position > 1 %}
Tournoi complet, {{ tournament.get_waiting_list_position }} équipes en liste d'attente actuellement.
{% elif tournament.get_waiting_list_position == 0 %}
Tournoi complet, vous seriez la première équipe en liste d'attente.
{% if cart_data.waiting_list_position == 1 %}
Tournoi complet, {{ cart_data.waiting_list_position }} équipe en liste d'attente actuellement.
{% elif cart_data.waiting_list_position > 1 %}
Tournoi complet, {{ cart_data.waiting_list_position }} équipes en liste d'attente actuellement.
{% elif cart_data.waiting_list_position == 0 %}
Tournoi complet, vous seriez la première équipe en liste d'attente.
{% endif %}
</div>
<div>
<button type="submit" name="register_team" class="rounded-button">
{% if tournament.get_waiting_list_position < 0 %}
Confirmer l'inscription
{% else %}
Se mettre en liste d'attente
{% endif %}
</button>
{% if tournament.should_request_payment and cart_data.waiting_list_position < 0 %}
<button type="submit" name="proceed_to_payment" class="rounded-button">
Procéder au paiement ({{ tournament.team_fee }}€)
</button>
{% else %}
<button type="submit" name="register_team" class="rounded-button">
{% if cart_data.waiting_list_position < 0 %}
Confirmer l'inscription
{% else %}
Se mettre en liste d'attente
{% endif %}
</button>
{% endif %}
</div>
{% endif %}
</form>

@ -43,8 +43,31 @@
<p>
<div>Inscrits le {{ team.local_registration_date }}</div>
</p>
{% if team and team.needs_confirmation %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<!-- Payment status information -->
{% if tournament.should_request_payment and team.is_in_waiting_list < 0 %}
<div style="margin-bottom: 40px;">
{% if team.is_paid %}
<div class="success-message topmargin20">
<span class="icon"></span> Paiement confirmé
</div>
{% else %}
<div>
<a href="{% url 'proceed_to_payment' tournament.id %}" class="rounded-button positive-button">
Procéder au paiement ({{ team.get_remaining_fee }}€)
</a>
</div>
{% endif %}
</div>
{% elif team.needs_confirmation %}
<div class="alert {% if team.get_confirmation_deadline %}alert-warning{% else %}alert-info{% endif %}">
<h4 class="semibold">Confirmation requise</h4>
<p>Votre place dans le tournoi a été libérée suite à une désinscription.</p>
@ -79,7 +102,7 @@
{% endif %}
{% if tournament.is_unregistration_possible %}
<p>
<div class="topmargin20">
<a href="{% url 'unregister_tournament' tournament.id %}"
class="rounded-button destructive-button"
@ -90,7 +113,7 @@
Se désinscrire
{% endif %}
</a>
</p>
</div>
<!-- {% if is_captain %}
{% else %}

@ -13,6 +13,8 @@ urlpatterns = [
path("clubs/<str:club_id>/", views.club, name="club"),
path("c/<str:broadcast_code>", views.club_broadcast, name="club-broadcast"),
path("c/<str:broadcast_code>/go", views.club_broadcast_auto, name="club-broadcast-auto"),
path('tournaments/<str:tournament_id>/payment/success/', views.tournament_payment_success, name='tournament-payment-success'),
path('tournaments/<str:tournament_id>/proceed-to-payment/', views.proceed_to_payment, name='proceed_to_payment'),
path("tournament/<str:tournament_id>/",
include([
path('', views.tournament, name='tournament'),

@ -7,6 +7,7 @@ from api.serializers import GroupStageSerializer, MatchSerializer, PlayerRegistr
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import logout
from .utils.extensions import is_not_sqlite_backend
import stripe
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.views import PasswordResetCompleteView
@ -25,7 +26,8 @@ from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from django.views.generic import View
from django.db.models import Q
from .models import Club, Tournament, CustomUser, Event, Round, Match, TeamScore, TeamRegistration, PlayerRegistration, UserOrigin
from .models import Club, Tournament, CustomUser, Event, Round, Match, TeamScore, TeamRegistration, PlayerRegistration, UserOrigin, PlayerPaymentType
from .models.player_registration import RegistrationStatus
from datetime import datetime, timedelta
import time
import json
@ -64,6 +66,7 @@ from .utils.apns import send_push_notification
from .utils.licence_validator import LicenseValidator
from django.views.generic.edit import UpdateView
from .forms import CustomPasswordChangeForm
from .services.email_service import TournamentEmailService
def index(request):
now = timezone.now()
@ -749,20 +752,25 @@ def register_tournament(request, tournament_id):
# Only initialize a fresh cart for GET requests
# For POST requests, use the existing cart to maintain state
if request.method == 'GET':
# ALWAYS initialize a fresh cart when entering the registration page (GET request)
# This ensures no old cart data persists
registration_cart.initialize_registration_cart(request, tournament_id)
# Auto-add the authenticated user with license
if request.user.is_authenticated and request.user.licence_id:
player_data = {
'first_name': request.user.first_name,
'last_name': request.user.last_name,
'licence_id': request.user.licence_id,
'email': request.user.email,
'phone': request.user.phone
}
registration_cart.add_player_to_cart(request, player_data)
# Check if we're returning from Stripe (don't reinitialize if we have a checkout session)
if 'stripe_checkout_session_id' in request.session:
# We're returning from Stripe, don't reinitialize the cart
pass
else:
# ALWAYS initialize a fresh cart when entering the registration page (GET request)
# This ensures no old cart data persists
registration_cart.initialize_registration_cart(request, tournament_id)
# Auto-add the authenticated user with license
if request.user.is_authenticated and request.user.licence_id:
player_data = {
'first_name': request.user.first_name,
'last_name': request.user.last_name,
'licence_id': request.user.licence_id,
'email': request.user.email,
'phone': request.user.phone
}
registration_cart.add_player_to_cart(request, player_data)
else:
# For POST, ensure tournament ID is correct
current_tournament_id = registration_cart.get_tournament_from_cart(request)
@ -790,7 +798,8 @@ def register_tournament(request, tournament_id):
context = {
'tournament': tournament,
'current_players': cart_data['players'],
'registration_successful': False
'registration_successful': False,
'cart_data': cart_data # Add this line
}
# Initialize forms
@ -908,9 +917,8 @@ def register_tournament(request, tournament_id):
# Checkout and create registration
success, result = registration_cart.checkout_registration_cart(request)
if success:
waiting_list_position = tournament.get_waiting_list_position()
waiting_list_position = cart_data.get('waiting_list_position', -1)
if is_not_sqlite_backend():
from .services.email_service import TournamentEmailService
email_service = TournamentEmailService()
email_service.send_registration_confirmation(
request,
@ -929,6 +937,72 @@ def register_tournament(request, tournament_id):
for error in errors:
messages.error(request, f"{field}: {error}")
context['team_form'] = team_form
elif 'proceed_to_payment' in request.POST:
team_form = TournamentRegistrationForm(request.POST)
if team_form.is_valid():
# Update cart with contact info
registration_cart.update_cart_contact_info(
request,
mobile_number=team_form.cleaned_data.get('mobile_number')
)
# Create payment session
try:
stripe.api_key = settings.STRIPE_SECRET_KEY
# Get user email if authenticated
customer_email = request.user.email if request.user.is_authenticated else None
# Create the Stripe checkout session
checkout_session_params = {
'payment_method_types': ['card'],
'line_items': [{
'price_data': {
'currency': 'eur',
'product_data': {
'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}',
'description': f'Lieu {tournament.event.club.name}',
},
'unit_amount': int(team_registration.team_fee() * 100), # Amount in cents
# 'unit_amount': int(tournament.entry_fee * 100), # Amount in cents
},
'quantity': 2,
}],
'mode': 'payment',
'success_url': request.build_absolute_uri(
reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id})
),
'cancel_url': request.build_absolute_uri(
# Use the correct URL name from your urls.py
reverse('register_tournament', kwargs={'tournament_id': tournament_id})
),
'metadata': {
'tournament_id': tournament_id,
'registration_cart_id': cart_data['cart_id'],
'user_id': request.user.id if request.user.is_authenticated else None,
}
}
# Add customer_email if available
if customer_email:
checkout_session_params['customer_email'] = customer_email
# Create the checkout session
checkout_session = stripe.checkout.Session.create(**checkout_session_params)
# Store checkout session ID in session
request.session['stripe_checkout_session_id'] = checkout_session.id
request.session.modified = True
# Redirect to Stripe checkout
return redirect(checkout_session.url)
except Exception as e:
messages.error(request, f"Erreur lors de la création de la session de paiement: {str(e)}")
else:
for field, errors in team_form.errors.items():
for error in errors:
messages.error(request, f"{field}: {error}")
context['team_form'] = team_form
# Debug session content before rendering
print("===== SESSION DUMP BEFORE RENDER =====")
@ -939,10 +1013,103 @@ def register_tournament(request, tournament_id):
return render(request, 'register_tournament.html', context)
@login_required
def tournament_payment_success(request, tournament_id):
# Get checkout session ID from session
checkout_session_id = request.session.get('stripe_checkout_session_id')
if not checkout_session_id:
messages.error(request, "Session de paiement introuvable.")
return redirect('tournament-info', tournament_id=tournament_id)
try:
# Verify payment status with Stripe
stripe.api_key = settings.STRIPE_SECRET_KEY
checkout_session = stripe.checkout.Session.retrieve(checkout_session_id)
if checkout_session.payment_status == 'paid':
# Check if this is a direct payment from tournament info page
team_registration_id = request.session.get('team_registration_id')
if team_registration_id:
# This is a direct payment for an existing registration
team_registration = get_object_or_404(TeamRegistration, id=team_registration_id)
player_registrations = PlayerRegistration.objects.filter(team_registration=team_registration)
for player_reg in player_registrations:
player_reg.payment_type = PlayerPaymentType.CREDIT_CARD
player_reg.registration_status = RegistrationStatus.CONFIRMED
player_reg.payment_id = checkout_session.payment_intent
player_reg.save()
# Mark team as paid
team_registration.set_paid(True)
# Clear session data
if 'stripe_checkout_session_id' in request.session:
del request.session['stripe_checkout_session_id']
if 'team_registration_id' in request.session:
del request.session['team_registration_id']
messages.success(request, "Paiement réussi et inscription confirmée !")
return redirect('tournament-info', tournament_id=tournament_id)
else:
# This is a payment during registration
# Get cart data
cart_data = registration_cart.get_registration_cart_data(request)
# Checkout and create registration
success, result = registration_cart.checkout_registration_cart(request)
if success:
# Get the team registration and mark as paid
team_registration = result # result is team_registration object
player_registrations = PlayerRegistration.objects.filter(team_registration=team_registration)
for player_reg in player_registrations:
player_reg.payment_type = PlayerPaymentType.CREDIT_CARD
player_reg.registration_status = RegistrationStatus.CONFIRMED
player_reg.payment_id = checkout_session.payment_intent
player_reg.save()
# Mark team as paid
team_registration.set_paid(True)
waiting_list_position = cart_data.get('waiting_list_position', -1)
if is_not_sqlite_backend():
email_service = TournamentEmailService()
email_service.send_registration_confirmation(
request,
get_object_or_404(Tournament, id=tournament_id),
team_registration,
waiting_list_position
)
# Clear session data
if 'stripe_checkout_session_id' in request.session:
del request.session['stripe_checkout_session_id']
messages.success(request, "Paiement réussi et inscription confirmée !")
return redirect('tournament-info', tournament_id=tournament_id)
else:
messages.error(request, f"Paiement réussi mais erreur lors de l'inscription: {result}")
else:
messages.error(request, "Le paiement n'a pas été complété.")
except Exception as e:
messages.error(request, f"Erreur lors de la vérification du paiement: {str(e)}")
return redirect('tournament-info', tournament_id=tournament_id)
@login_required
def cancel_registration(request, tournament_id):
"""Cancel the current registration process and clear the cart"""
registration_cart.clear_registration_cart(request)
try:
tournament = Tournament.objects.get(id=tournament_id)
tournament.reserved_spots = max(0, tournament.reserved_spots - 1)
tournament.save()
except Tournament.DoesNotExist:
return False, "Tournoi introuvable."
messages.info(request, "Processus d'inscription annulé.")
return redirect('tournament-info', tournament_id=tournament_id)
@ -1355,6 +1522,80 @@ def confirm_tournament_registration(request, tournament_id):
return redirect('tournament_info', tournament_id=tournament_id)
@login_required
def proceed_to_payment(request, tournament_id):
tournament = get_object_or_404(Tournament, id=tournament_id)
# First check if the user is registered for this tournament
user_licence_id = request.user.licence_id
if not user_licence_id:
messages.error(request, "Votre licence n'est pas associée à votre compte.")
return redirect('tournament-info', tournament_id=tournament_id)
validator = LicenseValidator(user_licence_id)
stripped_license = validator.stripped_license
# Find the team registration
team_registration = TeamRegistration.objects.filter(
tournament=tournament,
player_registrations__licence_id__icontains=stripped_license,
walk_out=False
).first()
if not team_registration:
messages.error(request, "Vous n'êtes pas inscrit à ce tournoi.")
return redirect('tournament-info', tournament_id=tournament_id)
if team_registration.is_paid():
messages.info(request, "Votre paiement a déjà été confirmé.")
return redirect('tournament-info', tournament_id=tournament_id)
# Create Stripe checkout session
try:
stripe.api_key = settings.STRIPE_SECRET_KEY
# Create checkout session
checkout_session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': 'eur',
'product_data': {
'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}',
'description': f'Lieu {tournament.event.club.name}',
},
'unit_amount': int(team_registration.team_fee() * 100), # Amount in cents
},
'quantity': 2,
}],
mode='payment',
success_url=request.build_absolute_uri(
reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id})
),
cancel_url=request.build_absolute_uri(
reverse('tournament-info', kwargs={'tournament_id': tournament_id})
),
metadata={
'tournament_id': tournament_id,
'team_registration_id': str(team_registration.id),
'user_id': request.user.id,
},
customer_email=request.user.email,
)
# Store checkout session ID in session
request.session['stripe_checkout_session_id'] = checkout_session.id
request.session['team_registration_id'] = str(team_registration.id)
request.session.modified = True
# Redirect to Stripe checkout
return redirect(checkout_session.url)
except Exception as e:
messages.error(request, f"Erreur lors de la création de la session de paiement: {str(e)}")
return redirect('tournament-info', tournament_id=tournament_id)
class UserListExportView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
users = CustomUser.objects.order_by('date_joined')

Loading…
Cancel
Save