from django.db import models from infrasynth.shared.enums import ( EntitlementStatus, InvoiceStatus, MonetizationModel, PlanInterval, SubscriptionStatus, ) from infrasynth.tenancy.mixins import TenantOwnedModel class App(models.Model): """A deployed InfraSynth app in the catalog (global).""" slug = models.SlugField(max_length=100, unique=True) name = models.CharField(max_length=200) monetization = models.CharField(max_length=20, choices=MonetizationModel.choices) is_active = models.BooleanField(default=True) metadata = models.JSONField(default=dict) class Meta: db_table = "billing_app" def __str__(self): return self.name class Plan(models.Model): """A purchasable tier of an app (global). Money is integer minor units.""" app = models.ForeignKey(App, on_delete=models.CASCADE, related_name="plans") slug = models.SlugField(max_length=100) name = models.CharField(max_length=200) price_amount = models.BigIntegerField(help_text="Minor units (cents). Never a float.") price_currency = models.CharField(max_length=3, default="USD") interval = models.CharField(max_length=20, choices=PlanInterval.choices, default=PlanInterval.MONTHLY) trial_days = models.PositiveIntegerField(default=0) features = models.JSONField(default=dict) limits = models.JSONField(default=dict) is_active = models.BooleanField(default=True) gateway = models.ForeignKey( "PaymentGateway", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) external_id = models.CharField(max_length=200, blank=True) class Meta: db_table = "billing_plan" constraints = [models.UniqueConstraint(fields=["app", "slug"], name="uniq_plan_slug_per_app")] def __str__(self): return f"{self.app.slug}:{self.slug}" class Entitlement(TenantOwnedModel): """A tenant's right to use an app under a plan. The unit of enforcement.""" app = models.ForeignKey(App, on_delete=models.CASCADE, related_name="entitlements") plan = models.ForeignKey(Plan, on_delete=models.SET_NULL, null=True, blank=True, related_name="+") status = models.CharField(max_length=20, choices=EntitlementStatus.choices, default=EntitlementStatus.ACTIVE) started_at = models.DateTimeField(auto_now_add=True) current_period_end = models.DateTimeField(null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) cancel_at_period_end = models.BooleanField(default=False) source = models.CharField(max_length=20, default="manual") metadata = models.JSONField(default=dict) class Meta: db_table = "billing_entitlement" constraints = [ models.UniqueConstraint(fields=["tenant", "app"], name="uniq_tenant_app_entitlement"), ] def __str__(self): return f"{self.tenant_id}:{self.app.slug}:{self.status}" class PaymentGateway(models.Model): """Platform payment provider account (global). Credentials are Fernet-encrypted.""" slug = models.SlugField(max_length=100, primary_key=True) display_name = models.CharField(max_length=200) gateway_class = models.CharField(max_length=500) config = models.JSONField(default=dict) is_active = models.BooleanField(default=True) supported_currencies = models.JSONField(default=list) webhook_secret = models.CharField(max_length=500, blank=True) class Meta: db_table = "billing_gateway" def __str__(self): return self.display_name class Subscription(TenantOwnedModel): """An active subscription backing an entitlement (tenant-owned).""" entitlement = models.ForeignKey( Entitlement, on_delete=models.SET_NULL, null=True, blank=True, related_name="subscriptions", ) plan = models.ForeignKey( Plan, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) gateway = models.ForeignKey( PaymentGateway, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) external_id = models.CharField(max_length=255, blank=True) status = models.CharField(max_length=20, choices=SubscriptionStatus.choices) current_period_start = models.DateTimeField(null=True, blank=True) current_period_end = models.DateTimeField(null=True, blank=True) cancel_at_period_end = models.BooleanField(default=False) cancelled_at = models.DateTimeField(null=True, blank=True) trial_end = models.DateTimeField(null=True, blank=True) metadata = models.JSONField(default=dict) class Meta: db_table = "billing_subscription" indexes = [models.Index(fields=["tenant_id", "status"])] def __str__(self): return f"{self.tenant_id}#{self.plan}" class Invoice(TenantOwnedModel): """A generated invoice (tenant-owned). Money is integer minor units.""" subscription = models.ForeignKey( Subscription, on_delete=models.SET_NULL, null=True, blank=True, related_name="invoices", ) gateway = models.ForeignKey( PaymentGateway, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) external_id = models.CharField(max_length=255, blank=True) invoice_number = models.CharField(max_length=100) amount = models.BigIntegerField(help_text="Minor units (cents). Never a float.") currency = models.CharField(max_length=3, default="USD") tax_amount = models.BigIntegerField(default=0) tax_name = models.CharField(max_length=100, blank=True) status = models.CharField(max_length=20, choices=InvoiceStatus.choices) due_date = models.DateTimeField(null=True, blank=True) paid_at = models.DateTimeField(null=True, blank=True) line_items = models.JSONField(default=list) pdf_file = models.ForeignKey( "infrasynth_files.StoredFile", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) metadata = models.JSONField(default=dict) class Meta: db_table = "billing_invoice" constraints = [ models.UniqueConstraint(fields=["tenant", "invoice_number"], name="uniq_invoice_number_per_tenant"), ] def __str__(self): return self.invoice_number class PaymentTransaction(TenantOwnedModel): """A single payment attempt (tenant-owned).""" invoice = models.ForeignKey( Invoice, on_delete=models.SET_NULL, null=True, blank=True, related_name="transactions", ) gateway = models.ForeignKey( PaymentGateway, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) external_id = models.CharField(max_length=255, blank=True) amount = models.BigIntegerField(help_text="Minor units (cents). Never a float.") currency = models.CharField(max_length=3, default="USD") status = models.CharField(max_length=50, blank=True) payment_method = models.CharField(max_length=100, blank=True) metadata = models.JSONField(default=dict) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "billing_transaction" indexes = [models.Index(fields=["tenant_id", "status"])] def __str__(self): return f"txn_{self.pk}"