Add superadmin routes to API
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
"""Billing models package."""
|
||||
from gatehouse_app.models.billing.plan import Plan
|
||||
from gatehouse_app.models.billing.subscription import Subscription, SubscriptionStatus, BillingCycle
|
||||
|
||||
__all__ = ["Plan", "Subscription", "SubscriptionStatus", "BillingCycle"]
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Plan model for subscription tiers."""
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import Column, String, Integer, Boolean, DateTime, Text
|
||||
from gatehouse_app.models.base import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Plan(BaseModel):
|
||||
"""Subscription plan definition.
|
||||
|
||||
Represents different pricing tiers that organizations can subscribe to.
|
||||
"""
|
||||
__tablename__ = "plans"
|
||||
|
||||
name = Column(String(100), nullable=False)
|
||||
slug = Column(String(50), unique=True, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# Pricing in cents
|
||||
price_monthly = Column(Integer, nullable=False, default=0) # Price in cents
|
||||
price_yearly = Column(Integer, nullable=False, default=0) # Price in cents
|
||||
|
||||
# User limits
|
||||
included_users = Column(Integer, nullable=False, default=0) # 0 = unlimited
|
||||
|
||||
# Overage pricing (cents per user over limit)
|
||||
overage_rate_per_user = Column(Integer, nullable=False, default=0)
|
||||
|
||||
# Feature flags (JSON)
|
||||
features = Column(Text, nullable=True) # JSON string
|
||||
|
||||
# Stripe integration
|
||||
stripe_price_id_monthly = Column(String(100), nullable=True)
|
||||
stripe_price_id_yearly = Column(String(100), nullable=True)
|
||||
|
||||
# Active/inactive
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Plan {self.slug}: ${self.price_monthly / 100}/mo>"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert plan to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"slug": self.slug,
|
||||
"description": self.description,
|
||||
"price_monthly": self.price_monthly,
|
||||
"price_yearly": self.price_yearly,
|
||||
"included_users": self.included_users,
|
||||
"overage_rate_per_user": self.overage_rate_per_user,
|
||||
"features": self.features,
|
||||
"stripe_price_id_monthly": self.stripe_price_id_monthly,
|
||||
"stripe_price_id_yearly": self.stripe_price_id_yearly,
|
||||
"is_active": self.is_active,
|
||||
"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,
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"""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,
|
||||
}
|
||||
Reference in New Issue
Block a user