"""Organization API endpoints: view org, list tiers, upgrade/downgrade.""" from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.auth import get_current_user, get_current_org from app.dependencies import get_db from app.models.saas_models import Organization, OrganizationMember, User from app.services.tier_limits import ( TIER_LIMITS, get_org_limits, get_tier_info, ) router = APIRouter(tags=["organizations"]) # ── Response schemas ──────────────────────────────────────────────────────── class OrgMemberResponse(BaseModel): user_id: str email: str display_name: str | None role: str class OrgResponse(BaseModel): id: str slug: str name: str tier: str custom_domain: str | None member_count: int tier_info: dict class TierDetailResponse(BaseModel): tier: str limits: dict class UpgradeRequest(BaseModel): tier: str # "free" | "pro" | "team" # ── Endpoints ─────────────────────────────────────────────────────────────── @router.get("/tiers") async def list_tiers(): """List all available tiers and their limits (public endpoint).""" return { "tiers": [ { "name": tier_name, "display_name": { "free": "Free", "pro": "Pro ($9/mo)", "team": "Team ($29/mo)", }.get(tier_name, tier_name), "limits": limits, } for tier_name, limits in TIER_LIMITS.items() ] } @router.get("/my", response_model=OrgResponse) async def get_my_org( org: Organization = Depends(get_current_org), db: AsyncSession = Depends(get_db), ): """Get the current user's organization with tier limits info.""" # Count members result = await db.execute( select(OrganizationMember).where( OrganizationMember.organization_id == org.id ) ) members = result.scalars().all() return OrgResponse( id=org.id, slug=org.slug, name=org.name, tier=org.tier or "free", custom_domain=org.custom_domain, member_count=len(members), tier_info=get_tier_info(org), ) @router.patch("/my/tier", response_model=OrgResponse) async def update_org_tier( body: UpgradeRequest, org: Organization = Depends(get_current_org), user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Update the organization's tier. In production, this would be gated by Stripe payment verification. For now, this is an admin-only endpoint that directly sets the tier. Only the org owner can change the tier. """ # Verify user is owner result = await db.execute( select(OrganizationMember).where( OrganizationMember.organization_id == org.id, OrganizationMember.user_id == user.id, ) ) membership = result.scalar_one_or_none() if not membership or membership.role != "owner": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only the organization owner can change the plan tier.", ) if body.tier not in ("free", "pro", "team"): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid tier '{body.tier}'. Must be one of: free, pro, team.", ) org.tier = body.tier await db.flush() await db.refresh(org) # Count members for response members_result = await db.execute( select(OrganizationMember).where( OrganizationMember.organization_id == org.id ) ) members = members_result.scalars().all() return OrgResponse( id=org.id, slug=org.slug, name=org.name, tier=org.tier, custom_domain=org.custom_domain, member_count=len(members), tier_info=get_tier_info(org), ) @router.get("/my/limits") async def get_my_limits( org: Organization = Depends(get_current_org), ): """Get the current organization's tier limits and feature flags.""" return get_tier_info(org)