""" 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. """ from django.db import models 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}" 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})'