From 371bce35d7fac8bbc750bf032e8ac37516fe8e92 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 1 Oct 2025 13:08:11 +0200 Subject: [PATCH 1/7] Refactor contact information handling in tournament registration --- tournaments/forms.py | 2 +- tournaments/models/player_registration.py | 3 + tournaments/models/team_registration.py | 10 +++ tournaments/services/email_service.py | 14 ++-- tournaments/services/payment_service.py | 2 + .../services/tournament_registration.py | 23 ++++-- .../templates/register_tournament.html | 8 +- tournaments/views.py | 81 ++++++++++++++++++- 8 files changed, 124 insertions(+), 19 deletions(-) diff --git a/tournaments/forms.py b/tournaments/forms.py index 3c7c7f8..e38edeb 100644 --- a/tournaments/forms.py +++ b/tournaments/forms.py @@ -171,7 +171,7 @@ class SimpleForm(forms.Form): class TournamentRegistrationForm(forms.Form): #first_name = forms.CharField(label='Prénom', max_length=50) #last_name = forms.CharField(label='Nom', max_length=50) - email = forms.EmailField(label='E-mail', widget=forms.EmailInput(attrs={'readonly': 'readonly'})) + email = forms.EmailField(label='E-mail') mobile_number = forms.CharField( label='Téléphone', max_length=15, diff --git a/tournaments/models/player_registration.py b/tournaments/models/player_registration.py index 81879bd..4dbd9ad 100644 --- a/tournaments/models/player_registration.py +++ b/tournaments/models/player_registration.py @@ -228,3 +228,6 @@ class PlayerRegistration(TournamentSubModel): return self.team_registration.tournament.entry_fee else: return 0 + + def player_contact(self): + return self.contact_email or self.email diff --git a/tournaments/models/team_registration.py b/tournaments/models/team_registration.py index 6094e9d..09c3e66 100644 --- a/tournaments/models/team_registration.py +++ b/tournaments/models/team_registration.py @@ -550,3 +550,13 @@ class TeamRegistration(TournamentSubModel): # Check payment status for each player payment_statuses = [player.get_player_registration_fee() for player in player_registrations] return sum(payment_statuses) + + def team_contact(self): + if self.user: + return self.user.email + else: + player_registrations = self.players_sorted_by_captain + if len(player_registrations) > 0: + return player_registrations[0].player_contact() + + return None diff --git a/tournaments/services/email_service.py b/tournaments/services/email_service.py index 8b5ed54..a94a58d 100644 --- a/tournaments/services/email_service.py +++ b/tournaments/services/email_service.py @@ -531,7 +531,7 @@ class TournamentEmailService: topic = message_type.email_topic(tournament.federal_level_category, captain.time_to_confirm) email_subject = TournamentEmailService.email_subject(tournament, topic) - TournamentEmailService._send_email(captain.email, email_subject, email_body) + TournamentEmailService._send_email(captain.player_contact(), email_subject, email_body) @staticmethod def _build_email_content(message_type, recipient, tournament, tournament_details_str, other_player, request=None, waiting_list_position=None): @@ -721,11 +721,13 @@ class TournamentEmailService: tournament_prefix_that = federal_level_category.localized_prefix_that() processed_emails = set() for player in player_registrations: - if not player.email or not player.registered_online: + # Check both email and contact_email fields + player_email = player.player_contact() + if not player_email or not player.registered_online: continue - if player.email in processed_emails: + if player_email in processed_emails: continue - processed_emails.add(player.email) + processed_emails.add(player_email) tournament_details_str = tournament.build_tournament_details_str() other_player = team_registration.get_other_player(player) if len(player_registrations) > 1 else None @@ -772,7 +774,7 @@ class TournamentEmailService: email_body = "".join(body_parts) email_subject = TournamentEmailService.email_subject(tournament, "Confirmation de paiement") - TournamentEmailService._send_email(player.email, email_subject, email_body) + TournamentEmailService._send_email(player.player_contact(), email_subject, email_body) @staticmethod def send_refund_confirmation(tournament, team_registration, refund_details): @@ -856,4 +858,4 @@ class TournamentEmailService: email_body = "".join(body_parts) email_subject = TournamentEmailService.email_subject(tournament, "Confirmation de remboursement") - TournamentEmailService._send_email(player.email, email_subject, email_body) + TournamentEmailService._send_email(player.player_contact(), email_subject, email_body) diff --git a/tournaments/services/payment_service.py b/tournaments/services/payment_service.py index 0dd8430..d962dfe 100644 --- a/tournaments/services/payment_service.py +++ b/tournaments/services/payment_service.py @@ -76,6 +76,8 @@ class PaymentService: if not team_registration: print(f"[TOURNAMENT PAYMENT] Failed to create team registration") raise Exception("Erreur lors de la création de la réservation") + if not customer_email: + customer_email = team_registration.team_contact() team_registration_id = team_registration.id print(f"[TOURNAMENT PAYMENT] Created team registration: {team_registration_id}") diff --git a/tournaments/services/tournament_registration.py b/tournaments/services/tournament_registration.py index 1e8630b..3228177 100644 --- a/tournaments/services/tournament_registration.py +++ b/tournaments/services/tournament_registration.py @@ -38,7 +38,8 @@ class RegistrationCartManager: 'registration_tournament_id', 'registration_cart_players', 'registration_cart_expiry', - 'registration_mobile_number' + 'registration_mobile_number', + 'registration_email' ] for key in keys_to_delete: if key in self.session: @@ -132,9 +133,12 @@ class RegistrationCartManager: # Get user phone if authenticated user_phone = '' + user_email = '' if hasattr(self.request.user, 'phone'): user_phone = self.request.user.phone + if hasattr(self.request.user, 'email'): + user_email = self.request.user.email # Parse the expiry time from ISO format to datetime expiry_str = self.get_cart_expiry() expiry_datetime = None @@ -155,7 +159,8 @@ class RegistrationCartManager: 'is_cart_expired': self.is_cart_expired(), 'team_fee_from_cart_players': self.team_fee_from_cart_players(), 'team_fee_from_cart_players_formatted': self.team_fee_from_cart_players_formatted(), - 'mobile_number': self.session.get('registration_mobile_number', user_phone) + 'mobile_number': self.session.get('registration_mobile_number', user_phone), + 'email': self.session.get('registration_email', user_email), } # Debug: print the cart content @@ -370,11 +375,13 @@ class RegistrationCartManager: return True, "Joueur retiré." - def update_contact_info(self, mobile_number=None): + def update_contact_info(self, email=None, mobile_number=None): """Update contact info for the cart""" if self.is_cart_expired(): return False, "Votre session d'inscription a expiré, veuillez réessayer." + if email is not None: + self.session['registration_email'] = email if mobile_number is not None: self.session['registration_mobile_number'] = mobile_number @@ -394,6 +401,7 @@ class RegistrationCartManager: tournament_id = cart_data.get('tournament_id') players = cart_data.get('players') mobile_number = cart_data.get('mobile_number') + email = cart_data.get('email') # Validate cart data if not tournament_id: @@ -435,7 +443,7 @@ class RegistrationCartManager: registration_date=timezone.now(), walk_out=False, weight=weight, - user=self.request.user + user= self.request.user if self.request.user.is_authenticated else None ) for player_data in players: # Compute rank and sex using the original logic @@ -511,8 +519,8 @@ class RegistrationCartManager: rank=player_data.get('rank'), computed_rank=player_data.get('computed_rank'), licence_id=player_data.get('licence_id'), - email=matching_user.email if matching_user else player_data.get('email'), - phone_number=matching_user.phone if matching_user else player_data.get('mobile_number'), + contact_email=matching_user.email if matching_user else player_data.get('email', email), + contact_phone_number=matching_user.phone if matching_user else player_data.get('mobile_number', mobile_number), registration_status=RegistrationStatus.CONFIRMED if self.session.get('waiting_list_position', 0) < 0 else RegistrationStatus.WAITING ) @@ -533,7 +541,8 @@ class RegistrationCartManager: 'registration_tournament_id', 'registration_cart_players', 'registration_cart_expiry', - 'registration_mobile_number' + 'registration_mobile_number', + 'registration_email', ] for key in keys_to_clear: diff --git a/tournaments/templates/register_tournament.html b/tournaments/templates/register_tournament.html index d5746b3..b25cb0e 100644 --- a/tournaments/templates/register_tournament.html +++ b/tournaments/templates/register_tournament.html @@ -32,7 +32,13 @@

✅ Votre paiement a bien été effectué et enregistré.

{% endif %}

- Un email de confirmation a été envoyé à l'adresse associée à votre compte Padel Club ({{ user.email }}). Pensez à vérifier vos spams si vous ne recevez pas l'email. En cas de problème, contactez le juge-arbitre. + {% if user.email %} + Un email de confirmation a été envoyé à l'adresse associée à votre compte Padel Club ({{ user.email }}). Pensez à vérifier vos spams si vous ne recevez pas l'email. En cas de problème, contactez le juge-arbitre. + {% elif registered_team.team_contact %} + Un email de confirmation a été envoyé à l'adresse associée à votre compte Padel Club ({{ registered_team.team_contact }}). Pensez à vérifier vos spams si vous ne recevez pas l'email. En cas de problème, contactez le juge-arbitre. + {% else %} + Aucun email de confirmation n'a été envoyé car vous n'avez pas fourni d'adresse email. Contactez le juge-arbitre. + {% endif %}

{% else %} {% if not registration_successful %} diff --git a/tournaments/views.py b/tournaments/views.py index 41226ba..b64e6f8 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -1583,9 +1583,14 @@ def proceed_to_payment(request, tournament_id): 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) -@login_required def tournament_payment_success(request, tournament_id): """Handle successful Stripe payment for tournament registration""" + + # For unauthenticated users, process payment and redirect directly to registration page + if not request.user.is_authenticated: + return _handle_unauthenticated_payment_success(request, tournament_id) + + # Original logic for authenticated users # Get checkout session ID from session checkout_session_id = request.session.get('stripe_checkout_session_id') if not checkout_session_id: @@ -1641,6 +1646,68 @@ def tournament_payment_success(request, tournament_id): # For direct payments, go to tournament info return redirect('tournament-info', tournament_id=tournament_id) + +def _handle_unauthenticated_payment_success(request, tournament_id): + """Handle payment success for unauthenticated users""" + print(f"[PAYMENT SUCCESS] Handling unauthenticated user payment for tournament {tournament_id}") + + # Get checkout session ID from session + checkout_session_id = request.session.get('stripe_checkout_session_id') + if not checkout_session_id: + print(f"[PAYMENT SUCCESS] No checkout session ID found") + messages.error(request, "Session de paiement introuvable.") + return redirect('register_tournament', tournament_id=tournament_id) + + try: + # Verify payment status with Stripe + stripe.api_key = settings.STRIPE_SECRET_KEY + print(f"[PAYMENT SUCCESS] Retrieving checkout session: {checkout_session_id}") + + stripe_account_id = request.session.get('stripe_account_id') + if not stripe_account_id: + checkout_session = stripe.checkout.Session.retrieve(checkout_session_id) + else: + checkout_session = stripe.checkout.Session.retrieve(checkout_session_id, stripe_account=stripe_account_id) + + print(f"[PAYMENT SUCCESS] Payment status: {checkout_session.payment_status}") + + if checkout_session.payment_status == 'paid': + # Process the payment success + payment_service = PaymentService(request) + success = payment_service.process_successful_payment(checkout_session) + + print(f"[PAYMENT SUCCESS] Payment processing success: {success}") + + if success: + # Always set success flags for unauthenticated users since they come from registration + request.session['registration_successful'] = True + request.session['registration_paid'] = True + + # Clear payment-related session data + for key in ['stripe_checkout_session_id', 'team_registration_id', 'payment_source_page', 'stripe_account_id']: + if key in request.session: + del request.session[key] + + print(f"[PAYMENT SUCCESS] Redirecting to registration page with success flags") + # Redirect directly to registration page with success context + return redirect('register_tournament', tournament_id=tournament_id) + else: + messages.error(request, "Erreur lors du traitement du paiement.") + else: + messages.error(request, "Le paiement n'a pas été complété.") + + except Exception as e: + print(f"[PAYMENT SUCCESS] Payment processing error: {str(e)}") + messages.error(request, f"Erreur lors de la vérification du paiement: {str(e)}") + + # Clean up session variables even if there was an error + for key in ['stripe_checkout_session_id', 'team_registration_id', 'payment_source_page', 'stripe_account_id']: + if key in request.session: + del request.session[key] + + # Always redirect to registration page for unauthenticated users + return redirect('register_tournament', tournament_id=tournament_id) + @csrf_protect def register_tournament(request, tournament_id): tournament = get_object_or_404(Tournament, id=tournament_id) @@ -1657,7 +1724,7 @@ def register_tournament(request, tournament_id): # Check for registration_successful flag registration_successful = request.session.pop('registration_successful', False) registration_paid = request.session.pop('registration_paid', False) - + registered_team = None # Handle payment cancellation - check for cancelled team registration cancel_team_registration_id = request.session.pop('cancel_team_registration_id', None) if cancel_team_registration_id: @@ -1676,7 +1743,8 @@ def register_tournament(request, tournament_id): if not team_registration.is_paid(): team_registration.delete() print(f"[PAYMENT CANCEL] Deleted unpaid team registration {cancel_team_registration_id}") - + else: + registered_team = team_registration except TeamRegistration.DoesNotExist: print(f"[PAYMENT CANCEL] Team registration {cancel_team_registration_id} not found") except Exception as e: @@ -1702,6 +1770,7 @@ def register_tournament(request, tournament_id): 'tournament': tournament, 'registration_successful': True, 'registration_paid': registration_paid, + 'registered_team': registered_team, 'current_players': [], 'cart_data': {'players': []} } @@ -1823,6 +1892,7 @@ def handle_add_player_request(request, tournament, cart_manager, context): if team_form.is_valid(): # Update cart with mobile number before adding player cart_manager.update_contact_info( + email=team_form.cleaned_data.get('email'), mobile_number=team_form.cleaned_data.get('mobile_number') ) @@ -1834,7 +1904,7 @@ def handle_add_player_request(request, tournament, cart_manager, context): context['current_players'] = cart_data['players'] context['cart_data'] = cart_data context['team_form'] = TournamentRegistrationForm(initial={ - 'email': request.user.email if request.user.is_authenticated else '', + 'email': request.user.email if request.user.is_authenticated else cart_data.get('email', ''), 'mobile_number': cart_data.get('mobile_number', '') }) @@ -1898,6 +1968,7 @@ def handle_register_team_request(request, tournament, cart_manager, context): # Update cart with contact info cart_manager.update_contact_info( + email=team_form.cleaned_data.get('email'), mobile_number=team_form.cleaned_data.get('mobile_number') ) @@ -1922,6 +1993,7 @@ def handle_register_team_request(request, tournament, cart_manager, context): ) context['registration_successful'] = True + context['registered_team'] = result context['registration_paid'] = False context['current_players'] = [] context['add_player_form'] = None # No more adding players after success @@ -1939,6 +2011,7 @@ def handle_payment_request(request, cart_manager, context, tournament_id): if team_form.is_valid(): # Update cart with contact info cart_manager.update_contact_info( + email=team_form.cleaned_data.get('email'), mobile_number=team_form.cleaned_data.get('mobile_number') ) From 4fbfce8393da8a1ec904bfd9aeb060e00347b651 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 1 Oct 2025 13:47:26 +0200 Subject: [PATCH 2/7] Fix player contact email handling across service methods --- tournaments/models/player_registration.py | 4 +++- tournaments/services/email_service.py | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tournaments/models/player_registration.py b/tournaments/models/player_registration.py index 4dbd9ad..13a7018 100644 --- a/tournaments/models/player_registration.py +++ b/tournaments/models/player_registration.py @@ -230,4 +230,6 @@ class PlayerRegistration(TournamentSubModel): return 0 def player_contact(self): - return self.contact_email or self.email + if self.contact_email: + return self.contact_email + return self.email diff --git a/tournaments/services/email_service.py b/tournaments/services/email_service.py index a94a58d..206285a 100644 --- a/tournaments/services/email_service.py +++ b/tournaments/services/email_service.py @@ -516,8 +516,8 @@ class TournamentEmailService: @staticmethod def notify(captain, other_player, tournament, message_type: TeamEmailType): - print("TournamentEmailService.notify", captain.email, captain.registered_online, tournament, message_type) - if not captain or not captain.registered_online or not captain.email: + print("TournamentEmailService.notify", captain.player_contact(), captain.registered_online, tournament, message_type) + if not captain or not captain.registered_online or not captain.player_contact(): return tournament_details_str = tournament.build_tournament_details_str() @@ -607,7 +607,7 @@ class TournamentEmailService: print("TournamentEmailService.notify_team 2p", team) first_player, second_player = players TournamentEmailService.notify(first_player, second_player, tournament, message_type) - if first_player.email != second_player.email: + if first_player.player_contact() != second_player.player_contact(): TournamentEmailService.notify(second_player, first_player, tournament, message_type) elif len(players) == 1: print("TournamentEmailService.notify_team 1p", team) @@ -814,11 +814,11 @@ class TournamentEmailService: tournament_prefix_that = federal_level_category.localized_prefix_that() processed_emails = set() for player in player_registrations: - if not player.email or not player.registered_online: + if not player.player_contact() or not player.registered_online: continue - if player.email in processed_emails: + if player.player_contact() in processed_emails: continue - processed_emails.add(player.email) + processed_emails.add(player.player_contact()) tournament_details_str = tournament.build_tournament_details_str() other_player = team_registration.get_other_player(player) if len(player_registrations) > 1 else None From a5c9765366c193b423e1921c52bac186e00bbaf4 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 1 Oct 2025 15:18:26 +0200 Subject: [PATCH 3/7] Add Stripe payment links for tournament registration --- api/urls.py | 2 + api/views.py | 107 ++++++++++++++++++ tournaments/models/tournament.py | 2 + tournaments/services/email_service.py | 38 ++++++- tournaments/services/payment_service.py | 88 ++++++++++++++ .../templates/stripe/payment_complete.html | 30 +++++ tournaments/urls.py | 1 + tournaments/views.py | 33 ++++++ 8 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 tournaments/templates/stripe/payment_complete.html diff --git a/api/urls.py b/api/urls.py index 5e9d5ff..56164f5 100644 --- a/api/urls.py +++ b/api/urls.py @@ -63,5 +63,7 @@ urlpatterns = [ path('dj-rest-auth/', include('dj_rest_auth.urls')), path('stripe/create-account/', views.create_stripe_connect_account, name='create_stripe_account'), path('stripe/create-account-link/', views.create_stripe_account_link, name='create_account_link'), + path('payment-link/create//', views.create_payment_link, name='create-payment-link'), + path('payment-link/team//', views.get_payment_link, name='get-payment-link'), ] diff --git a/api/views.py b/api/views.py index e21a8d8..e197fa7 100644 --- a/api/views.py +++ b/api/views.py @@ -619,6 +619,113 @@ def validate_stripe_account(request): 'needs_onboarding': True, }, status=200) +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def create_payment_link(request, team_registration_id): + """ + Create a Stripe Payment Link for a team registration + """ + try: + # Verify team registration exists and user has permission + team_registration = TeamRegistration.objects.get(id=team_registration_id) + tournament = team_registration.tournament + + # Check if payment is required and team hasn't paid + if tournament.is_free() or team_registration.get_payment_status() == 'PAID': + return Response( + {'error': 'Payment not required or already completed'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Create payment link + payment_link_url = PaymentService.create_payment_link(team_registration_id) + + if payment_link_url: + return Response({ + 'success': True, + 'payment_link': payment_link_url, + 'team_registration_id': str(team_registration_id), + 'tournament_name': tournament.display_name(), + 'amount': team_registration.get_team_registration_fee(), + 'currency': tournament.currency_code + }) + else: + return Response( + {'error': 'Failed to create payment link'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + except TeamRegistration.DoesNotExist: + return Response( + {'error': 'Team registration not found'}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response( + {'error': f'Unexpected error: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_payment_link(request, team_registration_id): + """ + Get or create a payment link for a specific team registration + """ + try: + team_registration = TeamRegistration.objects.get( + id=team_registration_id + ) + + tournament = team_registration.tournament + + # Check payment status + payment_status = team_registration.get_payment_status() + + if payment_status == 'PAID': + return Response({ + 'success': False, + 'message': 'Payment already completed', + 'payment_status': 'PAID' + }) + + if tournament.is_free(): + return Response({ + 'success': False, + 'message': 'Tournament is free', + 'payment_status': 'NOT_REQUIRED' + }) + + # Get or create payment link + payment_link_url = PaymentService.get_or_create_payment_link(team_registration_id) + + if payment_link_url: + return Response({ + 'success': True, + 'payment_link': payment_link_url, + 'team_registration_id': str(team_registration_id), + 'tournament_name': tournament.display_name(), + 'amount': team_registration.get_team_registration_fee(), + 'currency': tournament.currency_code, + 'payment_status': payment_status + }) + else: + return Response( + {'error': 'Failed to create payment link'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + except (Tournament.DoesNotExist, TeamRegistration.DoesNotExist): + return Response( + {'error': 'Tournament or team registration not found'}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response( + {'error': f'Unexpected error: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + @api_view(['GET']) @permission_classes([IsAuthenticated]) def is_granted_unlimited_access(request): diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index ec85950..5e96228 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -96,6 +96,8 @@ class Tournament(BaseModel): club_member_fee_deduction = models.FloatField(null=True, blank=True) unregister_delta_in_hours = models.IntegerField(default=24) currency_code = models.CharField(null=True, blank=True, max_length=3, default='EUR') + parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, related_name='children') + loser_index = models.IntegerField(default=0) def delete_dependencies(self): for team_registration in self.team_registrations.all(): diff --git a/tournaments/services/email_service.py b/tournaments/services/email_service.py index 206285a..9b54041 100644 --- a/tournaments/services/email_service.py +++ b/tournaments/services/email_service.py @@ -681,12 +681,38 @@ class TournamentEmailService: # For unpaid teams, add payment instructions formatted_fee = currency_service.format_amount(tournament.entry_fee, tournament.currency_code) - payment_info = [ - "\n\n⚠️ Paiement des frais d'inscription requis", - f"Les frais d'inscription de {formatted_fee} par joueur 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" - ] + + print("team_registration.user", team_registration.user) + # Check if team has a user account attached + if team_registration.user: + # User has account - direct to login and pay + payment_info = [ + "\n\n⚠️ Paiement des frais d'inscription requis", + f"Les frais d'inscription de {formatted_fee} par joueur 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" + ] + else: + # No user account - create payment link + from .payment_service import PaymentService + payment_link = PaymentService.create_payment_link(team_registration.id) + + if payment_link: + payment_info = [ + "\n\n⚠️ Paiement des frais d'inscription requis", + f"Les frais d'inscription de {formatted_fee} par joueur doivent être payés pour confirmer votre participation.", + "Vous pouvez effectuer le paiement directement via ce lien sécurisé :", + f"💳 Payer maintenant: {payment_link}", + "\nAucun compte n'est requis pour effectuer le paiement." + ] + else: + # Fallback if payment link creation fails + payment_info = [ + "\n\n⚠️ Paiement des frais d'inscription requis", + f"Les frais d'inscription de {formatted_fee} par joueur doivent être payés pour confirmer votre participation.", + "Veuillez contacter l'organisateur du tournoi pour effectuer le paiement.", + f"Informations du tournoi: https://padelclub.app/tournament/{tournament.id}/info" + ] return "\n".join(payment_info) diff --git a/tournaments/services/payment_service.py b/tournaments/services/payment_service.py index d962dfe..c29f8d9 100644 --- a/tournaments/services/payment_service.py +++ b/tournaments/services/payment_service.py @@ -643,3 +643,91 @@ class PaymentService: import traceback traceback.print_exc() return HttpResponse(status=400) + + @staticmethod + def create_payment_link(team_registration_id): + """ + Create a Stripe Payment Link for a team registration + Returns the payment link URL or None if failed + """ + try: + team_registration = TeamRegistration.objects.get(id=team_registration_id) + tournament = team_registration.tournament + + if not tournament or tournament.is_free(): + return None + + stripe.api_key = settings.STRIPE_SECRET_KEY + currency_service = CurrencyService() + + # Calculate the team fee + team_fee = team_registration.get_team_registration_fee() + stripe_amount = currency_service.convert_to_stripe_amount(team_fee, tournament.currency_code) + + customer_email = team_registration.team_contact() + + # Create payment link + payment_link_params = { + 'line_items': [{ + 'price_data': { + 'currency': tournament.currency_code.lower(), + 'product_data': { + 'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}', + 'description': f'Lieu {tournament.event.club.name}', + }, + 'unit_amount': stripe_amount, + }, + 'quantity': 1, + }], + 'metadata': { + 'tournament_id': str(tournament.id), + 'team_registration_id': str(team_registration.id), + 'customer_email': customer_email or 'Non fourni', + 'payment_source': 'payment_link', + 'registration_type': 'direct', + }, + 'after_completion': { + 'type': 'redirect', + 'redirect': { + 'url': f'https://padelclub.app/stripe/payment_complete/?tournament_id={tournament.id}&team_registration_id={team_registration.id}&payment=success' + } + }, + 'automatic_tax': {'enabled': False}, + 'billing_address_collection': 'auto', + 'shipping_address_collection': None, + 'cancel_url': f'https://padelclub.app/stripe/payment_complete/?tournament_id={tournament.id}&team_registration_id={team_registration.id}&payment=cancel', + } + + # Add customer email if available + if customer_email: + payment_link_params['customer_creation'] = 'if_required' + # Note: Stripe Payment Links don't support customer_email parameter + # but will ask for email during checkout + + # Handle Stripe Connect account if needed + stripe_account_id = tournament.event.creator.stripe_account_id if hasattr(tournament.event.creator, 'stripe_account_id') else None + + if stripe_account_id: + payment_link = stripe.PaymentLink.create( + **payment_link_params, + stripe_account=stripe_account_id + ) + else: + payment_link = stripe.PaymentLink.create(**payment_link_params) + + print(f"[PAYMENT LINK] Created payment link: {payment_link.url}") + return payment_link.url + + except Exception as e: + print(f"[PAYMENT LINK] Error creating payment link: {str(e)}") + return None + + @staticmethod + def get_or_create_payment_link(team_registration_id): + """ + Get existing payment link or create a new one for a team registration + This method can be used to avoid creating multiple links for the same registration + """ + # In a real implementation, you might want to store payment links in the database + # and check if one already exists and is still valid + return PaymentService.create_payment_link(team_registration_id) diff --git a/tournaments/templates/stripe/payment_complete.html b/tournaments/templates/stripe/payment_complete.html new file mode 100644 index 0000000..c0f1db7 --- /dev/null +++ b/tournaments/templates/stripe/payment_complete.html @@ -0,0 +1,30 @@ +{% extends 'tournaments/base.html' %} +{% block head_title %} Paiement {% endblock %} +{% block first_title %} Padel Club {% endblock %} +{% block second_title %} Paiement {% endblock %} + +{% block content %} +{% load static %} +{% load tz %} + +
+
+
+ {% if payment_status == 'success' %} + +

Votre inscription a été confirmée. Un email de confirmation vous a été envoyé.

+ {% if show_details and tournament %} +

Tournoi : {{ tournament.display_name }}

+ {% endif %} + {% elif payment_status == 'cancel' %} + +

Votre paiement a été annulé. Aucun montant n'a été prélevé.

+ {% else %} + +

Votre paiement est en cours de traitement. Vous recevrez un email de confirmation sous peu.

+ {% endif %} +

Vous pouvez maintenant fermer cette page.

+
+
+
+{% endblock %} diff --git a/tournaments/urls.py b/tournaments/urls.py index 255845a..b9fd316 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -79,6 +79,7 @@ urlpatterns = [ path('activation-success/', views.activation_success, name='activation_success'), path('activation-failed/', views.activation_failed, name='activation_failed'), path('tournaments//confirm/', views.confirm_tournament_registration, name='confirm_tournament_registration'), + path('stripe/payment_complete/', views.stripe_payment_complete, name='stripe-payment-complete'), path('stripe-onboarding-complete/', views.stripe_onboarding_complete, name='stripe-onboarding-complete'), path('stripe-refresh-account-link/', views.stripe_refresh_account_link, name='stripe-refresh-account-link'), path('tournaments//toggle-private/', views.toggle_tournament_private, name='toggle_tournament_private'), diff --git a/tournaments/views.py b/tournaments/views.py index b64e6f8..3adce17 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -2304,6 +2304,39 @@ def tournament_live_matches(request, tournament_id): 'live_matches': live_matches, }) +def stripe_payment_complete(request): + """Handle payment complete page for Stripe Payment Links""" + tournament_id = request.GET.get('tournament_id') + team_registration_id = request.GET.get('team_registration_id') + payment_status = request.GET.get('payment', 'unknown') + + context = { + 'payment_status': payment_status, + 'tournament': None, + 'team_registration': None, + 'players': [], + 'show_details': False + } + + # Try to get tournament and team registration details + if tournament_id and team_registration_id: + try: + tournament = Tournament.objects.get(id=tournament_id) + team_registration = TeamRegistration.objects.get(id=team_registration_id) + + context.update({ + 'tournament': tournament, + 'team_registration': team_registration, + 'players': team_registration.players_sorted_by_captain, + 'show_details': True, + 'amount_paid': team_registration.get_team_registration_fee(), + 'currency': tournament.currency_code + }) + + except (Tournament.DoesNotExist, TeamRegistration.DoesNotExist): + print(f"Tournament or team registration not found: {tournament_id}, {team_registration_id}") + + return render(request, 'stripe/payment_complete.html', context) class UserListExportView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): From c004325ac8dcddb7ddcc4237a960e55dd7c0c4a4 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 1 Oct 2025 15:19:00 +0200 Subject: [PATCH 4/7] Remove unused tournament model fields --- tournaments/models/tournament.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 5e96228..ec85950 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -96,8 +96,6 @@ class Tournament(BaseModel): club_member_fee_deduction = models.FloatField(null=True, blank=True) unregister_delta_in_hours = models.IntegerField(default=24) currency_code = models.CharField(null=True, blank=True, max_length=3, default='EUR') - parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, related_name='children') - loser_index = models.IntegerField(default=0) def delete_dependencies(self): for team_registration in self.team_registrations.all(): From 22b06b44947dee462fc332aab6501447d1e37c9e Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 1 Oct 2025 15:23:40 +0200 Subject: [PATCH 5/7] Remove cancel_url from Stripe checkout params --- tournaments/services/payment_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tournaments/services/payment_service.py b/tournaments/services/payment_service.py index c29f8d9..20049bd 100644 --- a/tournaments/services/payment_service.py +++ b/tournaments/services/payment_service.py @@ -695,7 +695,6 @@ class PaymentService: 'automatic_tax': {'enabled': False}, 'billing_address_collection': 'auto', 'shipping_address_collection': None, - 'cancel_url': f'https://padelclub.app/stripe/payment_complete/?tournament_id={tournament.id}&team_registration_id={team_registration.id}&payment=cancel', } # Add customer email if available From f4d8b1a5364efc626af4def8d5e367387f0c3b9f Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 1 Oct 2025 15:53:48 +0200 Subject: [PATCH 6/7] Add support for corporate tournament Stripe payments The commit adds webhook handling for corporate tournaments using platform account (XLR) vs connected accounts, updates payment link creation logic, and improves logging. --- padelclub_backend/settings_local.py.dist | 1 + tournaments/services/payment_service.py | 99 +++++++++++++++++------- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/padelclub_backend/settings_local.py.dist b/padelclub_backend/settings_local.py.dist index f983539..5a4dc7c 100644 --- a/padelclub_backend/settings_local.py.dist +++ b/padelclub_backend/settings_local.py.dist @@ -42,6 +42,7 @@ STRIPE_PUBLISHABLE_KEY = '' STRIPE_SECRET_KEY = '' SHOP_STRIPE_WEBHOOK_SECRET = 'whsec_...' # Your existing webhook secret TOURNAMENT_STRIPE_WEBHOOK_SECRET = 'whsec_...' # New webhook secret for tournaments +XLR_STRIPE_WEBHOOK_SECRET = 'whsec_...' # New webhook secret for padel club STRIPE_FEE = 0.0075 TOURNAMENT_SETTINGS = { 'TIME_PROXIMITY_RULES': { diff --git a/tournaments/services/payment_service.py b/tournaments/services/payment_service.py index 20049bd..8c99a5e 100644 --- a/tournaments/services/payment_service.py +++ b/tournaments/services/payment_service.py @@ -592,20 +592,32 @@ class PaymentService: def stripe_webhook(request): payload = request.body sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') - print("Received webhook call") + + # Check if this is a Connect account webhook + stripe_account = request.META.get('HTTP_STRIPE_ACCOUNT') + + print("=== WEBHOOK DEBUG ===") print(f"Signature: {sig_header}") + print(f"Connect Account: {stripe_account}") + + if stripe_account: + # This is a connected account (regular umpire tournament) + webhook_secret = settings.TOURNAMENT_STRIPE_WEBHOOK_SECRET + print(f"Using umpire webhook secret for connected account: {stripe_account}") + else: + # This is platform account (corporate tournament) + webhook_secret = settings.XLR_STRIPE_WEBHOOK_SECRET + print("Using XLR company webhook secret") try: - event = stripe.Webhook.construct_event( - payload, sig_header, settings.TOURNAMENT_STRIPE_WEBHOOK_SECRET - ) + event = stripe.Webhook.construct_event(payload, sig_header, webhook_secret) print(f"Tournament webhook event type: {event['type']}") + # Debug metadata stripe_object = event['data']['object'] - - # Debug: Print the object type - object_type = stripe_object.get('object', 'unknown') - print(f"Stripe object type: {object_type}") + metadata = stripe_object.get('metadata', {}) + print(f"is_corporate_tournament: {metadata.get('is_corporate_tournament', 'unknown')}") + print(f"payment_source: {metadata.get('payment_source', 'unknown')}") if event['type'] == 'checkout.session.completed': success = PaymentService.process_direct_payment(stripe_object) @@ -639,7 +651,7 @@ class PaymentService: return HttpResponse(status=200) except Exception as e: - print(f"Tournament webhook error: {str(e)}") + print(f"Webhook error: {str(e)}") import traceback traceback.print_exc() return HttpResponse(status=400) @@ -665,12 +677,26 @@ class PaymentService: stripe_amount = currency_service.convert_to_stripe_amount(team_fee, tournament.currency_code) customer_email = team_registration.team_contact() + currency_code = tournament.currency_code or 'EUR' + + print(f"[PAYMENT LINK] Tournament: {tournament.name}") + print(f"[PAYMENT LINK] is_corporate_tournament: {tournament.is_corporate_tournament}") + + # Base metadata (same as checkout session) + base_metadata = { + 'tournament_id': str(tournament.id), + 'team_registration_id': str(team_registration.id), + 'customer_email': customer_email or 'Non fourni', + 'payment_source': 'payment_link', + 'registration_type': 'direct', + 'currency_code': currency_code, + } - # Create payment link + # Create payment link params payment_link_params = { 'line_items': [{ 'price_data': { - 'currency': tournament.currency_code.lower(), + 'currency': currency_code.lower(), 'product_data': { 'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}', 'description': f'Lieu {tournament.event.club.name}', @@ -679,13 +705,6 @@ class PaymentService: }, 'quantity': 1, }], - 'metadata': { - 'tournament_id': str(tournament.id), - 'team_registration_id': str(team_registration.id), - 'customer_email': customer_email or 'Non fourni', - 'payment_source': 'payment_link', - 'registration_type': 'direct', - }, 'after_completion': { 'type': 'redirect', 'redirect': { @@ -694,31 +713,53 @@ class PaymentService: }, 'automatic_tax': {'enabled': False}, 'billing_address_collection': 'auto', - 'shipping_address_collection': None, } - # Add customer email if available - if customer_email: - payment_link_params['customer_creation'] = 'if_required' - # Note: Stripe Payment Links don't support customer_email parameter - # but will ask for email during checkout + # Handle corporate vs regular tournaments (same logic as checkout session) + if tournament.is_corporate_tournament: + print(f"[PAYMENT LINK] Corporate tournament - creating on platform account") + # Corporate tournament - create on platform account (no Connect account) + metadata = { + **base_metadata, + 'is_corporate_tournament': 'true', + 'stripe_account_type': 'direct' + } + payment_link_params['metadata'] = metadata - # Handle Stripe Connect account if needed - stripe_account_id = tournament.event.creator.stripe_account_id if hasattr(tournament.event.creator, 'stripe_account_id') else None + # Create payment link on platform account + payment_link = stripe.PaymentLink.create(**payment_link_params) + + else: + print(f"[PAYMENT LINK] Regular tournament - creating on connected account") + # Regular tournament - create on connected account + stripe_account_id = tournament.stripe_account_id + if not stripe_account_id: + print(f"[PAYMENT LINK] ERROR: No Stripe account ID for umpire") + return None + + metadata = { + **base_metadata, + 'is_corporate_tournament': 'false', + 'stripe_account_type': 'connect', + 'stripe_account_id': stripe_account_id + } + payment_link_params['metadata'] = metadata - if stripe_account_id: + print(f"[PAYMENT LINK] Creating payment link for connected account: {stripe_account_id}") + + # Create payment link on connected account payment_link = stripe.PaymentLink.create( **payment_link_params, stripe_account=stripe_account_id ) - else: - payment_link = stripe.PaymentLink.create(**payment_link_params) print(f"[PAYMENT LINK] Created payment link: {payment_link.url}") return payment_link.url except Exception as e: print(f"[PAYMENT LINK] Error creating payment link: {str(e)}") + import traceback + traceback.print_exc() return None @staticmethod From 1404adc8025ad32b718289bac968693524942037 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 1 Oct 2025 16:24:51 +0200 Subject: [PATCH 7/7] Replace payment link endpoints with resend email option --- api/urls.py | 3 +- api/views.py | 109 +++++++-------------------------------------------- 2 files changed, 16 insertions(+), 96 deletions(-) diff --git a/api/urls.py b/api/urls.py index 56164f5..f82d616 100644 --- a/api/urls.py +++ b/api/urls.py @@ -63,7 +63,6 @@ urlpatterns = [ path('dj-rest-auth/', include('dj_rest_auth.urls')), path('stripe/create-account/', views.create_stripe_connect_account, name='create_stripe_account'), path('stripe/create-account-link/', views.create_stripe_account_link, name='create_account_link'), - path('payment-link/create//', views.create_payment_link, name='create-payment-link'), - path('payment-link/team//', views.get_payment_link, name='get-payment-link'), + path('resend-payment-email//', views.resend_payment_email, name='resend-payment-email'), ] diff --git a/api/views.py b/api/views.py index e197fa7..06e16bc 100644 --- a/api/views.py +++ b/api/views.py @@ -16,6 +16,7 @@ from django.shortcuts import get_object_or_404 from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer, ImageSerializer, ActivitySerializer, ProspectSerializer, EntitySerializer, TournamentSummarySerializer from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image +from tournaments.services.email_service import TournamentEmailService from biz.models import Activity, Prospect, Entity @@ -621,110 +622,30 @@ def validate_stripe_account(request): @api_view(['POST']) @permission_classes([IsAuthenticated]) -def create_payment_link(request, team_registration_id): +def resend_payment_email(request, team_registration_id): """ - Create a Stripe Payment Link for a team registration + Resend the registration confirmation email (which includes payment info/link) """ try: - # Verify team registration exists and user has permission team_registration = TeamRegistration.objects.get(id=team_registration_id) tournament = team_registration.tournament - # Check if payment is required and team hasn't paid - if tournament.is_free() or team_registration.get_payment_status() == 'PAID': - return Response( - {'error': 'Payment not required or already completed'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # Create payment link - payment_link_url = PaymentService.create_payment_link(team_registration_id) - - if payment_link_url: - return Response({ - 'success': True, - 'payment_link': payment_link_url, - 'team_registration_id': str(team_registration_id), - 'tournament_name': tournament.display_name(), - 'amount': team_registration.get_team_registration_fee(), - 'currency': tournament.currency_code - }) - else: - return Response( - {'error': 'Failed to create payment link'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - except TeamRegistration.DoesNotExist: - return Response( - {'error': 'Team registration not found'}, - status=status.HTTP_404_NOT_FOUND + TournamentEmailService.send_registration_confirmation( + request, + tournament, + team_registration, + waiting_list_position=-1 ) - except Exception as e: - return Response( - {'error': f'Unexpected error: {str(e)}'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def get_payment_link(request, team_registration_id): - """ - Get or create a payment link for a specific team registration - """ - try: - team_registration = TeamRegistration.objects.get( - id=team_registration_id - ) - - tournament = team_registration.tournament - - # Check payment status - payment_status = team_registration.get_payment_status() - - if payment_status == 'PAID': - return Response({ - 'success': False, - 'message': 'Payment already completed', - 'payment_status': 'PAID' - }) - if tournament.is_free(): - return Response({ - 'success': False, - 'message': 'Tournament is free', - 'payment_status': 'NOT_REQUIRED' - }) - - # Get or create payment link - payment_link_url = PaymentService.get_or_create_payment_link(team_registration_id) - - if payment_link_url: - return Response({ - 'success': True, - 'payment_link': payment_link_url, - 'team_registration_id': str(team_registration_id), - 'tournament_name': tournament.display_name(), - 'amount': team_registration.get_team_registration_fee(), - 'currency': tournament.currency_code, - 'payment_status': payment_status - }) - else: - return Response( - {'error': 'Failed to create payment link'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) + return Response({ + 'success': True, + 'message': 'Email de paiement renvoyé' + }) - except (Tournament.DoesNotExist, TeamRegistration.DoesNotExist): - return Response( - {'error': 'Tournament or team registration not found'}, - status=status.HTTP_404_NOT_FOUND - ) + except TeamRegistration.DoesNotExist: + return Response({'error': 'Team not found'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: - return Response( - {'error': f'Unexpected error: {str(e)}'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['GET']) @permission_classes([IsAuthenticated])