diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 8d9d456..590b182 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index be01bfa..54296d7 100644 --- a/config/settings.py +++ b/config/settings.py @@ -69,6 +69,7 @@ MIDDLEWARE = [ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'core.middleware.SubscriptionMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', # Disable X-Frame-Options middleware to allow Flatlogic preview iframes. # 'django.middleware.clickjacking.XFrameOptionsMiddleware', diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 91bf804..c4cf650 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/middleware.cpython-311.pyc b/core/__pycache__/middleware.cpython-311.pyc index b62f2da..6acfa1c 100644 Binary files a/core/__pycache__/middleware.cpython-311.pyc and b/core/__pycache__/middleware.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 8fa5624..508f24d 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ac7f5f7..36d4735 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 34d549c..ac18520 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index ba958ef..656f462 100644 --- a/core/admin.py +++ b/core/admin.py @@ -4,6 +4,8 @@ from django.shortcuts import render from django.http import HttpResponseRedirect from django.contrib import messages from django.utils.translation import gettext_lazy as _ +from django.utils import timezone +from django.utils.html import format_html from .models import Profile, Truck, Shipment, Bid, Message, WhatsAppConfig, Country, City, TruckType, AppSetting, Banner, HomeSection from .whatsapp import send_whatsapp_message @@ -25,9 +27,24 @@ class TruckTypeAdmin(admin.ModelAdmin): @admin.register(Profile) class ProfileAdmin(admin.ModelAdmin): - list_display = ('user', 'role', 'country_code', 'phone_number', 'subscription_plan', 'is_subscription_active') + list_display = ('user', 'role', 'phone_number', 'subscription_plan', 'subscription_expiry', 'subscription_status') list_filter = ('role', 'subscription_plan', 'is_subscription_active') search_fields = ('user__username', 'phone_number') + + def subscription_status(self, obj): + if obj.subscription_plan == 'NONE': + return format_html('{}', _('No Plan')) + + if obj.is_expired(): + return format_html('{}', _('Expired')) + + days = obj.days_until_expiry() + if days <= 7: + return format_html('{} ({} {})', _('Expiring soon'), days, _('days')) + + return format_html('{}', _('Active')) + + subscription_status.short_description = _('Subscription Status') @admin.register(Truck) class TruckAdmin(admin.ModelAdmin): @@ -117,4 +134,4 @@ class HomeSectionAdmin(admin.ModelAdmin): list_display = ('title', 'section_type', 'order', 'is_active') list_editable = ('order', 'is_active') list_filter = ('section_type', 'is_active', 'background_color') - search_fields = ('title', 'title_ar', 'subtitle', 'subtitle_ar', 'content', 'content_ar') \ No newline at end of file + search_fields = ('title', 'title_ar', 'subtitle', 'subtitle_ar', 'content', 'content_ar') diff --git a/core/management/commands/check_subscriptions.py b/core/management/commands/check_subscriptions.py new file mode 100644 index 0000000..146d0ec --- /dev/null +++ b/core/management/commands/check_subscriptions.py @@ -0,0 +1,57 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from core.models import Profile +from core.whatsapp import send_whatsapp_message +from django.utils.translation import gettext as _ +from datetime import timedelta + +class Command(BaseCommand): + help = 'Checks for expiring subscriptions and sends reminders via WhatsApp' + + def handle(self, *args, **options): + today = timezone.now().date() + + # Reminders for 7 days, 3 days, and 1 day before expiry + reminder_days = [7, 3, 1] + + for days in reminder_days: + expiry_date = today + timedelta(days=days) + expiring_profiles = Profile.objects.filter( + subscription_expiry=expiry_date, + subscription_plan__in=['MONTHLY', 'ANNUAL'] + ) + + for profile in expiring_profiles: + full_phone = profile.full_phone_number + if full_phone: + message = _( + "Hello %(user)s, your MASAR CARGO subscription (%(plan)s) will expire in %(days)s days on %(date)s. " + "Please renew to avoid account suspension." + ) % { + 'user': profile.user.username, + 'plan': profile.get_subscription_plan_display(), + 'days': days, + 'date': profile.subscription_expiry.strftime('%d/%m/%Y') + } + + self.stdout.write(f"Sending reminder to {profile.user.username} ({full_phone}) - {days} days left") + send_whatsapp_message(full_phone, message) + + # Also check for profiles that expired TODAY and send a notification + expired_today = Profile.objects.filter( + subscription_expiry=today, + subscription_plan__in=['MONTHLY', 'ANNUAL'] + ) + for profile in expired_today: + full_phone = profile.full_phone_number + if full_phone: + message = _( + "Hello %(user)s, your MASAR CARGO subscription has EXPIRED today. " + "Your account is now suspended. Please contact support to renew." + ) % { + 'user': profile.user.username + } + self.stdout.write(f"Sending expiry notice to {profile.user.username} ({full_phone})") + send_whatsapp_message(full_phone, message) + + self.stdout.write(self.style.SUCCESS('Successfully checked subscriptions')) diff --git a/core/middleware.py b/core/middleware.py index 724145c..d5ee3ba 100644 --- a/core/middleware.py +++ b/core/middleware.py @@ -1,4 +1,8 @@ from django.utils.translation import get_language +from django.shortcuts import redirect +from django.urls import reverse +from django.contrib import messages +from django.utils.translation import gettext as _ import logging logger = logging.getLogger(__name__) @@ -10,5 +14,43 @@ class LanguageDebugMiddleware: def __call__(self, request): response = self.get_response(request) # Log the current language for debugging - print(f"DEBUG: Path: {request.path}, Lang: {get_language()}, Cookie: {request.COOKIES.get('django_language')}") + # print(f"DEBUG: Path: {request.path}, Lang: {get_language()}, Cookie: {request.COOKIES.get('django_language')}") return response + +class SubscriptionMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Whitelisted paths that are always accessible + whitelisted_paths = [ + reverse('logout'), + reverse('login'), + reverse('register'), + reverse('subscription_expired'), + reverse('home'), + '/admin/', + '/static/', + '/media/', + '/i18n/', + ] + + # Check if the current path starts with any whitelisted path + is_whitelisted = any(request.path.startswith(path) for path in whitelisted_paths) + + if request.user.is_authenticated and not request.user.is_superuser: + try: + profile = request.user.profile + # If they are an admin role (not superuser but ADMIN role in profile), maybe don't suspend? + # Usually admins are exempted. + if profile.role == 'ADMIN': + return self.get_response(request) + + if profile.is_expired() and not is_whitelisted: + # Only redirect if they are not already on a whitelisted page + return redirect('subscription_expired') + except Exception as e: + logger.error(f"SubscriptionMiddleware error: {e}") + + response = self.get_response(request) + return response \ No newline at end of file diff --git a/core/models.py b/core/models.py index 062934d..cf5ba9c 100644 --- a/core/models.py +++ b/core/models.py @@ -61,6 +61,18 @@ class Profile(models.Model): subscription_plan = models.CharField(max_length=20, choices=SUBSCRIPTION_CHOICES, default='NONE') subscription_expiry = models.DateField(null=True, blank=True) is_subscription_active = models.BooleanField(default=False) + def is_expired(self): + if self.subscription_plan == "NONE": + return True + if not self.subscription_expiry: + return True + return self.subscription_expiry < timezone.now().date() + + def days_until_expiry(self): + if not self.subscription_expiry: + return 0 + delta = self.subscription_expiry - timezone.now().date() + return delta.days country_code = models.CharField(max_length=5, blank=True, default="966") phone_number = models.CharField(max_length=20, unique=True, null=True) # Changed to unique and nullable for migration safety diff --git a/core/templates/core/subscription_expired.html b/core/templates/core/subscription_expired.html new file mode 100644 index 0000000..01b2bd5 --- /dev/null +++ b/core/templates/core/subscription_expired.html @@ -0,0 +1,53 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} + +{% block content %} +
+
+
+
+
+ +
+

{% trans "Subscription Expired" %}

+

+ {% blocktrans with name=request.user.username %} + Hello {{ name }}, your subscription to MASAR CARGO has expired. + {% endblocktrans %} +

+
+
    +
  • {% trans "Expired on:" %} {{ profile.subscription_expiry|date:"d/m/Y" }}
  • +
  • {% trans "Current Plan:" %} {{ profile.get_subscription_plan_display }}
  • +
+
+

+ {% trans "To continue using our services, please renew your subscription. You can contact our support team for renewal details." %} +

+ + {% if app_settings.contact_phone or app_settings.contact_email %} +
+
{% trans "Contact for Renewal" %}
+ {% if app_settings.contact_phone %} +

{{ app_settings.contact_phone }}

+ {% endif %} + {% if app_settings.contact_email %} +

{{ app_settings.contact_email }}

+ {% endif %} +
+ {% endif %} + + +
+
+
+
+{% endblock %} diff --git a/core/templates/registration/register.html b/core/templates/registration/register.html index 8b142ec..e9596cf 100644 --- a/core/templates/registration/register.html +++ b/core/templates/registration/register.html @@ -104,44 +104,70 @@ {% if subscription_enabled %} {% endif %} diff --git a/core/urls.py b/core/urls.py index cc689f2..9f88dc6 100644 --- a/core/urls.py +++ b/core/urls.py @@ -22,4 +22,5 @@ urlpatterns = [ path("bid//reject/", views.reject_bid, name="reject_bid"), path("privacy-policy/", views.privacy_policy, name="privacy_policy"), path("terms-of-service/", views.terms_of_service, name="terms_of_service"), + path("subscription-expired/", views.subscription_expired, name="subscription_expired"), ] diff --git a/core/views.py b/core/views.py index d6d8d13..a5102b8 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,4 @@ +from datetime import timedelta from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib.auth import login, authenticate, logout @@ -27,15 +28,16 @@ def register(request): app_settings = AppSetting.objects.first() subscription_enabled = app_settings.subscription_enabled if app_settings else False - # Fees for JS - using float for cleaner JSON and explicit formatting + # Simplified fees dictionary for JS + # Ensuring keys are exactly as they appear in Profile.ROLE_CHOICES fees = { 'SHIPPER': { - 'MONTHLY': "{:.2f}".format(app_settings.shipper_monthly_fee) if app_settings else "0.00", - 'ANNUAL': "{:.2f}".format(app_settings.shipper_annual_fee) if app_settings else "0.00", + 'MONTHLY': str(app_settings.shipper_monthly_fee) if app_settings else "0.00", + 'ANNUAL': str(app_settings.shipper_annual_fee) if app_settings else "0.00", }, 'TRUCK_OWNER': { - 'MONTHLY': "{:.2f}".format(app_settings.truck_owner_monthly_fee) if app_settings else "0.00", - 'ANNUAL': "{:.2f}".format(app_settings.truck_owner_annual_fee) if app_settings else "0.00", + 'MONTHLY': str(app_settings.truck_owner_monthly_fee) if app_settings else "0.00", + 'ANNUAL': str(app_settings.truck_owner_annual_fee) if app_settings else "0.00", } } @@ -103,6 +105,10 @@ def verify_otp_registration(request): profile.subscription_plan = registration_data.get('subscription_plan', 'NONE') if profile.subscription_plan != 'NONE': profile.is_subscription_active = True + if profile.subscription_plan == 'MONTHLY': + profile.subscription_expiry = timezone.now().date() + timedelta(days=30) + elif profile.subscription_plan == 'ANNUAL': + profile.subscription_expiry = timezone.now().date() + timedelta(days=365) profile.save() login(request, user) @@ -427,3 +433,15 @@ def terms_of_service(request): } } return render(request, 'core/article_detail.html', context) +@login_required +def subscription_expired(request): + profile = request.user.profile + if not profile.is_expired(): + return redirect('dashboard') + + app_settings = AppSetting.objects.first() + return render(request, 'core/subscription_expired.html', { + 'profile': profile, + 'app_settings': app_settings + }) + diff --git a/locale/ar/LC_MESSAGES/django.mo b/locale/ar/LC_MESSAGES/django.mo index 5549a4a..3000531 100644 Binary files a/locale/ar/LC_MESSAGES/django.mo and b/locale/ar/LC_MESSAGES/django.mo differ diff --git a/locale/ar/LC_MESSAGES/django.po b/locale/ar/LC_MESSAGES/django.po index 1c43742..6143ff4 100644 --- a/locale/ar/LC_MESSAGES/django.po +++ b/locale/ar/LC_MESSAGES/django.po @@ -1420,4 +1420,38 @@ msgstr "سياسة الخصوصية ستتوفر قريباً." #: core/views.py:426 msgid "Terms of service are coming soon." -msgstr "شروط الخدمة ستتوفر قريباً." \ No newline at end of file +msgstr "شروط الخدمة ستتوفر قريباً." +msgid "Subscription Expired" +msgstr "انتهى الاشتراك" + +msgid "No Plan" +msgstr "بدون خطة" + +msgid "Expired" +msgstr "منتهي" + +msgid "Expiring soon" +msgstr "تنتهي قريبا" + +msgid "days" +msgstr "أيام" + +msgid "Active" +msgstr "نشط" + + +msgid "Hello %(name)s, your subscription to MASAR CARGO has expired." +msgstr "مرحباً %(name)s، لقد انتهى اشتراكك في مسار كارغو." + +msgid "Expired on:" +msgstr "انتهى في:" + +msgid "Current Plan:" +msgstr "الخطة الحالية:" + +msgid "To continue using our services, please renew your subscription. You can contact our support team for renewal details." +msgstr "لمواصلة استخدام خدماتنا، يرجى تجديد اشتراكك. يمكنك الاتصال بفريق الدعم لدينا للحصول على تفاصيل التجديد." + +msgid "Contact for Renewal" +msgstr "الاتصال للتجديد" +