|
|
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
|
|
|
from django.db import transaction
|
|
|
import stripe
|
|
|
from datetime import datetime, timedelta
|
|
|
import traceback
|
|
|
|
|
|
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")
|
|
|
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}")
|
|
|
|
|
|
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 True
|
|
|
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')
|
|
|
|
|
|
# Wrap all database operations in an atomic transaction
|
|
|
# This ensures either all changes are saved or none are
|
|
|
try:
|
|
|
with transaction.atomic():
|
|
|
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}")
|
|
|
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
|
|
|
|
|
|
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():
|
|
|
print(f"Team registration {team_registration.id} is already paid")
|
|
|
return True
|
|
|
|
|
|
# Update player registration with payment info
|
|
|
team_registration.confirm_registration(checkout_session.payment_intent)
|
|
|
print(f"✅ Registration confirmed and committed to database")
|
|
|
|
|
|
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 database operations: {str(e)}")
|
|
|
traceback.print_exc()
|
|
|
return False
|
|
|
|
|
|
# After successful database commit, send confirmation email
|
|
|
# Email failures won't affect the payment confirmation
|
|
|
try:
|
|
|
print(f"Sending payment confirmation email...")
|
|
|
TournamentEmailService.send_payment_confirmation(team_registration, checkout_session.payment_intent)
|
|
|
print(f"✅ Email sent successfully")
|
|
|
except Exception as email_error:
|
|
|
print(f"⚠️ Warning: Email sending failed but payment was confirmed: {str(email_error)}")
|
|
|
traceback.print_exc()
|
|
|
# Don't return False - payment is still confirmed
|
|
|
|
|
|
return True
|
|
|
|
|
|
@staticmethod
|
|
|
@csrf_exempt
|
|
|
@require_POST
|
|
|
def stripe_webhook(request):
|
|
|
payload = request.body
|
|
|
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
|
|
|
|
|
|
# Check if this is a Connect account webhook (header method - for direct API calls)
|
|
|
stripe_account_header = request.META.get('HTTP_STRIPE_ACCOUNT')
|
|
|
|
|
|
print("=== WEBHOOK DEBUG ===")
|
|
|
print(f"Signature: {sig_header}")
|
|
|
print(f"Connect Account Header: {stripe_account_header}")
|
|
|
|
|
|
# First, try to construct the event with any available webhook secret to inspect the payload
|
|
|
webhook_secrets = []
|
|
|
if hasattr(settings, 'XLR_STRIPE_WEBHOOK_SECRET') and settings.XLR_STRIPE_WEBHOOK_SECRET:
|
|
|
webhook_secrets.append(('XLR', settings.XLR_STRIPE_WEBHOOK_SECRET))
|
|
|
if hasattr(settings, 'TOURNAMENT_STRIPE_WEBHOOK_SECRET') and settings.TOURNAMENT_STRIPE_WEBHOOK_SECRET:
|
|
|
webhook_secrets.append(('TOURNAMENT', settings.TOURNAMENT_STRIPE_WEBHOOK_SECRET))
|
|
|
if hasattr(settings, 'SHOP_STRIPE_WEBHOOK_SECRET') and settings.SHOP_STRIPE_WEBHOOK_SECRET:
|
|
|
webhook_secrets.append(('SHOP', settings.SHOP_STRIPE_WEBHOOK_SECRET))
|
|
|
|
|
|
print(f"Available webhook secrets: {[name for name, _ in webhook_secrets]}")
|
|
|
|
|
|
event = None
|
|
|
used_secret = None
|
|
|
|
|
|
# Try to verify with each secret to get the event payload
|
|
|
for secret_name, secret_value in webhook_secrets:
|
|
|
try:
|
|
|
print(f"Trying {secret_name} webhook secret...")
|
|
|
event = stripe.Webhook.construct_event(payload, sig_header, secret_value)
|
|
|
used_secret = secret_name
|
|
|
print(f"SUCCESS: Webhook verified with {secret_name} secret")
|
|
|
break
|
|
|
except stripe.error.SignatureVerificationError as e:
|
|
|
print(f"Failed with {secret_name} secret: {str(e)}")
|
|
|
continue
|
|
|
|
|
|
if not event:
|
|
|
print("ERROR: No webhook secret worked")
|
|
|
return HttpResponse("Webhook signature verification failed", status=400)
|
|
|
|
|
|
# Now check if this is a Connect webhook by looking at the payload
|
|
|
connect_account_id = event.get('account') # This is how Connect webhooks are identified
|
|
|
print(f"Connect Account ID from payload: {connect_account_id}")
|
|
|
|
|
|
# Log webhook details
|
|
|
print(f"Event ID: {event.get('id')}")
|
|
|
print(f"Event Type: {event.get('type')}")
|
|
|
print(f"Live Mode: {event.get('livemode')}")
|
|
|
|
|
|
# Determine if this should have used a different webhook secret based on the account
|
|
|
if connect_account_id:
|
|
|
print(f"This is a Connect webhook from account: {connect_account_id}")
|
|
|
# Check if the account matches the expected tournament account
|
|
|
if connect_account_id == "acct_1S0jbSAs9xuFLROy":
|
|
|
print("This matches the expected tournament Connect account")
|
|
|
if used_secret != 'TOURNAMENT':
|
|
|
print(f"WARNING: Used {used_secret} secret but should probably use TOURNAMENT secret")
|
|
|
else:
|
|
|
print(f"Unknown Connect account: {connect_account_id}")
|
|
|
else:
|
|
|
print("This is a platform/direct webhook (no Connect account)")
|
|
|
if used_secret != 'XLR':
|
|
|
print(f"WARNING: Used {used_secret} secret but should probably use XLR secret")
|
|
|
|
|
|
try:
|
|
|
# Process the webhook event
|
|
|
stripe_object = event['data']['object']
|
|
|
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')}")
|
|
|
print(f"stripe_account_type: {metadata.get('stripe_account_type', 'unknown')}")
|
|
|
print(f"stripe_account_id: {metadata.get('stripe_account_id', 'unknown')}")
|
|
|
|
|
|
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)
|
|
|
|
|
|
# Handle other event types if needed
|
|
|
elif event['type'] == 'payment_intent.succeeded':
|
|
|
print(f"Payment intent succeeded - you might want to handle this")
|
|
|
return HttpResponse(status=200)
|
|
|
|
|
|
else:
|
|
|
print(f"Unhandled event type: {event['type']}")
|
|
|
return HttpResponse(status=200)
|
|
|
|
|
|
except Exception as e:
|
|
|
print(f"Error processing webhook: {str(e)}")
|
|
|
import traceback
|
|
|
traceback.print_exc()
|
|
|
return HttpResponse("Webhook processing failed", status=500)
|
|
|
|
|
|
@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()
|
|
|
currency_code = tournament.currency_code or 'EUR'
|
|
|
|
|
|
print(f"[PAYMENT LINK] Tournament: {tournament.display_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 params
|
|
|
payment_link_params = {
|
|
|
'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,
|
|
|
}],
|
|
|
'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',
|
|
|
}
|
|
|
|
|
|
# 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
|
|
|
|
|
|
# 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
|
|
|
|
|
|
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
|
|
|
)
|
|
|
|
|
|
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
|
|
|
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)
|
|
|
|