from django.db import models from core.models import TimeStampedModel, UUIDPrimaryKeyModel class Channel(models.TextChoices): EMAIL = "email", "Email" SMS = "sms", "SMS" POSTCARD = "postcard", "Postcard" class Contact(UUIDPrimaryKeyModel, TimeStampedModel): class Source(models.TextChoices): CONTACT_FORM = "contact_form", "Contact form" CAREERS = "careers", "Careers" IMPORT = "import", "Import" MANUAL = "manual", "Manual" NOTIFY_ME = "notify_me", "Notify me" OTHER = "other", "Other" email = models.EmailField(unique=True, blank=True, null=True) phone = models.CharField(max_length=32, blank=True) first_name = models.CharField(max_length=100, blank=True) last_name = models.CharField(max_length=100, blank=True) postal_address = models.JSONField(default=dict, blank=True) source = models.CharField( max_length=32, choices=Source.choices, default=Source.OTHER ) notes = models.TextField(blank=True) class Meta: ordering = ["-created_at"] def __str__(self) -> str: name = f"{self.first_name} {self.last_name}".strip() return name or self.email or self.phone or str(self.pk) @property def full_name(self) -> str: return f"{self.first_name} {self.last_name}".strip() @staticmethod def make_postal_address( *, line1: str = "", line2: str = "", city: str = "", state: str = "", zip_code: str = "", country: str = "US", ) -> dict: """Normalize Lob-shaped postal address dict.""" return { "line1": (line1 or "").strip(), "line2": (line2 or "").strip(), "city": (city or "").strip(), "state": (state or "").strip(), "zip": (zip_code or "").strip(), "country": ((country or "").strip() or "US"), } @staticmethod def postal_address_has_content(addr: dict | None) -> bool: if not addr: return False return any( (addr.get(key) or "").strip() for key in ("line1", "line2", "city", "state", "zip") ) class ConsentRecord(TimeStampedModel): contact = models.ForeignKey( Contact, on_delete=models.CASCADE, related_name="consents" ) channel = models.CharField(max_length=16, choices=Channel.choices) opted_in = models.BooleanField(default=False) changed_at = models.DateTimeField(auto_now=True) reason = models.CharField(max_length=255, blank=True) class Meta: unique_together = ("contact", "channel") ordering = ["-changed_at"] def __str__(self) -> str: state = "in" if self.opted_in else "out" return f"{self.contact} {self.channel} opt-{state}" class Suppression(TimeStampedModel): contact = models.ForeignKey( Contact, on_delete=models.CASCADE, related_name="suppressions" ) channel = models.CharField(max_length=16, choices=Channel.choices) reason = models.CharField(max_length=255, blank=True) active = models.BooleanField(default=True) class Meta: unique_together = ("contact", "channel") def __str__(self) -> str: return f"suppress {self.contact} {self.channel}"