272 lines
8.9 KiB
Python
272 lines
8.9 KiB
Python
from datetime import timedelta
|
|
from decimal import Decimal
|
|
import re
|
|
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
|
|
from cart.views import get_cart, save_cart
|
|
from products.models import Product
|
|
|
|
from .models import Order, OrderItem
|
|
|
|
|
|
STATUS_SEQUENCE = ['Pending', 'Paid', 'Shipped', 'Delivered']
|
|
|
|
|
|
def _build_timeline(status):
|
|
current_index = STATUS_SEQUENCE.index(status) if status in STATUS_SEQUENCE else 0
|
|
timeline = []
|
|
for idx, step in enumerate(STATUS_SEQUENCE):
|
|
state = 'done' if idx < current_index else 'current' if idx == current_index else 'upcoming'
|
|
timeline.append({'label': step, 'state': state})
|
|
return timeline
|
|
|
|
|
|
def _sync_demo_status(order):
|
|
elapsed = timezone.now() - order.created_at
|
|
if elapsed >= timedelta(minutes=3):
|
|
target = 'Delivered'
|
|
elif elapsed >= timedelta(minutes=2):
|
|
target = 'Shipped'
|
|
elif elapsed >= timedelta(minutes=1):
|
|
target = 'Paid'
|
|
else:
|
|
target = 'Pending'
|
|
|
|
current_index = STATUS_SEQUENCE.index(order.status) if order.status in STATUS_SEQUENCE else 0
|
|
target_index = STATUS_SEQUENCE.index(target)
|
|
if target_index > current_index:
|
|
order.status = target
|
|
order.save(update_fields=['status'])
|
|
|
|
|
|
def _get_checkout_source(request):
|
|
buy_now = request.session.get('buy_now', {})
|
|
if isinstance(buy_now, dict) and buy_now:
|
|
return buy_now, 'buy_now'
|
|
return get_cart(request), 'cart'
|
|
|
|
|
|
def _clear_checkout_source(request, source_key):
|
|
if source_key == 'buy_now':
|
|
request.session.pop('buy_now', None)
|
|
request.session.modified = True
|
|
else:
|
|
save_cart(request, {})
|
|
|
|
|
|
def _validate_phone(phone):
|
|
digits = re.sub(r'\D+', '', phone)
|
|
return 7 <= len(digits) <= 15
|
|
|
|
|
|
@login_required
|
|
def checkout(request):
|
|
source_items, source_key = _get_checkout_source(request)
|
|
if not source_items:
|
|
messages.warning(request, 'Your cart is empty. Add products before checking out.')
|
|
return redirect('cart')
|
|
|
|
cart_products = []
|
|
total = Decimal('0')
|
|
|
|
for id, qty in source_items.items():
|
|
try:
|
|
product_id = int(id)
|
|
quantity = int(qty)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
|
|
if quantity <= 0:
|
|
continue
|
|
|
|
product = Product.objects.filter(id=product_id).first()
|
|
if not product:
|
|
messages.error(request, 'One of the selected products is no longer available.')
|
|
_clear_checkout_source(request, source_key)
|
|
return redirect('cart')
|
|
|
|
if quantity > product.stock:
|
|
if source_key == 'buy_now':
|
|
messages.error(request, f'Only {product.stock} units of {product.name} are available.')
|
|
_clear_checkout_source(request, source_key)
|
|
return redirect('product_detail', product_id=product.id)
|
|
|
|
messages.error(request, f'Only {product.stock} units of {product.name} are available.')
|
|
return redirect('cart')
|
|
|
|
product.qty = quantity
|
|
product.subtotal = product.display_price * quantity
|
|
total += product.subtotal
|
|
cart_products.append(product)
|
|
|
|
if not cart_products:
|
|
messages.warning(request, 'No valid products found for checkout.')
|
|
_clear_checkout_source(request, source_key)
|
|
return redirect('cart')
|
|
|
|
if request.method == 'POST':
|
|
full_name = request.POST.get('full_name', '').strip()
|
|
phone = request.POST.get('phone', '').strip()
|
|
address = request.POST.get('address', '').strip()
|
|
|
|
if not full_name or not phone or not address:
|
|
messages.error(request, 'Please fill in full name, phone, and address.')
|
|
return render(
|
|
request,
|
|
'order/checkout.html',
|
|
{
|
|
'products': cart_products,
|
|
'total': total,
|
|
'delivery': {
|
|
'full_name': full_name,
|
|
'phone': phone,
|
|
'address': address,
|
|
},
|
|
},
|
|
)
|
|
|
|
if not _validate_phone(phone):
|
|
messages.error(request, 'Please enter a valid phone number.')
|
|
return render(
|
|
request,
|
|
'order/checkout.html',
|
|
{
|
|
'products': cart_products,
|
|
'total': total,
|
|
'delivery': {
|
|
'full_name': full_name,
|
|
'phone': phone,
|
|
'address': address,
|
|
},
|
|
},
|
|
)
|
|
|
|
order = Order.objects.create(
|
|
user=request.user,
|
|
total_price=total,
|
|
status='Pending',
|
|
full_name=full_name,
|
|
phone=phone,
|
|
address=address,
|
|
)
|
|
|
|
for product in cart_products:
|
|
OrderItem.objects.create(order=order, product=product, quantity=product.qty, price=product.display_price)
|
|
product.stock = max(product.stock - product.qty, 0)
|
|
product.save(update_fields=['stock'])
|
|
|
|
_clear_checkout_source(request, source_key)
|
|
messages.success(request, 'Order created successfully. Complete payment to finish checkout.')
|
|
return redirect('payment', order_id=order.id)
|
|
|
|
return render(
|
|
request,
|
|
'order/checkout.html',
|
|
{
|
|
'products': cart_products,
|
|
'total': total,
|
|
'delivery': {
|
|
'full_name': request.user.get_full_name(),
|
|
'phone': '',
|
|
'address': '',
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
GATEWAY_OPTIONS = {
|
|
'esewa': {
|
|
'name': 'eSewa',
|
|
'description': 'Pay securely using eSewa mobile wallet or QR code.',
|
|
'button_text': 'Pay with eSewa',
|
|
},
|
|
'khalti': {
|
|
'name': 'Khalti',
|
|
'description': 'Complete payment instantly with Khalti.',
|
|
'button_text': 'Pay with Khalti',
|
|
},
|
|
'fonpay': {
|
|
'name': 'Fonpay',
|
|
'description': 'Use Fonpay to pay from your mobile wallet.',
|
|
'button_text': 'Pay with Fonpay',
|
|
},
|
|
}
|
|
|
|
|
|
@login_required
|
|
def payment_page(request, order_id):
|
|
order = get_object_or_404(Order, id=order_id, user=request.user)
|
|
|
|
if order.status == 'Paid':
|
|
messages.info(request, 'This order has already been paid.')
|
|
return redirect('order_detail', order_id=order.id)
|
|
|
|
if request.method == 'POST':
|
|
method = request.POST.get('payment')
|
|
if not method:
|
|
messages.error(request, 'Please select a payment method.')
|
|
return redirect('payment', order_id=order.id)
|
|
|
|
method_key = method.lower().replace(' ', '')
|
|
if method_key in GATEWAY_OPTIONS:
|
|
return redirect('payment_gateway', order_id=order.id, gateway=method_key)
|
|
|
|
if method == 'Cash on Delivery':
|
|
order.payment_method = method
|
|
order.status = 'Paid'
|
|
order.save(update_fields=['payment_method', 'status'])
|
|
messages.success(request, 'Cash on Delivery selected. Your order is confirmed.')
|
|
return redirect('success')
|
|
|
|
messages.error(request, 'Selected payment method is not supported.')
|
|
return redirect('payment', order_id=order.id)
|
|
|
|
return render(request, 'order/payment.html', {'order': order, 'gateways': GATEWAY_OPTIONS})
|
|
|
|
|
|
@login_required
|
|
def payment_gateway(request, order_id, gateway):
|
|
order = get_object_or_404(Order, id=order_id, user=request.user)
|
|
|
|
if order.status == 'Paid':
|
|
messages.info(request, 'This order has already been paid.')
|
|
return redirect('order_detail', order_id=order.id)
|
|
|
|
gateway_data = GATEWAY_OPTIONS.get(gateway)
|
|
if gateway_data is None:
|
|
messages.error(request, 'Selected payment gateway is not available.')
|
|
return redirect('payment', order_id=order.id)
|
|
|
|
if request.method == 'POST':
|
|
order.payment_method = gateway_data['name']
|
|
order.status = 'Paid'
|
|
order.save(update_fields=['payment_method', 'status'])
|
|
messages.success(request, f'Payment completed through {gateway_data["name"]}.')
|
|
return redirect('success')
|
|
|
|
return render(request, 'order/payment_gateway.html', {'order': order, 'gateway': gateway_data})
|
|
|
|
|
|
def success(request):
|
|
return render(request, 'order/success.html')
|
|
|
|
|
|
@login_required
|
|
def my_orders(request):
|
|
orders = list(Order.objects.filter(user=request.user).order_by('-created_at'))
|
|
for order in orders:
|
|
_sync_demo_status(order)
|
|
return render(request, 'order/my_orders.html', {'orders': orders})
|
|
|
|
|
|
@login_required
|
|
def order_detail(request, order_id):
|
|
order = get_object_or_404(Order, id=order_id, user=request.user)
|
|
_sync_demo_status(order)
|
|
timeline = _build_timeline(order.status)
|
|
return render(request, 'order/order_detail.html', {'order': order, 'timeline': timeline})
|