from django.conf import settings from django.db import models from contacts.models import Channel, Contact from core.models import TimeStampedModel, UUIDPrimaryKeyModel class MessageTemplate(UUIDPrimaryKeyModel, TimeStampedModel): name = models.CharField(max_length=120) channel = models.CharField(max_length=16, choices=Channel.choices) subject = models.CharField(max_length=255, blank=True) body = models.TextField() postcard_front = models.JSONField(default=dict, blank=True) postcard_back = models.JSONField(default=dict, blank=True) def __str__(self) -> str: return f"{self.name} ({self.channel})" class Campaign(UUIDPrimaryKeyModel, TimeStampedModel): class Status(models.TextChoices): DRAFT = "draft", "Draft" SCHEDULED = "scheduled", "Scheduled" SENDING = "sending", "Sending" COMPLETED = "completed", "Completed" CANCELLED = "cancelled", "Cancelled" class Audience(models.TextChoices): EMAIL_OPT_IN = "email_opt_in", "Mailing list · email opt-in" SMS_OPT_IN = "sms_opt_in", "Mailing list · SMS opt-in" POSTCARD_OPT_IN = "postcard_opt_in", "Mailing list · postcard opt-in" name = models.CharField(max_length=120) channel = models.CharField(max_length=16, choices=Channel.choices) audience = models.CharField( max_length=32, choices=Audience.choices, blank=True, default="", ) template = models.ForeignKey( MessageTemplate, null=True, blank=True, on_delete=models.SET_NULL, related_name="campaigns", ) status = models.CharField( max_length=16, choices=Status.choices, default=Status.DRAFT ) scheduled_for = models.DateTimeField(null=True, blank=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, ) subject_override = models.CharField(max_length=255, blank=True) body_override = models.TextField(blank=True) # Set when realtor summary email is sent (campaign COMPLETED). notify_sent_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ["-created_at"] def __str__(self) -> str: return self.name class Message(UUIDPrimaryKeyModel, TimeStampedModel): class Status(models.TextChoices): DRAFT = "draft", "Draft" SCHEDULED = "scheduled", "Scheduled" QUEUED = "queued", "Queued" SENT = "sent", "Sent" DELIVERED = "delivered", "Delivered" OPENED = "opened", "Opened" CLICKED = "clicked", "Clicked" FAILED = "failed", "Failed" BOUNCED = "bounced", "Bounced" SUPPRESSED = "suppressed", "Suppressed" campaign = models.ForeignKey( Campaign, on_delete=models.CASCADE, related_name="messages" ) contact = models.ForeignKey( Contact, on_delete=models.CASCADE, related_name="messages" ) channel = models.CharField(max_length=16, choices=Channel.choices) status = models.CharField( max_length=16, choices=Status.choices, default=Status.DRAFT ) provider = models.CharField(max_length=64, blank=True) provider_message_id = models.CharField(max_length=255, blank=True) scheduled_for = models.DateTimeField(null=True, blank=True) sent_at = models.DateTimeField(null=True, blank=True) error = models.TextField(blank=True) body_snapshot = models.TextField(blank=True) class Meta: ordering = ["-created_at"] def __str__(self) -> str: return f"{self.channel} → {self.contact} ({self.status})" class ProviderEvent(TimeStampedModel): message = models.ForeignKey( Message, null=True, blank=True, on_delete=models.SET_NULL, related_name="events", ) provider = models.CharField(max_length=64) event_type = models.CharField(max_length=64) payload = models.JSONField(default=dict, blank=True) class StoredFile(UUIDPrimaryKeyModel, TimeStampedModel): """Binary file blob in the database (no filesystem media storage).""" class Kind(models.TextChoices): CAMPAIGN_IMAGE = "campaign_image", "Campaign image" SOCIAL_IMAGE = "social_image", "Social image" SOCIAL_VIDEO = "social_video", "Social video" kind = models.CharField( max_length=32, choices=Kind.choices, default=Kind.CAMPAIGN_IMAGE ) filename = models.CharField(max_length=255, blank=True) content_type = models.CharField(max_length=128) size = models.PositiveIntegerField(default=0) data = models.BinaryField() uploaded_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="uploaded_files", ) class Meta: ordering = ["-created_at"] def __str__(self) -> str: return self.filename or str(self.pk)