2026-04-20 11:41:46 +05:30
|
|
|
"""
|
|
|
|
|
Two distinct concerns live in this app:
|
|
|
|
|
|
|
|
|
|
1. ``Notification`` — consumer-facing in-app inbox entries surfaced on the mobile
|
|
|
|
|
SPA (/api/notifications/list/). One row per user per alert.
|
|
|
|
|
|
|
|
|
|
2. ``NotificationSchedule`` + ``NotificationRecipient`` — admin-side recurring
|
|
|
|
|
email jobs configured from the Command Center Settings tab and dispatched by
|
|
|
|
|
the ``send_scheduled_notifications`` management command (host cron).
|
|
|
|
|
Not user-facing; strictly operational.
|
|
|
|
|
"""
|
2026-04-07 12:56:25 +05:30
|
|
|
from django.db import models
|
2026-04-20 11:41:46 +05:30
|
|
|
|
2026-04-07 12:56:25 +05:30
|
|
|
from accounts.models import User
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Notification(models.Model):
|
|
|
|
|
NOTIFICATION_TYPES = [
|
|
|
|
|
('event', 'Event'),
|
|
|
|
|
('promo', 'Promotion'),
|
|
|
|
|
('system', 'System'),
|
|
|
|
|
('booking', 'Booking'),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
|
|
|
|
|
title = models.CharField(max_length=255)
|
|
|
|
|
message = models.TextField()
|
|
|
|
|
notification_type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES, default='system')
|
|
|
|
|
is_read = models.BooleanField(default=False)
|
|
|
|
|
action_url = models.URLField(blank=True, null=True)
|
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
ordering = ['-created_at']
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"{self.notification_type}: {self.title} → {self.user.email}"
|
2026-04-20 11:41:46 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
class NotificationSchedule(models.Model):
|
|
|
|
|
"""One configurable recurring email job.
|
|
|
|
|
|
|
|
|
|
New types are added by registering a builder in ``notifications/emails.py``
|
|
|
|
|
and adding the slug to ``TYPE_CHOICES`` below. Cron expression is evaluated
|
|
|
|
|
in ``Asia/Kolkata`` by the dispatcher (matches operations team timezone).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
TYPE_EVENTS_EXPIRING_THIS_WEEK = 'events_expiring_this_week'
|
|
|
|
|
|
|
|
|
|
TYPE_CHOICES = [
|
|
|
|
|
(TYPE_EVENTS_EXPIRING_THIS_WEEK, 'Events Expiring This Week'),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
STATUS_SUCCESS = 'success'
|
|
|
|
|
STATUS_ERROR = 'error'
|
|
|
|
|
|
|
|
|
|
name = models.CharField(max_length=200)
|
|
|
|
|
notification_type = models.CharField(
|
|
|
|
|
max_length=64, choices=TYPE_CHOICES, db_index=True,
|
|
|
|
|
)
|
|
|
|
|
cron_expression = models.CharField(
|
|
|
|
|
max_length=100, default='0 0 * * 1',
|
|
|
|
|
help_text='Standard 5-field cron (minute hour dom month dow). '
|
|
|
|
|
'Evaluated in Asia/Kolkata.',
|
|
|
|
|
)
|
|
|
|
|
is_active = models.BooleanField(default=True, db_index=True)
|
|
|
|
|
last_run_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
last_status = models.CharField(max_length=20, blank=True, default='')
|
|
|
|
|
last_error = models.TextField(blank=True, default='')
|
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
ordering = ['-created_at']
|
|
|
|
|
indexes = [models.Index(fields=['is_active', 'notification_type'])]
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f'{self.name} ({self.notification_type})'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NotificationRecipient(models.Model):
|
|
|
|
|
"""Free-form recipient — not tied to a User row so external stakeholders
|
|
|
|
|
(vendors, partners, sponsors) can receive notifications without needing
|
|
|
|
|
platform accounts."""
|
|
|
|
|
|
|
|
|
|
schedule = models.ForeignKey(
|
|
|
|
|
NotificationSchedule,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name='recipients',
|
|
|
|
|
)
|
|
|
|
|
email = models.EmailField()
|
|
|
|
|
display_name = models.CharField(max_length=200, blank=True, default='')
|
|
|
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
unique_together = [('schedule', 'email')]
|
|
|
|
|
ordering = ['display_name', 'email']
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
label = self.display_name or self.email
|
|
|
|
|
return f'{label} ({self.schedule.name})'
|