feat(billing): add GET /billing/state aggregating subscription + plan + features
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,61 @@ class BillingService:
|
||||
)
|
||||
return session.url
|
||||
|
||||
@staticmethod
|
||||
async def get_billing_state(db: AsyncSession, account):
|
||||
"""Aggregate Subscription + PlanLimits + PlanBilling + resolved feature
|
||||
flags for the account."""
|
||||
from app.models.plan_limits import PlanLimits
|
||||
from app.models.plan_billing import PlanBilling
|
||||
from app.models.feature_flag import (
|
||||
FeatureFlag, PlanFeatureDefault, AccountFeatureOverride,
|
||||
)
|
||||
|
||||
sub = (await db.execute(
|
||||
select(Subscription).where(Subscription.account_id == account.id)
|
||||
)).scalar_one_or_none()
|
||||
if sub is None:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=404, detail="No subscription for account")
|
||||
|
||||
pl = (await db.execute(
|
||||
select(PlanLimits).where(PlanLimits.plan == sub.plan)
|
||||
)).scalar_one_or_none()
|
||||
pb = (await db.execute(
|
||||
select(PlanBilling).where(PlanBilling.plan == sub.plan)
|
||||
)).scalar_one_or_none()
|
||||
|
||||
# Resolved feature flags: plan defaults overridden by account overrides
|
||||
defaults = (await db.execute(
|
||||
select(PlanFeatureDefault, FeatureFlag)
|
||||
.join(FeatureFlag, PlanFeatureDefault.flag_id == FeatureFlag.id)
|
||||
.where(PlanFeatureDefault.plan == sub.plan)
|
||||
)).all()
|
||||
resolved = {flag.flag_key: pfd.enabled for pfd, flag in defaults}
|
||||
overrides = (await db.execute(
|
||||
select(AccountFeatureOverride, FeatureFlag)
|
||||
.join(FeatureFlag, AccountFeatureOverride.flag_id == FeatureFlag.id)
|
||||
.where(AccountFeatureOverride.account_id == account.id)
|
||||
)).all()
|
||||
for ovr, flag in overrides:
|
||||
resolved[flag.flag_key] = ovr.enabled
|
||||
|
||||
return {
|
||||
"subscription": {
|
||||
"status": sub.status,
|
||||
"plan": sub.plan,
|
||||
"current_period_start": sub.current_period_start,
|
||||
"current_period_end": sub.current_period_end,
|
||||
"cancel_at_period_end": sub.cancel_at_period_end,
|
||||
"seat_limit": sub.seat_limit,
|
||||
"has_pro_entitlement": sub.has_pro_entitlement,
|
||||
"is_paid": sub.is_paid,
|
||||
},
|
||||
"plan_billing": pb,
|
||||
"plan_limits": _plan_limits_to_dict(pl) if pl else {},
|
||||
"enabled_features": resolved,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def apply_subscription_event(
|
||||
db: AsyncSession, event_id: str, event_type: str, payload: dict
|
||||
@@ -136,6 +191,10 @@ class BillingService:
|
||||
return True
|
||||
|
||||
|
||||
def _plan_limits_to_dict(pl) -> dict:
|
||||
return {c.name: getattr(pl, c.name) for c in pl.__table__.columns}
|
||||
|
||||
|
||||
def _excerpt(payload: dict) -> dict:
|
||||
obj = payload.get("data", {}).get("object", {})
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user