diff --git a/api/urls.py b/api/urls.py index 5e9d5ff..56164f5 100644 --- a/api/urls.py +++ b/api/urls.py @@ -63,5 +63,7 @@ urlpatterns = [ path('dj-rest-auth/', include('dj_rest_auth.urls')), path('stripe/create-account/', views.create_stripe_connect_account, name='create_stripe_account'), path('stripe/create-account-link/', views.create_stripe_account_link, name='create_account_link'), + path('payment-link/create//', views.create_payment_link, name='create-payment-link'), + path('payment-link/team//', views.get_payment_link, name='get-payment-link'), ] diff --git a/api/views.py b/api/views.py index e21a8d8..e197fa7 100644 --- a/api/views.py +++ b/api/views.py @@ -619,6 +619,113 @@ def validate_stripe_account(request): 'needs_onboarding': True, }, status=200) +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def create_payment_link(request, team_registration_id): + """ + Create a Stripe Payment Link for a team registration + """ + try: + # Verify team registration exists and user has permission + team_registration = TeamRegistration.objects.get(id=team_registration_id) + tournament = team_registration.tournament + + # Check if payment is required and team hasn't paid + if tournament.is_free() or team_registration.get_payment_status() == 'PAID': + return Response( + {'error': 'Payment not required or already completed'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Create payment link + payment_link_url = PaymentService.create_payment_link(team_registration_id) + + if payment_link_url: + return Response({ + 'success': True, + 'payment_link': payment_link_url, + 'team_registration_id': str(team_registration_id), + 'tournament_name': tournament.display_name(), + 'amount': team_registration.get_team_registration_fee(), + 'currency': tournament.currency_code + }) + else: + return Response( + {'error': 'Failed to create payment link'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + except TeamRegistration.DoesNotExist: + return Response( + {'error': 'Team registration not found'}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response( + {'error': f'Unexpected error: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_payment_link(request, team_registration_id): + """ + Get or create a payment link for a specific team registration + """ + try: + team_registration = TeamRegistration.objects.get( + id=team_registration_id + ) + + tournament = team_registration.tournament + + # Check payment status + payment_status = team_registration.get_payment_status() + + if payment_status == 'PAID': + return Response({ + 'success': False, + 'message': 'Payment already completed', + 'payment_status': 'PAID' + }) + + if tournament.is_free(): + return Response({ + 'success': False, + 'message': 'Tournament is free', + 'payment_status': 'NOT_REQUIRED' + }) + + # Get or create payment link + payment_link_url = PaymentService.get_or_create_payment_link(team_registration_id) + + if payment_link_url: + return Response({ + 'success': True, + 'payment_link': payment_link_url, + 'team_registration_id': str(team_registration_id), + 'tournament_name': tournament.display_name(), + 'amount': team_registration.get_team_registration_fee(), + 'currency': tournament.currency_code, + 'payment_status': payment_status + }) + else: + return Response( + {'error': 'Failed to create payment link'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + except (Tournament.DoesNotExist, TeamRegistration.DoesNotExist): + return Response( + {'error': 'Tournament or team registration not found'}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response( + {'error': f'Unexpected error: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + @api_view(['GET']) @permission_classes([IsAuthenticated]) def is_granted_unlimited_access(request): diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index ec85950..5e96228 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -96,6 +96,8 @@ class Tournament(BaseModel): club_member_fee_deduction = models.FloatField(null=True, blank=True) unregister_delta_in_hours = models.IntegerField(default=24) currency_code = models.CharField(null=True, blank=True, max_length=3, default='EUR') + parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, related_name='children') + loser_index = models.IntegerField(default=0) def delete_dependencies(self): for team_registration in self.team_registrations.all(): diff --git a/tournaments/services/email_service.py b/tournaments/services/email_service.py index 206285a..9b54041 100644 --- a/tournaments/services/email_service.py +++ b/tournaments/services/email_service.py @@ -681,12 +681,38 @@ class TournamentEmailService: # For unpaid teams, add payment instructions formatted_fee = currency_service.format_amount(tournament.entry_fee, tournament.currency_code) - payment_info = [ - "\n\n⚠️ Paiement des frais d'inscription requis", - f"Les frais d'inscription de {formatted_fee} par joueur doivent être payés pour confirmer votre participation.", - "Vous pouvez effectuer le paiement en vous connectant à votre compte Padel Club.", - f"Lien pour payer: https://padelclub.app/tournament/{tournament.id}/info" - ] + + print("team_registration.user", team_registration.user) + # Check if team has a user account attached + if team_registration.user: + # User has account - direct to login and pay + payment_info = [ + "\n\n⚠️ Paiement des frais d'inscription requis", + f"Les frais d'inscription de {formatted_fee} par joueur doivent être payés pour confirmer votre participation.", + "Vous pouvez effectuer le paiement en vous connectant à votre compte Padel Club.", + f"Lien pour payer: https://padelclub.app/tournament/{tournament.id}/info" + ] + else: + # No user account - create payment link + from .payment_service import PaymentService + payment_link = PaymentService.create_payment_link(team_registration.id) + + if payment_link: + payment_info = [ + "\n\n⚠️ Paiement des frais d'inscription requis", + f"Les frais d'inscription de {formatted_fee} par joueur doivent être payés pour confirmer votre participation.", + "Vous pouvez effectuer le paiement directement via ce lien sécurisé :", + f"💳 Payer maintenant: {payment_link}", + "\nAucun compte n'est requis pour effectuer le paiement." + ] + else: + # Fallback if payment link creation fails + payment_info = [ + "\n\n⚠️ Paiement des frais d'inscription requis", + f"Les frais d'inscription de {formatted_fee} par joueur doivent être payés pour confirmer votre participation.", + "Veuillez contacter l'organisateur du tournoi pour effectuer le paiement.", + f"Informations du tournoi: https://padelclub.app/tournament/{tournament.id}/info" + ] return "\n".join(payment_info) diff --git a/tournaments/services/payment_service.py b/tournaments/services/payment_service.py index d962dfe..c29f8d9 100644 --- a/tournaments/services/payment_service.py +++ b/tournaments/services/payment_service.py @@ -643,3 +643,91 @@ class PaymentService: import traceback traceback.print_exc() return HttpResponse(status=400) + + @staticmethod + def create_payment_link(team_registration_id): + """ + Create a Stripe Payment Link for a team registration + Returns the payment link URL or None if failed + """ + try: + team_registration = TeamRegistration.objects.get(id=team_registration_id) + tournament = team_registration.tournament + + if not tournament or tournament.is_free(): + return None + + stripe.api_key = settings.STRIPE_SECRET_KEY + currency_service = CurrencyService() + + # Calculate the team fee + team_fee = team_registration.get_team_registration_fee() + stripe_amount = currency_service.convert_to_stripe_amount(team_fee, tournament.currency_code) + + customer_email = team_registration.team_contact() + + # Create payment link + payment_link_params = { + 'line_items': [{ + 'price_data': { + 'currency': tournament.currency_code.lower(), + 'product_data': { + 'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}', + 'description': f'Lieu {tournament.event.club.name}', + }, + 'unit_amount': stripe_amount, + }, + 'quantity': 1, + }], + 'metadata': { + 'tournament_id': str(tournament.id), + 'team_registration_id': str(team_registration.id), + 'customer_email': customer_email or 'Non fourni', + 'payment_source': 'payment_link', + 'registration_type': 'direct', + }, + 'after_completion': { + 'type': 'redirect', + 'redirect': { + 'url': f'https://padelclub.app/stripe/payment_complete/?tournament_id={tournament.id}&team_registration_id={team_registration.id}&payment=success' + } + }, + 'automatic_tax': {'enabled': False}, + 'billing_address_collection': 'auto', + 'shipping_address_collection': None, + 'cancel_url': f'https://padelclub.app/stripe/payment_complete/?tournament_id={tournament.id}&team_registration_id={team_registration.id}&payment=cancel', + } + + # Add customer email if available + if customer_email: + payment_link_params['customer_creation'] = 'if_required' + # Note: Stripe Payment Links don't support customer_email parameter + # but will ask for email during checkout + + # Handle Stripe Connect account if needed + stripe_account_id = tournament.event.creator.stripe_account_id if hasattr(tournament.event.creator, 'stripe_account_id') else None + + if stripe_account_id: + payment_link = stripe.PaymentLink.create( + **payment_link_params, + stripe_account=stripe_account_id + ) + else: + payment_link = stripe.PaymentLink.create(**payment_link_params) + + print(f"[PAYMENT LINK] Created payment link: {payment_link.url}") + return payment_link.url + + except Exception as e: + print(f"[PAYMENT LINK] Error creating payment link: {str(e)}") + return None + + @staticmethod + def get_or_create_payment_link(team_registration_id): + """ + Get existing payment link or create a new one for a team registration + This method can be used to avoid creating multiple links for the same registration + """ + # In a real implementation, you might want to store payment links in the database + # and check if one already exists and is still valid + return PaymentService.create_payment_link(team_registration_id) diff --git a/tournaments/templates/stripe/payment_complete.html b/tournaments/templates/stripe/payment_complete.html new file mode 100644 index 0000000..c0f1db7 --- /dev/null +++ b/tournaments/templates/stripe/payment_complete.html @@ -0,0 +1,30 @@ +{% extends 'tournaments/base.html' %} +{% block head_title %} Paiement {% endblock %} +{% block first_title %} Padel Club {% endblock %} +{% block second_title %} Paiement {% endblock %} + +{% block content %} +{% load static %} +{% load tz %} + +
+
+
+ {% if payment_status == 'success' %} + +

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

+ {% if show_details and tournament %} +

Tournoi : {{ tournament.display_name }}

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

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

+ {% else %} + +

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

+ {% endif %} +

Vous pouvez maintenant fermer cette page.

+
+
+
+{% endblock %} diff --git a/tournaments/urls.py b/tournaments/urls.py index 255845a..b9fd316 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -79,6 +79,7 @@ urlpatterns = [ path('activation-success/', views.activation_success, name='activation_success'), path('activation-failed/', views.activation_failed, name='activation_failed'), path('tournaments//confirm/', views.confirm_tournament_registration, name='confirm_tournament_registration'), + path('stripe/payment_complete/', views.stripe_payment_complete, name='stripe-payment-complete'), path('stripe-onboarding-complete/', views.stripe_onboarding_complete, name='stripe-onboarding-complete'), path('stripe-refresh-account-link/', views.stripe_refresh_account_link, name='stripe-refresh-account-link'), path('tournaments//toggle-private/', views.toggle_tournament_private, name='toggle_tournament_private'), diff --git a/tournaments/views.py b/tournaments/views.py index b64e6f8..3adce17 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -2304,6 +2304,39 @@ def tournament_live_matches(request, tournament_id): 'live_matches': live_matches, }) +def stripe_payment_complete(request): + """Handle payment complete page for Stripe Payment Links""" + tournament_id = request.GET.get('tournament_id') + team_registration_id = request.GET.get('team_registration_id') + payment_status = request.GET.get('payment', 'unknown') + + context = { + 'payment_status': payment_status, + 'tournament': None, + 'team_registration': None, + 'players': [], + 'show_details': False + } + + # Try to get tournament and team registration details + if tournament_id and team_registration_id: + try: + tournament = Tournament.objects.get(id=tournament_id) + team_registration = TeamRegistration.objects.get(id=team_registration_id) + + context.update({ + 'tournament': tournament, + 'team_registration': team_registration, + 'players': team_registration.players_sorted_by_captain, + 'show_details': True, + 'amount_paid': team_registration.get_team_registration_fee(), + 'currency': tournament.currency_code + }) + + except (Tournament.DoesNotExist, TeamRegistration.DoesNotExist): + print(f"Tournament or team registration not found: {tournament_id}, {team_registration_id}") + + return render(request, 'stripe/payment_complete.html', context) class UserListExportView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs):