add coupon in shop

timetoconfirm
Raz 8 months ago
parent 7574087bf8
commit dbe470c7ff
  1. 32
      shop/admin.py
  2. 4
      shop/forms.py
  3. 54
      shop/migrations/0024_coupon_order_discount_amount_order_coupon_and_more.py
  4. 48
      shop/models.py
  5. 97
      shop/signals.py
  6. 15
      shop/static/shop/css/shop.css
  7. 40
      shop/stripe_utils.py
  8. 149
      shop/templates/shop/cart.html
  9. 17
      shop/templates/shop/payment_success.html
  10. 2
      shop/urls.py
  11. 168
      shop/views.py

@ -1,5 +1,5 @@
from django.contrib import admin
from .models import Product, Color, Size, Order, OrderItem, GuestUser
from .models import Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage
from django.utils.html import format_html
@admin.register(Product)
@ -53,3 +53,33 @@ class GuestUserOrderInline(admin.TabularInline):
class GuestUserAdmin(admin.ModelAdmin):
list_display = ('email', 'phone')
inlines = [GuestUserOrderInline]
@admin.register(Coupon)
class CouponAdmin(admin.ModelAdmin):
list_display = ('code', 'discount_amount', 'discount_percent', 'is_active',
'valid_from', 'valid_to', 'current_uses', 'max_uses')
list_filter = ('is_active', 'valid_from', 'valid_to')
search_fields = ('code', 'description')
readonly_fields = ('current_uses', 'created_at', 'stripe_coupon_id')
fieldsets = (
('Basic Information', {
'fields': ('code', 'description', 'is_active')
}),
('Discount', {
'fields': ('discount_amount', 'discount_percent')
}),
('Validity', {
'fields': ('valid_from', 'valid_to', 'max_uses', 'current_uses')
}),
('Stripe Information', {
'fields': ('stripe_coupon_id',),
'classes': ('collapse',)
}),
)
@admin.register(CouponUsage)
class CouponUsageAdmin(admin.ModelAdmin):
list_display = ('coupon', 'user', 'guest_email', 'order', 'used_at')
list_filter = ('used_at',)
search_fields = ('coupon__code', 'user__username', 'user__email', 'guest_email')
readonly_fields = ('used_at',)

@ -1,5 +1,9 @@
from django import forms
from .models import Coupon
class GuestCheckoutForm(forms.Form):
email = forms.EmailField(required=True)
phone = forms.CharField(max_length=20, required=True, label="Téléphone portable")
class CouponApplyForm(forms.Form):
code = forms.CharField(max_length=50)

@ -0,0 +1,54 @@
# Generated by Django 5.1 on 2025-03-27 14:34
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0023_alter_color_options_color_ordering'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Coupon',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50, unique=True)),
('description', models.CharField(blank=True, max_length=255)),
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
('is_active', models.BooleanField(default=True)),
('valid_from', models.DateTimeField()),
('valid_to', models.DateTimeField()),
('max_uses', models.IntegerField(default=0)),
('current_uses', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('stripe_coupon_id', models.CharField(blank=True, max_length=100, null=True)),
],
),
migrations.AddField(
model_name='order',
name='discount_amount',
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10),
),
migrations.AddField(
model_name='order',
name='coupon',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.coupon'),
),
migrations.CreateModel(
name='CouponUsage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('guest_email', models.EmailField(blank=True, max_length=254, null=True)),
('used_at', models.DateTimeField(auto_now_add=True)),
('coupon', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='usages', to='shop.coupon')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='coupon_usages', to='shop.order')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

@ -76,6 +76,38 @@ class GuestUser(models.Model):
def __str__(self):
return f"{self.email}"
class Coupon(models.Model):
code = models.CharField(max_length=50, unique=True)
description = models.CharField(max_length=255, blank=True)
discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
discount_percent = models.DecimalField(max_digits=5, decimal_places=2, default=0)
is_active = models.BooleanField(default=True)
valid_from = models.DateTimeField()
valid_to = models.DateTimeField()
max_uses = models.IntegerField(default=0) # 0 for unlimited
current_uses = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
stripe_coupon_id = models.CharField(max_length=100, blank=True, null=True)
def __str__(self):
return self.code
def is_valid(self):
from django.utils import timezone
now = timezone.now()
if not self.is_active:
return False
if now < self.valid_from or now > self.valid_to:
return False
if self.max_uses > 0 and self.current_uses >= self.max_uses:
return False
return True
def get_discount_amount_for_total(self, total_price):
"""Calculate the discount amount for a given total price"""
if self.discount_percent > 0:
return (self.discount_percent / 100) * total_price
return self.discount_amount
class Order(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True)
@ -95,10 +127,15 @@ class Order(models.Model):
('test', 'Test Mode'),
('live', 'Live Mode'),
])
coupon = models.ForeignKey(Coupon, on_delete=models.SET_NULL, null=True, blank=True)
discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
def __str__(self):
return f"Order #{self.id} - {self.status}"
def get_total_after_discount(self):
return max(self.total_price - self.discount_amount, 0)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
@ -115,3 +152,14 @@ class OrderItem(models.Model):
def get_total_price(self):
return self.price * self.quantity
class CouponUsage(models.Model):
coupon = models.ForeignKey(Coupon, on_delete=models.CASCADE, related_name='usages')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True)
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='coupon_usages')
guest_email = models.EmailField(blank=True, null=True)
used_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
user_identifier = self.user.username if self.user else self.guest_email
return f"{self.coupon.code} - {user_identifier} - {self.used_at}"

@ -66,6 +66,21 @@ def _get_order_details(instance):
"UNPAID": "NON PAYÉE", "PAID": "PAYÉE", "FAILED": "ÉCHOUÉE"
}
# Calculate discount information
has_coupon = instance.coupon is not None
coupon_info = ""
final_price = instance.total_price
if has_coupon:
coupon_code = instance.coupon.code
discount_amount = instance.discount_amount
final_price = instance.get_total_after_discount()
if instance.coupon.discount_percent > 0:
coupon_info = f"Code promo: {coupon_code} ({instance.coupon.discount_percent}%)"
else:
coupon_info = f"Code promo: {coupon_code} (€{discount_amount})"
return {
'order_id': instance.id,
'status': instance.status,
@ -73,6 +88,10 @@ def _get_order_details(instance):
'payment_status': instance.payment_status,
'payment_status_fr': payment_status_fr_map.get(instance.payment_status, instance.payment_status),
'total_price': instance.total_price,
'has_coupon': has_coupon,
'coupon_info': coupon_info,
'discount_amount': instance.discount_amount if has_coupon else 0,
'final_price': final_price,
'customer_info': customer_info,
'customer_email': customer_email,
'date_ordered': instance.date_ordered,
@ -101,13 +120,22 @@ def _send_internal_notification(instance, action, order_details, items_list):
"""Send notification email to shop managers."""
action_fr = _translate_action(action)
# Build price information with coupon details if applicable
price_info = f"Prix total: {order_details['total_price']}"
if order_details['has_coupon']:
price_info = f"""
Prix total: {order_details['total_price']}
{order_details['coupon_info']}
Réduction: -{order_details['discount_amount']}
Montant payé: {order_details['final_price']}"""
subject = f"Commande #{order_details['order_id']} {action_fr}: {order_details['status_fr']}"
message = f"""
La commande #{order_details['order_id']} a été {action_fr.lower()}
Statut: {order_details['status_fr']}
Statut de paiement: {order_details['payment_status_fr']}
Prix total: {order_details['total_price']}
{price_info}
{order_details['customer_info']}
@ -147,6 +175,10 @@ def _send_customer_notification(instance, order_details, items_list):
date_formatted,
order_details['status_fr'],
order_details['total_price'],
order_details['has_coupon'],
order_details['coupon_info'],
order_details['discount_amount'],
order_details['final_price'],
items_list,
contact_email,
shop_url
@ -166,15 +198,24 @@ def _send_customer_notification(instance, order_details, items_list):
)
def _get_customer_email_content(status, payment_status, order_id, date, status_fr,
total_price, items_list, contact_email, shop_url):
total_price, has_coupon, coupon_info, discount_amount,
final_price, items_list, contact_email, shop_url):
"""Get the appropriate customer email content based on order status."""
# Build price information with coupon details if applicable
price_info = f"Prix total: {total_price}"
if has_coupon:
price_info = f"""Prix total: {total_price}
{coupon_info}
Réduction: -{discount_amount}
Montant payé: {final_price}"""
# Payment confirmation email
if status == OrderStatus.PAID and payment_status == "PAID":
return {
'subject': f"Confirmation de votre commande #{order_id} - PadelClub",
'subject': f"Confirmation de votre commande #{order_id} - Padel Club",
'message': _build_payment_confirmation_email(order_id, date, status_fr,
total_price, items_list,
price_info, items_list,
contact_email, shop_url)
}
@ -187,41 +228,41 @@ def _get_customer_email_content(status, payment_status, order_id, date, status_f
}.get(status, "")
return {
'subject': f"Mise à jour de votre commande #{order_id} - PadelClub",
'subject': f"Mise à jour de votre commande #{order_id} - Padel Club",
'message': _build_status_update_email(order_id, date, status_message, status_fr,
total_price, items_list, contact_email)
price_info, items_list, contact_email)
}
# Payment issue notification
elif payment_status == "FAILED":
return {
'subject': f"Problème de paiement pour votre commande #{order_id} - PadelClub",
'message': _build_payment_issue_email(order_id, date, total_price,
'subject': f"Problème de paiement pour votre commande #{order_id} - Padel Club",
'message': _build_payment_issue_email(order_id, date, price_info,
items_list, contact_email, shop_url)
}
# Payment reminder for unpaid orders
elif payment_status == "UNPAID" and status != OrderStatus.PENDING:
return {
'subject': f"Rappel de paiement pour votre commande #{order_id} - PadelClub",
'message': _build_payment_reminder_email(order_id, date, total_price,
'subject': f"Rappel de paiement pour votre commande #{order_id} - Padel Club",
'message': _build_payment_reminder_email(order_id, date, price_info,
items_list, contact_email)
}
# No email needed
return None
def _build_payment_confirmation_email(order_id, date, status_fr, total_price, items_list, contact_email, shop_url):
def _build_payment_confirmation_email(order_id, date, status_fr, price_info, items_list, contact_email, shop_url):
"""Build payment confirmation email message."""
return f"""
Bonjour,
Nous vous remercions pour votre commande sur PadelClub !
Nous vous remercions pour votre commande sur Padel Club !
Récapitulatif de votre commande #{order_id} du {date} :
Statut: {status_fr}
Prix total: {total_price}
{price_info}
Détail de votre commande :
{items_list}
@ -234,22 +275,22 @@ Pour toute question concernant votre commande, n'hésitez pas à contacter notre
Visitez notre boutique pour découvrir d'autres produits :
{shop_url}
Merci de votre confiance et à bientôt sur PadelClub !
Merci de votre confiance et à bientôt sur Padel Club !
L'équipe PadelClub
L'équipe Padel Club
"""
def _build_status_update_email(order_id, date, status_message, status_fr, total_price, items_list, contact_email):
def _build_status_update_email(order_id, date, status_message, status_fr, price_info, items_list, contact_email):
"""Build status update email message."""
return f"""
Bonjour,
Mise à jour concernant votre commande PadelClub #{order_id} du {date} :
Mise à jour concernant votre commande Padel Club #{order_id} du {date} :
{status_message}
Statut actuel: {status_fr}
Prix total: {total_price}
{price_info}
Détail de votre commande :
{items_list}
@ -257,21 +298,21 @@ Détail de votre commande :
Pour toute question concernant votre commande, n'hésitez pas à contacter notre service client :
{contact_email}
Merci de votre confiance et à bientôt sur PadelClub !
Merci de votre confiance et à bientôt sur Padel Club !
L'équipe PadelClub
L'équipe Padel Club
"""
def _build_payment_issue_email(order_id, date, total_price, items_list, contact_email, shop_url):
def _build_payment_issue_email(order_id, date, price_info, items_list, contact_email, shop_url):
"""Build payment issue email message."""
return f"""
Bonjour,
Nous avons rencontré un problème lors du traitement du paiement de votre commande PadelClub #{order_id}.
Nous avons rencontré un problème lors du traitement du paiement de votre commande Padel Club #{order_id}.
Détails de la commande :
Date: {date}
Prix total: {total_price}
{price_info}
Articles:
{items_list}
@ -284,18 +325,18 @@ Vous pouvez également visiter notre boutique pour finaliser votre achat :
Merci de votre compréhension.
L'équipe PadelClub
L'équipe Padel Club
"""
def _build_payment_reminder_email(order_id, date, total_price, items_list, contact_email):
def _build_payment_reminder_email(order_id, date, price_info, items_list, contact_email):
"""Build payment reminder email message."""
return f"""
Bonjour,
Nous vous rappelons que votre commande PadelClub #{order_id} du {date} n'a pas encore été payée.
Nous vous rappelons que votre commande Padel Club #{order_id} du {date} n'a pas encore été payée.
Détails de la commande :
Prix total: {total_price}
{price_info}
Articles:
{items_list}
@ -307,7 +348,7 @@ Si vous rencontrez des difficultés ou si vous avez des questions, n'hésitez pa
Merci de votre confiance.
L'équipe PadelClub
L'équipe Padel Club
"""
@receiver(user_logged_in)

@ -119,13 +119,28 @@
height: 36px;
}
.coupon-section {
color: #707070;
font-size: 12px;
font-weight: 600;
text-decoration: none;
margin-top: 20px;
}
.confirm-nav-button {
background-color: #90ee90;
color: #707070;
font-size: 12px;
font-weight: 600;
text-decoration: none;
}
.cancel-nav-button {
background-color: #e84039;
color: white;
font-size: 12px;
font-weight: 600;
text-decoration: none;
}
.remove-btn {

@ -25,21 +25,31 @@ class StripeService:
mode_str = "TEST" if self.is_test_mode else "LIVE"
logger.debug(f"Initialized StripeService in {mode_str} mode")
def create_checkout_session(self, customer_email, line_items, success_url, cancel_url, metadata=None):
"""Create a Stripe Checkout Session for one-time payments"""
if self.is_test_mode:
logger.info(f"Creating checkout session in TEST mode with metadata: {metadata}")
session = stripe.checkout.Session.create(
customer_email=customer_email,
payment_method_types=['card'],
line_items=line_items,
mode='payment',
success_url=success_url,
cancel_url=cancel_url,
metadata=metadata or {},
)
return session
def create_checkout_session(self, customer_email=None, line_items=None,
success_url=None, cancel_url=None, metadata=None,
discounts=None):
"""Create a Stripe checkout session with the given parameters"""
# Initialize session parameters
session_params = {
'payment_method_types': ['card'],
'line_items': line_items,
'mode': 'payment',
'success_url': success_url,
'cancel_url': cancel_url,
'metadata': metadata or {},
}
# Add customer email if provided
if customer_email:
session_params['customer_email'] = customer_email
# Add discounts if provided
if discounts:
session_params['discounts'] = discounts
# Create and return the session
return stripe.checkout.Session.create(**session_params)
def verify_webhook_signature(self, payload, signature):
"""Verify webhook signature using mode-appropriate secret"""

@ -31,6 +31,32 @@
{% if display_data.items %}
{% include 'shop/partials/order_items_display.html' with items=display_data.items total_quantity=display_data.total_quantity total_price=display_data.total_price edit_mode=True %}
<div class="coupon-section">
<div>Avez-vous un code de réduction?</div>
<div class="coupon-form" style="display: flex;">
<input type="text" id="coupon-code" style="flex-grow: 1; margin-right: 10px; border-radius: 12px" placeholder="Entrez votre code">
<button id="apply-coupon" class="confirm-nav-button" style="min-width: 100px; border-radius: 12px; height: 40px;">Appliquer</button>
<button id="remove-coupon" class="cancel-nav-button" style="min-width: 100px; border-radius: 12px; height: 40px; display: none;">Supprimer</button>
</div>
<div id="coupon-message"></div>
<!-- Discount display -->
<div id="discount-info" style="margin-top: 10px; display: none;">
<div class="discount-row" style="display: flex; justify-content: space-between; margin-bottom: 5px;">
<span>Sous-total:</span>
<span id="subtotal-amount">€{{ display_data.total_price }}</span>
</div>
<div class="discount-row" style="display: flex; justify-content: space-between; margin-bottom: 5px;">
<span>Réduction:</span>
<span id="discount-amount" style="color: #e67e22;">-€0.00</span>
</div>
<div class="discount-row" style="display: flex; justify-content: space-between; font-weight: bold;">
<span>Total:</span>
<span id="final-total">€{{ display_data.total_price }}</span>
</div>
</div>
</div>
<div class="cart-summary">
{% if user.is_authenticated %}
@ -79,6 +105,129 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
const checkoutButton = document.getElementById('checkout-button');
const applyButton = document.getElementById('apply-coupon');
const removeButton = document.getElementById('remove-coupon');
const codeInput = document.getElementById('coupon-code');
const messageElement = document.getElementById('coupon-message');
const discountInfo = document.getElementById('discount-info');
const subtotalAmount = document.getElementById('subtotal-amount');
const discountAmount = document.getElementById('discount-amount');
const finalTotal = document.getElementById('final-total');
// Initial values
const originalTotal = parseFloat('{{ display_data.total_price }}');
// Function to get CSRF token
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue; // FIX: Proper closing of the function here
}
// Apply coupon
applyButton.addEventListener('click', function() {
const code = codeInput.value.trim();
if (!code) {
messageElement.textContent = 'Veuillez entrer un code';
messageElement.style.color = '#e74c3c';
return;
}
// Show loading state
applyButton.textContent = 'Chargement...';
applyButton.disabled = true;
// Send request to server
fetch('{% url "shop:apply_coupon" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': getCookie('csrftoken')
},
body: `code=${encodeURIComponent(code)}`
})
.then(response => response.json())
.then(data => {
// Reset button state
applyButton.textContent = 'Appliquer';
applyButton.disabled = false;
// Show message
messageElement.textContent = data.message;
if (data.status === 'success') {
// Update UI for success
messageElement.style.color = '#27ae60';
applyButton.style.display = 'none';
removeButton.style.display = 'block';
codeInput.disabled = true;
// Update price display
discountInfo.style.display = 'block';
discountAmount.textContent = `-€${data.discount.toFixed(2)}`;
finalTotal.textContent = `€${data.new_total.toFixed(2)}`;
} else {
// Update UI for error
messageElement.style.color = '#e74c3c';
}
})
.catch(error => {
console.error('Error:', error);
applyButton.textContent = 'Appliquer';
applyButton.disabled = false;
messageElement.textContent = 'Une erreur est survenue. Veuillez réessayer.';
messageElement.style.color = '#e74c3c';
});
});
// Remove coupon
removeButton.addEventListener('click', function() {
// Show loading state
removeButton.textContent = 'Chargement...';
removeButton.disabled = true;
// Send request to server
fetch('{% url "shop:remove_coupon" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(response => response.json())
.then(data => {
// Reset button state
removeButton.textContent = 'Supprimer';
removeButton.disabled = false;
if (data.status === 'success') {
// Reset UI
messageElement.textContent = '';
codeInput.value = '';
codeInput.disabled = false;
applyButton.style.display = 'block';
removeButton.style.display = 'none';
discountInfo.style.display = 'none';
finalTotal.textContent = `€${originalTotal.toFixed(2)}`;
}
})
.catch(error => {
console.error('Error:', error);
removeButton.textContent = 'Supprimer';
removeButton.disabled = false;
messageElement.textContent = 'Une erreur est survenue. Veuillez réessayer.';
messageElement.style.color = '#e74c3c';
});
});
checkoutButton.addEventListener('click', function() {
// Show a loading indicator

@ -29,6 +29,23 @@
<h3>Détails de la commande</h3>
{% include 'shop/partials/order_items_display.html' with items=display_data.items total_quantity=display_data.total_quantity total_price=display_data.total_price edit_mode=False %}
{% if has_discount %}
<div class="order-summary" style="margin-top: 20px;">
<div class="order-row" style="display: flex; justify-content: space-between;">
<span>Sous-total:</span>
<span>€{{ order.total_price }}</span>
</div>
<div class="order-row" style="display: flex; justify-content: space-between; color: #e67e22;">
<span>Réduction:</span>
<span>-€{{ discount_amount }}</span>
</div>
<div class="order-row" style="display: flex; justify-content: space-between; font-weight: bold;">
<span>Total payé:</span>
<span>€{{ total_after_discount }}</span>
</div>
</div>
{% endif %}
<div class="cart-summary">
<a class="confirm-nav-button checkout-button" href="{% url 'shop:product_list' %}">Retour à la boutique</a>
</div>

@ -22,5 +22,7 @@ urlpatterns = [
path('cart/remove-item/', views.remove_from_cart, name='remove_from_cart'),
path('debug/simulate-payment-success/', views.simulate_payment_success, name='simulate_payment_success'),
path('debug/simulate-payment-failure/', views.simulate_payment_failure, name='simulate_payment_failure'),
path('apply-coupon/', views.apply_coupon, name='apply_coupon'),
path('remove-coupon/', views.remove_coupon, name='remove_coupon'),
]

@ -1,7 +1,7 @@
from .stripe_utils import stripe_service
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from .models import Product, Order, OrderItem, GuestUser, OrderStatus
from .models import Product, Order, OrderItem, GuestUser, OrderStatus, Coupon, CouponUsage
from django.db.models import Sum
from .forms import GuestCheckoutForm
import stripe
@ -12,6 +12,7 @@ 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 django.utils import timezone
from . import cart
@ -64,15 +65,56 @@ def _create_stripe_checkout_session(request, order, line_items):
metadata['guest_email'] = request.session.get('guest_email', '')
customer_email = request.session.get('guest_email', '')
# Handle coupon discount in Stripe
discounts = None
if order.coupon and order.discount_amount > 0:
try:
# Create or retrieve a Stripe coupon
if order.coupon.stripe_coupon_id:
# Use existing Stripe coupon
stripe_coupon_id = order.coupon.stripe_coupon_id
else:
# Create a new Stripe coupon
if order.coupon.discount_percent > 0:
stripe_coupon = stripe.Coupon.create(
percent_off=float(order.coupon.discount_percent),
duration='once',
name=order.coupon.code
)
else:
stripe_coupon = stripe.Coupon.create(
amount_off=int(float(order.coupon.discount_amount) * 100), # Convert to cents
currency='eur',
duration='once',
name=order.coupon.code
)
# Save the Stripe coupon ID for future use
stripe_coupon_id = stripe_coupon.id
order.coupon.stripe_coupon_id = stripe_coupon_id
order.coupon.save()
# Add the coupon to the checkout session
discounts = [{'coupon': stripe_coupon_id}]
except Exception as e:
print(f"Error creating Stripe coupon: {str(e)}")
# Continue without the discount if there was an error
session_params = {
'customer_email': customer_email,
'line_items': line_items,
'success_url': success_url,
'cancel_url': cancel_url,
'metadata': metadata,
}
# Add discounts if available
if discounts:
session_params['discounts'] = discounts
try:
# Use the service to create the session
checkout_session = stripe_service.create_checkout_session(
customer_email=customer_email,
line_items=line_items,
success_url=success_url,
cancel_url=cancel_url,
metadata=metadata,
)
checkout_session = stripe_service.create_checkout_session(**session_params)
# Save the checkout session ID to the order
order.stripe_checkout_session_id = checkout_session.id
@ -172,11 +214,34 @@ def create_order(request):
if total_price <= 0:
return None
# Check for applied coupon
coupon = None
discount_amount = 0
if 'coupon_id' in request.session:
try:
coupon_id = request.session['coupon_id']
coupon = Coupon.objects.get(id=coupon_id)
# Validate coupon
if coupon.is_valid():
# Calculate discount
discount_amount = coupon.get_discount_amount_for_total(total_price)
else:
# Coupon is no longer valid, remove from session
del request.session['coupon_id']
coupon = None
except Coupon.DoesNotExist:
# Invalid coupon in session, remove it
del request.session['coupon_id']
if request.user.is_authenticated:
# Authenticated user order
order = Order.objects.create(
user=request.user,
total_price=total_price,
coupon=coupon,
discount_amount=discount_amount,
stripe_mode=stripe_service.mode # Add this line
)
@ -187,6 +252,8 @@ def create_order(request):
order = Order.objects.create(
guest_user=guest_user,
total_price=total_price,
coupon=coupon,
discount_amount=discount_amount,
stripe_mode=stripe_service.mode # Add this line
)
@ -194,6 +261,8 @@ def create_order(request):
# No guest user information, create order without user
order = Order.objects.create(
total_price=total_price,
coupon=coupon,
discount_amount=discount_amount,
stripe_mode=stripe_service.mode # Add this line
)
@ -208,6 +277,27 @@ def create_order(request):
price=cart_item.product.price
)
# Record coupon usage if applicable
if coupon:
guest_email = None
if not request.user.is_authenticated and 'guest_email' in request.session:
guest_email = request.session['guest_email']
CouponUsage.objects.create(
coupon=coupon,
user=request.user if request.user.is_authenticated else None,
order=order,
guest_email=guest_email
)
# Update coupon usage count
coupon.current_uses += 1
coupon.save()
# Remove coupon from session after using it
if 'coupon_id' in request.session:
del request.session['coupon_id']
# Note: Cart is not cleared here, only after successful payment
return order
@ -334,10 +424,16 @@ def payment_success(request, order_id):
order_items = order.items.all()
display_data = prepare_item_display_data(order_items, is_cart=False)
return render(request, 'shop/payment_success.html', {
# Add discount information to the context
context = {
'order': order,
'display_data': display_data,
})
'has_discount': order.discount_amount > 0,
'discount_amount': order.discount_amount,
'total_after_discount': order.get_total_after_discount(),
}
return render(request, 'shop/payment_success.html', context)
def payment_cancel(request, order_id):
"""Handle cancelled payment"""
@ -604,3 +700,55 @@ def handle_payment_intent_failed(payment_intent):
except Order.DoesNotExist:
print(f"Error: Order {order_id} not found for payment intent {payment_intent.id}")
@require_POST
def apply_coupon(request):
"""Appliquer un code promo à la session actuelle"""
code = request.POST.get('code')
try:
coupon = Coupon.objects.get(code__iexact=code, is_active=True)
# Vérifier si le coupon est valide
now = timezone.now()
if now < coupon.valid_from or now > coupon.valid_to:
return JsonResponse({'status': 'error', 'message': 'Ce code promo a expiré'})
# Vérifier la limite d'utilisation
if coupon.max_uses > 0 and coupon.current_uses >= coupon.max_uses:
return JsonResponse({'status': 'error', 'message': 'Ce code promo a atteint sa limite d\'utilisation'})
# Stocker le coupon dans la session
request.session['coupon_id'] = coupon.id
# Calculer la remise et le nouveau total
cart_items = cart.get_cart_items(request)
total_price = cart.get_cart_total(request)
if not cart_items.exists() or total_price <= 0:
return JsonResponse({'status': 'error', 'message': 'Votre panier est vide'})
discount_amount = coupon.get_discount_amount_for_total(total_price)
new_total = max(total_price - discount_amount, 0)
# Formater le message en fonction du type de remise
if coupon.discount_percent > 0:
message = f"Remise de {coupon.discount_percent}% appliquée"
else:
message = f"Remise de {coupon.discount_amount} € appliquée"
return JsonResponse({
'status': 'success',
'message': message,
'discount': float(discount_amount),
'new_total': float(new_total)
})
except Coupon.DoesNotExist:
return JsonResponse({'status': 'error', 'message': 'Code promo invalide'})
@require_POST
def remove_coupon(request):
"""Remove the applied coupon from the session"""
if 'coupon_id' in request.session:
del request.session['coupon_id']
return JsonResponse({'status': 'success', 'message': 'Coupon supprimé'})

Loading…
Cancel
Save