2026-05-20 10:50:30 +00:00

1018 lines
38 KiB
Python

import logging
from decimal import Decimal
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from accounts.delivery import (
build_delivery_payload,
delivery_payload_from_profile,
derive_location_label,
parse_decimal_value,
phone_has_valid_digits,
save_profile_delivery_defaults,
)
from cart.views import get_cart, save_cart
from products.models import Product
from .models import Order, OrderItem
from .payments import (
PaymentGatewayError,
build_esewa_redirect,
construct_webhook_event,
create_checkout_session,
esewa_configured,
initiate_khalti_payment,
khalti_configured,
payment_currency,
retrieve_checkout_session,
stripe_configured,
stripe_value,
verify_esewa_payment,
verify_khalti_payment,
)
logger = logging.getLogger(__name__)
STATUS_SEQUENCE = ['Pending', 'Paid', 'Shipped', 'Delivered']
UNSET = object()
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 _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 _build_delivery_shortcuts(user, profile, *, limit=4):
shortcuts = []
seen = set()
default_full_name = user.get_full_name() or user.username
def add_shortcut(
identifier,
*,
badge,
title,
note='',
full_name='',
phone='',
address='',
location_label='',
delivery_notes='',
latitude=None,
longitude=None,
location_accuracy_m=None,
):
payload = build_delivery_payload(
full_name=full_name,
phone=phone,
address=address,
location_label=location_label,
delivery_notes=delivery_notes,
latitude=latitude,
longitude=longitude,
location_accuracy_m=location_accuracy_m,
save_as_default=True,
)
dedupe_key = (
payload['phone'].lower(),
payload['address'].lower(),
payload['location_label'].lower(),
)
if not payload['address'] or dedupe_key in seen:
return
seen.add(dedupe_key)
shortcuts.append(
{
'id': identifier,
'badge': badge,
'title': title,
'note': note,
'full_name': payload['full_name'],
'phone': payload['phone'],
'address': payload['address'],
'location_label': payload['location_label'],
'delivery_notes': payload['delivery_notes'],
'latitude': payload['latitude'],
'longitude': payload['longitude'],
'location_accuracy_m': payload['location_accuracy_m'],
'has_precise_location': bool(payload['latitude'] and payload['longitude']),
'selected': False,
}
)
if profile is not None and (profile.phone or profile.formatted_delivery_address):
add_shortcut(
'profile-default',
badge='Saved profile',
title=profile.short_location or 'Default account address',
note='Use your currently saved checkout defaults.',
full_name=default_full_name,
phone=profile.phone,
address=profile.formatted_delivery_address,
location_label=profile.short_location,
latitude=profile.latitude,
longitude=profile.longitude,
location_accuracy_m=profile.location_accuracy_m,
)
recent_orders = (
Order.objects.filter(user=user)
.exclude(address='')
.order_by('-created_at')
.only(
'id',
'full_name',
'phone',
'address',
'location_label',
'delivery_notes',
'latitude',
'longitude',
'location_accuracy_m',
'created_at',
)
)
for order in recent_orders:
add_shortcut(
f'order-{order.id}',
badge=f'Order #{order.id}',
title=order.location_label or order.address.splitlines()[0].strip() or f'Order #{order.id}',
note=f'Last used {order.created_at:%b %d, %Y}.',
full_name=order.full_name or default_full_name,
phone=order.phone,
address=order.address,
location_label=order.location_label,
delivery_notes=order.delivery_notes,
latitude=order.latitude,
longitude=order.longitude,
location_accuracy_m=order.location_accuracy_m,
)
if len(shortcuts) >= limit:
break
return shortcuts
def _resolve_delivery_shortcut(shortcuts, shortcut_id):
if not shortcut_id:
return None
return next((shortcut for shortcut in shortcuts if shortcut['id'] == shortcut_id), None)
def _set_selected_delivery_shortcut(shortcuts, shortcut_id):
for shortcut in shortcuts:
shortcut['selected'] = shortcut['id'] == shortcut_id
def _render_checkout(request, *, cart_products, total, delivery, delivery_shortcuts):
return render(
request,
'order/checkout.html',
{
'products': cart_products,
'total': total,
'delivery': delivery,
'delivery_shortcuts': delivery_shortcuts,
},
)
def _save_order_updates(order, **changes):
update_fields = []
for field, value in changes.items():
if value is UNSET:
continue
if getattr(order, field) != value:
setattr(order, field, value)
update_fields.append(field)
if update_fields:
order.save(update_fields=update_fields)
def _mark_order_paid(
order,
*,
payment_method=UNSET,
payment_provider=UNSET,
payment_reference=UNSET,
payment_session_id=UNSET,
payment_currency_value=UNSET,
):
_save_order_updates(
order,
status='Paid',
payment_status='Paid',
payment_method=payment_method,
payment_provider=payment_provider,
payment_reference=payment_reference,
payment_session_id=payment_session_id,
payment_currency=payment_currency_value,
paid_at=order.paid_at or timezone.now(),
)
def _mark_order_paid_from_stripe(order, session):
payment_reference = stripe_value(session, 'payment_intent', '') or UNSET
payment_session_id = stripe_value(session, 'id', '') or UNSET
payment_currency_value = (stripe_value(session, 'currency', '') or settings.STRIPE_CURRENCY).upper()
_mark_order_paid(
order,
payment_method='Card / Wallet',
payment_provider='stripe',
payment_reference=payment_reference,
payment_session_id=payment_session_id,
payment_currency_value=payment_currency_value,
)
def _build_payment_options(order):
stripe_ready = stripe_configured()
esewa_ready = esewa_configured()
khalti_ready = khalti_configured()
options = [
{
'value': 'esewa',
'title': 'eSewa ePay',
'badge': 'Sandbox / UAT' if settings.ESEWA_SANDBOX else 'Live gateway',
'badge_muted': not esewa_ready,
'description': 'Redirect to the official eSewa wallet page and verify the transaction on return before marking the order paid.',
'note': (
'Ready to test with the official eSewa sandbox merchant.'
if esewa_ready and settings.ESEWA_SANDBOX
else 'Add your live eSewa merchant code and secret to switch this gateway to production.'
if esewa_ready
else 'Configure eSewa credentials to enable this wallet.'
),
'ready': esewa_ready,
},
{
'value': 'khalti',
'title': 'Khalti',
'badge': 'KPG-2',
'badge_muted': not khalti_ready,
'description': 'Create a real Khalti payment session on the server and verify its final status using the lookup API.',
'note': (
'Ready with your configured Khalti secret key.'
if khalti_ready
else 'Add KHALTI_SECRET_KEY to enable Khalti ePayment.'
),
'ready': khalti_ready,
},
{
'value': 'fonepay',
'title': 'Fonepay',
'badge': 'Merchant onboarding',
'badge_muted': True,
'description': 'Reserved for Fonepay Business / Dynamic QR integration once merchant onboarding details are available.',
'note': 'Kept disabled so the app never claims a Fonepay payment succeeded without official verification.',
'ready': False,
},
]
if stripe_ready:
options.append(
{
'value': 'stripe',
'title': 'Card / Wallet',
'badge': 'Stripe Checkout',
'badge_muted': False,
'description': 'Fallback card checkout with Stripe, verified before the order is marked paid.',
'note': f'Charges are created in {settings.STRIPE_CURRENCY.upper()} and verified on return/webhook.',
'ready': True,
}
)
options.append(
{
'value': 'cod',
'title': 'Cash on Delivery',
'badge': 'Offline',
'badge_muted': True,
'description': 'Confirm the order now and collect payment when the package arrives.',
'note': 'You can still return later and switch to eSewa or Khalti before delivery.',
'ready': True,
}
)
ready_option_values = {option['value'] for option in options if option['ready']}
selected_payment = None
if order.payment_provider in ready_option_values:
selected_payment = order.payment_provider
elif order.payment_method == 'Cash on Delivery':
selected_payment = 'cod'
else:
selected_payment = next((option['value'] for option in options if option['ready'] and option['value'] != 'cod'), 'cod')
for option in options:
option['checked'] = option['value'] == selected_payment
online_ready = any(option['ready'] and option['value'] != 'cod' for option in options)
return {
'payment_options': options,
'online_ready': online_ready,
'stripe_ready': stripe_ready,
'esewa_ready': esewa_ready,
'khalti_ready': khalti_ready,
}
@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')
profile = getattr(request.user, 'profile', None)
delivery_shortcuts = _build_delivery_shortcuts(request.user, profile)
selected_shortcut_id = (
request.POST.get('selected_shortcut_id', '').strip()
if request.method == 'POST'
else request.GET.get('delivery_shortcut', '').strip()
)
selected_shortcut = _resolve_delivery_shortcut(delivery_shortcuts, selected_shortcut_id)
if selected_shortcut is None:
selected_shortcut_id = ''
_set_selected_delivery_shortcut(delivery_shortcuts, selected_shortcut_id)
if request.method == 'POST':
full_name = request.POST.get('full_name', '').strip()
phone = request.POST.get('phone', '').strip()
address = request.POST.get('address', '').strip()
location_label = request.POST.get('location_label', '').strip()
delivery_notes = request.POST.get('delivery_notes', '').strip()
save_as_default = request.POST.get('save_as_default') == 'on'
try:
latitude = parse_decimal_value(
request.POST.get('latitude'),
field_label='latitude',
minimum=Decimal('-90'),
maximum=Decimal('90'),
)
longitude = parse_decimal_value(
request.POST.get('longitude'),
field_label='longitude',
minimum=Decimal('-180'),
maximum=Decimal('180'),
)
location_accuracy_m = parse_decimal_value(
request.POST.get('location_accuracy_m'),
field_label='location accuracy',
minimum=Decimal('0'),
maximum=Decimal('100000'),
)
except ValueError as exc:
messages.error(request, str(exc))
return _render_checkout(
request,
cart_products=cart_products,
total=total,
delivery=build_delivery_payload(
full_name=full_name,
phone=phone,
address=address,
location_label=location_label,
delivery_notes=delivery_notes,
latitude=request.POST.get('latitude', ''),
longitude=request.POST.get('longitude', ''),
location_accuracy_m=request.POST.get('location_accuracy_m', ''),
save_as_default=save_as_default,
selected_shortcut_id=selected_shortcut_id,
),
delivery_shortcuts=delivery_shortcuts,
)
if (latitude is None) != (longitude is None):
messages.error(request, 'GPS location is incomplete. Please retry the location button or continue without GPS.')
return _render_checkout(
request,
cart_products=cart_products,
total=total,
delivery=build_delivery_payload(
full_name=full_name,
phone=phone,
address=address,
location_label=location_label,
delivery_notes=delivery_notes,
latitude=latitude,
longitude=longitude,
location_accuracy_m=location_accuracy_m,
save_as_default=save_as_default,
selected_shortcut_id=selected_shortcut_id,
),
delivery_shortcuts=delivery_shortcuts,
)
location_label = derive_location_label(location_label, address)
if not full_name or not phone or not address:
messages.error(request, 'Please fill in full name, phone, and address.')
return _render_checkout(
request,
cart_products=cart_products,
total=total,
delivery=build_delivery_payload(
full_name=full_name,
phone=phone,
address=address,
location_label=location_label,
delivery_notes=delivery_notes,
latitude=latitude,
longitude=longitude,
location_accuracy_m=location_accuracy_m,
save_as_default=save_as_default,
selected_shortcut_id=selected_shortcut_id,
),
delivery_shortcuts=delivery_shortcuts,
)
if not phone_has_valid_digits(phone):
messages.error(request, 'Please enter a valid phone number.')
return _render_checkout(
request,
cart_products=cart_products,
total=total,
delivery=build_delivery_payload(
full_name=full_name,
phone=phone,
address=address,
location_label=location_label,
delivery_notes=delivery_notes,
latitude=latitude,
longitude=longitude,
location_accuracy_m=location_accuracy_m,
save_as_default=save_as_default,
selected_shortcut_id=selected_shortcut_id,
),
delivery_shortcuts=delivery_shortcuts,
)
order = Order.objects.create(
user=request.user,
total_price=total,
status='Pending',
payment_status='Unpaid',
payment_method='Pending selection',
payment_currency=payment_currency(),
full_name=full_name,
phone=phone,
address=address,
location_label=location_label,
latitude=latitude,
longitude=longitude,
location_accuracy_m=location_accuracy_m,
delivery_notes=delivery_notes,
)
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'])
if save_as_default:
save_profile_delivery_defaults(
profile,
phone=phone,
address=address,
location_label=location_label,
latitude=latitude,
longitude=longitude,
location_accuracy_m=location_accuracy_m,
)
if location_label:
request.session['delivery_location'] = location_label
else:
request.session.pop('delivery_location', None)
_clear_checkout_source(request, source_key)
messages.success(request, 'Order created successfully. Complete the payment step to finish checkout.')
return redirect('payment', order_id=order.id)
initial_delivery = delivery_payload_from_profile(
profile,
full_name=request.user.get_full_name() or request.user.username,
selected_shortcut_id=selected_shortcut_id,
)
if selected_shortcut is not None:
initial_delivery = build_delivery_payload(
full_name=selected_shortcut['full_name'] or request.user.get_full_name() or request.user.username,
phone=selected_shortcut['phone'],
address=selected_shortcut['address'],
location_label=selected_shortcut['location_label'],
delivery_notes=selected_shortcut['delivery_notes'],
latitude=selected_shortcut['latitude'],
longitude=selected_shortcut['longitude'],
location_accuracy_m=selected_shortcut['location_accuracy_m'],
save_as_default=True,
selected_shortcut_id=selected_shortcut_id,
)
return _render_checkout(
request,
cart_products=cart_products,
total=total,
delivery=initial_delivery,
delivery_shortcuts=delivery_shortcuts,
)
@login_required
def payment_page(request, order_id):
order = get_object_or_404(Order, id=order_id, user=request.user)
payment_context = _build_payment_options(order)
if order.payment_status == 'Paid' or order.status == 'Paid':
messages.info(request, 'This order has already been paid.')
return redirect('order_detail', order_id=order.id)
if request.GET.get('cancelled'):
messages.warning(request, 'Payment was cancelled. Your order is still saved and waiting for payment.')
if request.method == 'POST':
method = request.POST.get('payment', '').strip()
if method == 'esewa':
if not payment_context['esewa_ready']:
messages.error(request, 'eSewa is not configured yet. Add the merchant credentials to enable it.')
return redirect('payment', order_id=order.id)
try:
gateway = build_esewa_redirect(request, order)
except PaymentGatewayError as exc:
messages.error(request, str(exc))
return redirect('payment', order_id=order.id)
except Exception:
logger.exception('Unable to start eSewa payment for order %s', order.id)
messages.error(request, 'We could not start the eSewa payment window right now. Please try again in a moment.')
return redirect('payment', order_id=order.id)
_save_order_updates(
order,
payment_method='eSewa',
payment_provider='esewa',
payment_status='Pending',
payment_reference='',
payment_session_id=gateway['session_id'],
payment_currency=gateway['currency'],
)
return render(
request,
'order/payment_redirect.html',
{
'order': order,
'gateway_name': 'eSewa',
'action_url': gateway['action_url'],
'fields': gateway['fields'],
},
)
if method == 'khalti':
if not payment_context['khalti_ready']:
messages.error(request, 'Khalti is not configured yet. Add the Khalti secret key to enable it.')
return redirect('payment', order_id=order.id)
try:
gateway = initiate_khalti_payment(request, order)
except PaymentGatewayError as exc:
messages.error(request, str(exc))
return redirect('payment', order_id=order.id)
except Exception:
logger.exception('Unable to start Khalti payment for order %s', order.id)
messages.error(request, 'We could not start Khalti right now. Please try again in a moment.')
return redirect('payment', order_id=order.id)
_save_order_updates(
order,
payment_method='Khalti',
payment_provider='khalti',
payment_status='Pending',
payment_reference=gateway['reference'],
payment_session_id=gateway['session_id'],
payment_currency=gateway['currency'],
)
return redirect(gateway['payment_url'])
if method == 'fonepay':
messages.error(
request,
'Fonepay automation is intentionally disabled until the official merchant onboarding/API details are available.',
)
return redirect('payment', order_id=order.id)
if method == 'stripe':
if not payment_context['stripe_ready']:
messages.error(request, 'Online card payments are not configured yet. Add a Stripe secret key to enable them.')
return redirect('payment', order_id=order.id)
try:
session = create_checkout_session(request, order)
except Exception:
logger.exception('Unable to start Stripe Checkout for order %s', order.id)
messages.error(request, 'We could not start the secure payment window right now. Please try again in a moment.')
return redirect('payment', order_id=order.id)
_save_order_updates(
order,
payment_method='Card / Wallet',
payment_provider='stripe',
payment_status='Pending',
payment_currency=settings.STRIPE_CURRENCY.upper(),
payment_session_id=stripe_value(session, 'id', '') or '',
)
checkout_url = stripe_value(session, 'url')
if not checkout_url:
messages.error(request, 'Stripe did not return a checkout URL. Please try again.')
return redirect('payment', order_id=order.id)
return redirect(checkout_url)
if method == 'cod':
_save_order_updates(
order,
payment_method='Cash on Delivery',
payment_provider='offline',
payment_status='Pending',
payment_reference='',
payment_session_id='',
payment_currency=payment_currency(),
)
messages.success(request, 'Cash on delivery selected. You can still switch to an online wallet later if you want.')
return redirect(f"{reverse('success')}?order_id={order.id}")
messages.error(request, 'Please choose a supported payment method.')
return redirect('payment', order_id=order.id)
return render(
request,
'order/payment.html',
{
'order': order,
'currency_code': payment_currency(),
'esewa_sandbox': settings.ESEWA_SANDBOX,
**payment_context,
},
)
@login_required
def payment_gateway(request, order_id, gateway):
messages.info(request, 'Use the payment selection page to start the gateway securely.')
return redirect('payment', order_id=order_id)
@login_required
def esewa_return(request):
order_id = request.GET.get('order_id')
order = get_object_or_404(Order, id=order_id, user=request.user)
if order.payment_status == 'Paid':
messages.info(request, 'This order has already been paid.')
return redirect(f"{reverse('success')}?order_id={order.id}")
if request.GET.get('failed'):
_save_order_updates(
order,
payment_method='eSewa',
payment_provider='esewa',
payment_status='Failed',
payment_currency=payment_currency(),
)
messages.warning(request, 'eSewa payment was not completed. Your order is still saved and waiting for payment.')
return redirect('payment', order_id=order.id)
try:
result = verify_esewa_payment(order, request.GET.get('data', ''))
except PaymentGatewayError as exc:
logger.warning('Unable to verify eSewa payment for order %s: %s', order.id, exc)
messages.error(request, str(exc))
return redirect('payment', order_id=order.id)
except Exception:
logger.exception('Unexpected eSewa verification error for order %s', order.id)
messages.error(request, 'We could not verify the eSewa payment yet. Please try again.')
return redirect('payment', order_id=order.id)
if result['status'] == 'Paid':
_mark_order_paid(
order,
payment_method='eSewa',
payment_provider='esewa',
payment_reference=result['reference'],
payment_session_id=result['session_id'],
payment_currency_value=result['currency'],
)
messages.success(request, result['message'])
return redirect(f"{reverse('success')}?order_id={order.id}")
if result['status'] == 'Pending':
_save_order_updates(
order,
payment_method='eSewa',
payment_provider='esewa',
payment_status='Pending',
payment_session_id=result['session_id'],
payment_currency=result['currency'],
)
messages.info(request, result['message'])
else:
_save_order_updates(
order,
payment_method='eSewa',
payment_provider='esewa',
payment_status='Failed',
payment_session_id=result['session_id'],
payment_currency=result['currency'],
)
messages.warning(request, result['message'])
return redirect('payment', order_id=order.id)
@login_required
def khalti_return(request):
order_id = request.GET.get('order_id')
order = get_object_or_404(Order, id=order_id, user=request.user)
if order.payment_status == 'Paid':
messages.info(request, 'This order has already been paid.')
return redirect(f"{reverse('success')}?order_id={order.id}")
pidx = request.GET.get('pidx', '').strip()
if not pidx:
_save_order_updates(
order,
payment_method='Khalti',
payment_provider='khalti',
payment_status='Failed',
payment_currency=payment_currency(),
)
messages.warning(request, 'Khalti payment was not completed. Your order is still saved and waiting for payment.')
return redirect('payment', order_id=order.id)
try:
result = verify_khalti_payment(order, pidx)
except PaymentGatewayError as exc:
logger.warning('Unable to verify Khalti payment for order %s: %s', order.id, exc)
messages.error(request, str(exc))
return redirect('payment', order_id=order.id)
except Exception:
logger.exception('Unexpected Khalti verification error for order %s', order.id)
messages.error(request, 'We could not verify the Khalti payment yet. Please try again.')
return redirect('payment', order_id=order.id)
if result['status'] == 'Paid':
_mark_order_paid(
order,
payment_method='Khalti',
payment_provider='khalti',
payment_reference=result['reference'],
payment_session_id=result['session_id'],
payment_currency_value=result['currency'],
)
messages.success(request, result['message'])
return redirect(f"{reverse('success')}?order_id={order.id}")
if result['status'] == 'Pending':
_save_order_updates(
order,
payment_method='Khalti',
payment_provider='khalti',
payment_status='Pending',
payment_session_id=result['session_id'],
payment_currency=result['currency'],
)
messages.info(request, result['message'])
else:
_save_order_updates(
order,
payment_method='Khalti',
payment_provider='khalti',
payment_status='Failed',
payment_session_id=result['session_id'],
payment_currency=result['currency'],
)
messages.warning(request, result['message'])
return redirect('payment', order_id=order.id)
@login_required
def success(request):
order_id = request.GET.get('order_id')
session_id = request.GET.get('session_id', '').strip()
if not order_id:
messages.info(request, 'Open an order to view its payment status.')
return redirect('my_orders')
order = get_object_or_404(Order, id=order_id, user=request.user)
payment_verified = order.payment_status == 'Paid'
verification_message = 'Your order has been created successfully.'
if session_id:
if not stripe_configured():
verification_message = 'The order exists, but Stripe is not configured on the server to verify the payment session.'
else:
try:
session = retrieve_checkout_session(session_id)
except Exception:
logger.exception('Unable to verify Stripe checkout session %s for order %s', session_id, order.id)
verification_message = 'We could not verify the payment yet. Please refresh this page or open the order details page.'
else:
metadata = stripe_value(session, 'metadata', {}) or {}
session_order_id = str(metadata.get('order_id') or stripe_value(session, 'client_reference_id', '') or '')
if session_order_id != str(order.id):
verification_message = 'The returned payment session did not match this order, so payment could not be confirmed.'
elif stripe_value(session, 'payment_status') == 'paid':
_mark_order_paid_from_stripe(order, session)
payment_verified = True
verification_message = 'Payment verified successfully. Your order is confirmed.'
else:
verification_message = 'Your order was created, but Stripe has not marked the payment as paid yet.'
elif order.payment_status == 'Paid':
payment_verified = True
verification_message = 'Payment verified successfully. Your order is confirmed.'
elif order.payment_status == 'Failed':
provider_name = order.payment_provider_label if order.payment_provider else 'payment gateway'
verification_message = f'The last {provider_name} attempt did not complete. You can retry from the payment page.'
elif order.payment_method == 'Cash on Delivery':
verification_message = 'Your order is confirmed. Payment will be collected on delivery, and you can still switch to an online wallet before shipment if needed.'
elif order.payment_provider == 'esewa':
verification_message = 'Your order was created, but eSewa has not marked the payment as complete yet.'
elif order.payment_provider == 'khalti':
verification_message = 'Your order was created, but Khalti has not marked the payment as complete yet.'
elif order.payment_provider == 'stripe':
verification_message = 'Your order was created, but Stripe has not marked the payment as complete yet.'
else:
verification_message = 'Your order is waiting for payment. You can return to the payment page any time.'
return render(
request,
'order/success.html',
{
'order': order,
'payment_verified': payment_verified,
'verification_message': verification_message,
},
)
@csrf_exempt
def stripe_webhook(request):
if request.method != 'POST':
return HttpResponse(status=405)
if not stripe_configured() or not settings.STRIPE_WEBHOOK_SECRET:
return HttpResponse('Stripe webhook is not configured.', status=503)
signature = request.META.get('HTTP_STRIPE_SIGNATURE', '')
try:
event = construct_webhook_event(request.body, signature)
except Exception:
logger.exception('Invalid Stripe webhook received')
return HttpResponse(status=400)
event_type = event.get('type', '')
session = event.get('data', {}).get('object', {})
metadata = stripe_value(session, 'metadata', {}) or {}
order_id = str(metadata.get('order_id') or stripe_value(session, 'client_reference_id', '') or '').strip()
order = Order.objects.filter(id=order_id).first() if order_id else None
if order and event_type in {'checkout.session.completed', 'checkout.session.async_payment_succeeded'}:
_mark_order_paid_from_stripe(order, session)
elif order and event_type == 'checkout.session.expired' and order.payment_status != 'Paid':
_save_order_updates(order, payment_status='Failed')
return HttpResponse(status=200)
@login_required
def my_orders(request):
status_filter = request.GET.get('status', '').strip()
payment_filter = request.GET.get('payment', '').strip()
valid_statuses = [choice[0] for choice in Order.STATUS_CHOICES]
valid_payment_statuses = [choice[0] for choice in Order.PAYMENT_STATUS_CHOICES]
if status_filter not in valid_statuses:
status_filter = ''
if payment_filter not in valid_payment_statuses:
payment_filter = ''
base_orders = Order.objects.filter(user=request.user)
orders = base_orders.prefetch_related('items__product').order_by('-created_at')
if status_filter:
orders = orders.filter(status=status_filter)
if payment_filter:
orders = orders.filter(payment_status=payment_filter)
order_summary = {
'total': base_orders.count(),
'paid': base_orders.filter(payment_status='Paid').count(),
'pending_payment': base_orders.exclude(payment_status='Paid').count(),
'shipped': base_orders.filter(status='Shipped').count(),
'delivered': base_orders.filter(status='Delivered').count(),
}
return render(
request,
'order/my_orders.html',
{
'orders': orders,
'order_summary': order_summary,
'status_filters': valid_statuses,
'payment_filters': valid_payment_statuses,
'selected_status': status_filter,
'selected_payment_status': payment_filter,
'has_order_filters': bool(status_filter or payment_filter),
},
)
@login_required
def order_detail(request, order_id):
order = get_object_or_404(Order.objects.prefetch_related('items__product'), id=order_id, user=request.user)
timeline = _build_timeline(order.status)
return render(request, 'order/order_detail.html', {'order': order, 'timeline': timeline})