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
217 lines
7.3 KiB
Python
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}"
|