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.
 
 
 
 
padelclub_backend/shop/views.py

997 lines
35 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)
shipping_address = None
# 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 shipping_form.is_valid():
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)