add webhook

shop
Raz 8 months ago
parent 9a3b920058
commit 8777706865
  1. 5
      padelclub_backend/settings.py
  2. 18
      shop/migrations/0016_order_webhook_processed.py
  3. 1
      shop/models.py
  4. 59
      shop/signals.py
  5. 2
      shop/templates/shop/cart.html
  6. 2
      shop/urls.py
  7. 149
      shop/views.py

@ -157,8 +157,11 @@ SESSION_COOKIE_SECURE = True # Si vous utilisez HTTPS
# Stripe Settings
STRIPE_PUBLISHABLE_KEY = 'pk_test_51R4LrTPEZkECCx484C2KbmRpcO2ZkZb0NoNi8QJB4X3E5JFu3bvLk4JZQmz9grKbk6O40z3xI8DawHrGyUY0fOT600VEKC9ran' # Replace with your actual key
STRIPE_SECRET_KEY = 'sk_test_51R4LrTPEZkECCx48PkSbEYarhts7J7XNYpS1mJgows5z5dcv38l0G2tImvhXCjzvMgUH9ML0vLMOEPeyUBtYVf5H00Qvz8t3rE' # Replace with your actual key
STRIPE_WEBHOOK_SECRET = 'your_webhook_secret' # Optional for later
STRIPE_WEBHOOK_SECRET = 'whsec_cbaa9c0c7b24041136e063a7d60fb674ec0646b2c4b821512c41a27634d7b1ba' # Optional for later
STRIPE_CURRENCY = 'eur' # Set to your preferred currency
# STRIPE_PUBLISHABLE_KEY = os.environ.get('STRIPE_PUBLISHABLE_KEY', 'your_test_publishable_key')
# STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY', 'your_test_secret_key')
# STRIPE_WEBHOOK_SECRET = os.environ.get('STRIPE_WEBHOOK_SECRET', 'your_test_webhook_secret')
# Add managers who should receive internal emails
MANAGERS = [

@ -0,0 +1,18 @@
# Generated by Django 4.2.11 on 2025-03-21 05:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0015_alter_product_image'),
]
operations = [
migrations.AddField(
model_name='order',
name='webhook_processed',
field=models.BooleanField(default=False),
),
]

@ -79,6 +79,7 @@ class Order(models.Model):
('PAID', 'Paid'),
('FAILED', 'Failed'),
])
webhook_processed = models.BooleanField(default=False)
def __str__(self):
return f"Order #{self.id} - {self.status}"

@ -46,17 +46,20 @@ def _send_order_email(instance, **kwargs):
admin_url = f"{settings.SITE_URL}{reverse('admin:shop_order_change', args=[order_id])}"
# Get customer info
customer_email = None
if instance.user:
customer_info = f"Utilisateur: {instance.user.email}"
customer_email = instance.user.email
elif instance.guest_user:
customer_info = f"Invité: {instance.guest_user.email} ({instance.guest_user.phone})"
customer_email = instance.guest_user.email
else:
customer_info = "Client inconnu"
# Construire les détails des articles - obtenir une requête fraîche des articles de la commande
# Build order item details
items_list = ""
if action != "DELETED":
# Utiliser une requête fraîche pour s'assurer d'avoir les données les plus récentes
# Use a fresh query to ensure we have the most recent data
order_items = OrderItem.objects.filter(order_id=order_id).select_related('product', 'color', 'size')
for item in order_items:
color = item.color.name if item.color else "N/A"
@ -64,7 +67,7 @@ def _send_order_email(instance, **kwargs):
item_line = f"- {item.quantity}x {item.product.title} (Couleur: {color}, Taille: {size}, Prix: {item.price}€)\n"
items_list += item_line
# Composer l'email
# Compose the email
if action == "CREATED":
action_fr = "CRÉÉE"
elif action == "UPDATED":
@ -74,7 +77,7 @@ def _send_order_email(instance, **kwargs):
else:
action_fr = action
# Traduire le statut actuel
# Translate current status
status_fr_map = {
"PENDING": "EN ATTENTE",
"PAID": "PAYÉE",
@ -84,7 +87,7 @@ def _send_order_email(instance, **kwargs):
}
status_fr = status_fr_map.get(status, status)
# Traduire le statut de paiement
# Translate payment status
payment_status_fr_map = {
"UNPAID": "NON PAYÉE",
"PAID": "PAYÉE",
@ -92,6 +95,7 @@ def _send_order_email(instance, **kwargs):
}
payment_status_fr = payment_status_fr_map.get(instance.payment_status, instance.payment_status)
# Send internal notification email
subject = f"Commande #{order_id} {action_fr}: {status_fr}"
message = f"""
La commande #{order_id} a été {action_fr.lower()}
@ -110,7 +114,7 @@ Voir la commande dans le panneau d'administration: {admin_url}
Ceci est un message automatique. Merci de ne pas répondre.
"""
# Send email
# Send internal email
recipient_list = [email for name, email in settings.MANAGERS]
if not recipient_list:
recipient_list = [settings.DEFAULT_FROM_EMAIL]
@ -122,3 +126,46 @@ Ceci est un message automatique. Merci de ne pas répondre.
recipient_list=recipient_list,
fail_silently=False,
)
# Only send customer email for PAID status and if we have customer email
if status == OrderStatus.PAID and customer_email and instance.payment_status == "PAID":
# Generate customer-facing URLs
shop_url = f"{settings.SITE_URL}/shop"
contact_email = "support@padelclub.app"
# Create a customer receipt email
customer_subject = f"Confirmation de votre commande #{order_id} - PadelClub"
customer_message = f"""
Bonjour,
Nous vous remercions pour votre commande sur PadelClub !
Récapitulatif de votre commande #{order_id} du {instance.date_ordered.strftime('%d/%m/%Y')} :
Statut: {status_fr}
Prix total: {total_price}
Détail de votre commande :
{items_list}
Nous nous occupons de préparer votre commande dans les plus brefs délais.
Pour toute question concernant votre commande, n'hésitez pas à contacter notre service client :
{contact_email}
Visitez notre boutique pour découvrir d'autres produits :
{shop_url}
Merci de votre confiance et à bientôt sur PadelClub !
L'équipe PadelClub
"""
# Send email to customer
send_mail(
subject=customer_subject,
message=customer_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[customer_email],
fail_silently=False,
)

@ -65,7 +65,7 @@
</div>
</div>
{% if user.is_authenticated and cart_items %}
{% if user.is_authenticated and display_data.items %}
<!-- Stripe JavaScript for authenticated users -->
<script src="https://js.stripe.com/v3/"></script>
<script>

@ -16,7 +16,7 @@ urlpatterns = [
path('payment/<int:order_id>/', views.payment, name='payment'),
path('payment/success/<int:order_id>/', views.payment_success, name='payment_success'),
path('payment/cancel/<int:order_id>/', views.payment_cancel, name='payment_cancel'),
# path('webhook/stripe/', views.stripe_webhook, name='stripe_webhook'),
path('webhook/stripe/', views.stripe_webhook, name='stripe_webhook'),
path('create-checkout-session/', views.create_checkout_session, name='create_checkout_session'),
path('cart/update-item/', views.update_cart_item, name='update_cart_item'),
path('cart/remove-item/', views.remove_from_cart, name='remove_from_cart'),

@ -2,9 +2,6 @@ from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from .models import Product, Order, OrderItem, GuestUser, OrderStatus
from django.db.models import Sum
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from .forms import GuestCheckoutForm
import stripe
from django.conf import settings
@ -12,6 +9,8 @@ from django.urls import reverse
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import ensure_csrf_cookie
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from . import cart
@ -47,12 +46,12 @@ def _create_stripe_checkout_session(request, order, line_items):
# Create metadata to identify this order
metadata = {
'order_id': order.id,
'order_id': str(order.id), # Convert to string to be safe
}
# Add user info to metadata if available
if request.user.is_authenticated:
metadata['user_id'] = request.user.id
metadata['user_id'] = str(request.user.id)
elif 'guest_email' in request.session:
metadata['guest_email'] = request.session.get('guest_email', '')
@ -298,31 +297,36 @@ def payment_success(request, order_id):
# Clear cart after successful payment
cart.clear_cart(request)
# Update order status
print("payment_success")
order.status = OrderStatus.PAID
order.payment_status = 'PAID'
order.save()
# Only update if not already processed by webhook
if not order.webhook_processed:
print(f"Updating order {order_id} via redirect (not webhook)")
order.status = OrderStatus.PAID
order.payment_status = 'PAID'
order.save()
else:
print(f"Order {order_id} already processed by webhook")
# Get order items for template
order_items = order.items.all()
total = sum(item.get_total_price() for item in order_items)
display_data = prepare_item_display_data(order_items, is_cart=False)
print(display_data)
return render(request, 'shop/payment_success.html', {
'order': order,
'display_data' : display_data,
'display_data': display_data,
})
def payment_cancel(request, order_id):
"""Handle cancelled payment"""
order = get_object_or_404(Order, id=order_id)
# Update order status
order.status = OrderStatus.CANCELED
order.payment_status = 'FAILED'
order.save()
# Only update if not already processed by webhook
if not order.webhook_processed:
print(f"Updating order {order_id} to CANCELED via redirect (not webhook)")
order.status = OrderStatus.CANCELED
order.payment_status = 'FAILED'
order.save()
else:
print(f"Order {order_id} already processed by webhook")
messages.warning(request, "Your payment was cancelled.")
return render(request, 'shop/payment_cancel.html', {'order': order})
@ -467,3 +471,114 @@ def simulate_payment_failure(request):
return redirect('shop:view_cart')
return redirect('shop:payment_cancel', order_id=order.id)
@require_POST
@csrf_exempt # Stripe can't provide CSRF tokens
def stripe_webhook(request):
"""Handle Stripe webhook events"""
payload = request.body
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
# Log the received webhook for debugging
print(f"Webhook received: {sig_header}")
if not sig_header:
print("No signature header")
return HttpResponse(status=400)
try:
# Initialize Stripe
stripe.api_key = settings.STRIPE_SECRET_KEY
# Verify the event using webhook signing secret
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
# Log the event type
print(f"Webhook event type: {event['type']}")
except ValueError as e:
# Invalid payload
print(f"Invalid payload: {str(e)}")
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
print(f"Invalid signature: {str(e)}")
return HttpResponse(status=400)
# Handle the event based on type
event_type = event['type']
if event_type == 'checkout.session.completed':
handle_checkout_session_completed(event['data']['object'])
elif event_type == 'payment_intent.succeeded':
handle_payment_intent_succeeded(event['data']['object'])
elif event_type == 'payment_intent.payment_failed':
handle_payment_intent_failed(event['data']['object'])
# Return success response
return HttpResponse(status=200)
def handle_checkout_session_completed(session):
"""Handle completed checkout session webhook"""
print(f"Processing checkout.session.completed for session {session.id}")
# Get order from metadata
order_id = session.get('metadata', {}).get('order_id')
if not order_id:
print(f"Warning: No order_id in metadata for session {session.id}")
return
try:
order = Order.objects.get(id=order_id)
# Update order status if not already processed
if not order.webhook_processed:
print(f"Updating order {order_id} to PAID via webhook")
order.status = OrderStatus.PAID
order.payment_status = 'PAID'
order.webhook_processed = True
order.save()
# You could add additional processing here
# - Send order confirmation email
# - Update inventory
# - Etc.
else:
print(f"Order {order_id} already processed by webhook")
except Order.DoesNotExist:
print(f"Error: Order {order_id} not found for session {session.id}")
def handle_payment_intent_succeeded(payment_intent):
"""Handle successful payment intent webhook"""
print(f"Processing payment_intent.succeeded for intent {payment_intent.id}")
# If you're using payment intents directly, handle them here
# For Checkout Sessions, you'll likely rely on checkout.session.completed instead
def handle_payment_intent_failed(payment_intent):
"""Handle failed payment intent webhook"""
print(f"Processing payment_intent.payment_failed for intent {payment_intent.id}")
# Get order from metadata
order_id = payment_intent.get('metadata', {}).get('order_id')
if not order_id:
print(f"No order_id in metadata for payment intent {payment_intent.id}")
return
try:
order = Order.objects.get(id=order_id)
# Update order status
if not order.webhook_processed or order.payment_status != 'FAILED':
print(f"Updating order {order_id} to FAILED via webhook")
order.status = OrderStatus.CANCELED
order.payment_status = 'FAILED'
order.webhook_processed = True
order.save()
except Order.DoesNotExist:
print(f"Error: Order {order_id} not found for payment intent {payment_intent.id}")

Loading…
Cancel
Save