Autosave: 20260128-021353

This commit is contained in:
Flatlogic Bot 2026-01-28 02:13:53 +00:00
parent a982102796
commit e6c45971eb
16 changed files with 457 additions and 157 deletions

Binary file not shown.

View File

@ -1,7 +1,7 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from .models import Profile, Parcel, Country, Governate, City, PlatformProfile, Testimonial, DriverRating
from .models import Profile, Parcel, Country, Governate, City, PlatformProfile, Testimonial, DriverRating, NotificationTemplate
from django.utils.translation import gettext_lazy as _
from django.urls import path, reverse
from django.shortcuts import render
@ -234,4 +234,30 @@ admin.site.register(Governate)
admin.site.register(City)
admin.site.register(PlatformProfile, PlatformProfileAdmin)
admin.site.register(Testimonial, TestimonialAdmin)
admin.site.register(DriverRating)
admin.site.register(DriverRating)
class NotificationTemplateAdmin(admin.ModelAdmin):
list_display = ('key', 'description')
readonly_fields = ('key', 'description', 'available_variables')
search_fields = ('key', 'description')
fieldsets = (
(None, {
'fields': ('key', 'description', 'available_variables')
}),
(_('Email Content'), {
'fields': ('subject_en', 'subject_ar', 'email_body_en', 'email_body_ar'),
'description': _('For emails, the body is wrapped in a base template. Use HTML if needed.')
}),
(_('WhatsApp Content'), {
'fields': ('whatsapp_body_en', 'whatsapp_body_ar'),
'description': _('For WhatsApp, use plain text with newlines.')
}),
)
def has_add_permission(self, request):
return False # Prevent adding new keys manually
def has_delete_permission(self, request, obj=None):
return False
admin.site.register(NotificationTemplate, NotificationTemplateAdmin)

View File

@ -55,31 +55,36 @@ def send_html_email(subject, message, recipient_list, title=None, action_url=Non
def send_contact_message(name, email, message):
"""
Sends a contact form message to the platform admins.
Args:
name (str): Sender's name
email (str): Sender's email
message (str): The message content
Returns:
bool: True if sent successfully, False otherwise
"""
try:
subject = f"New Contact Message from {name}"
full_message = f"You have received a new message from your website contact form.\n\n" \
f"Name: {name}\n" \
f"Email: {email}\n\n" \
f"Message:\n{message}"
from .notifications import get_notification_content
context = {'name': name, 'email': email, 'message': message}
# Admin alerts default to EN
subj, email_msg, wa_msg = get_notification_content('contact_form_admin', context, language='en')
recipient_list = settings.CONTACT_EMAIL_TO or [settings.DEFAULT_FROM_EMAIL]
# Use HTML email for contact form too, for consistency
return send_html_email(
subject=subject,
message=full_message,
# Email
email_sent = send_html_email(
subject=subj,
message=email_msg,
recipient_list=recipient_list,
title="New Contact Message"
)
# WhatsApp (New feature: Notify admin on WhatsApp too)
try:
from .models import PlatformProfile
from .whatsapp_utils import send_whatsapp_message
profile = PlatformProfile.objects.first()
if profile and profile.phone_number:
send_whatsapp_message(profile.phone_number, wa_msg)
except Exception as e:
logger.warning(f"Failed to send admin WhatsApp for contact form: {e}")
return email_sent
except Exception as e:
logger.error(f"Failed to send contact message: {e}")
return False
return False

View File

@ -0,0 +1,30 @@
from django.core.management.base import BaseCommand
from core.models import NotificationTemplate
from core.notifications import DEFAULT_TEMPLATES, get_notification_content
class Command(BaseCommand):
help = 'Initialize default notification templates'
def handle(self, *args, **options):
count = 0
for key, default in DEFAULT_TEMPLATES.items():
obj, created = NotificationTemplate.objects.get_or_create(
key=key,
defaults={
'description': default.get('description', ''),
'available_variables': default.get('variables', ''),
'subject_en': default.get('subject_en', ''),
'subject_ar': default.get('subject_ar', ''),
'email_body_en': default.get('email_body_en', ''),
'email_body_ar': default.get('email_body_ar', ''),
'whatsapp_body_en': default.get('whatsapp_body_en', ''),
'whatsapp_body_ar': default.get('whatsapp_body_ar', ''),
}
)
if created:
self.stdout.write(self.style.SUCCESS(f'Created template: {key}'))
count += 1
else:
self.stdout.write(f'Template exists: {key}')
self.stdout.write(self.style.SUCCESS(f'Initialized {count} new templates.'))

View File

@ -0,0 +1,32 @@
# Generated by Django 5.2.7 on 2026-01-28 01:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0021_remove_platformprofile_privacy_policy_and_more'),
]
operations = [
migrations.CreateModel(
name='NotificationTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(choices=[('otp_registration', 'OTP Registration'), ('otp_login', 'OTP Login'), ('otp_profile_update', 'OTP Profile Update'), ('shipment_created_shipper', 'Shipment Created (Shipper)'), ('payment_success_shipper', 'Payment Success (Shipper)'), ('shipment_visible_receiver', 'Shipment Visible (Receiver)'), ('driver_pickup_shipper', 'Driver Pickup (Shipper)'), ('driver_pickup_receiver', 'Driver Pickup (Receiver)'), ('driver_pickup_driver', 'Driver Pickup (Driver/Carrier)'), ('shipment_status_update', 'Shipment Status Update'), ('admin_alert_driver_accept', 'Admin Alert: Driver Accepted'), ('contact_form_admin', 'Contact Form (Admin)')], max_length=50, unique=True)),
('description', models.CharField(help_text='Description of where this notification is used.', max_length=255)),
('available_variables', models.TextField(blank=True, help_text='Comma-separated list of variables available in this template (e.g. {{ code }}, {{ name }}).')),
('subject_en', models.CharField(blank=True, max_length=255, verbose_name='Email Subject (EN)')),
('subject_ar', models.CharField(blank=True, max_length=255, verbose_name='Email Subject (AR)')),
('email_body_en', models.TextField(blank=True, help_text='HTML allowed.', verbose_name='Email Body (EN)')),
('email_body_ar', models.TextField(blank=True, help_text='HTML allowed.', verbose_name='Email Body (AR)')),
('whatsapp_body_en', models.TextField(blank=True, verbose_name='WhatsApp Message (EN)')),
('whatsapp_body_ar', models.TextField(blank=True, verbose_name='WhatsApp Message (AR)')),
],
options={
'verbose_name': 'Notification Template',
'verbose_name_plural': 'Notification Templates',
},
),
]

View File

@ -294,4 +294,41 @@ class DriverRating(models.Model):
class Meta:
verbose_name = _('Driver Rating')
verbose_name_plural = _('Driver Ratings')
verbose_name_plural = _('Driver Ratings')
class NotificationTemplate(models.Model):
KEY_CHOICES = (
('otp_registration', 'OTP Registration'),
('otp_login', 'OTP Login'),
('otp_profile_update', 'OTP Profile Update'),
('shipment_created_shipper', 'Shipment Created (Shipper)'),
('payment_success_shipper', 'Payment Success (Shipper)'),
('shipment_visible_receiver', 'Shipment Visible (Receiver)'),
('driver_pickup_shipper', 'Driver Pickup (Shipper)'),
('driver_pickup_receiver', 'Driver Pickup (Receiver)'),
('driver_pickup_driver', 'Driver Pickup (Driver/Carrier)'),
('shipment_status_update', 'Shipment Status Update'),
('admin_alert_driver_accept', 'Admin Alert: Driver Accepted'),
('contact_form_admin', 'Contact Form (Admin)'),
)
key = models.CharField(max_length=50, choices=KEY_CHOICES, unique=True)
description = models.CharField(max_length=255, help_text="Description of where this notification is used.")
available_variables = models.TextField(help_text="Comma-separated list of variables available in this template (e.g. {{ code }}, {{ name }}).", blank=True)
# Email
subject_en = models.CharField(max_length=255, blank=True, verbose_name="Email Subject (EN)")
subject_ar = models.CharField(max_length=255, blank=True, verbose_name="Email Subject (AR)")
email_body_en = models.TextField(blank=True, verbose_name="Email Body (EN)", help_text="HTML allowed.")
email_body_ar = models.TextField(blank=True, verbose_name="Email Body (AR)", help_text="HTML allowed.")
# WhatsApp
whatsapp_body_en = models.TextField(blank=True, verbose_name="WhatsApp Message (EN)")
whatsapp_body_ar = models.TextField(blank=True, verbose_name="WhatsApp Message (AR)")
def __str__(self):
return f"{self.get_key_display()}"
class Meta:
verbose_name = _('Notification Template')
verbose_name_plural = _('Notification Templates')

183
core/notifications.py Normal file
View File

@ -0,0 +1,183 @@
from django.utils.translation import get_language
from .models import NotificationTemplate
from django.template import Template, Context
import logging
logger = logging.getLogger(__name__)
DEFAULT_TEMPLATES = {
'otp_registration': {
'description': 'Sent when a user registers (Email/WhatsApp)',
'variables': '{{ code }}',
'subject_en': 'Verification Code',
'subject_ar': 'رمز التحقق',
'email_body_en': 'Your Masar Verification Code is {{ code }}',
'email_body_ar': 'رمز التحقق الخاص بك هو {{ code }}',
'whatsapp_body_en': 'Your Masar Verification Code is {{ code }}',
'whatsapp_body_ar': 'رمز التحقق الخاص بك هو {{ code }}',
},
'otp_login': {
'description': 'Sent for 2FA Login (Email/WhatsApp)',
'variables': '{{ code }}',
'subject_en': 'Login OTP',
'subject_ar': 'رمز الدخول',
'email_body_en': 'Your Masar Login Code is {{ code }}. Do not share this code.',
'email_body_ar': 'رمز دخول مسار هو {{ code }}. لا تشارك هذا الرمز.',
'whatsapp_body_en': 'Your Masar Login Code is {{ code }}. Do not share this code.',
'whatsapp_body_ar': 'رمز دخول مسار هو {{ code }}. لا تشارك هذا الرمز.',
},
'otp_profile_update': {
'description': 'Sent when updating profile sensitive info',
'variables': '{{ code }}',
'subject_en': 'Verification Code',
'subject_ar': 'رمز التحقق',
'email_body_en': 'Your Masar Update Code is {{ code }}',
'email_body_ar': 'رمز التحديث الخاص بك هو {{ code }}',
'whatsapp_body_en': 'Your Masar Update Code is {{ code }}',
'whatsapp_body_ar': 'رمز التحديث الخاص بك هو {{ code }}',
},
'shipment_created_shipper': {
'description': 'Sent to Shipper when they create a shipment',
'variables': '{{ name }}, {{ description }}, {{ tracking_number }}, {{ status }}',
'subject_en': 'Shipment Request Received - {{ tracking_number }}',
'subject_ar': 'تم استلام طلب الشحنة - {{ tracking_number }}',
'email_body_en': "Hello {{ name }},\n\nYour shipment request for '{{ description }}' has been received.\nTracking Number: {{ tracking_number }}\nStatus: {{ status }}\n\nPlease proceed to payment to make it visible to drivers.",
'email_body_ar': "مرحباً {{ name }}،\n\nتم استلام طلب الشحنة '{{ description }}'.\nرقم التتبع: {{ tracking_number }}\nالحالة: {{ status }}\n\nيرجى متابعة الدفع لجعلها مرئية للسائقين.",
'whatsapp_body_en': "Hello {{ name }},\nYour shipment request for '{{ description }}' has been received.\nTracking Number: {{ tracking_number }}\nStatus: {{ status }}\nPlease proceed to payment.",
'whatsapp_body_ar': "مرحباً {{ name }}،\nتم استلام طلب الشحنة '{{ description }}'.\nرقم التتبع: {{ tracking_number }}\nالحالة: {{ status }}\nيرجى الدفع.",
},
'payment_success_shipper': {
'description': 'Sent to Shipper after payment',
'variables': '{{ tracking_number }}',
'subject_en': 'Payment Successful - {{ tracking_number }}',
'subject_ar': 'تم الدفع بنجاح - {{ tracking_number }}',
'email_body_en': 'Payment successful for shipment {{ tracking_number }}.\nYour shipment is now visible to available drivers.',
'email_body_ar': 'تم الدفع بنجاح للشحنة {{ tracking_number }}.\nشحنتك الآن مرئية للسائقين المتاحين.',
'whatsapp_body_en': 'Payment successful for shipment {{ tracking_number }}.\nYour shipment is now visible to available drivers.',
'whatsapp_body_ar': 'تم الدفع بنجاح للشحنة {{ tracking_number }}.\nشحنتك الآن مرئية للسائقين المتاحين.',
},
'shipment_visible_receiver': {
'description': 'Sent to Receiver when shipment is paid/ready',
'variables': '{{ receiver_name }}, {{ shipper_name }}, {{ tracking_number }}, {{ status }}',
'subject_en': 'Incoming Shipment - {{ tracking_number }}',
'subject_ar': 'شحنة واردة - {{ tracking_number }}',
'email_body_en': 'Hello {{ receiver_name }},\n\nA shipment is coming your way from {{ shipper_name }}.\nTracking Number: {{ tracking_number }}\nStatus: {{ status }}',
'email_body_ar': 'مرحباً {{ receiver_name }}،\n\nشحنة قادمة إليك من {{ shipper_name }}.\nرقم التتبع: {{ tracking_number }}\nالحالة: {{ status }}',
'whatsapp_body_en': 'Hello {{ receiver_name }},\nA shipment is coming your way from {{ shipper_name }}.\nTracking Number: {{ tracking_number }}\nStatus: {{ status }}',
'whatsapp_body_ar': 'مرحباً {{ receiver_name }}،\nشحنة قادمة إليك من {{ shipper_name }}.\nرقم التتبع: {{ tracking_number }}\nالحالة: {{ status }}',
},
'driver_pickup_shipper': {
'description': 'Sent to Shipper when driver picks up',
'variables': '{{ tracking_number }}, {{ driver_name }}, {{ car_plate_number }}, {{ status }}',
'subject_en': 'Driver Assigned - {{ tracking_number }}',
'subject_ar': 'تم تعيين سائق - {{ tracking_number }}',
'email_body_en': 'Shipment {{ tracking_number }} has been picked up by {{ driver_name }}.\nCar Plate: {{ car_plate_number }}\nStatus: {{ status }}',
'email_body_ar': 'الشحنة {{ tracking_number }} تم استلامها بواسطة {{ driver_name }}.\nرقم اللوحة: {{ car_plate_number }}\nالحالة: {{ status }}',
'whatsapp_body_en': 'Shipment {{ tracking_number }} has been picked up by {{ driver_name }}.\nCar Plate: {{ car_plate_number }}\nStatus: {{ status }}',
'whatsapp_body_ar': 'الشحنة {{ tracking_number }} تم استلامها بواسطة {{ driver_name }}.\nرقم اللوحة: {{ car_plate_number }}\nالحالة: {{ status }}',
},
'driver_pickup_receiver': {
'description': 'Sent to Receiver when driver picks up',
'variables': '{{ tracking_number }}, {{ shipper_name }}, {{ driver_name }}, {{ car_plate_number }}',
'subject_en': 'Shipment On The Way - {{ tracking_number }}',
'subject_ar': 'الشحنة في الطريق - {{ tracking_number }}',
'email_body_en': 'Shipment {{ tracking_number }} from {{ shipper_name }} is on the way (Picked up).\nDriver: {{ driver_name }}\nCar Plate: {{ car_plate_number }}',
'email_body_ar': 'الشحنة {{ tracking_number }} من {{ shipper_name }} في الطريق (تم الاستلام).\nالسائق: {{ driver_name }}\nرقم اللوحة: {{ car_plate_number }}',
'whatsapp_body_en': 'Shipment {{ tracking_number }} from {{ shipper_name }} is on the way (Picked up).\nDriver: {{ driver_name }}\nCar Plate: {{ car_plate_number }}',
'whatsapp_body_ar': 'الشحنة {{ tracking_number }} من {{ shipper_name }} في الطريق (تم الاستلام).\nالسائق: {{ driver_name }}\nرقم اللوحة: {{ car_plate_number }}',
},
'driver_pickup_driver': {
'description': 'Sent to Driver upon acceptance',
'variables': '{{ tracking_number }}, {{ shipper_name }}, {{ pickup_address }}, {{ delivery_address }}, {{ price }}',
'subject_en': 'Shipment Accepted - {{ tracking_number }}',
'subject_ar': 'تم قبول الشحنة - {{ tracking_number }}',
'email_body_en': 'You have successfully accepted Shipment {{ tracking_number }}.\nShipper: {{ shipper_name }}\nPickup: {{ pickup_address }}\nDelivery: {{ delivery_address }}\nPrice: {{ price }} OMR',
'email_body_ar': 'لقد قبلت الشحنة {{ tracking_number }} بنجاح.\nالشاحن: {{ shipper_name }}\nالاستلام: {{ pickup_address }}\nالتوصيل: {{ delivery_address }}\nالسعر: {{ price }} ر.ع',
'whatsapp_body_en': 'You have successfully accepted Shipment {{ tracking_number }}.\nShipper: {{ shipper_name }}\nPickup: {{ pickup_address }}\nDelivery: {{ delivery_address }}\nPrice/Bid: {{ price }} OMR',
'whatsapp_body_ar': 'لقد قبلت الشحنة {{ tracking_number }} بنجاح.\nالشاحن: {{ shipper_name }}\nالاستلام: {{ pickup_address }}\nالتوصيل: {{ delivery_address }}\nالسعر: {{ price }} ر.ع',
},
'shipment_status_update': {
'description': 'Sent on general status change (In Transit, Delivered)',
'variables': '{{ tracking_number }}, {{ status }}',
'subject_en': 'Shipment Update - {{ tracking_number }}',
'subject_ar': 'تحديث الشحنة - {{ tracking_number }}',
'email_body_en': 'Update for shipment {{ tracking_number }}:\nNew Status: {{ status }}',
'email_body_ar': 'تحديث للشحنة {{ tracking_number }}:\nالحالة الجديدة: {{ status }}',
'whatsapp_body_en': 'Update for shipment {{ tracking_number }}:\nNew Status: {{ status }}',
'whatsapp_body_ar': 'تحديث للشحنة {{ tracking_number }}:\nالحالة الجديدة: {{ status }}',
},
'admin_alert_driver_accept': {
'description': 'Sent to Admin when driver accepts shipment',
'variables': '{{ driver_name }}, {{ car_plate_number }}, {{ tracking_number }}, {{ shipper_name }}, {{ price }}',
'subject_en': 'Shipment Accepted ({{ tracking_number }})',
'subject_ar': 'تم قبول الشحنة ({{ tracking_number }})',
'email_body_en': 'Driver {{ driver_name }} ({{ car_plate_number }}) accepted shipment {{ tracking_number }} from {{ shipper_name }}.\nPrice: {{ price }} OMR',
'email_body_ar': 'قام السائق {{ driver_name }} ({{ car_plate_number }}) بقبول الشحنة {{ tracking_number }} من {{ shipper_name }}.\nالسعر: {{ price }} ر.ع',
'whatsapp_body_en': 'Driver {{ driver_name }} ({{ car_plate_number }}) accepted shipment {{ tracking_number }} from {{ shipper_name }}.\nPrice: {{ price }} OMR',
'whatsapp_body_ar': 'قام السائق {{ driver_name }} ({{ car_plate_number }}) بقبول الشحنة {{ tracking_number }} من {{ shipper_name }}.\nالسعر: {{ price }} ر.ع',
},
'contact_form_admin': {
'description': 'Sent to Admin when contact form is submitted',
'variables': '{{ name }}, {{ email }}, {{ message }}',
'subject_en': 'New Contact Message from {{ name }}',
'subject_ar': 'رسالة جديدة من {{ name }}',
'email_body_en': 'You have received a new message from your website contact form.\n\nName: {{ name }}\nEmail: {{ email }}\n\nMessage:\n{{ message }}',
'email_body_ar': 'لقد تلقيت رسالة جديدة من نموذج الاتصال.\n\nالاسم: {{ name }}\nالبريد: {{ email }}\n\nالرسالة:\n{{ message }}',
'whatsapp_body_en': 'New Message from {{ name }}:\n{{ message }}',
'whatsapp_body_ar': 'رسالة جديدة من {{ name }}:\n{{ message }}',
}
}
def get_notification_content(key, context, language=None):
if not language:
language = get_language() or 'en'
# 1. Fetch or Create Template
try:
template_obj = NotificationTemplate.objects.get(key=key)
except NotificationTemplate.DoesNotExist:
# Create default
default = DEFAULT_TEMPLATES.get(key)
if default:
template_obj = NotificationTemplate.objects.create(
key=key,
description=default.get('description', ''),
available_variables=default.get('variables', ''),
subject_en=default.get('subject_en', ''),
subject_ar=default.get('subject_ar', ''),
email_body_en=default.get('email_body_en', ''),
email_body_ar=default.get('email_body_ar', ''),
whatsapp_body_en=default.get('whatsapp_body_en', ''),
whatsapp_body_ar=default.get('whatsapp_body_ar', ''),
)
else:
# Fallback if key unknown
return f"[{key}] Subject", f"[{key}] Body", f"[{key}] WA"
# 2. Select Language Fields
# Note: If translation is missing, fallback to EN
if language == 'ar':
subject = template_obj.subject_ar or template_obj.subject_en
email_body = template_obj.email_body_ar or template_obj.email_body_en
whatsapp_body = template_obj.whatsapp_body_ar or template_obj.whatsapp_body_en
else:
subject = template_obj.subject_en
email_body = template_obj.email_body_en
whatsapp_body = template_obj.whatsapp_body_en
# 3. Render
# Use Django Template engine for variable substitution
def render(text, ctx):
if not text: return ""
try:
# Convert context to dict if it isn't already
if not isinstance(ctx, dict):
ctx = {}
t = Template(text)
return t.render(Context(ctx))
except Exception as e:
logger.error(f"Template rendering error for {key}: {e}")
return text
return render(subject, context), render(email_body, context), render(whatsapp_body, context)

View File

@ -20,6 +20,7 @@ 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,
@ -100,16 +101,16 @@ def register_shipper(request):
# Send OTP
method = form.cleaned_data.get('verification_method', 'email')
otp_msg = _("Your Masar Verification Code is %(code)s") % {'code': code}
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, otp_msg)
send_whatsapp_message(phone, wa_msg)
messages.info(request, _("Verification code sent to WhatsApp."))
else:
send_html_email(
subject=_('Verification Code'),
message=otp_msg,
subject=subj,
message=email_msg,
recipient_list=[user.email],
title=_('Welcome to Masar!'),
request=request
@ -143,16 +144,16 @@ def register_driver(request):
# Send OTP
method = form.cleaned_data.get('verification_method', 'email')
otp_msg = _("Your Masar Verification Code is %(code)s") % {'code': code}
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, otp_msg)
send_whatsapp_message(phone, wa_msg)
messages.info(request, _("Verification code sent to WhatsApp."))
else:
send_html_email(
subject=_('Verification Code'),
message=otp_msg,
subject=subj,
message=email_msg,
recipient_list=[user.email],
title=_('Welcome to Masar!'),
request=request
@ -489,22 +490,22 @@ def edit_profile(request):
# 4. Send OTP
method = data.get('otp_method', 'email')
otp_msg = _("Your Masar Update Code is %(code)s") % {'code': code}
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, otp_msg)
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=_('Verification Code'),
message=otp_msg,
subject=subj,
message=email_msg,
recipient_list=[target_email],
title=_('Profile Update Verification'),
title=subj,
request=request
)
messages.info(request, _("Verification code sent to email."))
@ -641,20 +642,21 @@ def request_login_otp(request):
# 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
otp_msg = _("Your Masar Login Code is %(code)s. Do not share this code.") % {'code': code}
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, otp_msg)
send_whatsapp_message(phone, wa_msg)
message_sent = _("OTP sent to your WhatsApp.")
else:
send_html_email(
subject=_('Login OTP'),
message=otp_msg,
subject=subj,
message=email_msg,
recipient_list=[user.email],
title=_('Login Verification'),
request=request
@ -983,6 +985,7 @@ def select_2fa_method(request):
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()
@ -993,7 +996,7 @@ def select_2fa_method(request):
try:
send_html_email(
subject=_("Your Login OTP"),
message=f"Your verification code is: {code}",
message=email_msg,
recipient_list=[user.email],
title=_("Login Verification")
)
@ -1006,7 +1009,7 @@ def select_2fa_method(request):
elif method == 'whatsapp':
if hasattr(user, 'profile') and user.profile.phone_number:
if send_whatsapp_message(user.profile.phone_number, f"Your login verification code is: {code}"):
if send_whatsapp_message(user.profile.phone_number, wa_msg):
messages.success(request, _("OTP sent to your WhatsApp."))
return redirect('verify_2fa_otp')
else:

View File

@ -6,6 +6,7 @@ from django.core.mail import send_mail
from django.utils.translation import gettext_lazy as _
from .models import PlatformProfile
from .mail import send_html_email
from .notifications import get_notification_content
logger = logging.getLogger(__name__)
@ -16,7 +17,6 @@ def get_whatsapp_credentials():
"""
# Defaults
api_token = settings.WHATSAPP_API_KEY if hasattr(settings, 'WHATSAPP_API_KEY') else ""
# We repurpose Phone ID as Domain in settings if needed, or default to Wablas DEU
domain = "https://deu.wablas.com"
secret_key = ""
source = "Settings/Env"
@ -25,19 +25,15 @@ def get_whatsapp_credentials():
try:
profile = PlatformProfile.objects.first()
if profile:
# Check for token override
if profile.whatsapp_access_token:
api_token = profile.whatsapp_access_token.strip()
source = "Database (PlatformProfile)"
# Check for secret key override
if profile.whatsapp_app_secret:
secret_key = profile.whatsapp_app_secret.strip()
# Check for domain override
if profile.whatsapp_business_phone_number_id:
domain = profile.whatsapp_business_phone_number_id.strip()
# Ensure no trailing slash
if domain.endswith('/'):
domain = domain[:-1]
@ -71,28 +67,21 @@ def send_whatsapp_message_detailed(phone_number, message):
logger.warning(msg)
return False, msg
# Normalize phone number (Wablas expects international format without +, e.g. 628123...)
clean_phone = str(phone_number).replace('+', '').replace(' ', '')
# Endpoint: /api/send-message (Simple Text)
# Ensure domain has schema
if not domain.startswith('http'):
domain = f"https://{domain}"
# Using the exact endpoint provided in user example
url = f"{domain}/api/send-message"
# Header construction logic from user example
auth_header = api_token
if secret_key:
auth_header = f"{api_token}.{secret_key}"
headers = {
"Authorization": auth_header,
# requests will set Content-Type to application/x-www-form-urlencoded when using 'data' param
}
# Payload as form data (not JSON)
data = {
"phone": clean_phone,
"message": message,
@ -100,18 +89,14 @@ def send_whatsapp_message_detailed(phone_number, message):
try:
logger.info(f"Attempting to send WhatsApp message to {clean_phone} via {url}")
# Use data=data for form-urlencoded
response = requests.post(url, headers=headers, data=data, timeout=15)
# Handle non-JSON response (HTML error pages)
try:
response_data = response.json()
except ValueError:
response_data = response.text
# Wablas success usually has status: true
if response.status_code == 200:
# Check for logical success in JSON
if isinstance(response_data, dict):
if response_data.get('status') is True:
logger.info(f"WhatsApp message sent to {clean_phone} via Wablas")
@ -119,7 +104,6 @@ def send_whatsapp_message_detailed(phone_number, message):
else:
return False, f"Wablas API Logic Error (Source: {source}): {response_data}"
else:
# If text, assume success if 200 OK? Or inspect text.
return True, f"Message sent (Raw Response). (Source: {source})"
else:
error_msg = f"Wablas API error (Source: {source}): {response.status_code} - {response_data}"
@ -130,14 +114,19 @@ def send_whatsapp_message_detailed(phone_number, message):
logger.error(error_msg)
return False, error_msg
def notify_admin(subject, message):
"""Notifies the admin via Email and WhatsApp (if configured in PlatformProfile)."""
def notify_admin_alert(key, context):
"""Notifies the admin via Email and WhatsApp using a template key."""
# 1. Get Template Content (Admin likely prefers EN, or system default?)
# Let's force EN for admin alerts for now, or check generic language setting?
# Usually admin alerts are in EN or the default site language.
subject, email_body, whatsapp_body = get_notification_content(key, context, language='en')
# Email
try:
if hasattr(settings, 'CONTACT_EMAIL_TO') and settings.CONTACT_EMAIL_TO:
send_html_email(
subject=f"Admin Alert: {subject}",
message=message,
message=email_body,
recipient_list=settings.CONTACT_EMAIL_TO,
title="Admin Alert"
)
@ -148,146 +137,141 @@ def notify_admin(subject, message):
try:
profile = PlatformProfile.objects.first()
if profile and profile.phone_number:
# Assuming profile.phone_number is a valid WhatsApp number for Admin alerts
send_whatsapp_message(profile.phone_number, f"ADMIN ALERT: {subject}\n{message}")
send_whatsapp_message(profile.phone_number, f"ADMIN ALERT: {subject}\n{whatsapp_body}")
except Exception:
pass
def notify_shipment_created(parcel):
"""Notifies the shipper that the shipment request was received via WhatsApp and Email."""
shipper_name = parcel.shipper.get_full_name() or parcel.shipper.username
message = f"""Hello {shipper_name},
Your shipment request for '{parcel.description}' has been received.
Tracking Number: {parcel.tracking_number}
Status: {parcel.get_status_display()}
Please proceed to payment to make it visible to drivers."""
context = {
'name': shipper_name,
'description': parcel.description,
'tracking_number': parcel.tracking_number,
'status': parcel.get_status_display()
}
# Render for Shipper (check user language preference? For now assume session/request unavailable so maybe default or EN,
# OR we need a user language profile field. The user didn't ask for user-pref language yet, just bilingual templates.
# I'll default to EN unless I can guess.)
# Actually, if I can't determine, EN is safe.
# Future improvement: Add language to User Profile.
subj, email_msg, wa_msg = get_notification_content('shipment_created_shipper', context)
# WhatsApp
if hasattr(parcel.shipper, 'profile') and parcel.shipper.profile.phone_number:
send_whatsapp_message(parcel.shipper.profile.phone_number, message)
else:
logger.warning(f"No phone number found for shipper {shipper_name}, skipping WhatsApp.")
send_whatsapp_message(parcel.shipper.profile.phone_number, wa_msg)
# Email
if parcel.shipper.email:
try:
send_html_email(
subject='Shipment Request Received - ' + parcel.tracking_number,
message=message,
recipient_list=[parcel.shipper.email],
title='Shipment Request Received'
)
logger.info(f"Shipment created email sent to {parcel.shipper.email}")
except Exception as e:
logger.error(f"Failed to send shipment created email to {parcel.shipper.email}: {e}")
send_html_email(
subject=subj,
message=email_msg,
recipient_list=[parcel.shipper.email],
title=subj
)
return True
def notify_payment_received(parcel):
"""Notifies the shipper and receiver about successful payment via WhatsApp and Email."""
# Notify Shipper
# Shipper
shipper_name = parcel.shipper.get_full_name() or parcel.shipper.username
shipper_msg = f"""Payment successful for shipment {parcel.tracking_number}.
Your shipment is now visible to available drivers."""
context_shipper = {
'tracking_number': parcel.tracking_number
}
subj, email_msg, wa_msg = get_notification_content('payment_success_shipper', context_shipper)
# WhatsApp Shipper
if hasattr(parcel.shipper, 'profile') and parcel.shipper.profile.phone_number:
send_whatsapp_message(parcel.shipper.profile.phone_number, shipper_msg)
send_whatsapp_message(parcel.shipper.profile.phone_number, wa_msg)
# Email Shipper
if parcel.shipper.email:
try:
send_html_email(
subject='Payment Successful - ' + parcel.tracking_number,
message=shipper_msg,
recipient_list=[parcel.shipper.email],
title='Payment Successful'
)
except Exception as e:
logger.error(f"Failed to send payment email to {parcel.shipper.email}: {e}")
send_html_email(
subject=subj,
message=email_msg,
recipient_list=[parcel.shipper.email],
title=subj
)
# Notify Receiver
receiver_msg = f"""Hello {parcel.receiver_name},
A shipment is coming your way from {shipper_name}.
Tracking Number: {parcel.tracking_number}
Status: {parcel.get_status_display()}"""
send_whatsapp_message(parcel.receiver_phone, receiver_msg)
# Receiver
context_receiver = {
'receiver_name': parcel.receiver_name,
'shipper_name': shipper_name,
'tracking_number': parcel.tracking_number,
'status': parcel.get_status_display()
}
_, _, wa_msg_rx = get_notification_content('shipment_visible_receiver', context_receiver)
send_whatsapp_message(parcel.receiver_phone, wa_msg_rx)
def notify_driver_assigned(parcel):
"""Notifies the shipper, receiver, driver, and admin that a driver has picked up the parcel."""
driver_name = parcel.carrier.get_full_name() or parcel.carrier.username
shipper_name = parcel.shipper.get_full_name() or parcel.shipper.username
# Get Car Plate
car_plate = ""
if hasattr(parcel.carrier, 'profile'):
car_plate = parcel.carrier.profile.car_plate_number
# 1. Notify Shipper
msg_shipper = f"""Shipment {parcel.tracking_number} has been picked up by {driver_name}.
Status: {parcel.get_status_display()}"""
context_shipper = {
'tracking_number': parcel.tracking_number,
'driver_name': driver_name,
'car_plate_number': car_plate,
'status': parcel.get_status_display()
}
subj_s, email_s, wa_s = get_notification_content('driver_pickup_shipper', context_shipper)
if hasattr(parcel.shipper, 'profile') and parcel.shipper.profile.phone_number:
send_whatsapp_message(parcel.shipper.profile.phone_number, msg_shipper)
send_whatsapp_message(parcel.shipper.profile.phone_number, wa_s)
if parcel.shipper.email:
try:
send_html_email(
subject='Driver Assigned - ' + parcel.tracking_number,
message=msg_shipper,
recipient_list=[parcel.shipper.email],
title='Driver Assigned'
)
except Exception:
pass
send_html_email(subject=subj_s, message=email_s, recipient_list=[parcel.shipper.email], title=subj_s)
# 2. Notify Receiver
msg_receiver = f"""Shipment {parcel.tracking_number} from {shipper_name} is on the way (Picked up).
Driver: {driver_name}"""
send_whatsapp_message(parcel.receiver_phone, msg_receiver)
context_receiver = {
'tracking_number': parcel.tracking_number,
'shipper_name': shipper_name,
'driver_name': driver_name,
'car_plate_number': car_plate
}
_, _, wa_r = get_notification_content('driver_pickup_receiver', context_receiver)
send_whatsapp_message(parcel.receiver_phone, wa_r)
# 3. Notify Driver (Confirmation)
msg_driver = f"""You have successfully accepted Shipment {parcel.tracking_number}.
Shipper: {shipper_name}
Pickup: {parcel.pickup_address}
Delivery: {parcel.delivery_address}
Price/Bid: {parcel.price} OMR"""
# 3. Notify Driver
context_driver = {
'tracking_number': parcel.tracking_number,
'shipper_name': shipper_name,
'pickup_address': parcel.pickup_address,
'delivery_address': parcel.delivery_address,
'price': parcel.price
}
subj_d, email_d, wa_d = get_notification_content('driver_pickup_driver', context_driver)
if hasattr(parcel.carrier, 'profile') and parcel.carrier.profile.phone_number:
send_whatsapp_message(parcel.carrier.profile.phone_number, msg_driver)
send_whatsapp_message(parcel.carrier.profile.phone_number, wa_d)
if parcel.carrier.email:
try:
send_html_email(
subject='Shipment Accepted - ' + parcel.tracking_number,
message=msg_driver,
recipient_list=[parcel.carrier.email],
title='Shipment Accepted'
)
except Exception:
pass
send_html_email(subject=subj_d, message=email_d, recipient_list=[parcel.carrier.email], title=subj_d)
# 4. Notify Admin
notify_admin(
subject=f"Shipment Accepted ({parcel.tracking_number})",
message=f"Driver {driver_name} accepted shipment {parcel.tracking_number} from {shipper_name}.\nPrice: {parcel.price} OMR"
)
context_admin = {
'driver_name': driver_name,
'car_plate_number': car_plate,
'tracking_number': parcel.tracking_number,
'shipper_name': shipper_name,
'price': parcel.price
}
notify_admin_alert('admin_alert_driver_accept', context_admin)
def notify_status_change(parcel):
"""Notifies parties about general status updates (In Transit, Delivered)."""
msg = f"""Update for shipment {parcel.tracking_number}:
New Status: {parcel.get_status_display()}"""
context = {
'tracking_number': parcel.tracking_number,
'status': parcel.get_status_display()
}
subj, email_msg, wa_msg = get_notification_content('shipment_status_update', context)
if hasattr(parcel.shipper, 'profile') and parcel.shipper.profile.phone_number:
send_whatsapp_message(parcel.shipper.profile.phone_number, msg)
send_whatsapp_message(parcel.shipper.profile.phone_number, wa_msg)
if parcel.shipper.email:
try:
send_html_email(
subject='Shipment Update - ' + parcel.tracking_number,
message=msg,
recipient_list=[parcel.shipper.email],
title='Shipment Update'
)
except Exception:
pass
send_html_email(subject=subj, message=email_msg, recipient_list=[parcel.shipper.email], title=subj)
send_whatsapp_message(parcel.receiver_phone, msg)
send_whatsapp_message(parcel.receiver_phone, wa_msg)