From 31b87cea2f6057725ee1b5c32e2b286e8246756f Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 4 Sep 2025 11:29:36 +0200 Subject: [PATCH] Add payment session expiration and logging The main changes in this commit are: - Adding 30-minute expiration to Stripe checkout sessions - Adding comprehensive logging throughout payment flow - Improving payment failure and expiration handling - Moving payment processing to static methods - Refactoring payment session creation and handling - Adding better error handling for Stripe operations ``` Add payment session expiration and logging ``` --- tournaments/services/payment_service.py | 531 +++++++++++++----- .../services/tournament_registration.py | 34 +- tournaments/views.py | 90 ++- 3 files changed, 467 insertions(+), 188 deletions(-) diff --git a/tournaments/services/payment_service.py b/tournaments/services/payment_service.py index 064d732..6ed1a9d 100644 --- a/tournaments/services/payment_service.py +++ b/tournaments/services/payment_service.py @@ -5,12 +5,11 @@ from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST import stripe +from datetime import datetime, timedelta from ..models import TeamRegistration, PlayerRegistration, Tournament from ..models.player_registration import PlayerPaymentType from .email_service import TournamentEmailService -from .tournament_registration import RegistrationCartManager -from ..utils.extensions import is_not_sqlite_backend from tournaments.services.currency_service import CurrencyService class PaymentService: @@ -22,51 +21,101 @@ class PaymentService: self.request = request self.stripe_api_key = settings.STRIPE_SECRET_KEY - def create_checkout_session(self, tournament_id, team_fee, cart_data=None, team_registration_id=None): + def create_checkout_session(self, tournament_id, team_fee, cart_manager=None, team_registration_id=None): """ Create a Stripe checkout session for tournament payment """ + print(f"[TOURNAMENT PAYMENT] Creating checkout session") + print(f"[TOURNAMENT PAYMENT] Tournament ID: {tournament_id}") + print(f"[TOURNAMENT PAYMENT] Team fee: {team_fee}") + print(f"[TOURNAMENT PAYMENT] Cart manager: {cart_manager}") + print(f"[TOURNAMENT PAYMENT] Team registration ID: {team_registration_id}") + print(f"[TOURNAMENT PAYMENT] User: {self.request.user}") + session_duration_minutes = 30 + expires_at = int((datetime.now() + timedelta(minutes=session_duration_minutes)).timestamp()) + stripe.api_key = self.stripe_api_key tournament = get_object_or_404(Tournament, id=tournament_id) + user = self.request.user if self.request.user.is_authenticated else None + customer_email = user.email if user else None + customer_name = f"{user.first_name} {user.last_name}".strip() if user and user.first_name else None + registration_type = 'direct' + if not team_registration_id: + registration_type = 'cart' + + print(f"[TOURNAMENT PAYMENT] Tournament: {tournament.name}") + print(f"[TOURNAMENT PAYMENT] Customer email: {customer_email}") + print(f"[TOURNAMENT PAYMENT] Customer name: {customer_name}") + print(f"[TOURNAMENT PAYMENT] Registration type: {registration_type}") currency_service = CurrencyService() # Check if payments are enabled for this tournament if not tournament.should_request_payment(): + print(f"[TOURNAMENT PAYMENT] Payments not enabled for tournament") raise Exception("Les paiements ne sont pas activés pour ce tournoi.") # Get currency code and validate it currency_code = tournament.currency_code or 'EUR' if not currency_service.validate_currency_code(currency_code): + print(f"[TOURNAMENT PAYMENT] Invalid currency code: {currency_code}") raise Exception(f"Devise non supportée: {currency_code}") + print(f"[TOURNAMENT PAYMENT] Currency code: {currency_code}") + # Convert amount to Stripe format stripe_amount = currency_service.convert_to_stripe_amount(team_fee, currency_code) + print(f"[TOURNAMENT PAYMENT] Stripe amount: {stripe_amount}") # Get user email if authenticated customer_email = self.request.user.email if self.request.user.is_authenticated else None # Determine the appropriate cancel URL based on the context - if team_registration_id: + if not team_registration_id and cart_manager: + team_registration = self._process_pre_registration_payment(cart_manager) + 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") + team_registration_id = team_registration.id + print(f"[TOURNAMENT PAYMENT] Created team registration: {team_registration_id}") + + if not team_registration_id: + print(f"[TOURNAMENT PAYMENT] No team registration ID available") + raise Exception("Erreur lors de la création de la réservation") + + # Determine the appropriate cancel URL based on the context + if registration_type == 'direct': # If we're paying for an existing registration, go back to tournament info cancel_url = self.request.build_absolute_uri( reverse('tournament-info', kwargs={'tournament_id': tournament_id}) ) else: # If we're in the registration process, go back to registration form + self.request.session['cancel_team_registration_id'] = str(team_registration_id) cancel_url = self.request.build_absolute_uri( - reverse('register_tournament', kwargs={'tournament_id': tournament_id}) + reverse('register_tournament', kwargs={ + 'tournament_id': tournament_id, + }) ) + print(f"[TOURNAMENT PAYMENT] Cancel URL: {cancel_url}") + base_metadata = { 'tournament_id': str(tournament_id), + 'team_registration_id': str(team_registration_id), + 'customer_name': customer_name or 'Non fourni', + 'customer_email': customer_email or 'Non fourni', 'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None, 'payment_source': 'tournament', # Identify payment source + 'registration_type': registration_type, 'source_page': 'tournament_info' if team_registration_id else 'register_tournament', 'currency_code': currency_code, # Store currency for later reference } + print(f"[TOURNAMENT PAYMENT] Base metadata: {base_metadata}") + if tournament.is_corporate_tournament: + print(f"[TOURNAMENT PAYMENT] Corporate tournament - using direct payment") # Corporate tournament metadata metadata = { **base_metadata, @@ -74,6 +123,7 @@ class PaymentService: 'stripe_account_type': 'direct' } else: + print(f"[TOURNAMENT PAYMENT] Regular tournament - using Stripe Connect") # Regular tournament metadata metadata = { **base_metadata, @@ -82,19 +132,7 @@ class PaymentService: 'stripe_account_id': tournament.stripe_account_id } - if cart_data: - metadata.update({ - 'registration_cart_id': str(cart_data['cart_id']), - 'registration_type': 'cart', - 'player_count': str(cart_data.get('player_count', 0)), - 'waiting_list_position': str(cart_data.get('waiting_list_position', -1)) - }) - elif team_registration_id: - metadata.update({ - 'team_registration_id': str(team_registration_id), - 'registration_type': 'direct' - }) - self.request.session['team_registration_id'] = str(team_registration_id) + self.request.session['team_registration_id'] = str(team_registration_id) metadata.update({ 'tournament_name': tournament.broadcast_display_name(), @@ -103,11 +141,14 @@ class PaymentService: 'tournament_fee': str(team_fee) }) + print(f"[TOURNAMENT PAYMENT] Final metadata: {metadata}") + # Common checkout session parameters if tournament.is_corporate_tournament: # Direct charge without transfers when umpire is platform owner checkout_session_params = { 'payment_method_types': ['card'], + 'expires_at': expires_at, 'line_items': [{ 'price_data': { 'currency': currency_code.lower(), # Use tournament currency @@ -126,28 +167,37 @@ class PaymentService: 'cancel_url': cancel_url, 'metadata': metadata } + print(f"[TOURNAMENT PAYMENT] Corporate checkout params: {checkout_session_params}") else: # Get the umpire's Stripe account ID stripe_account_id = tournament.stripe_account_id if not stripe_account_id: + print(f"[TOURNAMENT PAYMENT] No Stripe account ID for umpire") raise Exception("L'arbitre n'a pas configuré son compte Stripe.") + print(f"[TOURNAMENT PAYMENT] Umpire Stripe account: {stripe_account_id}") + # Calculate commission commission_rate = tournament.event.creator.effective_commission_rate() platform_amount = currency_service.convert_to_stripe_amount( team_fee * commission_rate, currency_code ) + print(f"[TOURNAMENT PAYMENT] Commission rate: {commission_rate}") + print(f"[TOURNAMENT PAYMENT] Platform amount: {platform_amount}") + + # Direct charge on connected account - this will appear in umpire's dashboard checkout_session_params = { 'payment_method_types': ['card'], + 'expires_at': expires_at, 'line_items': [{ 'price_data': { - 'currency': currency_code.lower(), # Use tournament currency + 'currency': 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, # Amount in proper currency format + 'unit_amount': stripe_amount, }, 'quantity': 1, }], @@ -157,133 +207,100 @@ class PaymentService: ), 'cancel_url': cancel_url, 'payment_intent_data': { - 'application_fee_amount': platform_amount, - 'transfer_data': { - 'destination': stripe_account_id, - }, + 'application_fee_amount': platform_amount, # Your commission + 'description': f'Inscription {tournament.broadcast_display_name()} du {tournament.formatted_start_date()}', + 'metadata': metadata # All metadata will be visible to umpire }, - 'metadata': metadata - } + 'metadata': metadata # Session metadata + } + print(f"[TOURNAMENT PAYMENT] Direct charge checkout params: {checkout_session_params}") # Add customer_email if available if customer_email: checkout_session_params['customer_email'] = customer_email + print(f"[TOURNAMENT PAYMENT] Added customer email to params") # Create the checkout session try: - checkout_session = stripe.checkout.Session.create(**checkout_session_params) + print(f"[TOURNAMENT PAYMENT] Creating Stripe checkout session...") + if tournament.is_corporate_tournament: + # Create on platform account + checkout_session = stripe.checkout.Session.create(**checkout_session_params) + else: + # Create on connected account (direct charge) + stripe_account_id = tournament.stripe_account_id + self.request.session['stripe_account_id'] = stripe_account_id if not tournament.is_corporate_tournament else None + checkout_session = stripe.checkout.Session.create( + **checkout_session_params, + stripe_account=stripe_account_id # This creates the session on the connected account + ) # Store checkout session ID and source page in session self.request.session['stripe_checkout_session_id'] = checkout_session.id self.request.session['payment_source_page'] = 'tournament_info' if team_registration_id else 'register_tournament' self.request.session.modified = True + print(f"[TOURNAMENT PAYMENT] Checkout session created successfully: {checkout_session.id}") + print(f"[TOURNAMENT PAYMENT] Stored session data - checkout_session_id: {checkout_session.id}") + return checkout_session except stripe.error.StripeError as e: + print(f"[TOURNAMENT PAYMENT] Stripe error: {str(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, checkout_session): """ Process a successful Stripe payment Returns a tuple (success, redirect_response) """ - print(f"Processing payment for tournament {tournament_id}") - tournament = get_object_or_404(Tournament, id=tournament_id) - - # Check if this is a payment for an existing team registration - team_registration_id = self.request.session.get('team_registration_id') - print(f"Team registration ID from session: {team_registration_id}") - - # Track payment statuses for debugging - payment_statuses = [] - if team_registration_id: - success = self._process_direct_payment(checkout_session) - payment_statuses.append(success) - print(f"Direct payment processing result: {success}") - else: - # This is a payment during registration process - success = self._process_registration_payment(tournament, checkout_session) - payment_statuses.append(success) - print(f"Registration payment processing result: {success}") + success = PaymentService.process_direct_payment(checkout_session) # Print combined payment status - print(f"Payment statuses: {payment_statuses}") - print(any(payment_statuses)) + print(f"process_direct_payment: {success}") # Clear checkout session ID if 'stripe_checkout_session_id' in self.request.session: del self.request.session['stripe_checkout_session_id'] - return any(payment_statuses) - - def _process_direct_payment(self, checkout_session): - """Process payment for an existing team registration""" - team_registration_id = self.request.session.get('team_registration_id') - if not team_registration_id: - print("No team registration ID found in session") - return False - - try: - print(f"Looking for team registration with ID: {team_registration_id}") - team_registration = TeamRegistration.objects.get(id=team_registration_id) - success = self._update_registration_payment_info( - team_registration, - checkout_session.payment_intent - ) - - # Clean up session - if 'team_registration_id' in self.request.session: - del self.request.session['team_registration_id'] + return success - if success: - TournamentEmailService.send_payment_confirmation(team_registration, checkout_session.payment_intent) - return success - except TeamRegistration.DoesNotExist: - print(f"Team registration not found with ID: {team_registration_id}") - return False - except Exception as e: - print(f"Error in _process_direct_payment: {str(e)}") - return False - - def _process_registration_payment(self, tournament, checkout_session): + def _process_pre_registration_payment(self, cart_manager): """Process payment made during registration""" - cart_manager = RegistrationCartManager(self.request) - cart_data = cart_manager.get_cart_data() - # Checkout and create registration - success, result = cart_manager.checkout() + success, result = cart_manager.checkout(confirmed=False) if not success: - return False + return None # Process payment for the new registration team_registration = result # result is team_registration object - self._update_registration_payment_info( - team_registration, - checkout_session.payment_intent - ) - - # Send confirmation email if appropriate - waiting_list_position = cart_data.get('waiting_list_position', -1) - if is_not_sqlite_backend(): - email_service = TournamentEmailService() - email_service.send_registration_confirmation( - self.request, - tournament, - team_registration, - waiting_list_position - ) - - return True - - def _update_registration_payment_info(self, team_registration, payment_intent_id): - """Update player registrations with payment information""" - team_registration.confirm_registration(payment_intent_id) - return True + team_registration.confirm_pre_registration() + # self._update_registration_payment_info( + # team_registration, + # checkout_session.payment_intent + # ) + + # # Send confirmation email if appropriate + # waiting_list_position = cart_data.get('waiting_list_position', -1) + # if is_not_sqlite_backend(): + # email_service = TournamentEmailService() + # email_service.send_registration_confirmation( + # self.request, + # tournament, + # team_registration, + # waiting_list_position + # ) + + return team_registration + + # def _update_registration_payment_info(self, team_registration, payment_intent_id): + # """Update player registrations with payment information""" + # team_registration.confirm_registration(payment_intent_id) + # return True def process_refund(self, team_registration_id, force_refund=False): """ @@ -314,25 +331,52 @@ class PaymentService: 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) + # Get the Stripe payment intent from the correct account + try: + if tournament.is_corporate_tournament: + # Corporate tournament - payment on platform account + payment_intent = stripe.PaymentIntent.retrieve(payment_id) + else: + # Regular tournament - payment on connected account + if not tournament.stripe_account_id: + return False, "Compte Stripe de l'arbitre non configuré.", None + + #legacy retrieval + try: + payment_intent = stripe.PaymentIntent.retrieve(payment_id) + except: + payment_intent = stripe.PaymentIntent.retrieve( + payment_id, + stripe_account=tournament.stripe_account_id + ) + except stripe.error.InvalidRequestError as e: + return False, f"Paiement introuvable: {str(e)}", None 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 - with different parameters based on tournament type - refund_params = { - 'payment_intent': payment_id - } - - # Only include transfer reversal for non-corporate tournaments - if not tournament.is_corporate_tournament: - refund_params.update({ - 'refund_application_fee': True, - 'reverse_transfer': True - }) + if tournament.is_corporate_tournament: + # Corporate tournament refund (simple refund) + refund = stripe.Refund.create( + payment_intent=payment_id + ) + else: + # Direct charge refund (on connected account) + # Check if there's an application fee to refund + refund_params = { + 'payment_intent': payment_id, + 'stripe_account': tournament.stripe_account_id + } + + # Only add refund_application_fee if there's actually an application fee + if hasattr(payment_intent, 'application_fee_amount') and payment_intent.application_fee_amount and payment_intent.application_fee_amount > 0: + refund_params['refund_application_fee'] = True + print(f"[REFUND] Refunding application fee of {payment_intent.application_fee_amount}") + else: + print(f"[REFUND] No application fee to refund") - refund = stripe.Refund.create(**refund_params) + refund = stripe.Refund.create(**refund_params) for player_reg in player_registrations: player_reg.payment_type = None @@ -349,6 +393,197 @@ class PaymentService: except Exception as e: return False, f"Erreur lors du remboursement: {str(e)}", None + + @staticmethod + def process_failed_payment_intent(payment_intent): + """Process a failed Payment Intent""" + if hasattr(payment_intent, 'metadata'): + metadata = payment_intent.metadata + elif isinstance(payment_intent, dict) and 'metadata' in payment_intent: + metadata = payment_intent['metadata'] + else: + print("[FAILED PAYMENT] No metadata found in payment intent") + return False + + print(f"[FAILED PAYMENT] Processing failed payment intent") + print(f"[FAILED PAYMENT] Metadata: {metadata}") + + tournament_id = metadata.get('tournament_id') + team_registration_id = metadata.get('team_registration_id') + registration_type = metadata.get('registration_type') + + print(f"[FAILED PAYMENT] Tournament ID: {tournament_id}") + print(f"[FAILED PAYMENT] Team Registration ID: {team_registration_id}") + print(f"[FAILED PAYMENT] Registration Type: {registration_type}") + + # Release reserved spot for cart registrations + if registration_type == 'cart' and tournament_id: + try: + tournament = Tournament.objects.get(id=tournament_id) + old_spots = tournament.reserved_spots + tournament.reserved_spots = max(0, tournament.reserved_spots - 1) + tournament.save() + print(f"[FAILED PAYMENT] Released reserved spot: {old_spots} → {tournament.reserved_spots}") + except Tournament.DoesNotExist: + print(f"[FAILED PAYMENT] Tournament {tournament_id} not found") + except Exception as e: + print(f"[FAILED PAYMENT] Error updating tournament reserved spots: {str(e)}") + + # Clean up unpaid team registration + if not team_registration_id: + print("[FAILED PAYMENT] No team registration ID found in metadata") + return False + + try: + print(f"[FAILED PAYMENT] Looking for team registration with ID: {team_registration_id}") + team_registration = TeamRegistration.objects.get(id=team_registration_id) + + if not team_registration.is_paid(): + team_registration.delete() + print(f"[FAILED PAYMENT] Deleted unpaid team registration {team_registration_id}") + else: + print(f"[FAILED PAYMENT] Team registration {team_registration_id} is already paid") + + return True + + except TeamRegistration.DoesNotExist: + print(f"[FAILED PAYMENT] Team registration {team_registration_id} not found") + return False + except Exception as e: + print(f"[FAILED PAYMENT] Error processing team registration: {str(e)}") + return False + + @staticmethod + def process_expired_checkout_session(checkout_session): + """Process an expired Checkout Session""" + if hasattr(checkout_session, 'metadata'): + metadata = checkout_session.metadata + elif isinstance(checkout_session, dict) and 'metadata' in checkout_session: + metadata = checkout_session['metadata'] + else: + print("[EXPIRED SESSION] No metadata found in checkout session") + return False + + print(f"[EXPIRED SESSION] Processing expired checkout session") + print(f"[EXPIRED SESSION] Metadata: {metadata}") + + tournament_id = metadata.get('tournament_id') + team_registration_id = metadata.get('team_registration_id') + registration_type = metadata.get('registration_type') + + print(f"[EXPIRED SESSION] Tournament ID: {tournament_id}") + print(f"[EXPIRED SESSION] Team Registration ID: {team_registration_id}") + print(f"[EXPIRED SESSION] Registration Type: {registration_type}") + + # Release reserved spot for cart registrations + if registration_type == 'cart' and tournament_id: + try: + tournament = Tournament.objects.get(id=tournament_id) + old_spots = tournament.reserved_spots + tournament.reserved_spots = max(0, tournament.reserved_spots - 1) + tournament.save() + print(f"[EXPIRED SESSION] Released reserved spot: {old_spots} → {tournament.reserved_spots}") + except Tournament.DoesNotExist: + print(f"[EXPIRED SESSION] Tournament {tournament_id} not found") + except Exception as e: + print(f"[EXPIRED SESSION] Error updating tournament reserved spots: {str(e)}") + + # Clean up unpaid team registration + if not team_registration_id: + print("[EXPIRED SESSION] No team registration ID found in metadata") + return False + + try: + print(f"[EXPIRED SESSION] Looking for team registration with ID: {team_registration_id}") + team_registration = TeamRegistration.objects.get(id=team_registration_id) + + if not team_registration.is_paid(): + team_registration.delete() + print(f"[EXPIRED SESSION] Deleted unpaid team registration {team_registration_id}") + else: + print(f"[EXPIRED SESSION] Team registration {team_registration_id} is already paid") + + return True + + except TeamRegistration.DoesNotExist: + print(f"[EXPIRED SESSION] Team registration {team_registration_id} not found") + return False + except Exception as e: + print(f"[EXPIRED SESSION] Error processing team registration: {str(e)}") + return False + + @staticmethod + def process_failed_or_expired_session(stripe_object): + """Legacy method for backward compatibility - delegates to specific handlers""" + # Try to determine object type and delegate + if hasattr(stripe_object, 'object'): + object_type = stripe_object.object + elif isinstance(stripe_object, dict) and 'object' in stripe_object: + object_type = stripe_object['object'] + else: + print("[LEGACY HANDLER] Cannot determine stripe object type") + return False + + print(f"[LEGACY HANDLER] Delegating {object_type} to specific handler") + + if object_type == 'payment_intent': + return PaymentService.process_failed_payment_intent(stripe_object) + elif object_type == 'checkout.session': + return PaymentService.process_expired_checkout_session(stripe_object) + else: + print(f"[LEGACY HANDLER] Unknown object type: {object_type}") + return False + + @staticmethod + def process_direct_payment(checkout_session): + """Process payment for an existing team registration""" + metadata = checkout_session.metadata + tournament_id = metadata.get('tournament_id') + team_registration_id = metadata.get('team_registration_id') + registration_type = metadata.get('registration_type') + + if tournament_id and registration_type == 'cart': + try: + tournament = Tournament.objects.get(id=tournament_id) + tournament.reserved_spots = max(0, tournament.reserved_spots - 1) + tournament.save() + print(f"Decreased reserved spots for tournament {tournament_id} after payment failure") + except Tournament.DoesNotExist: + print(f"Tournament not found with ID: {tournament_id}") + except Exception as e: + print(f"Error saving tournament for team registration: {str(e)}") + + if not team_registration_id: + print("No team registration ID found in session") + return False + + try: + print(f"Looking for team registration with ID: {team_registration_id}") + team_registration = TeamRegistration.objects.get(id=team_registration_id) + if tournament_id and registration_type == 'cart' and team_registration.tournament is None: + try: + tournament = Tournament.objects.get(id=tournament_id) + team_registration.tournament = tournament + team_registration.save() + print(f"Saved tournament for team registration {team_registration.id}") + except Tournament.DoesNotExist: + print(f"Tournament not found with ID: {tournament_id}") + except Exception as e: + print(f"Error saving tournament for team registration: {str(e)}") + + if team_registration.is_paid(): + return True + + team_registration.confirm_registration(checkout_session.payment_intent) + + TournamentEmailService.send_payment_confirmation(team_registration, checkout_session.payment_intent) + return True + except TeamRegistration.DoesNotExist: + print(f"Team registration not found with ID: {team_registration_id}") + except Exception as e: + print(f"Error in _process_direct_payment: {str(e)}") + return False + @staticmethod @csrf_exempt @require_POST @@ -364,45 +599,45 @@ class PaymentService: ) print(f"Tournament webhook event type: {event['type']}") - if event['type'] == 'checkout.session.completed': - session = event['data']['object'] - metadata = session.get('metadata', {}) - tournament_id = metadata.get('tournament_id') - - if not tournament_id: - print("No tournament_id in metadata") - return HttpResponse(status=400) + stripe_object = event['data']['object'] - payment_service = PaymentService(request) - success = payment_service.process_successful_payment(tournament_id, session) + # Debug: Print the object type + object_type = stripe_object.get('object', 'unknown') + print(f"Stripe object type: {object_type}") + if event['type'] == 'checkout.session.completed': + success = PaymentService.process_direct_payment(stripe_object) if success: - print(f"Successfully processed webhook payment for tournament {tournament_id}") + print(f"Successfully processed completed checkout session") return HttpResponse(status=200) else: - print(f"Failed to process webhook payment for tournament {tournament_id}") + print(f"Failed to process completed checkout session") return HttpResponse(status=400) elif event['type'] == 'payment_intent.payment_failed': - intent = event['data']['object'] - metadata = intent.get('metadata', {}) + success = PaymentService.process_failed_payment_intent(stripe_object) + if success: + print(f"Successfully processed failed payment intent") + return HttpResponse(status=200) + else: + print(f"Failed to process failed payment intent") + return HttpResponse(status=400) - tournament_id = metadata.get('tournament_id') - source_page = metadata.get('source_page') + elif event['type'] == 'checkout.session.expired': + success = PaymentService.process_expired_checkout_session(stripe_object) + if success: + print(f"Successfully processed expired checkout session") + return HttpResponse(status=200) + else: + print(f"Failed to process expired checkout session") + return HttpResponse(status=400) - if tournament_id and source_page == 'register_tournament': - try: - tournament = Tournament.objects.get(id=tournament_id) - # Decrease reserved spots, minimum 0 - tournament.reserved_spots = max(0, tournament.reserved_spots - 1) - tournament.save() - print(f"Decreased reserved spots for tournament {tournament_id} after payment failure") - except Tournament.DoesNotExist: - print(f"Tournament {tournament_id} not found") - except Exception as e: - print(f"Error updating tournament reserved spots: {str(e)}") - - return HttpResponse(status=200) + else: + print(f"Unhandled event type: {event['type']}") + return HttpResponse(status=200) except Exception as e: print(f"Tournament webhook error: {str(e)}") + import traceback + traceback.print_exc() + return HttpResponse(status=400) diff --git a/tournaments/services/tournament_registration.py b/tournaments/services/tournament_registration.py index c46da48..a1aea6f 100644 --- a/tournaments/services/tournament_registration.py +++ b/tournaments/services/tournament_registration.py @@ -357,8 +357,9 @@ class RegistrationCartManager: return True, "Informations de contact mises à jour." - def checkout(self): + def checkout(self, confirmed): """Convert cart to an actual tournament registration""" + print("Checkout") if self.is_cart_expired(): return False, "Votre session d'inscription a expiré, veuillez réessayer." @@ -399,8 +400,12 @@ class RegistrationCartManager: weight = sum(int(player_data.get('computed_rank', 0) or 0) for player_data in players) # Create team registration + if confirmed: + target_tournament = tournament + else: + target_tournament = None team_registration = TeamRegistration.objects.create( - tournament=tournament, + tournament=target_tournament, registration_date=timezone.now(), walk_out=False, weight=weight, @@ -421,15 +426,16 @@ class RegistrationCartManager: data_source = PlayerDataSource.FRENCH_FEDERATION # Now using the enum value User = get_user_model() - matching_user = self.request.user - if player_licence_id and (stripped_license is None or is_captain is False): - try: - # Using icontains for case-insensitive match - matching_user = User.objects.get(licence_id__icontains=player_licence_id) - if matching_user is None: - matching_user = self.request.user - except User.DoesNotExist: - pass + if is_captain: + matching_user = self.request.user + else: + matching_user = None + if player_licence_id and (stripped_license is None or is_captain is False): + try: + # Using icontains for case-insensitive match + matching_user = User.objects.get(licence_id__icontains=player_licence_id) + except User.DoesNotExist: + pass is_woman = player_data.get('is_woman') if is_woman is not None: @@ -486,8 +492,10 @@ class RegistrationCartManager: # Clear the cart self.clear_cart() - tournament.reserved_spots = max(0, tournament.reserved_spots - 1) - tournament.save() + + if confirmed: + tournament.reserved_spots = max(0, tournament.reserved_spots - 1) + tournament.save() return True, team_registration diff --git a/tournaments/views.py b/tournaments/views.py index d8267ac..c8d9bb7 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -1385,12 +1385,17 @@ def tournament_payment_success(request, tournament_id): try: # Verify payment status with Stripe stripe.api_key = settings.STRIPE_SECRET_KEY - checkout_session = stripe.checkout.Session.retrieve(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) if checkout_session.payment_status == 'paid': # Process the payment success payment_service = PaymentService(request) - success = payment_service.process_successful_payment(str(tournament_id), checkout_session) + success = payment_service.process_successful_payment(checkout_session) if success: # Set a flag for successful registration if the payment was from registration page @@ -1413,7 +1418,7 @@ def tournament_payment_success(request, tournament_id): source_page = request.session.get('payment_source_page', 'tournament_info') # Clean up session variables - for key in ['stripe_checkout_session_id', 'team_registration_id', 'payment_source_page']: + 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] @@ -1429,11 +1434,51 @@ def tournament_payment_success(request, tournament_id): @csrf_protect def register_tournament(request, tournament_id): tournament = get_object_or_404(Tournament, id=tournament_id) + # Print full session content for debugging + print("=" * 60) + print(f"[SESSION DEBUG] register_tournament called for tournament {tournament_id}") + print(f"[SESSION DEBUG] Request method: {request.method}") + print(f"[SESSION DEBUG] Session key: {request.session.session_key}") + print("[SESSION DEBUG] Full session contents:") + for key, value in request.session.items(): + print(f" {key}: {value}") + print("=" * 60) # Check for registration_successful flag registration_successful = request.session.pop('registration_successful', False) registration_paid = request.session.pop('registration_paid', False) + # 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: + print(f"[PAYMENT CANCEL] Handling cancelled team registration: {cancel_team_registration_id}") + try: + # Get the team registration that was created but payment was cancelled + team_registration = TeamRegistration.objects.get(id=cancel_team_registration_id) + + # Release the reserved spot if this was a cart registration + if tournament.reserved_spots > 0: + tournament.reserved_spots -= 1 + tournament.save() + print(f"[PAYMENT CANCEL] Released reserved spot for tournament {tournament_id}") + + # Delete the unpaid team registration if it's not paid + if not team_registration.is_paid(): + team_registration.delete() + print(f"[PAYMENT CANCEL] Deleted unpaid team registration {cancel_team_registration_id}") + + except TeamRegistration.DoesNotExist: + print(f"[PAYMENT CANCEL] Team registration {cancel_team_registration_id} not found") + except Exception as e: + print(f"[PAYMENT CANCEL] Error handling cancellation: {str(e)}") + + # Clean up any other payment-related session data + for key in ['stripe_checkout_session_id', 'team_registration_id', 'cancel_team_registration_id', 'stripe_account_id']: + request.session.pop(key, None) + + # Add a user message about the cancellation + messages.warning(request, "Paiement annulé. Vous pouvez relancer votre inscription ci-dessous.") + # If registration was successful, render success page immediately if registration_successful: storage = messages.get_messages(request) @@ -1470,27 +1515,21 @@ 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': - # 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 + print("Initializing cart") + storage = messages.get_messages(request) + for _ in storage: pass - else: - - print("Initializing cart") - storage = messages.get_messages(request) - for _ in storage: - pass - if len(storage._loaded_messages) == 1: - del storage._loaded_messages[0] + if len(storage._loaded_messages) == 1: + del storage._loaded_messages[0] - # ALWAYS initialize a fresh cart when entering the registration page (GET request) - # This ensures no old cart data persists - cart_manager.initialize_cart(tournament_id) + # ALWAYS initialize a fresh cart when entering the registration page (GET request) + # This ensures no old cart data persists + cart_manager.initialize_cart(tournament_id) - # Auto-add the authenticated user with license - if request.user.is_authenticated and request.user.licence_id: - cart_manager.add_authenticated_user() + # Auto-add the authenticated user with license + if request.user.is_authenticated and request.user.licence_id: + cart_manager.add_authenticated_user() else: # For POST, ensure tournament ID is correct current_tournament_id = cart_manager.get_tournament_id() @@ -1532,7 +1571,7 @@ def register_tournament(request, tournament_id): elif 'register_team' in request.POST: handle_register_team_request(request, tournament, cart_manager, context) elif 'proceed_to_payment' in request.POST: - result = handle_payment_request(request, tournament, cart_manager, context, tournament_id) + result = handle_payment_request(request, cart_manager, context, tournament_id) if result: return result # This is the redirect to Stripe checkout @@ -1659,7 +1698,7 @@ def handle_register_team_request(request, tournament, cart_manager, context): print_session_debug(request, "SESSION DUMP BEFORE CHECKOUT") # Checkout and create registration - success, result = cart_manager.checkout() + success, result = cart_manager.checkout(confirmed=True) if success: waiting_list_position = cart_data.get('waiting_list_position', -1) if is_not_sqlite_backend(): @@ -1683,7 +1722,7 @@ def handle_register_team_request(request, tournament, cart_manager, context): messages.error(request, f"{field}: {error}") context['team_form'] = team_form -def handle_payment_request(request, tournament, cart_manager, context, tournament_id): +def handle_payment_request(request, cart_manager, context, tournament_id): """Handle the 'proceed_to_payment' POST action""" team_form = TournamentRegistrationForm(request.POST) if team_form.is_valid(): @@ -1694,15 +1733,12 @@ def handle_payment_request(request, tournament, cart_manager, context, tournamen # Create payment session try: - # Get cart data for payment metadata - cart_data = cart_manager.get_cart_data() - # Create and redirect to payment session payment_service = PaymentService(request) checkout_session = payment_service.create_checkout_session( tournament_id=tournament_id, team_fee=cart_manager.team_fee_from_cart_players(), # Use the appropriate fee field - cart_data=cart_data + cart_manager=cart_manager ) # Redirect to Stripe checkout