add registration webhook

timetoconfirm
Raz 7 months ago
parent e019cea984
commit 2908519a84
  1. 3
      padelclub_backend/settings_local.py.dist
  2. 2
      shop/stripe_utils.py
  3. 130
      tournaments/services/payment_service.py
  4. 3
      tournaments/urls.py
  5. 2
      tournaments/utils/apns.py
  6. 42
      tournaments/views.py

@ -40,4 +40,5 @@ DATABASES = {
STRIPE_MODE = 'test' STRIPE_MODE = 'test'
STRIPE_PUBLISHABLE_KEY = '' STRIPE_PUBLISHABLE_KEY = ''
STRIPE_SECRET_KEY = '' STRIPE_SECRET_KEY = ''
STRIPE_WEBHOOK_SECRET = '' SHOP_STRIPE_WEBHOOK_SECRET = 'whsec_...' # Your existing webhook secret
TOURNAMENT_STRIPE_WEBHOOK_SECRET = 'whsec_...' # New webhook secret for tournaments

@ -15,7 +15,7 @@ class StripeService:
# Get appropriate keys based on mode # Get appropriate keys based on mode
self.api_key = settings.STRIPE_SECRET_KEY self.api_key = settings.STRIPE_SECRET_KEY
self.publishable_key = settings.STRIPE_PUBLISHABLE_KEY self.publishable_key = settings.STRIPE_PUBLISHABLE_KEY
self.webhook_secret = settings.STRIPE_WEBHOOK_SECRET self.webhook_secret = settings.SHOP_STRIPE_WEBHOOK_SECRET
self.currency = getattr(settings, 'STRIPE_CURRENCY', 'eur') self.currency = getattr(settings, 'STRIPE_CURRENCY', 'eur')
# Configure Stripe library # Configure Stripe library

@ -1,6 +1,9 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.conf import settings from django.conf import settings
from django.urls import reverse 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 import stripe
from ..models import TeamRegistration, PlayerRegistration, Tournament from ..models import TeamRegistration, PlayerRegistration, Tournament
@ -44,6 +47,50 @@ class PaymentService:
reverse('register_tournament', kwargs={'tournament_id': tournament_id}) reverse('register_tournament', kwargs={'tournament_id': tournament_id})
) )
base_metadata = {
'tournament_id': str(tournament_id),
'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None,
'payment_source': 'tournament', # Identify payment source
'source_page': 'tournament_info' if team_registration_id else 'register_tournament',
}
if tournament.is_corporate_tournament:
# Corporate tournament metadata
metadata = {
**base_metadata,
'is_corporate_tournament': 'true',
'stripe_account_type': 'direct'
}
else:
# Regular tournament metadata
metadata = {
**base_metadata,
'is_corporate_tournament': 'false',
'stripe_account_type': 'connect',
'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)
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)
})
# Common checkout session parameters # Common checkout session parameters
if tournament.is_corporate_tournament: if tournament.is_corporate_tournament:
# Direct charge without transfers when umpire is platform owner # Direct charge without transfers when umpire is platform owner
@ -65,12 +112,7 @@ class PaymentService:
reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id}) reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id})
), ),
'cancel_url': cancel_url, 'cancel_url': cancel_url,
'metadata': { 'metadata': metadata
'tournament_id': str(tournament_id),
'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None,
'source_page': 'tournament_info' if team_registration_id else 'register_tournament',
'is_corporate_tournament': 'true'
}
} }
else: else:
# Get the umpire's Stripe account ID # Get the umpire's Stripe account ID
@ -106,19 +148,15 @@ class PaymentService:
'destination': stripe_account_id, 'destination': stripe_account_id,
}, },
}, },
'metadata': { 'metadata': metadata
'tournament_id': str(tournament_id), # Convert UUID to string
'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None, # Convert UUID to string
'source_page': 'tournament_info' if team_registration_id else 'register_tournament',
}
} }
# Add cart or team data to metadata based on payment context # # Add cart or team data to metadata based on payment context
if cart_data: # if cart_data:
checkout_session_params['metadata']['registration_cart_id'] = str(cart_data['cart_id']) # Convert to string # checkout_session_params['metadata']['registration_cart_id'] = str(cart_data['cart_id']) # Convert to string
elif team_registration_id: # elif team_registration_id:
checkout_session_params['metadata']['team_registration_id'] = str(team_registration_id) # Convert to string # checkout_session_params['metadata']['team_registration_id'] = str(team_registration_id) # Convert to string
self.request.session['team_registration_id'] = str(team_registration_id) # Convert to string # self.request.session['team_registration_id'] = str(team_registration_id) # Convert to string
# Add customer_email if available # Add customer_email if available
if customer_email: if customer_email:
@ -287,3 +325,61 @@ class PaymentService:
return False, f"Erreur de remboursement Stripe: {str(e)}", None return False, f"Erreur de remboursement Stripe: {str(e)}", None
except Exception as e: except Exception as e:
return False, f"Erreur lors du remboursement: {str(e)}", None return False, f"Erreur lors du remboursement: {str(e)}", None
@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']}")
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)
payment_service = PaymentService(request)
success = payment_service.process_successful_payment(tournament_id, session)
if success:
print(f"Successfully processed webhook payment for tournament {tournament_id}")
return HttpResponse(status=200)
else:
print(f"Failed to process webhook payment for tournament {tournament_id}")
return HttpResponse(status=400)
elif event['type'] == 'payment_intent.payment_failed':
intent = event['data']['object']
metadata = intent.get('metadata', {})
tournament_id = metadata.get('tournament_id')
source_page = metadata.get('source_page')
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)
except Exception as e:
print(f"Tournament webhook error: {str(e)}")

@ -4,6 +4,7 @@ from django.urls import include, path
from .custom_views import CustomLoginView from .custom_views import CustomLoginView
from . import views from . import views
from django.conf import settings from django.conf import settings
from .services import payment_service
urlpatterns = [ urlpatterns = [
path('reset/<uidb64>/<token>/', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'), path('reset/<uidb64>/<token>/', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
@ -13,6 +14,7 @@ urlpatterns = [
path("clubs/<str:club_id>/", views.club, name="club"), path("clubs/<str:club_id>/", views.club, name="club"),
path("c/<str:broadcast_code>", views.club_broadcast, name="club-broadcast"), path("c/<str:broadcast_code>", views.club_broadcast, name="club-broadcast"),
path("c/<str:broadcast_code>/go", views.club_broadcast_auto, name="club-broadcast-auto"), path("c/<str:broadcast_code>/go", views.club_broadcast_auto, name="club-broadcast-auto"),
path('tournaments/webhook/stripe/', payment_service.PaymentService.stripe_webhook, name='stripe_webhook'),
path('tournaments/<str:tournament_id>/payment/success/', views.tournament_payment_success, name='tournament-payment-success'), path('tournaments/<str:tournament_id>/payment/success/', views.tournament_payment_success, name='tournament-payment-success'),
path('tournaments/<str:tournament_id>/proceed-to-payment/', views.proceed_to_payment, name='proceed_to_payment'), path('tournaments/<str:tournament_id>/proceed-to-payment/', views.proceed_to_payment, name='proceed_to_payment'),
path("tournament/<str:tournament_id>/", path("tournament/<str:tournament_id>/",
@ -50,6 +52,7 @@ urlpatterns = [
path('download/', views.download, name='download'), path('download/', views.download, name='download'),
path('apns/', views.test_apns, name='test-apns'), path('apns/', views.test_apns, name='test-apns'),
path('terms-of-use/', views.terms_of_use, name='terms-of-use'), path('terms-of-use/', views.terms_of_use, name='terms-of-use'),
path('utils/validate-stripe-account/', views.validate_stripe_account, name='validate_stripe_account'),
path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'), path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'),
path('mail-test/', views.simple_form_view, name='mail-test'), path('mail-test/', views.simple_form_view, name='mail-test'),
path('login/', CustomLoginView.as_view(), name='custom-login'), path('login/', CustomLoginView.as_view(), name='custom-login'),

@ -1,9 +1,7 @@
import http.client
import json import json
import jwt import jwt
import time import time
import httpx import httpx
import asyncio
# APPLE WARNING: Reuse a connection as long as possible. # APPLE WARNING: Reuse a connection as long as possible.
# In most cases, you can reuse a connection for many hours to days. # In most cases, you can reuse a connection for many hours to days.

@ -8,6 +8,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import logout from django.contrib.auth import logout
from .utils.extensions import is_not_sqlite_backend from .utils.extensions import is_not_sqlite_backend
import stripe import stripe
from django.views.decorators.http import require_http_methods
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.views import PasswordResetCompleteView from django.contrib.auth.views import PasswordResetCompleteView
@ -1513,6 +1514,47 @@ def cancel_registration(request, tournament_id):
messages.info(request, "Processus d'inscription annulé.") messages.info(request, "Processus d'inscription annulé.")
return redirect('tournament-info', tournament_id=tournament_id) return redirect('tournament-info', tournament_id=tournament_id)
@csrf_exempt
@require_http_methods(["POST"])
def validate_stripe_account(request):
stripe.api_key = settings.STRIPE_SECRET_KEY
# Parse the request body
data = json.loads(request.body)
account_id = data.get('account_id')
if not account_id:
return JsonResponse({
'valid': False,
'error': 'Account ID is required'
}, status=400)
# Try to retrieve the account from Stripe
try:
# Basic account verification
account = stripe.Account.retrieve(account_id)
# Only check if the account can receive payments
is_valid = account.id is not None
return JsonResponse({
'valid': is_valid,
'account': {
'id': account.id
}
})
except stripe.error.PermissionError:
return JsonResponse({
'valid': False,
'error': 'No permission to access this account'
}, status=403)
except stripe.error.InvalidRequestError:
return JsonResponse({
'valid': False,
'error': 'Invalid account ID'
}, status=400)
class UserListExportView(LoginRequiredMixin, View): class UserListExportView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
users = CustomUser.objects.order_by('date_joined') users = CustomUser.objects.order_by('date_joined')

Loading…
Cancel
Save