from django.contrib.auth.views import LoginView from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth import login, authenticate, logout from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from .models import Parcel, Profile, Country, Governate, City, OTPVerification, PlatformProfile, Testimonial, DriverRating from .forms import UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm, DriverRatingForm, ShipperRegistrationForm, DriverRegistrationForm from django.utils.translation import gettext_lazy as _ from django.utils.translation import get_language from django.contrib import messages from django.http import JsonResponse, HttpResponse from django.urls import reverse from .payment_utils import ThawaniPay from django.conf import settings from django.core.mail import send_mail from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.views.decorators.http import require_POST from django.db.models import Avg, Count from django.template.loader import render_to_string import random import string from .notifications import get_notification_content from .whatsapp_utils import ( notify_shipment_created, notify_payment_received, notify_driver_assigned, notify_status_change, send_whatsapp_message ) from .mail import send_contact_message, send_html_email import json from ai.local_ai_api import LocalAIApi import weasyprint import qrcode from io import BytesIO import base64 from .pricing import get_pricing_breakdown # Import pricing logic def index(request): # If tracking_id is present, redirect to the new track view tracking_id = request.GET.get('tracking_id') if tracking_id: return redirect(f"{reverse('track')}?tracking_number={tracking_id}") testimonials = Testimonial.objects.filter(is_active=True) # Top 5 Drivers (by Average Rating) top_drivers = Profile.objects.filter(role='car_owner').annotate( avg_rating=Avg('user__received_ratings__rating'), rating_count=Count('user__received_ratings') ).filter(rating_count__gt=0).order_by('-avg_rating')[:5] # Top 5 Shippers (by Shipment Count) top_shippers = Profile.objects.filter(role='shipper').annotate( shipment_count=Count('user__sent_parcels') ).order_by('-shipment_count')[:5] return render(request, 'core/index.html', { 'testimonials': testimonials, 'top_drivers': top_drivers, 'top_shippers': top_shippers }) def track_parcel(request): tracking_number = request.GET.get('tracking_number') parcel = None error = None if tracking_number: try: parcel = Parcel.objects.get(tracking_number__iexact=tracking_number.strip()) except Parcel.DoesNotExist: error = _("Parcel not found with this Tracking ID.") return render(request, 'core/track.html', { 'parcel': parcel, 'error': error, 'tracking_number': tracking_number }) def register(request): return render(request, 'core/register_choice.html') def register_shipper(request): if request.method == 'POST': form = ShipperRegistrationForm(request.POST) if form.is_valid(): # Save user but inactive user = form.save(commit=True) user.is_active = False user.save() # Auto-approve Shippers if hasattr(user, 'profile'): user.profile.is_approved = True user.profile.save() # Generate OTP code = ''.join(random.choices(string.digits, k=6)) OTPVerification.objects.create(user=user, code=code, purpose='registration') # Send OTP method = form.cleaned_data.get('verification_method', 'email') subj, email_msg, wa_msg = get_notification_content('otp_registration', {'code': code}, language=get_language()) if method == 'whatsapp': phone = user.profile.phone_number send_whatsapp_message(phone, wa_msg) messages.info(request, _("Verification code sent to WhatsApp.")) else: send_html_email( subject=subj, message=email_msg, recipient_list=[user.email], title=_('Welcome to Masar!'), request=request ) messages.info(request, _("Verification code sent to email.")) request.session['registration_user_id'] = user.id return redirect('verify_registration') else: form = ShipperRegistrationForm() return render(request, 'core/register_shipper.html', {'form': form}) def register_driver(request): if request.method == 'POST': form = DriverRegistrationForm(request.POST, request.FILES) if form.is_valid(): # Save user but inactive user = form.save(commit=True) user.is_active = False user.save() # Drivers are NOT approved by default (model default is False) # We can rely on the default, but explicit is fine too if we want to be sure if hasattr(user, 'profile'): user.profile.is_approved = False user.profile.save() # Generate OTP code = ''.join(random.choices(string.digits, k=6)) OTPVerification.objects.create(user=user, code=code, purpose='registration') # Send OTP method = form.cleaned_data.get('verification_method', 'email') subj, email_msg, wa_msg = get_notification_content('otp_registration', {'code': code}, language=get_language()) if method == 'whatsapp': phone = user.profile.phone_number send_whatsapp_message(phone, wa_msg) messages.info(request, _("Verification code sent to WhatsApp.")) else: send_html_email( subject=subj, message=email_msg, recipient_list=[user.email], title=_('Welcome to Masar!'), request=request ) messages.info(request, _("Verification code sent to email.")) request.session['registration_user_id'] = user.id return redirect('verify_registration') else: form = DriverRegistrationForm() return render(request, 'core/register_driver.html', {'form': form}) def verify_registration(request): if 'registration_user_id' not in request.session: messages.error(request, _("Session expired or invalid.")) return redirect('register') if request.method == 'POST': code = request.POST.get('code') user_id = request.session['registration_user_id'] try: user = User.objects.get(id=user_id) otp = OTPVerification.objects.filter( user=user, code=code, purpose='registration', is_verified=False ).latest('created_at') if otp.is_valid(): # Activate User user.is_active = True user.save() # Cleanup otp.is_verified = True otp.save() del request.session['registration_user_id'] # Login login(request, user) messages.success(request, _("Account verified successfully!")) return redirect('dashboard') else: messages.error(request, _("Invalid or expired code.")) except (User.DoesNotExist, OTPVerification.DoesNotExist): messages.error(request, _("Invalid code.")) return render(request, 'core/verify_registration.html') @login_required def dashboard(request): # Ensure profile exists profile, created = Profile.objects.get_or_create(user=request.user) if profile.role == 'shipper': all_parcels = Parcel.objects.filter(shipper=request.user).order_by('-created_at') active_parcels_list = all_parcels.exclude(status__in=['delivered', 'cancelled']) # Split history into delivered and cancelled delivered_parcels = all_parcels.filter(status='delivered') cancelled_parcels = all_parcels.filter(status='cancelled') # Pagination for Active Shipments page = request.GET.get('page', 1) paginator = Paginator(active_parcels_list, 9) # Show 9 parcels per page try: active_parcels = paginator.page(page) except PageNotAnInteger: active_parcels = paginator.page(1) except EmptyPage: active_parcels = paginator.page(paginator.num_pages) platform_profile = PlatformProfile.objects.first() payments_enabled = platform_profile.enable_payment if platform_profile else True return render(request, 'core/shipper_dashboard.html', { 'active_parcels': active_parcels, 'delivered_parcels': delivered_parcels, 'cancelled_parcels': cancelled_parcels, 'payments_enabled': payments_enabled, 'platform_profile': platform_profile # Pass full profile just in case }) else: # Car Owner view platform_profile = PlatformProfile.objects.first() payments_enabled = platform_profile.enable_payment if platform_profile else True if payments_enabled: available_parcels_list = Parcel.objects.filter(status='pending', payment_status='paid').order_by('-created_at') else: available_parcels_list = Parcel.objects.filter(status='pending').order_by('-created_at') # Check Approval Status if not profile.is_approved: messages.warning(request, _("Your account is pending approval. You cannot accept shipments yet.")) # Empty list if not approved available_parcels_list = Parcel.objects.none() # Pagination for Available Shipments page = request.GET.get('page', 1) paginator = Paginator(available_parcels_list, 9) # Show 9 parcels per page try: available_parcels = paginator.page(page) except PageNotAnInteger: available_parcels = paginator.page(1) except EmptyPage: available_parcels = paginator.page(paginator.num_pages) # Active: Picked up or In Transit my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status__in=['delivered', 'cancelled']).order_by('-created_at') # History: Delivered completed_parcels = Parcel.objects.filter(carrier=request.user, status='delivered').order_by('-created_at') # Cancelled cancelled_parcels = Parcel.objects.filter(carrier=request.user, status='cancelled').order_by('-created_at') return render(request, 'core/driver_dashboard.html', { 'available_parcels': available_parcels, 'my_parcels': my_parcels, 'completed_parcels': completed_parcels, 'cancelled_parcels': cancelled_parcels, 'is_approved': profile.is_approved # Pass to template }) @login_required def shipment_request(request): profile, created = Profile.objects.get_or_create(user=request.user) if profile.role != 'shipper': messages.error(request, _("Only shippers can request shipments.")) return redirect('dashboard') if request.method == 'POST': form = ParcelForm(request.POST) if form.is_valid(): parcel = form.save(commit=False) parcel.shipper = request.user # Recalculate price on backend to ensure integrity # We trust the form's distance/weight if populated, but good to verify # Ideally we recalculate from PricingRule here too breakdown = get_pricing_breakdown(parcel.distance_km, parcel.weight) if 'error' not in breakdown: parcel.price = breakdown['price'] parcel.platform_fee = breakdown['platform_fee'] parcel.platform_fee_percentage = breakdown['platform_fee_percentage'] parcel.driver_amount = breakdown['driver_amount'] parcel.save() # WhatsApp Notification notify_shipment_created(parcel) messages.success(request, _("Shipment requested successfully! Tracking ID: ") + parcel.tracking_number) return redirect('dashboard') else: form = ParcelForm() platform_profile = PlatformProfile.objects.first() google_maps_api_key = platform_profile.google_maps_api_key if platform_profile else None return render(request, 'core/shipment_request.html', { 'form': form, 'google_maps_api_key': google_maps_api_key }) @login_required def accept_parcel(request, parcel_id): profile, created = Profile.objects.get_or_create(user=request.user) if profile.role != 'car_owner': messages.error(request, _("Only car owners can accept shipments.")) return redirect('dashboard') # Check Approval if not profile.is_approved: messages.error(request, _("Your account is pending approval. You cannot accept shipments yet.")) return redirect('dashboard') platform_profile = PlatformProfile.objects.first() payments_enabled = platform_profile.enable_payment if platform_profile else True if payments_enabled: parcel = get_object_or_404(Parcel, id=parcel_id, status='pending', payment_status='paid') else: parcel = get_object_or_404(Parcel, id=parcel_id, status='pending') parcel.carrier = request.user parcel.status = 'picked_up' parcel.save() # WhatsApp Notification notify_driver_assigned(parcel) messages.success(request, _("You have accepted the shipment!")) return redirect('dashboard') @login_required def update_status(request, parcel_id): parcel = get_object_or_404(Parcel, id=parcel_id, carrier=request.user) if request.method == 'POST': new_status = request.POST.get('status') if new_status in dict(Parcel.STATUS_CHOICES): parcel.status = new_status parcel.save() # WhatsApp Notification notify_status_change(parcel) messages.success(request, _("Status updated successfully!")) return redirect('dashboard') @login_required def initiate_payment(request, parcel_id): # Check if payments are enabled platform_profile = PlatformProfile.objects.first() if platform_profile and not platform_profile.enable_payment: messages.error(request, _("Payments are currently disabled by the administrator.")) return redirect('dashboard') parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user, payment_status='pending') thawani = ThawaniPay() success_url = request.build_absolute_uri(reverse('payment_success')) + f"?session_id={{CHECKOUT_SESSION_ID}}&parcel_id={parcel.id}" cancel_url = request.build_absolute_uri(reverse('payment_cancel')) + f"?parcel_id={parcel.id}" session_id = thawani.create_checkout_session(parcel, success_url, cancel_url) if session_id: parcel.thawani_session_id = session_id parcel.save() checkout_url = f"{settings.THAWANI_API_URL.replace('/api/v1', '')}/pay/{session_id}?key={settings.THAWANI_PUBLISHABLE_KEY}" return redirect(checkout_url) else: messages.error(request, _("Could not initiate payment. Please try again later.")) return redirect('dashboard') @login_required def payment_success(request): session_id = request.GET.get('session_id') parcel_id = request.GET.get('parcel_id') parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user) thawani = ThawaniPay() session_data = thawani.get_checkout_session(session_id) if session_data and session_data.get('payment_status') == 'paid': parcel.payment_status = 'paid' parcel.save() # WhatsApp Notification notify_payment_received(parcel) messages.success(request, _("Payment successful! Your shipment is now active.")) else: messages.warning(request, _("Payment status is pending or failed. Please check your dashboard.")) return redirect('dashboard') @login_required def payment_cancel(request): messages.info(request, _("Payment was cancelled.")) return redirect('dashboard') def article_detail(request): return render(request, 'core/article_detail.html') def get_governates(request): country_id = request.GET.get('country_id') lang = get_language() field_name = 'name_ar' if lang == 'ar' else 'name_en' governates = Governate.objects.filter(country_id=country_id).order_by(field_name) data = [{'id': g.id, 'name': getattr(g, field_name)} for g in governates] return JsonResponse(data, safe=False) def get_cities(request): governate_id = request.GET.get('governate_id') lang = get_language() field_name = 'name_ar' if lang == 'ar' else 'name_en' cities = City.objects.filter(governate_id=governate_id).order_by(field_name) data = [{'id': c.id, 'name': getattr(c, field_name)} for c in cities] return JsonResponse(data, safe=False) def privacy_policy(request): return render(request, 'core/privacy_policy.html') def terms_conditions(request): return render(request, 'core/terms_conditions.html') def contact(request): if request.method == 'POST': form = ContactForm(request.POST) if form.is_valid(): # Send email sent = send_contact_message( name=form.cleaned_data['name'], email=form.cleaned_data['email'], message=form.cleaned_data['message'] ) if sent: messages.success(request, _("Your message has been sent successfully!")) else: messages.error(request, _("There was an error sending your message. Please try again later.")) return redirect('contact') else: form = ContactForm() return render(request, 'core/contact.html', {'form': form}) @login_required def profile_view(request): profile = request.user.profile reviews = [] if profile.role == 'car_owner': reviews = request.user.received_ratings.all().order_by('-created_at') return render(request, 'core/profile.html', { 'profile': profile, 'reviews': reviews }) @login_required def edit_profile(request): if request.method == 'POST': form = UserProfileForm(request.POST, request.FILES, instance=request.user.profile) if form.is_valid(): # 1. Handle Image immediately (easier than session storage) if 'profile_picture' in request.FILES: request.user.profile.profile_picture = request.FILES['profile_picture'] request.user.profile.save() # 2. Store other data in session for verification data = form.cleaned_data # Remove objects that can't be serialized or we've already handled safe_data = { 'first_name': data['first_name'], 'last_name': data['last_name'], 'email': data['email'], 'phone_number': data['phone_number'], 'address': data['address'], 'country_id': data['country'].id if data['country'] else None, 'governate_id': data['governate'].id if data['governate'] else None, 'city_id': data['city'].id if data['city'] else None, } request.session['pending_profile_update'] = safe_data # 3. Generate OTP code = ''.join(random.choices(string.digits, k=6)) OTPVerification.objects.create(user=request.user, code=code, purpose='profile_update') # 4. Send OTP method = data.get('otp_method', 'email') subj, email_msg, wa_msg = get_notification_content('otp_profile_update', {'code': code}, language=get_language()) if method == 'whatsapp': # Use current phone if available, else new phone phone = request.user.profile.phone_number or data['phone_number'] send_whatsapp_message(phone, wa_msg) messages.info(request, _("Verification code sent to WhatsApp.")) else: # Default to email # Send to the NEW email address (from the form), not the old one target_email = data['email'] send_html_email( subject=subj, message=email_msg, recipient_list=[target_email], title=subj, request=request ) messages.info(request, _("Verification code sent to email.")) return redirect('verify_otp') else: form = UserProfileForm(instance=request.user.profile) return render(request, 'core/edit_profile.html', {'form': form}) @login_required def verify_otp_view(request): if request.method == 'POST': code = request.POST.get('code') try: otp = OTPVerification.objects.filter( user=request.user, code=code, purpose='profile_update', is_verified=False ).latest('created_at') if otp.is_valid(): # Apply changes data = request.session.get('pending_profile_update') if data: # Update User request.user.first_name = data['first_name'] request.user.last_name = data['last_name'] request.user.email = data['email'] request.user.save() # Update Profile profile = request.user.profile profile.phone_number = data['phone_number'] profile.address = data['address'] if data.get('country_id'): profile.country_id = data['country_id'] if data.get('governate_id'): profile.governate_id = data['governate_id'] if data.get('city_id'): profile.city_id = data['city_id'] profile.save() # Cleanup otp.is_verified = True otp.save() del request.session['pending_profile_update'] messages.success(request, _("Profile updated successfully!")) return redirect('profile') else: messages.error(request, _("Session expired. Please try again.")) return redirect('edit_profile') else: messages.error(request, _("Invalid or expired code.")) except OTPVerification.DoesNotExist: messages.error(request, _("Invalid code.")) return render(request, 'core/verify_otp.html') @login_required def rate_driver(request, parcel_id): parcel = get_object_or_404(Parcel, id=parcel_id) # Validation if parcel.shipper != request.user: messages.error(request, _("You are not authorized to rate this shipment.")) return redirect('dashboard') if parcel.status != 'delivered': messages.error(request, _("You can only rate delivered shipments.")) return redirect('dashboard') if not parcel.carrier: messages.error(request, _("No driver was assigned to this shipment.")) return redirect('dashboard') if hasattr(parcel, 'rating'): messages.info(request, _("You have already rated this shipment.")) return redirect('dashboard') if request.method == 'POST': form = DriverRatingForm(request.POST) if form.is_valid(): rating = form.save(commit=False) rating.parcel = parcel rating.driver = parcel.carrier rating.shipper = request.user rating.save() messages.success(request, _("Thank you for your feedback!")) return redirect('dashboard') else: form = DriverRatingForm() return render(request, 'core/rate_driver.html', { 'form': form, 'parcel': parcel }) @require_POST def request_login_otp(request): identifier = request.POST.get('identifier') if not identifier: return JsonResponse({'success': False, 'message': _('Please enter an email or phone number.')}) # Clean identifier identifier = identifier.strip() user = None method = 'email' # Try to find user by email user = User.objects.filter(email__iexact=identifier).first() # If not found, try by phone number if not user: profile = Profile.objects.filter(phone_number=identifier).first() if profile: user = profile.user method = 'whatsapp' else: # Fallback: maybe they entered a phone without country code or with? # For now, simplistic search pass if not user: # Don't reveal if user exists or not for security, but for UX on this project we can be a bit more helpful return JsonResponse({'success': False, 'message': _('User not found with this email or phone number.')}) if not user.is_active: return JsonResponse({'success': False, 'message': _('Account is inactive. Please verify registration first.')}) # Generate OTP code = ''.join(random.choices(string.digits, k=6)) subj, email_msg, wa_msg = get_notification_content('otp_login', {'code': code}, language=get_language()) OTPVerification.objects.create(user=user, code=code, purpose='login') # Send OTP subj, email_msg, wa_msg = get_notification_content('otp_login', {'code': code}, language=get_language()) try: if method == 'whatsapp': phone = user.profile.phone_number send_whatsapp_message(phone, wa_msg) message_sent = _("OTP sent to your WhatsApp.") else: send_html_email( subject=subj, message=email_msg, recipient_list=[user.email], title=_('Login Verification'), request=request ) message_sent = _("OTP sent to your email.") return JsonResponse({'success': True, 'message': message_sent, 'user_id': user.id}) except Exception as e: return JsonResponse({'success': False, 'message': _('Failed to send OTP. Please try again.')}) @require_POST def verify_login_otp(request): user_id = request.POST.get('user_id') code = request.POST.get('code') if not user_id or not code: return JsonResponse({'success': False, 'message': _('Invalid request.')}) try: user = User.objects.get(id=user_id) otp = OTPVerification.objects.filter( user=user, code=code, purpose='login', is_verified=False ).latest('created_at') if otp.is_valid(): # Cleanup otp.is_verified = True otp.save() # Login login(request, user) return JsonResponse({'success': True, 'redirect_url': reverse('dashboard')}) else: return JsonResponse({'success': False, 'message': _('Invalid or expired OTP.')}) except (User.DoesNotExist, OTPVerification.DoesNotExist): return JsonResponse({'success': False, 'message': _('Invalid OTP.')}) @require_POST def chatbot(request): try: data = json.loads(request.body) user_message = data.get("message", "") language = data.get("language", "en") if not user_message: return JsonResponse({"success": False, "error": "Empty message"}) system_prompt = ( "You are MasarX AI, a helpful and professional assistant for the Masar logistics platform. " "The platform connects shippers with drivers for small parcel deliveries. " "Answer the user's questions about shipping, tracking, becoming a driver, or general support. " "If the user speaks Arabic, reply in Arabic. If English, reply in English. " "Keep responses concise and helpful." ) if language == "ar": system_prompt += " The user is currently browsing in Arabic." else: system_prompt += " The user is currently browsing in English." response = LocalAIApi.create_response({ "input": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ] }) if response.get("success"): text = LocalAIApi.extract_text(response) return JsonResponse({"success": True, "response": text}) else: return JsonResponse({"success": False, "error": response.get("error", "AI Error")}) except json.JSONDecodeError: return JsonResponse({"success": False, "error": "Invalid JSON"}) except Exception as e: return JsonResponse({"success": False, "error": str(e)}) @login_required def generate_parcel_label(request, parcel_id): parcel = get_object_or_404(Parcel, id=parcel_id) # Security check: only shipper or carrier can print label if parcel.shipper != request.user and parcel.carrier != request.user: messages.error(request, _("You are not authorized to print this label.")) return redirect('dashboard') # Generate QR Code qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4, ) qr.add_data(parcel.tracking_number) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buffer = BytesIO() img.save(buffer, format="PNG") qr_image_base64 = base64.b64encode(buffer.getvalue()).decode() # Get Logo Base64 logo_base64 = None platform_profile = PlatformProfile.objects.first() if platform_profile and platform_profile.logo: try: with open(platform_profile.logo.path, "rb") as image_file: logo_base64 = base64.b64encode(image_file.read()).decode() except Exception: pass # Render Template html_string = render_to_string('core/parcel_label.html', { 'parcel': parcel, 'qr_code': qr_image_base64, 'logo_base64': logo_base64, 'platform_profile': platform_profile, }) # Generate PDF html = weasyprint.HTML(string=html_string, base_url=request.build_absolute_uri()) pdf_file = html.write_pdf() response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = f'inline; filename="label_{parcel.tracking_number}.pdf"' return response @login_required def generate_invoice(request, parcel_id): parcel = get_object_or_404(Parcel, id=parcel_id) # Security check: only shipper can view invoice (or admin) if parcel.shipper != request.user and not request.user.is_staff: messages.error(request, _("You are not authorized to view this invoice.")) return redirect('dashboard') # Get Logo Base64 logo_base64 = None platform_profile = PlatformProfile.objects.first() if platform_profile and platform_profile.logo: try: with open(platform_profile.logo.path, "rb") as image_file: logo_base64 = base64.b64encode(image_file.read()).decode() except Exception: pass # Render Template html_string = render_to_string('core/invoice.html', { 'parcel': parcel, 'logo_base64': logo_base64, 'platform_profile': platform_profile, 'request': request, }) # Generate PDF html = weasyprint.HTML(string=html_string, base_url=request.build_absolute_uri()) pdf_file = html.write_pdf() response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = f'inline; filename="invoice_{parcel.tracking_number}.pdf"' return response @login_required def scan_qr_view(request): """Renders the QR Scanner page for drivers.""" # Optional: Restrict to drivers only # if request.user.profile.role != 'car_owner': # messages.error(request, _("Only drivers can access the scanner.")) # return redirect('dashboard') return render(request, 'core/scan_qr.html') @login_required def get_parcel_details(request): """API to fetch parcel details by tracking number.""" tracking_number = request.GET.get('tracking_number') if not tracking_number: return JsonResponse({'success': False, 'error': _('Tracking number required')}) try: parcel = Parcel.objects.get(tracking_number__iexact=tracking_number.strip()) # Check permissions: Is user the assigned driver? Or is it pending (for acceptance)? is_driver = request.user.profile.role == 'car_owner' is_assigned = parcel.carrier == request.user can_update = False if is_driver: if is_assigned: can_update = True elif parcel.status == 'pending': # Check if payments are enabled and paid platform_profile = PlatformProfile.objects.first() payments_enabled = platform_profile.enable_payment if platform_profile else True if not payments_enabled or parcel.payment_status == 'paid': can_update = True data = { 'success': True, 'parcel': { 'id': parcel.id, 'tracking_number': parcel.tracking_number, 'status': parcel.status, 'status_display': parcel.get_status_display(), 'price': float(parcel.price), 'from': f"{parcel.pickup_governate.name} / {parcel.pickup_city.name}", 'to': f"{parcel.delivery_governate.name} / {parcel.delivery_city.name}", 'description': parcel.description, 'can_update': can_update } } return JsonResponse(data) except Parcel.DoesNotExist: return JsonResponse({'success': False, 'error': _('Parcel not found')}) @login_required @require_POST def update_parcel_status_ajax(request): """API to update parcel status from scanner.""" try: data = json.loads(request.body) parcel_id = data.get('parcel_id') action = data.get('action') parcel = get_object_or_404(Parcel, id=parcel_id) # Logic for actions if action == 'accept': # Similar to accept_parcel view if request.user.profile.role != 'car_owner': return JsonResponse({'success': False, 'error': _('Only drivers can accept shipments')}) if parcel.status != 'pending': return JsonResponse({'success': False, 'error': _('Parcel is not available')}) # Check payment status if enabled platform_profile = PlatformProfile.objects.first() payments_enabled = platform_profile.enable_payment if platform_profile else True if payments_enabled and parcel.payment_status != 'paid': return JsonResponse({'success': False, 'error': _('Payment pending')}) # Check Approval for Driver via AJAX if not request.user.profile.is_approved: return JsonResponse({'success': False, 'error': _('Account pending approval')}) parcel.carrier = request.user parcel.status = 'picked_up' # Or 'assigned'? Logic says 'picked_up' in accept_parcel parcel.save() notify_driver_assigned(parcel) elif action == 'delivered': if parcel.carrier != request.user: return JsonResponse({'success': False, 'error': _('Not authorized')}) parcel.status = 'delivered' parcel.save() notify_status_change(parcel) else: return JsonResponse({'success': False, 'error': _('Invalid action')}) return JsonResponse({'success': True}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) @login_required def edit_parcel(request, parcel_id): parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user) if parcel.status != 'pending': messages.error(request, _("You can only edit pending shipments.")) return redirect('dashboard') if request.method == 'POST': form = ParcelForm(request.POST, instance=parcel) if form.is_valid(): parcel_updated = form.save(commit=False) # Recalculate if fields changed breakdown = get_pricing_breakdown(parcel_updated.distance_km, parcel_updated.weight) if 'error' not in breakdown: parcel_updated.price = breakdown['price'] parcel_updated.platform_fee = breakdown['platform_fee'] parcel_updated.platform_fee_percentage = breakdown['platform_fee_percentage'] parcel_updated.driver_amount = breakdown['driver_amount'] parcel_updated.save() messages.success(request, _("Shipment updated successfully.")) return redirect('dashboard') else: form = ParcelForm(instance=parcel) platform_profile = PlatformProfile.objects.first() google_maps_api_key = platform_profile.google_maps_api_key if platform_profile else None return render(request, 'core/edit_parcel.html', {'form': form, 'parcel': parcel, 'google_maps_api_key': google_maps_api_key}) @login_required def cancel_parcel(request, parcel_id): parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user) if parcel.status != 'pending': messages.error(request, _("You can only cancel pending shipments.")) else: parcel.status = 'cancelled' parcel.save() messages.success(request, _("Shipment cancelled successfully.")) return redirect('dashboard') class CustomLoginView(LoginView): template_name = 'core/login.html' def form_valid(self, form): # Authenticate checks are done by the form user = form.get_user() # Store user ID in session for 2FA step self.request.session['pre_2fa_user_id'] = user.id self.request.session.set_expiry(600) # 10 minutes expiry for this session part return redirect('select_2fa_method') def select_2fa_method(request): user_id = request.session.get('pre_2fa_user_id') if not user_id: return redirect('login') try: user = User.objects.get(id=user_id) except User.DoesNotExist: return redirect('login') if request.method == 'POST': method = request.POST.get('method') code = ''.join(random.choices(string.digits, k=6)) subj, email_msg, wa_msg = get_notification_content('otp_login', {'code': code}, language=get_language()) # Invalidate old login OTPs OTPVerification.objects.filter(user=user, purpose='login').delete() OTPVerification.objects.create(user=user, code=code, purpose='login') if method == 'email': if user.email: try: send_html_email( subject=_("Your Login OTP"), message=email_msg, recipient_list=[user.email], title=_("Login Verification") ) messages.success(request, _("OTP sent to your email.")) return redirect('verify_2fa_otp') except Exception as e: messages.error(request, _("Failed to send email: ") + str(e)) else: messages.error(request, _("No email address associated with this account.")) elif method == 'whatsapp': if hasattr(user, 'profile') and user.profile.phone_number: if send_whatsapp_message(user.profile.phone_number, wa_msg): messages.success(request, _("OTP sent to your WhatsApp.")) return redirect('verify_2fa_otp') else: messages.error(request, _("Failed to send WhatsApp message. Please check the logs.")) else: messages.error(request, _("No phone number found for this account.")) return render(request, 'core/select_2fa_method.html') def verify_2fa_otp(request): user_id = request.session.get('pre_2fa_user_id') if not user_id: return redirect('login') try: user = User.objects.get(id=user_id) except User.DoesNotExist: return redirect('login') if request.method == 'POST': code = request.POST.get('otp') try: # Find the most recent valid login OTP for this user otp_record = OTPVerification.objects.filter( user=user, purpose='login', is_verified=False ).latest('created_at') if otp_record.code == code and otp_record.is_valid(): otp_record.is_verified = True otp_record.save() # ACTUAL LOGIN HAPPENS HERE login(request, user) # Clean up session if 'pre_2fa_user_id' in request.session: del request.session['pre_2fa_user_id'] request.session.set_expiry(None) messages.success(request, _("Logged in successfully.")) return redirect('dashboard') else: messages.error(request, _("Invalid or expired OTP.")) except OTPVerification.DoesNotExist: messages.error(request, _("No valid OTP found. Please request a new one.")) return render(request, 'core/verify_2fa_otp.html')