infrasynth-backend-kit/infrasynth/billing/models.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
Close the gaps between the documented contract (API-STANDARD, TENANCY,
ENTITLEMENTS) and the implementation, and remove committed build artifacts.

Security:
- verify + process inbound webhooks (HMAC/handler verify, size limit,
  timestamp tolerance, idempotency via InboundEvent.external_id)
- real 2FA login flow (pre-auth challenge; tokens only after verify/recovery)
- wire HybridPermission into security/audit views; add API-key rotate and
  users/<id>/permissions|roles endpoints
- tenant-scoped throttling on by default; webhook replay protection
- verify MercadoPago webhook signatures
- login brute-force guard, configurable password policy, real ALTCHA PoW

Correctness:
- apply verified billing webhooks idempotently (subscription/entitlement/
  invoice/PaymentTransaction); scheduled payment lifecycle jobs
- capture audit update diffs automatically; add audit retention purge
- working notification retries, per-channel rate limits, log retention
- pluggable virus scanner, upload-size limit, pipeline toggle
- feature rollout %/environment targeting; settings-driven registrations
- workflow guards (instance cap, route depth, self-assignment, clone on re-entry)
- wire every previously-dead INFRASYNTH_* setting; drop truly dead ones

Delivery:
- README + CHANGELOG; CI format check + coverage gate
- keep test media out of the tree; untrack .coverage, __pycache__,
  egg-info, docs/ and invoice artifacts
2026-09-24 10:41:21 -05:00

217 lines
7.3 KiB
Python

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}"