diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py
index 326ce52..7f19c3f 100644
--- a/padelclub_backend/settings.py
+++ b/padelclub_backend/settings.py
@@ -156,5 +156,11 @@ AUTHENTICATION_BACKENDS = [
CSRF_COOKIE_SECURE = True # if using 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_CURRENCY = 'eur' # Set to your preferred currency
+
from .settings_local import *
from .settings_app import *
diff --git a/shop/migrations/0012_order_payment_status_and_more.py b/shop/migrations/0012_order_payment_status_and_more.py
new file mode 100644
index 0000000..e3153cc
--- /dev/null
+++ b/shop/migrations/0012_order_payment_status_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.11 on 2025-03-19 12:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0011_order_guest_user'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='order',
+ name='payment_status',
+ field=models.CharField(choices=[('UNPAID', 'Unpaid'), ('PAID', 'Paid'), ('FAILED', 'Failed')], default='UNPAID', max_length=20),
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='stripe_checkout_session_id',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='stripe_payment_intent_id',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ ]
diff --git a/shop/models.py b/shop/models.py
index 1d44ebb..bff86f0 100644
--- a/shop/models.py
+++ b/shop/models.py
@@ -86,6 +86,13 @@ class Order(models.Model):
status = models.CharField(max_length=20, choices=OrderStatus.choices, default=OrderStatus.PENDING)
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
guest_user = models.ForeignKey(GuestUser, on_delete=models.CASCADE, null=True, blank=True)
+ stripe_payment_intent_id = models.CharField(max_length=255, blank=True, null=True)
+ stripe_checkout_session_id = models.CharField(max_length=255, blank=True, null=True)
+ payment_status = models.CharField(max_length=20, default='UNPAID', choices=[
+ ('UNPAID', 'Unpaid'),
+ ('PAID', 'Paid'),
+ ('FAILED', 'Failed'),
+ ])
def __str__(self):
return f"Order #{self.id} - {self.status}"
diff --git a/shop/stripe_utils.py b/shop/stripe_utils.py
new file mode 100644
index 0000000..bf43eef
--- /dev/null
+++ b/shop/stripe_utils.py
@@ -0,0 +1,47 @@
+import stripe
+from django.conf import settings
+
+# Configure Stripe with your secret key
+stripe.api_key = settings.STRIPE_SECRET_KEY
+
+def create_payment_intent(amount, currency=settings.STRIPE_CURRENCY, metadata=None):
+ """
+ Create a payment intent with Stripe
+
+ Args:
+ amount: Amount in cents (e.g., 1000 for €10.00)
+ currency: Currency code (default: settings.STRIPE_CURRENCY)
+ metadata: Additional info to attach to the payment intent
+
+ Returns:
+ The created payment intent object
+ """
+ intent = stripe.PaymentIntent.create(
+ amount=amount,
+ currency=currency,
+ metadata=metadata or {},
+ )
+ return intent
+
+def create_checkout_session(line_items, success_url, cancel_url, metadata=None):
+ """
+ Create a Stripe Checkout Session for one-time payments
+
+ Args:
+ line_items: List of items to purchase
+ success_url: URL to redirect on successful payment
+ cancel_url: URL to redirect on canceled payment
+ metadata: Additional info to attach to the session
+
+ Returns:
+ The created checkout session
+ """
+ session = stripe.checkout.Session.create(
+ payment_method_types=['card'],
+ line_items=line_items,
+ mode='payment',
+ success_url=success_url,
+ cancel_url=cancel_url,
+ metadata=metadata or {},
+ )
+ return session
diff --git a/shop/templates/shop/cart.html b/shop/templates/shop/cart.html
index 9db4b55..8c0c971 100644
--- a/shop/templates/shop/cart.html
+++ b/shop/templates/shop/cart.html
@@ -7,62 +7,130 @@
{% block content %}
-
-
Votre panier
-
- {% if cart_items %}
-
-
-
- | Produit |
- Couleur |
- Taille |
- Quantité |
- Prix |
-
-
-
- {% for item in cart_items %}
-
- | {{ item.product.title }} |
- {{ item.color.name }} |
- {{ item.size.name }} |
- {{ item.quantity }} |
- {{ item.get_total_price }} € |
-
- {% endfor %}
-
-
-
- | Total: |
- {{ cart_items.total_quantity }} |
- {{ total }} € |
-
-
-
-
-
- {% if cart_items %}
-
Vider le panier
- {% endif %}
+
+
Votre panier
+
+ {% if cart_items %}
+
+
+
+ | Produit |
+ Couleur |
+ Taille |
+ Quantité |
+ Prix |
+
+
+
+ {% for item in cart_items %}
+
+ | {{ item.product.title }} |
+ {{ item.color.name }} |
+ {{ item.size.name }} |
+ {{ item.quantity }} |
+ {{ item.get_total_price }} € |
+
+ {% endfor %}
+
+
+
+ | Total: |
+ {{ cart_items.total_quantity }} |
+ {{ total }} € |
+
+
+
-
Passer la commande
+
+ {% if user.is_authenticated %}
+
+
{% else %}
-
Votre panier est vide.
+
+
Passer la commande
+
+
Connectez-vous pour un paiement plus rapide.
+
Se connecter
+
{% endif %}
+
+ {% else %}
+
Votre panier est vide.
+ {% endif %}
+
+
+{% if user.is_authenticated and cart_items %}
+
+
+
+{% endif %}
{% endblock %}
diff --git a/shop/templates/shop/payment.html b/shop/templates/shop/payment.html
new file mode 100644
index 0000000..6b3bddf
--- /dev/null
+++ b/shop/templates/shop/payment.html
@@ -0,0 +1,87 @@
+{% extends 'tournaments/base.html' %}
+
+{% block head_title %}Paiement{% endblock %}
+{% block first_title %}La boutique Padel Club{% endblock %}
+{% block second_title %}Paiement{% endblock %}
+
+{% block content %}
+
+
+
+
+
Paiement
+
+
Résumé de votre commande
+
+
+
+
+
+ | Produit |
+ Couleur |
+ Taille |
+ Quantité |
+ Prix |
+
+
+
+ {% for item in order_items %}
+
+ | {{ item.product.title }} |
+ {{ item.color.name|default:"N/A" }} |
+ {{ item.size.name|default:"N/A" }} |
+ {{ item.quantity }} |
+ {{ item.get_total_price }} € |
+
+ {% endfor %}
+
+
+
+ | Total: |
+ {{ order_items.count }} |
+ {{ order.total_price }} € |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/shop/templates/shop/payment_cancel.html b/shop/templates/shop/payment_cancel.html
new file mode 100644
index 0000000..f1b6e4c
--- /dev/null
+++ b/shop/templates/shop/payment_cancel.html
@@ -0,0 +1,20 @@
+
+{% extends 'tournaments/base.html' %}
+
+{% block head_title %}Paiement annulé{% endblock %}
+
+{% block content %}
+
+
+
Paiement annulé
+
+
Le paiement a été annulé
+
Votre commande n'a pas été finalisée car le paiement a été annulé.
+
+
+
+
+
+{% endblock %}
diff --git a/shop/templates/shop/order_confirmation.html b/shop/templates/shop/payment_success.html
similarity index 69%
rename from shop/templates/shop/order_confirmation.html
rename to shop/templates/shop/payment_success.html
index d410492..ef9222b 100644
--- a/shop/templates/shop/order_confirmation.html
+++ b/shop/templates/shop/payment_success.html
@@ -1,11 +1,10 @@
{% extends 'tournaments/base.html' %}
-{% block head_title %}Confirmation de commande{% endblock %}
-{% block first_title %}Confirmation de commande{% endblock %}
-{% block second_title %}Merci pour votre commande !{% endblock %}
+{% block head_title %}Paiement réussi{% endblock %}
+{% block first_title %}La boutique Padel Club{% endblock %}
+{% block second_title %}Paiement réussi{% endblock %}
{% block content %}
-
+
-
Détails de la commande
+
Paiement réussi
- {% if order_items %}
+
Merci pour votre commande !
+
Votre paiement a été traité avec succès.
+
Numéro de commande: {{ order.id }}
+
+
+
Détails de la commande
@@ -36,8 +41,8 @@
{% for item in order_items %}
| {{ item.product.title }} |
- {{ item.color.name }} |
- {{ item.size.name }} |
+ {{ item.color.name|default:"N/A" }} |
+ {{ item.size.name|default:"N/A" }} |
{{ item.quantity }} |
{{ item.get_total_price }} € |
@@ -46,19 +51,15 @@
| Total: |
- {{ order_items.total_quantity }} |
+ {{ order_items.count }} |
{{ total }} € |
-
-
Votre commande a été passée avec succès !
-
Retour à la boutique
+
- {% else %}
-
Aucun élément dans la commande.
- {% endif %}
diff --git a/shop/urls.py b/shop/urls.py
index 6a4d43d..95d94b4 100644
--- a/shop/urls.py
+++ b/shop/urls.py
@@ -13,5 +13,10 @@ urlpatterns = [
path('cart/remove/
/', views.remove_from_cart_view, name='remove_from_cart'),
path('clear-cart/', views.clear_cart, name='clear_cart'),
path('checkout/', views.checkout, name='checkout'),
- path('order//confirmation/', views.order_confirmation, name='order_confirmation'),
+ path('payment//', views.payment, name='payment'),
+ path('payment/success//', views.payment_success, name='payment_success'),
+ path('payment/cancel//', views.payment_cancel, name='payment_cancel'),
+ # path('webhook/stripe/', views.stripe_webhook, name='stripe_webhook'),
+ path('create-checkout-session/', views.create_checkout_session, name='create_checkout_session'),
+
]
diff --git a/shop/views.py b/shop/views.py
index ffc20ea..4c9ba75 100644
--- a/shop/views.py
+++ b/shop/views.py
@@ -1,15 +1,86 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
-from .models import Product, Order, OrderItem, GuestUser
+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
+from django.urls import reverse
+from django.http import JsonResponse
+from django.views.decorators.http import require_POST
from . import cart
-# Create your views here.
+# Shared helper methods
+def _check_stripe_config():
+ """Check if Stripe API keys are properly configured"""
+ return hasattr(settings, 'STRIPE_SECRET_KEY') and settings.STRIPE_SECRET_KEY
+
+def _create_stripe_line_items(order_items):
+ """Create line items for Stripe checkout from order items"""
+ line_items = []
+ for item in order_items:
+ item_price = int(float(item.price) * 100) # Convert to cents
+
+ line_items.append({
+ 'price_data': {
+ 'currency': 'eur',
+ 'product_data': {
+ 'name': item.product.title,
+ 'description': f"Color: {item.color.name if item.color else 'N/A'}, Size: {item.size.name if item.size else 'N/A'}",
+ },
+ 'unit_amount': item_price,
+ },
+ 'quantity': item.quantity,
+ })
+ return line_items
+
+def _create_stripe_checkout_session(request, order, line_items):
+ """Create a Stripe checkout session for the order"""
+ # Create success and cancel URLs
+ success_url = request.build_absolute_uri(reverse('shop:payment_success', args=[order.id]))
+ cancel_url = request.build_absolute_uri(reverse('shop:payment_cancel', args=[order.id]))
+
+ # Create metadata to identify this order
+ metadata = {
+ 'order_id': order.id,
+ }
+
+ # Add user info to metadata if available
+ if request.user.is_authenticated:
+ metadata['user_id'] = request.user.id
+ elif 'guest_email' in request.session:
+ metadata['guest_email'] = request.session.get('guest_email', '')
+
+ try:
+ # Initialize Stripe with your secret key
+ stripe.api_key = settings.STRIPE_SECRET_KEY
+
+ # Create session
+ checkout_session = stripe.checkout.Session.create(
+ payment_method_types=['card'],
+ line_items=line_items,
+ mode='payment',
+ success_url=success_url,
+ cancel_url=cancel_url,
+ metadata=metadata,
+ )
+
+ # Save the checkout session ID to the order
+ order.stripe_checkout_session_id = checkout_session.id
+ order.save()
+
+ return checkout_session
+
+ except Exception as e:
+ # Log error and re-raise
+ print(f"Stripe error: {str(e)}")
+ raise
+
+# View functions
def product_list(request):
products = Product.objects.all()
cart_items = cart.get_cart_items(request)
@@ -25,11 +96,18 @@ def view_cart(request):
cart_items = cart.get_cart_items(request)
total = cart.get_cart_total(request)
total_quantity = cart_items.aggregate(total_quantity=Sum('quantity'))['total_quantity']
- return render(request, 'shop/cart.html', {
+
+ context = {
'cart_items': cart_items,
'total': total,
- 'total_quantity': total_quantity
- })
+ 'total_quantity': total_quantity,
+ }
+
+ # Add Stripe publishable key for authenticated users
+ if request.user.is_authenticated:
+ context['stripe_publishable_key'] = settings.STRIPE_PUBLISHABLE_KEY
+
+ return render(request, 'shop/cart.html', context)
def add_to_cart_view(request, product_id):
"""Add a product to the cart"""
@@ -62,17 +140,27 @@ def clear_cart(request):
return redirect('shop:product_list')
def create_order(request):
+ """Create an order from the current cart"""
cart_items = cart.get_cart_items(request)
+
+ # Check if cart is empty
+ if not cart_items.exists():
+ return None
+
total_price = sum(item.get_total_price() for item in cart_items)
+ # Check if total price is valid
+ if total_price <= 0:
+ return None
+
if request.user.is_authenticated:
- # L'utilisateur est authentifié, créer la commande avec l'utilisateur
+ # Authenticated user order
order = Order.objects.create(
user=request.user,
total_price=total_price
)
else:
- # L'utilisateur n'est pas authentifié, créer la commande avec l'utilisateur invité
+ # Guest user order
try:
guest_user = GuestUser.objects.get(email=request.session['guest_email'])
order = Order.objects.create(
@@ -80,12 +168,12 @@ def create_order(request):
total_price=total_price
)
except (KeyError, GuestUser.DoesNotExist):
- # Si l'utilisateur invité n'existe pas, créer une commande sans utilisateur
+ # No guest user information, create order without user
order = Order.objects.create(
total_price=total_price
)
-
+ # Create order items
for cart_item in cart_items:
OrderItem.objects.create(
order=order,
@@ -96,53 +184,163 @@ def create_order(request):
price=cart_item.product.price
)
- # Clear the cart after creating the order
- cart.clear_cart(request)
-
+ # Note: Cart is not cleared here, only after successful payment
return order
-def order_confirmation(request, order_id):
+def checkout(request):
+ """Handle checkout process for both authenticated and guest users"""
+ # Check if cart is empty
+ cart_items = cart.get_cart_items(request)
+ if not cart_items.exists():
+ messages.error(request, "Your cart is empty. Please add items before checkout.")
+ return redirect('shop:product_list')
+
+ if request.user.is_authenticated:
+ # Create order for authenticated user and go directly to payment
+ return _handle_authenticated_checkout(request)
+
+ # Handle guest checkout
+ if request.method == 'GET':
+ form = GuestCheckoutForm()
+ return render(request, 'shop/checkout.html', {'form': form})
+ elif request.method == 'POST':
+ return _handle_guest_checkout_post(request)
+
+ return redirect('shop:product_list')
+
+def _handle_authenticated_checkout(request):
+ """Helper function to handle checkout for authenticated users"""
+ order = create_order(request)
+
+ if not order:
+ messages.error(request, "There was an issue creating your order. Please try again.")
+ return redirect('shop:view_cart')
+
+ return redirect('shop:payment', order_id=order.id)
+
+def _handle_guest_checkout_post(request):
+ """Helper function to handle POST requests for guest checkout"""
+ form = GuestCheckoutForm(request.POST)
+ if form.is_valid():
+ # Create or get guest user
+ email = form.cleaned_data['email']
+ phone = form.cleaned_data['phone']
+ guest_user, created = GuestUser.objects.get_or_create(
+ email=email,
+ defaults={'phone': phone}
+ )
+
+ # Store email in session
+ request.session['guest_email'] = email
+
+ # Create order
+ order = create_order(request)
+
+ if not order:
+ messages.error(request, "There was an issue creating your order. Please try again.")
+ return redirect('shop:view_cart')
+
+ return redirect('shop:payment', order_id=order.id)
+
+ # Form invalid
+ return render(request, 'shop/checkout.html', {'form': form})
+
+def payment(request, order_id):
+ """Handle payment for an existing order"""
order = get_object_or_404(Order, id=order_id)
order_items = order.items.all()
+
+ # Check for valid order
+ if not order_items.exists() or order.total_price <= 0:
+ messages.error(request, "Cannot process an empty order.")
+ return redirect('shop:product_list')
+
+ # Log payment processing
+ print(f"Processing payment for order #{order_id}, total: {order.total_price}")
+
+ # Check Stripe configuration
+ if not _check_stripe_config():
+ messages.error(request, "Stripe API keys not configured properly.")
+ return redirect('shop:view_cart')
+
+ # Create line items
+ line_items = _create_stripe_line_items(order_items)
+
+ if not line_items:
+ messages.error(request, "Cannot create payment with no items.")
+ return redirect('shop:view_cart')
+
+ # Create checkout session
+ try:
+ checkout_session = _create_stripe_checkout_session(request, order, line_items)
+ checkout_session_id = checkout_session.id
+ except Exception as e:
+ messages.error(request, f"Payment processing error: {str(e)}")
+ return redirect('shop:view_cart')
+
+ # Render payment page
+ return render(request, 'shop/payment.html', {
+ 'order': order,
+ 'order_items': order_items,
+ 'checkout_session_id': checkout_session_id,
+ 'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY,
+ })
+
+def payment_success(request, order_id):
+ """Handle successful payment"""
+ order = get_object_or_404(Order, id=order_id)
+
+ # Clear cart after successful payment
+ cart.clear_cart(request)
+
+ # Update order status
+ order.status = OrderStatus.PAID
+ order.payment_status = 'PAID'
+ order.save()
+
+ # Get order items for template
+ order_items = order.items.all()
total = sum(item.get_total_price() for item in order_items)
- return render(request, 'shop/order_confirmation.html', {
+
+ return render(request, 'shop/payment_success.html', {
'order': order,
'order_items': order_items,
'total': total
})
-def checkout(request):
- if request.user.is_authenticated:
- # Créer la commande pour l'utilisateur authentifié
- order = create_order(request)
+def payment_cancel(request, order_id):
+ """Handle cancelled payment"""
+ order = get_object_or_404(Order, id=order_id)
- # Rediriger vers la confirmation de commande
- return redirect('shop:order_confirmation', order.id)
+ # Update order status
+ order.status = OrderStatus.CANCELED
+ order.payment_status = 'FAILED'
+ order.save()
- if request.method == 'GET':
- form = GuestCheckoutForm()
- return render(request, 'shop/checkout.html', {'form': form})
+ messages.warning(request, "Your payment was cancelled.")
+ return render(request, 'shop/payment_cancel.html', {'order': order})
- elif request.method == 'POST':
- # Gérer la soumission du formulaire ici
- form = GuestCheckoutForm(request.POST)
- if form.is_valid():
- # Créer ou récupérer l'utilisateur invité
- email = form.cleaned_data['email']
- phone = form.cleaned_data['phone']
- guest_user, created = GuestUser.objects.get_or_create(email=email, defaults={'phone': phone})
+@require_POST
+def create_checkout_session(request):
+ """API endpoint to create a Stripe checkout session directly from cart"""
+ if not request.user.is_authenticated:
+ return JsonResponse({'error': 'User must be authenticated'}, status=403)
- # Stocker l'e-mail de l'utilisateur invité dans la session
- request.session['guest_email'] = email
+ # Create the order
+ order = create_order(request)
- # Simuler le processus de paiement
- # ...
+ if not order:
+ return JsonResponse({'error': 'Could not create order from cart'}, status=400)
- # Créer la commande
- order = create_order(request)
+ # Get order items
+ order_items = order.items.all()
- # Rediriger vers la confirmation de commande
- return redirect('shop:order_confirmation', order.id)
+ # Create line items
+ line_items = _create_stripe_line_items(order_items)
- # Gérer les autres méthodes (par exemple, PUT, DELETE) si nécessaire
- return redirect('shop:product_list')
+ # Create checkout session
+ try:
+ checkout_session = _create_stripe_checkout_session(request, order, line_items)
+ return JsonResponse({'id': checkout_session.id})
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=400)
diff --git a/tournaments/urls.py b/tournaments/urls.py
index 402fbed..a071a15 100644
--- a/tournaments/urls.py
+++ b/tournaments/urls.py
@@ -77,8 +77,6 @@ urlpatterns = [
path('admin/users-export/', views.UserListExportView.as_view(), name='users_export'),
path('activation-success/', views.activation_success, name='activation_success'),
path('activation-failed/', views.activation_failed, name='activation_failed'),
- path('shop/', include('shop.urls')),
-
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)