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 %} - - - - - - - - - - - - {% for item in cart_items %} - - - - - - - - {% endfor %} - - - - - - - - -
ProduitCouleurTailleQuantitéPrix
{{ item.product.title }}{{ item.color.name }}{{ item.size.name }}{{ item.quantity }}{{ item.get_total_price }} €
Total:{{ cart_items.total_quantity }}{{ total }} €
- -
- {% if cart_items %} - Vider le panier - {% endif %} +
+

Votre panier

+
+ {% if cart_items %} + + + + + + + + + + + + {% for item in cart_items %} + + + + + + + + {% endfor %} + + + + + + + + +
ProduitCouleurTailleQuantitéPrix
{{ item.product.title }}{{ item.color.name }}{{ item.size.name }}{{ item.quantity }}{{ item.get_total_price }} €
Total:{{ cart_items.total_quantity }}{{ total }} €
- Passer la commande +
+ {% if cart_items %} + Vider le panier + {% endif %} -
+ {% 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

+ + + + + + + + + + + + + + {% for item in order_items %} + + + + + + + + {% endfor %} + + + + + + + + +
ProduitCouleurTailleQuantitéPrix
{{ item.product.title }}{{ item.color.name|default:"N/A" }}{{ item.size.name|default:"N/A" }}{{ item.quantity }}{{ item.get_total_price }} €
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 %} - - + + @@ -46,19 +51,15 @@ - +
{{ 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 }} €
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)