Add Stripe payment links for tournament registration

mailing
Razmig Sarkissian 1 month ago
parent 4fbfce8393
commit a5c9765366
  1. 2
      api/urls.py
  2. 107
      api/views.py
  3. 2
      tournaments/models/tournament.py
  4. 26
      tournaments/services/email_service.py
  5. 88
      tournaments/services/payment_service.py
  6. 30
      tournaments/templates/stripe/payment_complete.html
  7. 1
      tournaments/urls.py
  8. 33
      tournaments/views.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/<str:team_registration_id>/', views.create_payment_link, name='create-payment-link'),
path('payment-link/team/<str:team_registration_id>/', views.get_payment_link, name='get-payment-link'),
]

@ -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):

@ -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():

@ -681,12 +681,38 @@ class TournamentEmailService:
# For unpaid teams, add payment instructions
formatted_fee = currency_service.format_amount(tournament.entry_fee, tournament.currency_code)
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)

@ -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)

@ -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 %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 padding10">
{% if payment_status == 'success' %}
<label class="title">Paiement réussi !</label>
<p>Votre inscription a été confirmée. Un email de confirmation vous a été envoyé.</p>
{% if show_details and tournament %}
<p>Tournoi : <strong>{{ tournament.display_name }}</strong></p>
{% endif %}
{% elif payment_status == 'cancel' %}
<label class="title">Paiement annulé</label>
<p>Votre paiement a été annulé. Aucun montant n'a été prélevé.</p>
{% else %}
<label class="title">Paiement en cours de traitement</label>
<p>Votre paiement est en cours de traitement. Vous recevrez un email de confirmation sous peu.</p>
{% endif %}
<p>Vous pouvez maintenant fermer cette page.</p>
</div>
</div>
</div>
{% endblock %}

@ -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/<str:tournament_id>/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/<str:tournament_id>/toggle-private/', views.toggle_tournament_private, name='toggle_tournament_private'),

@ -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):

Loading…
Cancel
Save