from django.shortcuts import get_object_or_404 from django.conf import settings from django.urls import reverse 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 tournaments.services.currency_service import CurrencyService class PaymentService: """ Service for handling payment processing for tournament registrations """ def __init__(self, request): self.request = request self.stripe_api_key = settings.STRIPE_SECRET_KEY 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 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, }) ) 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, 'is_corporate_tournament': 'true', 'stripe_account_type': 'direct' } else: print(f"[TOURNAMENT PAYMENT] Regular tournament - using Stripe Connect") # Regular tournament metadata metadata = { **base_metadata, 'is_corporate_tournament': 'false', 'stripe_account_type': 'connect', 'stripe_account_id': tournament.stripe_account_id } self.request.session['team_registration_id'] = str(team_registration_id) metadata.update({ 'tournament_name': tournament.broadcast_display_name(), 'tournament_date': tournament.formatted_start_date(), 'tournament_club': tournament.event.club.name, '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 '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 }, 'quantity': 1, }], 'mode': 'payment', 'success_url': self.request.build_absolute_uri( reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id}) ), '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(), '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, }], 'mode': 'payment', 'success_url': self.request.build_absolute_uri( reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id}) ), 'cancel_url': cancel_url, 'payment_intent_data': { '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 # 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: 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, checkout_session): """ Process a successful Stripe payment Returns a tuple (success, redirect_response) """ success = PaymentService.process_direct_payment(checkout_session) # Print combined payment status 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 success def _process_pre_registration_payment(self, cart_manager): """Process payment made during registration""" # Checkout and create registration success, result = cart_manager.checkout(confirmed=False) if not success: return None # Process payment for the new registration team_registration = result # result is team_registration object 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): """ Process a refund for a tournament registration as part of unregistration Returns a tuple (success, message) """ stripe.api_key = self.stripe_api_key try: # Get the team registration team_registration = get_object_or_404(TeamRegistration, id=team_registration_id) tournament = team_registration.tournament # Check if refund is possible for this tournament if not tournament.is_refund_possible() and force_refund == False: return False, "Les remboursements ne sont plus possibles pour ce tournoi.", None # Get payment ID from player registrations player_registrations = PlayerRegistration.objects.filter(team_registration=team_registration) payment_id = None for player_reg in player_registrations: # Find the first valid payment ID if player_reg.payment_id and player_reg.payment_type == PlayerPaymentType.CREDIT_CARD: payment_id = player_reg.payment_id break if not payment_id: return False, "Aucun paiement trouvé pour cette équipe.", None # Get the Stripe payment intent 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 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) for player_reg in player_registrations: player_reg.payment_type = None player_reg.payment_id = None player_reg.save() TournamentEmailService.send_refund_confirmation(tournament, team_registration, refund) # Return success with refund object return True, "L'inscription a été remboursée automatiquement.", refund except stripe.error.StripeError as e: return False, f"Erreur de remboursement Stripe: {str(e)}", None except Exception as e: return False, f"Erreur lors du remboursement: {str(e)}", None @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 def stripe_webhook(request): payload = request.body sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') print("Received webhook call") print(f"Signature: {sig_header}") try: event = stripe.Webhook.construct_event( payload, sig_header, settings.TOURNAMENT_STRIPE_WEBHOOK_SECRET ) print(f"Tournament webhook event type: {event['type']}") stripe_object = event['data']['object'] # 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 completed checkout session") return HttpResponse(status=200) else: print(f"Failed to process completed checkout session") return HttpResponse(status=400) elif event['type'] == 'payment_intent.payment_failed': 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) 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) 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)