Add superadmin routes to API

This commit is contained in:
2026-04-21 17:11:03 +09:30
parent aaec6af6ad
commit 1778dd85d5
33 changed files with 4831 additions and 31 deletions
+5
View File
@@ -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"]
+61
View File
@@ -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,
}