You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
999 lines
36 KiB
999 lines
36 KiB
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, Coupon, CouponUsage
|
|
from django.db.models import Sum
|
|
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 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 .forms import ShippingAddressForm
|
|
import json # Add this with your other imports
|
|
|
|
from . import cart
|
|
|
|
# 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': str(order.id),
|
|
}
|
|
|
|
# Set up customer information
|
|
customer_email = None
|
|
|
|
# Add user info to metadata if available
|
|
if request.user.is_authenticated:
|
|
metadata['user_id'] = str(request.user.id)
|
|
customer_email = request.user.email
|
|
elif order.guest_user:
|
|
metadata['guest_email'] = order.guest_user.email
|
|
customer_email = order.guest_user.email
|
|
elif 'guest_email' in request.session:
|
|
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(**session_params)
|
|
|
|
# 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:
|
|
print(f"Stripe error: {str(e)}")
|
|
raise
|
|
|
|
# View functions
|
|
def product_list(request):
|
|
products = Product.objects.all()
|
|
cart_items = cart.get_cart_items(request)
|
|
total = cart.get_cart_total(request)
|
|
return render(request, 'shop/product_list.html', {
|
|
'products': products,
|
|
'cart_items': cart_items,
|
|
'total': total
|
|
})
|
|
|
|
def view_cart(request):
|
|
"""Display the shopping cart"""
|
|
cart_items = cart.get_cart_items(request)
|
|
total = cart.get_cart_total(request)
|
|
total_quantity = cart_items.aggregate(total_quantity=Sum('quantity'))['total_quantity']
|
|
display_data = prepare_item_display_data(cart_items, is_cart=True)
|
|
|
|
context = {
|
|
'display_data': display_data,
|
|
'total': total,
|
|
'total_quantity': total_quantity,
|
|
'settings': settings,
|
|
}
|
|
|
|
# Add shipping form and Stripe key for authenticated users
|
|
if request.user.is_authenticated:
|
|
context.update({
|
|
'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY,
|
|
'shipping_form': ShippingAddressForm()
|
|
})
|
|
|
|
return render(request, 'shop/cart.html', context)
|
|
|
|
@ensure_csrf_cookie
|
|
def add_to_cart_view(request, product_id):
|
|
"""Add a product to the cart"""
|
|
product = get_object_or_404(Product, id=product_id)
|
|
quantity = int(request.POST.get('quantity', 1))
|
|
color_id = request.POST.get('color')
|
|
size_id = request.POST.get('size')
|
|
|
|
cart_item = cart.add_to_cart(request, product_id, quantity, color_id, size_id)
|
|
message = f'{cart_item.quantity} x {product.title} dans le panier',
|
|
|
|
# Check if this is an AJAX request
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
cart_items = cart.get_cart_items(request)
|
|
total = cart.get_cart_total(request)
|
|
return JsonResponse({
|
|
'success': True,
|
|
'message': message,
|
|
'cart_total': total,
|
|
'cart_count': cart_items.count()
|
|
})
|
|
|
|
# For non-AJAX requests, fall back to the original behavior
|
|
messages.success(request, message)
|
|
return redirect('shop:product_list')
|
|
|
|
def update_cart_view(request, product_id):
|
|
"""Update cart item quantity"""
|
|
if request.method == 'POST':
|
|
quantity = int(request.POST.get('quantity', 0))
|
|
cart.update_cart_item(request, product_id, quantity)
|
|
return redirect('shop:view_cart')
|
|
|
|
def remove_from_cart_view(request, product_id):
|
|
"""Remove item from cart"""
|
|
cart.remove_from_cart(request, product_id)
|
|
return redirect('shop:view_cart')
|
|
|
|
def clear_cart(request):
|
|
"""Clear the cart"""
|
|
cart.clear_cart(request)
|
|
messages.success(request, "Your cart has been cleared.")
|
|
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
|
|
|
|
# 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
|
|
|
|
)
|
|
else:
|
|
# Guest user order
|
|
try:
|
|
guest_user = GuestUser.objects.get(email=request.session['guest_email'])
|
|
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
|
|
|
|
)
|
|
except (KeyError, GuestUser.DoesNotExist):
|
|
# 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
|
|
)
|
|
|
|
# Create order items
|
|
for cart_item in cart_items:
|
|
OrderItem.objects.create(
|
|
order=order,
|
|
product=cart_item.product,
|
|
quantity=cart_item.quantity,
|
|
color=cart_item.color,
|
|
size=cart_item.size,
|
|
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
|
|
|
|
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.status = OrderStatus.PENDING
|
|
order.payment_status = 'PENDING'
|
|
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')
|
|
|
|
display_data = prepare_item_display_data(order_items, is_cart=False)
|
|
|
|
# Render payment page
|
|
return render(request, 'shop/payment.html', {
|
|
'order': order,
|
|
'display_data': display_data,
|
|
'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)
|
|
|
|
# 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()
|
|
display_data = prepare_item_display_data(order_items, is_cart=False)
|
|
|
|
# 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"""
|
|
order = get_object_or_404(Order, id=order_id)
|
|
|
|
# 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})
|
|
|
|
@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)
|
|
|
|
# Parse shipping address data from request
|
|
try:
|
|
data = json.loads(request.body)
|
|
shipping_data = data.get('shipping_address', {})
|
|
except json.JSONDecodeError:
|
|
shipping_data = {}
|
|
|
|
# Validate shipping address
|
|
shipping_form = ShippingAddressForm(shipping_data)
|
|
if not shipping_form.is_valid():
|
|
return JsonResponse({'error': 'Invalid shipping address'}, status=400)
|
|
|
|
# Save shipping address
|
|
shipping_address = shipping_form.save()
|
|
|
|
# Create the order with shipping address
|
|
order = create_order(request)
|
|
if not order:
|
|
return JsonResponse({'error': 'Could not create order from cart'}, status=400)
|
|
|
|
# Attach shipping address to order
|
|
order.shipping_address = shipping_address
|
|
order.save()
|
|
|
|
# Create line items and checkout session as before
|
|
order_items = order.items.all()
|
|
line_items = _create_stripe_line_items(order_items)
|
|
|
|
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)
|
|
|
|
@require_POST
|
|
def update_cart_item(request):
|
|
"""Update a cart item quantity (increase/decrease)"""
|
|
item_id = request.POST.get('item_id')
|
|
action = request.POST.get('action')
|
|
|
|
try:
|
|
cart_item = cart.get_cart_item(request, item_id)
|
|
|
|
if action == 'increase':
|
|
# Increase quantity by 1
|
|
cart_item.quantity += 1
|
|
cart_item.save()
|
|
messages.success(request, "Quantity increased.")
|
|
|
|
elif action == 'decrease' and cart_item.quantity > 1:
|
|
# Decrease quantity by 1
|
|
cart_item.quantity -= 1
|
|
cart_item.save()
|
|
messages.success(request, "Quantity decreased.")
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"Error updating cart: {str(e)}")
|
|
|
|
return redirect('shop:view_cart')
|
|
|
|
@require_POST
|
|
def remove_from_cart(request):
|
|
"""Remove an item from cart by item_id"""
|
|
item_id = request.POST.get('item_id')
|
|
|
|
try:
|
|
cart_item = cart.get_cart_item(request, item_id)
|
|
cart_item.delete()
|
|
messages.success(request, "Item removed from cart.")
|
|
except Exception as e:
|
|
messages.error(request, f"Error removing item: {str(e)}")
|
|
|
|
return redirect('shop:view_cart')
|
|
|
|
def prepare_item_display_data(items, is_cart=True):
|
|
"""
|
|
Transform cart items or order items into a standardized format for display
|
|
|
|
Args:
|
|
items: QuerySet of CartItem or OrderItem
|
|
is_cart: True if items are CartItems, False if OrderItems
|
|
|
|
Returns:
|
|
Dictionary with standardized item data
|
|
"""
|
|
prepared_items = []
|
|
total_quantity = 0
|
|
total_price = 0
|
|
|
|
for item in items:
|
|
if is_cart:
|
|
# For CartItem
|
|
item_data = {
|
|
'id': item.id,
|
|
'product_title': item.product.title,
|
|
'product_description': item.product.description if item.product.description else None,
|
|
'color_name': item.color.name if item.color else 'N/A',
|
|
'color_hex': item.color.colorHex if item.color else '#FFFFFF',
|
|
'secondary_color_hex': item.color.secondary_hex_color if item.color and item.color.secondary_hex_color else None,
|
|
'size_name': item.size.name if item.size else 'N/A',
|
|
'quantity': item.quantity,
|
|
'total_price': item.get_total_price()
|
|
}
|
|
total_price += item.get_total_price()
|
|
else:
|
|
# For OrderItem
|
|
item_data = {
|
|
'id': item.id,
|
|
'product_title': item.product.title,
|
|
'product_description': item.product.description if item.product.description else None,
|
|
'color_name': item.color.name if item.color else 'N/A',
|
|
'color_hex': item.color.colorHex if item.color else '#FFFFFF',
|
|
'secondary_color_hex': item.color.secondary_hex_color if item.color and item.color.secondary_hex_color else None,
|
|
'size_name': item.size.name if item.size else 'N/A',
|
|
'quantity': item.quantity,
|
|
'total_price': item.get_total_price()
|
|
}
|
|
total_price += item.get_total_price()
|
|
|
|
total_quantity += item.quantity
|
|
prepared_items.append(item_data)
|
|
|
|
return {
|
|
'items': prepared_items,
|
|
'total_quantity': total_quantity,
|
|
'total_price': total_price
|
|
}
|
|
|
|
|
|
def simulate_payment_success(request):
|
|
"""Debug function to simulate successful payment without Stripe"""
|
|
# Create an order from the cart
|
|
order = create_order(request)
|
|
|
|
if not order:
|
|
messages.error(request, "Could not create order from cart")
|
|
return redirect('shop:view_cart')
|
|
|
|
# Clear the cart
|
|
cart.clear_cart(request)
|
|
|
|
return redirect('shop:payment_success', order_id=order.id)
|
|
|
|
def simulate_payment_failure(request):
|
|
"""Debug function to simulate failed payment without Stripe"""
|
|
# Create an order from the cart
|
|
order = create_order(request)
|
|
|
|
if not order:
|
|
messages.error(request, "Could not create order from cart")
|
|
return redirect('shop:view_cart')
|
|
|
|
return redirect('shop:payment_cancel', order_id=order.id)
|
|
|
|
|
|
@require_POST
|
|
@csrf_exempt
|
|
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:
|
|
# Use the service to verify the webhook
|
|
event = stripe_service.verify_webhook_signature(payload, sig_header)
|
|
|
|
# Log the event type and mode
|
|
mode = "TEST" if stripe_service.is_test_mode else "LIVE"
|
|
print(f"{mode} webhook event type: {event['type']}")
|
|
|
|
except ValueError as e:
|
|
print(f"Invalid payload: {str(e)}")
|
|
return HttpResponse(status=400)
|
|
except stripe.error.SignatureVerificationError as e:
|
|
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}")
|
|
|
|
@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é'})
|
|
|
|
def my_orders(request):
|
|
"""Display all orders for the logged-in user"""
|
|
if not request.user.is_authenticated:
|
|
messages.error(request, "Vous devez être connecté pour voir vos commandes.")
|
|
return redirect('login')
|
|
|
|
# Get all orders for the current user, ordered by date (newest first)
|
|
orders = Order.objects.filter(user=request.user).order_by('-date_ordered')
|
|
|
|
return render(request, 'shop/my_orders.html', {
|
|
'orders': orders,
|
|
})
|
|
|
|
def order_detail(request, order_id):
|
|
"""Display details for a specific order"""
|
|
if not request.user.is_authenticated:
|
|
messages.error(request, "Vous devez être connecté pour voir vos commandes.")
|
|
return redirect('login')
|
|
|
|
# Get the order, ensuring it belongs to the current user
|
|
order = get_object_or_404(Order, id=order_id, user=request.user)
|
|
|
|
# Get order items
|
|
order_items = order.items.all()
|
|
|
|
# Calculate total quantity
|
|
total_quantity = sum(item.quantity for item in order_items)
|
|
|
|
# Transform order items to match the display format expected by the template
|
|
items_for_display = []
|
|
for item in order_items:
|
|
items_for_display.append({
|
|
'id': item.id,
|
|
'product_title': item.product.title,
|
|
'product_description': item.product.description,
|
|
'color_name': item.color.name if item.color else 'N/A',
|
|
'color_hex': item.color.colorHex if item.color else '#FFFFFF',
|
|
'secondary_color_hex': item.color.secondary_hex_color if item.color and item.color.secondary_hex_color else None,
|
|
'size_name': item.size.name if item.size else 'N/A',
|
|
'quantity': item.quantity,
|
|
'total_price': item.get_total_price()
|
|
})
|
|
|
|
shipping_form = ShippingAddressForm(instance=order.shipping_address)
|
|
|
|
return render(request, 'shop/order_detail.html', {
|
|
'order': order,
|
|
'order_items': items_for_display,
|
|
'total_quantity': total_quantity,
|
|
'shipping_form': shipping_form,
|
|
})
|
|
|
|
@require_POST
|
|
def cancel_order(request, order_id):
|
|
"""Cancel an order and process refund if applicable"""
|
|
if not request.user.is_authenticated:
|
|
messages.error(request, "Vous devez être connecté pour annuler une commande.")
|
|
return redirect('login')
|
|
|
|
order = get_object_or_404(Order, id=order_id, user=request.user)
|
|
|
|
# Check if order can be cancelled
|
|
if not order.is_cancellable():
|
|
messages.error(request, "Cette commande ne peut pas être annulée.")
|
|
return redirect('shop:my_orders')
|
|
|
|
print("Order cancellation initiated", order.status, order.stripe_payment_intent_id)
|
|
# Process refund for paid orders
|
|
if order.status == OrderStatus.PAID and order.stripe_payment_intent_id:
|
|
try:
|
|
# Attempt to refund through Stripe
|
|
refund = stripe_service.create_refund(
|
|
order.stripe_payment_intent_id,
|
|
reason='requested_by_customer'
|
|
)
|
|
|
|
# Update order status
|
|
order.status = OrderStatus.REFUNDED
|
|
order.payment_status = 'REFUNDED' # Or 'REFUNDED' if you add this status
|
|
order.save()
|
|
|
|
messages.success(request, "Votre commande a été annulée et remboursée avec succès.")
|
|
except Exception as e:
|
|
print(f"Refund error: {str(e)}")
|
|
messages.error(request, "Un problème est survenu lors du remboursement. Veuillez contacter le support.")
|
|
else:
|
|
# For pending orders, just cancel without refund
|
|
order.status = OrderStatus.CANCELED
|
|
order.save()
|
|
messages.success(request, "Votre commande a été annulée avec succès.")
|
|
|
|
# Redirect back to the page they came from, or order list
|
|
referring_page = request.META.get('HTTP_REFERER')
|
|
if referring_page and 'order_detail' in referring_page:
|
|
return redirect('shop:order_detail', order_id=order.id)
|
|
else:
|
|
return redirect('shop:my_orders')
|
|
|
|
@require_POST
|
|
def cancel_order_item(request, order_id, item_id):
|
|
"""Cancel a specific item in an order and process partial refund if applicable"""
|
|
if not request.user.is_authenticated:
|
|
messages.error(request, "Vous devez être connecté pour annuler un article.")
|
|
return redirect('login')
|
|
|
|
# Get the order and ensure it belongs to the user
|
|
order = get_object_or_404(Order, id=order_id, user=request.user)
|
|
|
|
# Check if order can be modified
|
|
if not order.is_cancellable():
|
|
messages.error(request, "Cette commande ne peut plus être modifiée.")
|
|
return redirect('shop:order_detail', order_id=order.id)
|
|
|
|
# Get the specific item
|
|
try:
|
|
order_item = order.items.get(id=item_id)
|
|
except OrderItem.DoesNotExist:
|
|
messages.error(request, "Article non trouvé.")
|
|
return redirect('shop:order_detail', order_id=order.id)
|
|
|
|
# Calculate refund amount for this item
|
|
item_total = order_item.get_total_price()
|
|
|
|
# If this is the last item, cancel the whole order
|
|
remaining_items = order.items.exclude(id=item_id)
|
|
if not remaining_items.exists():
|
|
return cancel_order(request, order_id)
|
|
|
|
# Process refund for paid orders
|
|
if order.status == OrderStatus.PAID and order.stripe_payment_intent_id:
|
|
try:
|
|
# Calculate proportional discount if any
|
|
if order.discount_amount > 0:
|
|
discount_ratio = item_total / order.total_price
|
|
item_discount = order.discount_amount * discount_ratio
|
|
refund_amount = item_total - item_discount
|
|
else:
|
|
refund_amount = item_total
|
|
|
|
# Create partial refund through Stripe
|
|
refund = stripe_service.create_refund(
|
|
order.stripe_payment_intent_id,
|
|
amount=int(refund_amount * 100), # Convert to cents
|
|
reason='requested_by_customer'
|
|
)
|
|
|
|
# Update order totals
|
|
order.total_price -= item_total
|
|
if order.discount_amount > 0:
|
|
order.discount_amount -= item_discount
|
|
order.save()
|
|
|
|
# Delete the item
|
|
order_item.delete()
|
|
|
|
messages.success(request, "L'article a été annulé et remboursé avec succès.")
|
|
|
|
except Exception as e:
|
|
print(f"Refund error: {str(e)}")
|
|
messages.error(request, "Un problème est survenu lors du remboursement. Veuillez contacter le support.")
|
|
else:
|
|
# For pending orders, just remove the item and update totals
|
|
order.total_price -= item_total
|
|
order.save()
|
|
order_item.delete()
|
|
messages.success(request, "L'article a été annulé avec succès.")
|
|
|
|
return redirect('shop:order_detail', order_id=order.id)
|
|
|
|
def checkout_view(request):
|
|
if request.method == 'POST':
|
|
guest_form = GuestCheckoutForm(request.POST)
|
|
shipping_form = ShippingAddressForm(request.POST)
|
|
|
|
if guest_form.is_valid() and shipping_form.is_valid():
|
|
guest_user = guest_form.save()
|
|
shipping_address = shipping_form.save()
|
|
|
|
# Create order with shipping address
|
|
order = Order.objects.create(
|
|
guest_user=guest_user,
|
|
shipping_address=shipping_address,
|
|
# ... other order fields
|
|
)
|
|
|
|
return redirect('shop:order_detail', order_id=order.id)
|
|
else:
|
|
guest_form = GuestCheckoutForm()
|
|
shipping_form = ShippingAddressForm()
|
|
|
|
context = {
|
|
'form': guest_form,
|
|
'shipping_form': shipping_form,
|
|
}
|
|
return render(request, 'shop/checkout.html', context)
|
|
|
|
def update_shipping_address(request, order_id):
|
|
order = get_object_or_404(Order, id=order_id)
|
|
|
|
# Check if order can be edited
|
|
if not order.shipping_address_can_be_edited():
|
|
messages.error(request, "L'adresse ne peut plus être modifiée pour cette commande.")
|
|
return redirect('shop:order_detail', order_id=order.id)
|
|
|
|
if request.method == 'POST':
|
|
form = ShippingAddressForm(request.POST, instance=order.shipping_address)
|
|
print("Shipping address")
|
|
if form.is_valid():
|
|
shipping_address = form.save()
|
|
print("Shipping address updated")
|
|
if not order.shipping_address:
|
|
order.shipping_address = shipping_address
|
|
order.save()
|
|
messages.success(request, "L'adresse de livraison a été mise à jour.")
|
|
return redirect('shop:order_detail', order_id=order.id)
|
|
else:
|
|
form = ShippingAddressForm(instance=order.shipping_address)
|
|
|
|
context = {
|
|
'form': form,
|
|
'order': order
|
|
}
|
|
return redirect('shop:order_detail', order_id=order.id)
|
|
|