diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 13a0666..870921f 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/mail.cpython-311.pyc b/core/__pycache__/mail.cpython-311.pyc index a080a1b..79d3c6a 100644 Binary files a/core/__pycache__/mail.cpython-311.pyc and b/core/__pycache__/mail.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 6e3aca3..e161775 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/notifications.cpython-311.pyc b/core/__pycache__/notifications.cpython-311.pyc new file mode 100644 index 0000000..93140b8 Binary files /dev/null and b/core/__pycache__/notifications.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index aaf076f..9b92d9c 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/__pycache__/whatsapp_utils.cpython-311.pyc b/core/__pycache__/whatsapp_utils.cpython-311.pyc index 81f17cc..4a862f3 100644 Binary files a/core/__pycache__/whatsapp_utils.cpython-311.pyc and b/core/__pycache__/whatsapp_utils.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 820c80a..e34444c 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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) \ No newline at end of file +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) diff --git a/core/mail.py b/core/mail.py index a66e6a5..2abdd1c 100644 --- a/core/mail.py +++ b/core/mail.py @@ -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 \ No newline at end of file + return False diff --git a/core/management/commands/__pycache__/init_notifications.cpython-311.pyc b/core/management/commands/__pycache__/init_notifications.cpython-311.pyc new file mode 100644 index 0000000..58e3377 Binary files /dev/null and b/core/management/commands/__pycache__/init_notifications.cpython-311.pyc differ diff --git a/core/management/commands/init_notifications.py b/core/management/commands/init_notifications.py new file mode 100644 index 0000000..911d45c --- /dev/null +++ b/core/management/commands/init_notifications.py @@ -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.')) diff --git a/core/migrations/0022_notificationtemplate.py b/core/migrations/0022_notificationtemplate.py new file mode 100644 index 0000000..d1f3807 --- /dev/null +++ b/core/migrations/0022_notificationtemplate.py @@ -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', + }, + ), + ] diff --git a/core/migrations/__pycache__/0022_notificationtemplate.cpython-311.pyc b/core/migrations/__pycache__/0022_notificationtemplate.cpython-311.pyc new file mode 100644 index 0000000..cf57370 Binary files /dev/null and b/core/migrations/__pycache__/0022_notificationtemplate.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 6fca213..6545e00 100644 --- a/core/models.py +++ b/core/models.py @@ -294,4 +294,41 @@ class DriverRating(models.Model): class Meta: verbose_name = _('Driver Rating') - verbose_name_plural = _('Driver Ratings') \ No newline at end of file + 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') \ No newline at end of file diff --git a/core/notifications.py b/core/notifications.py new file mode 100644 index 0000000..dd62817 --- /dev/null +++ b/core/notifications.py @@ -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) \ No newline at end of file diff --git a/core/views.py b/core/views.py index aea9bc3..fc3c617 100644 --- a/core/views.py +++ b/core/views.py @@ -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: diff --git a/core/whatsapp_utils.py b/core/whatsapp_utils.py index 94d6fb5..07d5ef0 100644 --- a/core/whatsapp_utils.py +++ b/core/whatsapp_utils.py @@ -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)