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 %} +
+ {% blocktrans with name=request.user.username %} + Hello {{ name }}, your subscription to MASAR CARGO has expired. + {% endblocktrans %} +
++ {% 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 %} +{{ app_settings.contact_phone }}
+ {% endif %} + {% if app_settings.contact_email %} +{{ app_settings.contact_email }}
+ {% endif %} +