from django.conf import settings from django.db import models from django.utils import timezone from chat_backend.models import Company, TimeInfoBase class SubscriptionPlan(TimeInfoBase): """Catalog row for a billable (or complimentary) subscription tier.""" class Slug(models.TextChoices): FOUNDERS = "founders", "Founders" STANDARD = "standard", "Standard" PRO = "pro", "Pro / Creator" BUSINESS = "business", "Business Team" BACKER = "backer", "Backer" slug = models.SlugField(max_length=64, unique=True, db_index=True) name = models.CharField(max_length=128) description = models.TextField(blank=True, default="") price_cents = models.PositiveIntegerField( default=0, help_text="List price in cents (0 for complimentary tiers).", ) currency = models.CharField(max_length=8, default="usd") interval = models.CharField(max_length=16, default="month") stripe_price_id = models.CharField( max_length=255, blank=True, default="", help_text="Optional Stripe Price id; empty uses price_data at Checkout.", ) is_public = models.BooleanField( default=False, help_text="Shown in public pricing / plan list APIs.", ) is_selectable = models.BooleanField( default=False, help_text="Selectable at Checkout. Backer is never selectable.", ) allows_text_generation = models.BooleanField(default=True) allows_image_generation = models.BooleanField(default=False) allows_all_future_features = models.BooleanField( default=False, help_text="Founders/Backer: unlock new capabilities as they ship.", ) prompt_quota_per_window = models.PositiveIntegerField( help_text="Max prompts allowed in each rolling window.", ) prompt_window_hours = models.PositiveIntegerField( default=6, help_text="Length of the rolling prompt quota window in hours.", ) monthly_token_quota = models.PositiveIntegerField( null=True, blank=True, help_text=( "Optional billing-period token cap (tokens_in + tokens_out). " "Null = no token-period limit (prompt window still applies)." ), ) sort_order = models.PositiveIntegerField(default=0) class Meta: ordering = ["sort_order", "price_cents", "name"] def __str__(self) -> str: return f"{self.name} ({self.slug})" def allows_feature(self, feature: str) -> bool: if self.allows_all_future_features: return True if feature in ("text", "text_generation"): return self.allows_text_generation if feature in ("image", "image_generation"): return self.allows_image_generation return False class BackerEmail(TimeInfoBase): """Pre-registered emails that receive complimentary Backer (Founders-level) access.""" email = models.EmailField(unique=True, db_index=True) note = models.CharField(max_length=512, blank=True, default="") redeemed_at = models.DateTimeField(null=True, blank=True) redeemed_user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="backer_email_entries", ) class Meta: ordering = ["email"] verbose_name = "Backer email" verbose_name_plural = "Backer emails" def __str__(self) -> str: return self.email def save(self, *args, **kwargs): if self.email: self.email = self.email.strip().lower() super().save(*args, **kwargs) class UserSubscription(TimeInfoBase): """Per-user plan assignment (Stripe, Backer whitelist, or admin).""" class Status(models.TextChoices): NONE = "none", "None" ACTIVE = "active", "Active" PAST_DUE = "past_due", "Past due" CANCELED = "canceled", "Canceled" class Source(models.TextChoices): NONE = "none", "None" STRIPE = "stripe", "Stripe" BACKER = "backer", "Backer" ADMIN = "admin", "Admin" user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="subscription", ) plan = models.ForeignKey( SubscriptionPlan, on_delete=models.PROTECT, related_name="subscriptions", null=True, blank=True, ) status = models.CharField( max_length=32, choices=Status.choices, default=Status.NONE, db_index=True, ) source = models.CharField( max_length=32, choices=Source.choices, default=Source.NONE, ) stripe_subscription_id = models.CharField( max_length=255, blank=True, default="", db_index=True, ) cancel_at_period_end = models.BooleanField( default=False, help_text="Stripe: subscription will cancel at current_period_end.", ) current_period_end = models.DateTimeField( null=True, blank=True, help_text="Stripe billing period end (access remains until then when canceling).", ) monthly_token_quota_override = models.PositiveIntegerField( null=True, blank=True, help_text="Optional per-user override of plan monthly_token_quota.", ) class Meta: verbose_name = "User subscription" verbose_name_plural = "User subscriptions" def __str__(self) -> str: plan = self.plan.slug if self.plan_id else "none" return f"UserSubscription user={self.user_id} plan={plan} ({self.status})" @property def is_active(self) -> bool: return self.status == self.Status.ACTIVE and self.plan_id is not None def effective_monthly_token_quota(self): if self.monthly_token_quota_override is not None: return self.monthly_token_quota_override if self.plan_id: return self.plan.monthly_token_quota return None class Invoice(TimeInfoBase): """Local ledger row for a billed period / Stripe invoice or checkout session.""" class Provider(models.TextChoices): STRIPE = "stripe", "Stripe" class Status(models.TextChoices): DRAFT = "draft", "Draft" OPEN = "open", "Open" PAID = "paid", "Paid" VOID = "void", "Void" UNCOLLECTIBLE = "uncollectible", "Uncollectible" PAYMENT_FAILED = "payment_failed", "Payment failed" user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="invoices", ) company = models.ForeignKey( Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="invoices", ) provider = models.CharField( max_length=32, choices=Provider.choices, default=Provider.STRIPE, ) status = models.CharField( max_length=32, choices=Status.choices, default=Status.OPEN, db_index=True, ) currency = models.CharField(max_length=8, default="usd") amount_due = models.PositiveIntegerField( default=0, help_text="Amount due in the smallest currency unit (e.g. cents).", ) amount_paid = models.PositiveIntegerField( default=0, help_text="Amount paid in the smallest currency unit (e.g. cents).", ) period_start = models.DateTimeField(null=True, blank=True) period_end = models.DateTimeField(null=True, blank=True) stripe_invoice_id = models.CharField( max_length=255, blank=True, null=True, unique=True, db_index=True, ) stripe_checkout_session_id = models.CharField( max_length=255, blank=True, null=True, unique=True, db_index=True, ) stripe_subscription_id = models.CharField( max_length=255, blank=True, null=True, db_index=True, ) stripe_customer_id = models.CharField(max_length=255, blank=True, default="") hosted_invoice_url = models.URLField(blank=True, default="") description = models.CharField(max_length=512, blank=True, default="") class Meta: ordering = ["-created"] def __str__(self) -> str: return f"Invoice {self.pk} ({self.status}) user={self.user_id}" class Payment(TimeInfoBase): """Local ledger row for a payment attempt / Stripe PaymentIntent or charge.""" class Provider(models.TextChoices): STRIPE = "stripe", "Stripe" class Status(models.TextChoices): PENDING = "pending", "Pending" SUCCEEDED = "succeeded", "Succeeded" FAILED = "failed", "Failed" CANCELED = "canceled", "Canceled" REQUIRES_ACTION = "requires_action", "Requires action" invoice = models.ForeignKey( Invoice, on_delete=models.CASCADE, related_name="payments", null=True, blank=True, ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="payments", ) company = models.ForeignKey( Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="payments", ) provider = models.CharField( max_length=32, choices=Provider.choices, default=Provider.STRIPE, ) status = models.CharField( max_length=32, choices=Status.choices, default=Status.PENDING, db_index=True, ) currency = models.CharField(max_length=8, default="usd") amount = models.PositiveIntegerField( default=0, help_text="Amount in the smallest currency unit (e.g. cents).", ) stripe_payment_intent_id = models.CharField( max_length=255, blank=True, null=True, unique=True, db_index=True, ) stripe_charge_id = models.CharField( max_length=255, blank=True, null=True, unique=True, db_index=True, ) paid_at = models.DateTimeField(null=True, blank=True) failure_message = models.CharField(max_length=512, blank=True, default="") class Meta: ordering = ["-created"] def __str__(self) -> str: return f"Payment {self.pk} ({self.status}) user={self.user_id}" def mark_succeeded(self, *, paid_at=None): self.status = self.Status.SUCCEEDED self.paid_at = paid_at or timezone.now() self.save(update_fields=["status", "paid_at", "last_modified"])