Files
saas_backend/app/services/auth/subscription_plan_service.py
T

279 lines
10 KiB
Python

import uuid
from typing import List, Optional
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import or_, cast, String, asc, desc
from fastapi import HTTPException, status
from app.models.auth.subscription_plan_model import SubscriptionPlan
from app.models.auth.plan_access_model import PlanAccess
from app.models.auth.plan_module_access_model import PlanModuleAccess
from app.models.auth.tenant_model import Tenant
from app.models.auth.role_model import Role
from app.models.auth.tenant_module_model import TenantModule
from app.models.auth.module_environment_model import ModuleEnvironment
from app.schemas.auth.role_schema import RoleUpdate
from app.services.auth.role_service import RoleService
from app.services.auth.event_service import EventService
from app.schemas.auth.subscription_plan_schema import (
SubscriptionPlanCreate,
SubscriptionPlanUpdate,
SubscriptionPlanPaginatedResponse,
SubscriptionPlanResponse
)
class SubscriptionPlanService:
@staticmethod
def _resolve_environment_slug(
db: Session,
module_id: uuid.UUID,
assigned_environment_slug: Optional[str],
) -> Optional[str]:
if assigned_environment_slug:
return assigned_environment_slug
default_env = (
db.query(ModuleEnvironment)
.filter(
ModuleEnvironment.module_id == module_id,
ModuleEnvironment.is_default == True,
)
.first()
)
if default_env:
return default_env.slug
any_env = (
db.query(ModuleEnvironment)
.filter(ModuleEnvironment.module_id == module_id)
.first()
)
return any_env.slug if any_env else None
@staticmethod
def _sync_tenant_default_roles_for_plan(
db: Session,
plan_id: uuid.UUID,
) -> None:
plan = db.query(SubscriptionPlan).filter(SubscriptionPlan.id == plan_id).first()
if not plan:
return
plan_accesses = db.query(PlanAccess).filter(PlanAccess.plan_id == plan_id).all()
plan_module_accesses = (
db.query(PlanModuleAccess).filter(PlanModuleAccess.plan_id == plan_id).all()
)
effective_access_ids = [row.access_id for row in plan_accesses] + [
row.module_access_id for row in plan_module_accesses
]
tenants = db.query(Tenant).filter(Tenant.plan_id == plan_id).all()
for tenant in tenants:
default_role = (
db.query(Role)
.filter(Role.tenant_id == tenant.id, Role.is_default == True)
.first()
)
if default_role:
RoleService.update_role(
db,
default_role.id,
RoleUpdate(access_ids=effective_access_ids),
is_superadmin=True,
)
active_modules = (
db.query(TenantModule)
.filter(
TenantModule.tenant_id == tenant.id,
TenantModule.is_active == True,
)
.all()
)
if not active_modules:
continue
targets = []
for tm in active_modules:
env_slug = SubscriptionPlanService._resolve_environment_slug(
db,
tm.module_id,
tm.assigned_environment_slug,
)
if env_slug:
targets.append(
{
"module_id": str(tm.module_id),
"environment_slug": env_slug,
}
)
if not targets:
continue
EventService.emit_event(
db,
event_type="TENANT_UPDATED",
payload={
"tenant_id": str(tenant.id),
"tenant_name": tenant.tenant_name,
"tenant_domain": tenant.tenant_domain,
"tenant_logo_url": tenant.tenant_logo_url,
"max_users_allowed": plan.max_users_allowed,
"targets": targets,
},
tenant_id=tenant.id,
)
@staticmethod
def create_plan(db: Session, plan_data: SubscriptionPlanCreate) -> SubscriptionPlan:
existing = db.query(SubscriptionPlan).filter(SubscriptionPlan.name == plan_data.name).first()
if existing:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Plan name already exists")
plan = SubscriptionPlan(
name=plan_data.name,
description=plan_data.description,
price=plan_data.price,
duration_days=plan_data.duration_days,
max_users_allowed=plan_data.max_users_allowed,
is_public=plan_data.is_public,
status=plan_data.status
)
db.add(plan)
db.flush()
if plan_data.access_ids:
for acc_id in plan_data.access_ids:
db.add(PlanAccess(plan_id=plan.id, access_id=acc_id))
if plan_data.module_access_ids:
for macc_id in plan_data.module_access_ids:
db.add(PlanModuleAccess(plan_id=plan.id, module_access_id=macc_id))
db.commit()
db.refresh(plan)
return plan
@staticmethod
def update_plan(db: Session, plan_id: uuid.UUID, plan_data: SubscriptionPlanUpdate) -> SubscriptionPlan:
plan = db.query(SubscriptionPlan).filter(SubscriptionPlan.id == plan_id).first()
if not plan:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found")
update_dict = plan_data.model_dump(exclude_unset=True)
if "name" in update_dict and update_dict["name"] != plan.name:
existing = db.query(SubscriptionPlan).filter(SubscriptionPlan.name == update_dict["name"]).first()
if existing:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Plan name already exists")
if "access_ids" in update_dict:
access_ids = update_dict.pop("access_ids")
db.query(PlanAccess).filter(PlanAccess.plan_id == plan.id).delete()
if access_ids:
for acc_id in access_ids:
db.add(PlanAccess(plan_id=plan.id, access_id=acc_id))
if "module_access_ids" in update_dict:
module_access_ids = update_dict.pop("module_access_ids")
db.query(PlanModuleAccess).filter(PlanModuleAccess.plan_id == plan.id).delete()
if module_access_ids:
for macc_id in module_access_ids:
db.add(PlanModuleAccess(plan_id=plan.id, module_access_id=macc_id))
for key, value in update_dict.items():
setattr(plan, key, value)
db.commit()
db.refresh(plan)
SubscriptionPlanService._sync_tenant_default_roles_for_plan(db, plan.id)
db.commit()
return plan
@staticmethod
def get_plan(db: Session, plan_id: uuid.UUID) -> SubscriptionPlan:
plan = (
db.query(SubscriptionPlan)
.options(
joinedload(SubscriptionPlan.plan_accesses),
joinedload(SubscriptionPlan.plan_module_accesses)
)
.filter(SubscriptionPlan.id == plan_id)
.first()
)
if not plan:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found")
return plan
@staticmethod
def get_all_plans(db: Session, is_public: Optional[bool] = None, status: Optional[str] = None):
query = db.query(SubscriptionPlan)
if is_public is not None:
query = query.filter(SubscriptionPlan.is_public == is_public)
if status is not None:
query = query.filter(SubscriptionPlan.status == status)
return query.all()
@staticmethod
def get_paginated_plans(
db: Session,
page: int = 1,
page_size: int = 10,
search: Optional[str] = None,
filter_names: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
visibility: Optional[List[bool]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = None,
) -> SubscriptionPlanPaginatedResponse:
query = db.query(SubscriptionPlan)
if filter_names:
query = query.filter(SubscriptionPlan.name.in_(filter_names))
if statuses:
query = query.filter(SubscriptionPlan.status.in_(statuses))
if visibility is not None and len(visibility) > 0:
query = query.filter(SubscriptionPlan.is_public.in_(visibility))
if search:
query = query.filter(
or_(
SubscriptionPlan.name.ilike(f"%{search}%"),
cast(SubscriptionPlan.id, String).ilike(f"%{search}%")
)
)
sort_column_map = {
"name": SubscriptionPlan.name,
"price": SubscriptionPlan.price,
"status": SubscriptionPlan.status,
"visibility": SubscriptionPlan.is_public,
}
sort_column = sort_column_map.get(sort_by or "")
if sort_column is not None:
order_fn = desc if (sort_order or "").lower() == "desc" else asc
query = query.order_by(order_fn(sort_column))
total = query.count()
offset = (page - 1) * page_size
plans = query.offset(offset).limit(page_size).all()
total_pages = (total + page_size - 1) // page_size if total > 0 else 0
return SubscriptionPlanPaginatedResponse(
items=[SubscriptionPlanResponse.from_orm(p) for p in plans],
total=total,
page=page,
page_size=page_size,
total_pages=total_pages
)
@staticmethod
def delete_plan(db: Session, plan_id: uuid.UUID):
plan = SubscriptionPlanService.get_plan(db, plan_id)
db.delete(plan)
db.commit()
return {"message": "Plan deleted successfully"}