An unticked checkbox on registration subscribes the verified address to the listmonk list (preconfirmed) once the account exists. The profile page gets a subscribe/unsubscribe toggle, and verified email changes follow through to listmonk. Newsletter outages never block accounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
4.2 KiB
Python
Executable File
94 lines
4.2 KiB
Python
Executable File
import uuid
|
|
|
|
from django.db import models
|
|
|
|
|
|
class Account(models.Model):
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
uid = models.CharField(max_length=48, unique=True)
|
|
email = models.EmailField()
|
|
ldap_dn = models.CharField(max_length=1024, blank=True)
|
|
ldap_uuid = models.CharField(max_length=64, unique=True, null=True, blank=True)
|
|
oidc_issuer = models.CharField(max_length=2048, blank=True, default="")
|
|
oidc_sub = models.CharField(max_length=512, unique=True, null=True)
|
|
membership_level = models.PositiveSmallIntegerField(default=0)
|
|
stripe_customer = models.CharField(max_length=128, unique=True, null=True)
|
|
checkout_id = models.CharField(max_length=128, blank=True)
|
|
checkout_key = models.UUIDField(default=uuid.uuid4)
|
|
last_billing_sync = models.DateTimeField(null=True)
|
|
last_shkeeper_sync = models.DateTimeField(null=True)
|
|
last_ldap_sync = models.DateTimeField(null=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
class Registration(models.Model):
|
|
token_hash = models.CharField(max_length=64, primary_key=True)
|
|
uid = models.CharField(max_length=48)
|
|
email = models.EmailField()
|
|
newsletter = models.BooleanField(default=False)
|
|
expires_at = models.DateTimeField(db_index=True)
|
|
|
|
|
|
class ProfileEmailChange(models.Model):
|
|
token_hash = models.CharField(max_length=64, primary_key=True)
|
|
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
|
email = models.EmailField()
|
|
ldap_uuid = models.CharField(max_length=64)
|
|
expires_at = models.DateTimeField(db_index=True)
|
|
|
|
|
|
class RateWindow(models.Model):
|
|
key = models.CharField(max_length=64, primary_key=True)
|
|
count = models.PositiveIntegerField(default=0)
|
|
expires_at = models.DateTimeField(db_index=True)
|
|
|
|
|
|
class Subscription(models.Model):
|
|
stripe_id = models.CharField(max_length=128, primary_key=True)
|
|
account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="subscriptions")
|
|
tier = models.CharField(max_length=48, blank=True)
|
|
status = models.CharField(max_length=32)
|
|
paid_until = models.DateTimeField(null=True)
|
|
cancel_at_period_end = models.BooleanField(default=False)
|
|
|
|
|
|
class ManagedGroup(models.Model):
|
|
"""Retain old mappings so removing a tier from config also revokes its group."""
|
|
|
|
account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="managed_groups")
|
|
dn = models.CharField(max_length=1024)
|
|
|
|
class Meta:
|
|
constraints = [models.UniqueConstraint(fields=["account", "dn"], name="unique_account_group")]
|
|
|
|
|
|
class CryptoInvoice(models.Model):
|
|
"""Immutable prepaid order; one canonical invoice can grant one month only."""
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
account = models.ForeignKey(Account, on_delete=models.PROTECT)
|
|
tier = models.CharField(max_length=48)
|
|
usd_amount = models.DecimalField(max_digits=12, decimal_places=2)
|
|
crypto = models.CharField(max_length=32)
|
|
provider_id = models.CharField(max_length=128, blank=True)
|
|
wallet = models.CharField(max_length=512, blank=True)
|
|
crypto_amount = models.CharField(max_length=128, blank=True)
|
|
# SHKeeper invoices do not expire. Keep incomplete orders available for late
|
|
# payments; never silently discard a deposit address with money on the way.
|
|
status = models.CharField(max_length=24, default="creating")
|
|
credited_at = models.DateTimeField(null=True)
|
|
access_start = models.DateTimeField(null=True)
|
|
access_end = models.DateTimeField(null=True, db_index=True)
|
|
last_checked_at = models.DateTimeField(null=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
constraints = [models.UniqueConstraint(fields=["account"], condition=models.Q(credited_at__isnull=True), name="one_uncredited_crypto_invoice")]
|
|
|
|
|
|
class WebhookEvent(models.Model):
|
|
stripe_id = models.CharField(max_length=128, primary_key=True)
|
|
account = models.ForeignKey(Account, on_delete=models.CASCADE, null=True)
|
|
received_at = models.DateTimeField(auto_now_add=True)
|
|
processed_at = models.DateTimeField(null=True)
|