100 lines
3.2 KiB
Python
100 lines
3.2 KiB
Python
"""Subscription model for organization billing."""
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Enum
|
|
from gatehouse_app.models.base import BaseModel
|
|
import enum
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SubscriptionStatus(enum.Enum):
|
|
"""Subscription status values."""
|
|
TRIAL = "trial"
|
|
ACTIVE = "active"
|
|
PAST_DUE = "past_due"
|
|
CANCELLED = "cancelled"
|
|
SUSPENDED = "suspended"
|
|
|
|
|
|
class BillingCycle(enum.Enum):
|
|
"""Billing cycle values."""
|
|
MONTHLY = "monthly"
|
|
YEARLY = "yearly"
|
|
|
|
|
|
class Subscription(BaseModel):
|
|
"""Organization subscription record.
|
|
|
|
Links an organization to a plan and tracks billing state.
|
|
"""
|
|
__tablename__ = "subscriptions"
|
|
|
|
# Organization relation
|
|
organization_id = Column(
|
|
String(36),
|
|
ForeignKey("organizations.id", ondelete="CASCADE"),
|
|
unique=True,
|
|
nullable=False
|
|
)
|
|
|
|
# Plan relation
|
|
plan_id = Column(
|
|
String(36),
|
|
ForeignKey("plans.id", ondelete="SET NULL"),
|
|
nullable=True
|
|
)
|
|
|
|
# Status
|
|
status = Column(
|
|
Enum(SubscriptionStatus, name="subscription_status"),
|
|
nullable=False,
|
|
default=SubscriptionStatus.TRIAL
|
|
)
|
|
|
|
# Billing
|
|
billing_cycle = Column(
|
|
Enum(BillingCycle, name="billing_cycle"),
|
|
nullable=False,
|
|
default=BillingCycle.MONTHLY
|
|
)
|
|
|
|
# Period dates
|
|
current_period_start = Column(DateTime, nullable=True)
|
|
current_period_end = Column(DateTime, nullable=True)
|
|
|
|
# Trial
|
|
trial_ends_at = Column(DateTime, nullable=True)
|
|
|
|
# Stripe
|
|
stripe_subscription_id = Column(String(100), nullable=True)
|
|
|
|
# Overage
|
|
overage_enabled = Column(Boolean, nullable=False, default=True)
|
|
|
|
# Cancellation
|
|
cancelled_at = Column(DateTime, nullable=True)
|
|
cancel_at_period_end = Column(Boolean, nullable=False, default=False)
|
|
|
|
def __repr__(self):
|
|
return f"<Subscription org={self.organization_id} status={self.status.value}>"
|
|
|
|
def to_dict(self):
|
|
"""Convert subscription to dictionary."""
|
|
return {
|
|
"id": self.id,
|
|
"organization_id": self.organization_id,
|
|
"plan_id": self.plan_id,
|
|
"status": self.status.value if self.status else None,
|
|
"billing_cycle": self.billing_cycle.value if self.billing_cycle else None,
|
|
"current_period_start": self.current_period_start.isoformat() + "Z" if self.current_period_start else None,
|
|
"current_period_end": self.current_period_end.isoformat() + "Z" if self.current_period_end else None,
|
|
"trial_ends_at": self.trial_ends_at.isoformat() + "Z" if self.trial_ends_at else None,
|
|
"stripe_subscription_id": self.stripe_subscription_id,
|
|
"overage_enabled": self.overage_enabled,
|
|
"cancelled_at": self.cancelled_at.isoformat() + "Z" if self.cancelled_at else None,
|
|
"cancel_at_period_end": self.cancel_at_period_end,
|
|
"created_at": self.created_at.isoformat() + "Z" if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() + "Z" if self.updated_at else None,
|
|
}
|