REFACTOR ALL APIs
This commit is contained in:
@@ -1,421 +0,0 @@
|
||||
"""
|
||||
Subscription API endpoints for plan limits and feature validation
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Path, Query
|
||||
from typing import List, Dict, Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from app.services.subscription_limit_service import SubscriptionLimitService
|
||||
from app.services.payment_service import PaymentService
|
||||
from app.repositories import SubscriptionRepository
|
||||
from app.models.tenants import Subscription
|
||||
from shared.auth.decorators import get_current_user_dep, require_admin_role_dep
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.monitoring.metrics import track_endpoint_metrics
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
|
||||
# Dependency injection for subscription limit service
|
||||
def get_subscription_limit_service():
|
||||
try:
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
return SubscriptionLimitService(database_manager)
|
||||
except Exception as e:
|
||||
logger.error("Failed to create subscription limit service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Service initialization failed")
|
||||
|
||||
def get_payment_service():
|
||||
try:
|
||||
return PaymentService()
|
||||
except Exception as e:
|
||||
logger.error("Failed to create payment service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Payment service initialization failed")
|
||||
|
||||
def get_subscription_repository():
|
||||
try:
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
# This would need to be properly initialized with session
|
||||
# For now, we'll use the service pattern
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Failed to create subscription repository", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Repository initialization failed")
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/limits")
|
||||
async def get_subscription_limits(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Get current subscription limits for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
limits = await limit_service.get_tenant_subscription_limits(str(tenant_id))
|
||||
return limits
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get subscription limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get subscription limits"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/usage")
|
||||
async def get_usage_summary(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Get usage summary vs limits for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
usage = await limit_service.get_usage_summary(str(tenant_id))
|
||||
return usage
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get usage summary",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get usage summary"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/can-add-location")
|
||||
async def can_add_location(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant can add another location"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
result = await limit_service.can_add_location(str(tenant_id))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check location limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check location limits"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/can-add-product")
|
||||
async def can_add_product(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant can add another product"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
result = await limit_service.can_add_product(str(tenant_id))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check product limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check product limits"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/can-add-user")
|
||||
async def can_add_user(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant can add another user/member"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
result = await limit_service.can_add_user(str(tenant_id))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check user limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check user limits"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/features/{feature}")
|
||||
async def has_feature(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
feature: str = Path(..., description="Feature name"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant has access to a specific feature"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
result = await limit_service.has_feature(str(tenant_id), feature)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check feature access",
|
||||
tenant_id=str(tenant_id),
|
||||
feature=feature,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check feature access"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/validate-upgrade/{new_plan}")
|
||||
async def validate_plan_upgrade(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
new_plan: str = Path(..., description="New plan name"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Validate if tenant can upgrade to a new plan"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user is owner/admin of tenant
|
||||
result = await limit_service.validate_plan_upgrade(str(tenant_id), new_plan)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to validate plan upgrade",
|
||||
tenant_id=str(tenant_id),
|
||||
new_plan=new_plan,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to validate plan upgrade"
|
||||
)
|
||||
|
||||
@router.post("/subscriptions/{tenant_id}/upgrade")
|
||||
async def upgrade_subscription_plan(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
new_plan: str = Query(..., description="New plan name"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Upgrade subscription plan for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user is owner/admin of tenant
|
||||
|
||||
# First validate the upgrade
|
||||
validation = await limit_service.validate_plan_upgrade(str(tenant_id), new_plan)
|
||||
if not validation.get("can_upgrade", False):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=validation.get("reason", "Cannot upgrade to this plan")
|
||||
)
|
||||
|
||||
# TODO: Implement actual plan upgrade logic
|
||||
# This would involve:
|
||||
# 1. Update subscription in database
|
||||
# 2. Process payment changes
|
||||
# 3. Update billing cycle
|
||||
# 4. Send notifications
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Plan upgrade to {new_plan} initiated",
|
||||
"validation": validation
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to upgrade subscription plan",
|
||||
tenant_id=str(tenant_id),
|
||||
new_plan=new_plan,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to upgrade subscription plan"
|
||||
)
|
||||
|
||||
@router.get("/plans")
|
||||
async def get_available_plans():
|
||||
"""Get all available subscription plans with features and pricing - Public endpoint"""
|
||||
|
||||
try:
|
||||
# This could be moved to a config service or database
|
||||
plans = {
|
||||
"starter": {
|
||||
"name": "Starter",
|
||||
"description": "Ideal para panaderías pequeñas o nuevas",
|
||||
"monthly_price": 49.0,
|
||||
"max_users": 5,
|
||||
"max_locations": 1,
|
||||
"max_products": 50,
|
||||
"features": {
|
||||
"inventory_management": "basic",
|
||||
"demand_prediction": "basic",
|
||||
"production_reports": "basic",
|
||||
"analytics": "basic",
|
||||
"support": "email",
|
||||
"trial_days": 14,
|
||||
"locations": "1_location",
|
||||
"ai_model_configuration": "basic" # Added AI model configuration for all tiers
|
||||
},
|
||||
"trial_available": True
|
||||
},
|
||||
"professional": {
|
||||
"name": "Professional",
|
||||
"description": "Ideal para panaderías y cadenas en crecimiento",
|
||||
"monthly_price": 129.0,
|
||||
"max_users": 15,
|
||||
"max_locations": 2,
|
||||
"max_products": -1, # Unlimited
|
||||
"features": {
|
||||
"inventory_management": "advanced",
|
||||
"demand_prediction": "ai_92_percent",
|
||||
"production_management": "complete",
|
||||
"pos_integrated": True,
|
||||
"logistics": "basic",
|
||||
"analytics": "advanced",
|
||||
"support": "priority_24_7",
|
||||
"trial_days": 14,
|
||||
"locations": "1_2_locations",
|
||||
"ai_model_configuration": "advanced" # Enhanced AI model configuration for Professional
|
||||
},
|
||||
"trial_available": True,
|
||||
"popular": True
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
"description": "Ideal para cadenas con obradores centrales",
|
||||
"monthly_price": 399.0,
|
||||
"max_users": -1, # Unlimited
|
||||
"max_locations": -1, # Unlimited
|
||||
"max_products": -1, # Unlimited
|
||||
"features": {
|
||||
"inventory_management": "multi_location",
|
||||
"demand_prediction": "ai_personalized",
|
||||
"production_optimization": "capacity",
|
||||
"erp_integration": True,
|
||||
"logistics": "advanced",
|
||||
"analytics": "predictive",
|
||||
"api_access": "personalized",
|
||||
"account_manager": True,
|
||||
"demo": "personalized",
|
||||
"locations": "unlimited_obradores",
|
||||
"ai_model_configuration": "enterprise" # Full AI model configuration for Enterprise
|
||||
},
|
||||
"trial_available": False,
|
||||
"contact_sales": True
|
||||
}
|
||||
}
|
||||
|
||||
return {"plans": plans}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get available plans", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get available plans"
|
||||
)
|
||||
|
||||
# New endpoints for payment processing during registration
|
||||
@router.post("/subscriptions/register-with-subscription")
|
||||
async def register_with_subscription(
|
||||
user_data: Dict[str, Any],
|
||||
plan_id: str = Query(..., description="Plan ID to subscribe to"),
|
||||
payment_method_id: str = Query(..., description="Payment method ID from frontend"),
|
||||
use_trial: bool = Query(False, description="Whether to use trial period for pilot users"),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Process user registration with subscription creation"""
|
||||
|
||||
try:
|
||||
result = await payment_service.process_registration_with_subscription(
|
||||
user_data,
|
||||
plan_id,
|
||||
payment_method_id,
|
||||
use_trial
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Registration and subscription created successfully",
|
||||
"data": result
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to register with subscription", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to register with subscription"
|
||||
)
|
||||
|
||||
@router.post("/subscriptions/{tenant_id}/cancel")
|
||||
async def cancel_subscription(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Cancel subscription for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user is owner/admin of tenant
|
||||
# In a real implementation, you would need to retrieve the subscription ID from the database
|
||||
# For now, this is a placeholder
|
||||
subscription_id = "sub_test" # This would come from the database
|
||||
|
||||
result = await payment_service.cancel_subscription(subscription_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Subscription cancelled successfully",
|
||||
"data": {
|
||||
"subscription_id": result.id,
|
||||
"status": result.status
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to cancel subscription", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to cancel subscription"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/invoices")
|
||||
async def get_invoices(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Get invoices for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
# In a real implementation, you would need to retrieve the customer ID from the database
|
||||
# For now, this is a placeholder
|
||||
customer_id = "cus_test" # This would come from the database
|
||||
|
||||
invoices = await payment_service.get_invoices(customer_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": invoices
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get invoices", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get invoices"
|
||||
)
|
||||
161
services/tenant/app/api/tenant_members.py
Normal file
161
services/tenant/app/api/tenant_members.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Tenant Member Management API - ATOMIC operations
|
||||
Handles team member CRUD operations
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Path, Query
|
||||
from typing import List, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.tenants import TenantMemberResponse
|
||||
from app.services.tenant_service import EnhancedTenantService
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing.route_builder import RouteBuilder
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.monitoring.metrics import track_endpoint_metrics
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
route_builder = RouteBuilder("tenants")
|
||||
|
||||
# Dependency injection for enhanced tenant service
|
||||
def get_enhanced_tenant_service():
|
||||
try:
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
return EnhancedTenantService(database_manager)
|
||||
except Exception as e:
|
||||
logger.error("Failed to create enhanced tenant service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Service initialization failed")
|
||||
|
||||
@router.post(route_builder.build_base_route("{tenant_id}/members", include_tenant_prefix=False), response_model=TenantMemberResponse)
|
||||
@track_endpoint_metrics("tenant_add_member")
|
||||
async def add_team_member(
|
||||
user_id: str,
|
||||
role: str,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Add a team member to tenant with enhanced validation and role management"""
|
||||
|
||||
try:
|
||||
result = await tenant_service.add_team_member(
|
||||
str(tenant_id),
|
||||
user_id,
|
||||
role,
|
||||
current_user["user_id"]
|
||||
)
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Add team member failed",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=user_id,
|
||||
role=role,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to add team member"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("{tenant_id}/members", include_tenant_prefix=False), response_model=List[TenantMemberResponse])
|
||||
async def get_team_members(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
active_only: bool = Query(True, description="Only return active members"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get all team members for a tenant with enhanced filtering"""
|
||||
|
||||
try:
|
||||
members = await tenant_service.get_team_members(
|
||||
str(tenant_id),
|
||||
current_user["user_id"],
|
||||
active_only=active_only
|
||||
)
|
||||
return members
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Get team members failed",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get team members"
|
||||
)
|
||||
|
||||
@router.put(route_builder.build_base_route("{tenant_id}/members/{member_user_id}/role", include_tenant_prefix=False), response_model=TenantMemberResponse)
|
||||
@track_endpoint_metrics("tenant_update_member_role")
|
||||
async def update_member_role(
|
||||
new_role: str,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
member_user_id: str = Path(..., description="Member user ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Update team member role with enhanced permission validation"""
|
||||
|
||||
try:
|
||||
result = await tenant_service.update_member_role(
|
||||
str(tenant_id),
|
||||
member_user_id,
|
||||
new_role,
|
||||
current_user["user_id"]
|
||||
)
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Update member role failed",
|
||||
tenant_id=str(tenant_id),
|
||||
member_user_id=member_user_id,
|
||||
new_role=new_role,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update member role"
|
||||
)
|
||||
|
||||
@router.delete(route_builder.build_base_route("{tenant_id}/members/{member_user_id}", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("tenant_remove_member")
|
||||
async def remove_team_member(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
member_user_id: str = Path(..., description="Member user ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Remove team member from tenant with enhanced validation"""
|
||||
|
||||
try:
|
||||
success = await tenant_service.remove_team_member(
|
||||
str(tenant_id),
|
||||
member_user_id,
|
||||
current_user["user_id"]
|
||||
)
|
||||
|
||||
if success:
|
||||
return {"success": True, "message": "Team member removed successfully"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to remove team member"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Remove team member failed",
|
||||
tenant_id=str(tenant_id),
|
||||
member_user_id=member_user_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to remove team member"
|
||||
)
|
||||
787
services/tenant/app/api/tenant_operations.py
Normal file
787
services/tenant/app/api/tenant_operations.py
Normal file
@@ -0,0 +1,787 @@
|
||||
"""
|
||||
Tenant Operations API - BUSINESS operations
|
||||
Handles complex tenant operations, registration, search, subscriptions, and analytics
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Path, Query
|
||||
from typing import List, Dict, Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.tenants import (
|
||||
BakeryRegistration, TenantResponse, TenantAccessResponse,
|
||||
TenantSearchRequest
|
||||
)
|
||||
from app.services.tenant_service import EnhancedTenantService
|
||||
from app.services.subscription_limit_service import SubscriptionLimitService
|
||||
from app.services.payment_service import PaymentService
|
||||
from shared.auth.decorators import (
|
||||
get_current_user_dep,
|
||||
require_admin_role_dep
|
||||
)
|
||||
from shared.routing.route_builder import RouteBuilder
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.monitoring.metrics import track_endpoint_metrics
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
route_builder = RouteBuilder("tenants")
|
||||
|
||||
# Dependency injection for enhanced tenant service
|
||||
def get_enhanced_tenant_service():
|
||||
try:
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
return EnhancedTenantService(database_manager)
|
||||
except Exception as e:
|
||||
logger.error("Failed to create enhanced tenant service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Service initialization failed")
|
||||
|
||||
def get_subscription_limit_service():
|
||||
try:
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
return SubscriptionLimitService(database_manager)
|
||||
except Exception as e:
|
||||
logger.error("Failed to create subscription limit service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Service initialization failed")
|
||||
|
||||
def get_payment_service():
|
||||
try:
|
||||
return PaymentService()
|
||||
except Exception as e:
|
||||
logger.error("Failed to create payment service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Payment service initialization failed")
|
||||
|
||||
# ============================================================================
|
||||
# TENANT REGISTRATION & ACCESS OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
@router.post(route_builder.build_base_route("register", include_tenant_prefix=False), response_model=TenantResponse)
|
||||
async def register_bakery(
|
||||
bakery_data: BakeryRegistration,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Register a new bakery/tenant with enhanced validation and features"""
|
||||
|
||||
try:
|
||||
result = await tenant_service.create_bakery(
|
||||
bakery_data,
|
||||
current_user["user_id"]
|
||||
)
|
||||
|
||||
logger.info("Bakery registered successfully",
|
||||
name=bakery_data.name,
|
||||
owner_email=current_user.get('email'),
|
||||
tenant_id=result.id)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Bakery registration failed",
|
||||
name=bakery_data.name,
|
||||
owner_id=current_user["user_id"],
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Bakery registration failed"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("{tenant_id}/my-access", include_tenant_prefix=False), response_model=TenantAccessResponse)
|
||||
async def get_current_user_tenant_access(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep)
|
||||
):
|
||||
"""Get current user's access to tenant with role and permissions"""
|
||||
|
||||
try:
|
||||
# Create tenant service directly
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
tenant_service = EnhancedTenantService(database_manager)
|
||||
|
||||
access_info = await tenant_service.verify_user_access(current_user["user_id"], str(tenant_id))
|
||||
return access_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Current user access verification failed",
|
||||
user_id=current_user["user_id"],
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Access verification failed"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("{tenant_id}/access/{user_id}", include_tenant_prefix=False), response_model=TenantAccessResponse)
|
||||
async def verify_tenant_access(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
user_id: str = Path(..., description="User ID")
|
||||
):
|
||||
"""Verify if user has access to tenant - Enhanced version with detailed permissions"""
|
||||
|
||||
# Check if this is a service request
|
||||
if user_id in ["training-service", "data-service", "forecasting-service", "auth-service"]:
|
||||
# Services have access to all tenants for their operations
|
||||
return TenantAccessResponse(
|
||||
has_access=True,
|
||||
role="service",
|
||||
permissions=["read", "write"]
|
||||
)
|
||||
|
||||
try:
|
||||
# Create tenant service directly
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
tenant_service = EnhancedTenantService(database_manager)
|
||||
|
||||
access_info = await tenant_service.verify_user_access(user_id, str(tenant_id))
|
||||
return access_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Access verification failed",
|
||||
user_id=user_id,
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Access verification failed"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# TENANT SEARCH & DISCOVERY OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
@router.get(route_builder.build_base_route("subdomain/{subdomain}", include_tenant_prefix=False), response_model=TenantResponse)
|
||||
@track_endpoint_metrics("tenant_get_by_subdomain")
|
||||
async def get_tenant_by_subdomain(
|
||||
subdomain: str = Path(..., description="Tenant subdomain"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get tenant by subdomain with enhanced validation"""
|
||||
|
||||
tenant = await tenant_service.get_tenant_by_subdomain(subdomain)
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Verify user has access to this tenant
|
||||
access = await tenant_service.verify_user_access(current_user["user_id"], tenant.id)
|
||||
if not access.has_access:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant"
|
||||
)
|
||||
|
||||
return tenant
|
||||
|
||||
@router.get(route_builder.build_base_route("user/{user_id}/owned", include_tenant_prefix=False), response_model=List[TenantResponse])
|
||||
async def get_user_owned_tenants(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get all tenants owned by a user with enhanced data"""
|
||||
|
||||
# Users can only get their own tenants unless they're admin
|
||||
user_role = current_user.get('role', '').lower()
|
||||
if user_id != current_user["user_id"] and user_role != 'admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Can only access your own tenants"
|
||||
)
|
||||
|
||||
tenants = await tenant_service.get_user_tenants(user_id)
|
||||
return tenants
|
||||
|
||||
@router.get(route_builder.build_base_route("search", include_tenant_prefix=False), response_model=List[TenantResponse])
|
||||
@track_endpoint_metrics("tenant_search")
|
||||
async def search_tenants(
|
||||
search_term: str = Query(..., description="Search term"),
|
||||
business_type: Optional[str] = Query(None, description="Business type filter"),
|
||||
city: Optional[str] = Query(None, description="City filter"),
|
||||
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
limit: int = Query(50, ge=1, le=100, description="Maximum number of records to return"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Search tenants with advanced filters and pagination"""
|
||||
|
||||
tenants = await tenant_service.search_tenants(
|
||||
search_term=search_term,
|
||||
business_type=business_type,
|
||||
city=city,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
return tenants
|
||||
|
||||
@router.get(route_builder.build_base_route("nearby", include_tenant_prefix=False), response_model=List[TenantResponse])
|
||||
@track_endpoint_metrics("tenant_get_nearby")
|
||||
async def get_nearby_tenants(
|
||||
latitude: float = Query(..., description="Latitude coordinate"),
|
||||
longitude: float = Query(..., description="Longitude coordinate"),
|
||||
radius_km: float = Query(10.0, ge=0.1, le=100.0, description="Search radius in kilometers"),
|
||||
limit: int = Query(50, ge=1, le=100, description="Maximum number of results"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get tenants near a geographic location with enhanced geospatial search"""
|
||||
|
||||
tenants = await tenant_service.get_tenants_near_location(
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
radius_km=radius_km,
|
||||
limit=limit
|
||||
)
|
||||
return tenants
|
||||
|
||||
@router.get(route_builder.build_base_route("users/{user_id}", include_tenant_prefix=False), response_model=List[TenantResponse])
|
||||
@track_endpoint_metrics("tenant_get_user_tenants")
|
||||
async def get_user_tenants(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get all tenants owned by a user - Fixed endpoint for frontend"""
|
||||
|
||||
try:
|
||||
tenants = await tenant_service.get_user_tenants(user_id)
|
||||
logger.info("Retrieved user tenants", user_id=user_id, tenant_count=len(tenants))
|
||||
return tenants
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Get user tenants failed", user_id=user_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user tenants"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("members/user/{user_id}", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("tenant_get_user_memberships")
|
||||
async def get_user_memberships(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get all tenant memberships for a user (for authentication service)"""
|
||||
|
||||
try:
|
||||
memberships = await tenant_service.get_user_memberships(user_id)
|
||||
logger.info("Retrieved user memberships", user_id=user_id, membership_count=len(memberships))
|
||||
return memberships
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Get user memberships failed", user_id=user_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user memberships"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# TENANT MODEL STATUS OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
@router.put(route_builder.build_base_route("{tenant_id}/model-status", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("tenant_update_model_status")
|
||||
async def update_tenant_model_status(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
ml_model_trained: bool = Query(..., description="Whether model is trained"),
|
||||
last_training_date: Optional[datetime] = Query(None, description="Last training date"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Update tenant model training status with enhanced tracking"""
|
||||
|
||||
try:
|
||||
result = await tenant_service.update_model_status(
|
||||
str(tenant_id),
|
||||
ml_model_trained,
|
||||
current_user["user_id"],
|
||||
last_training_date
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Model status update failed",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update model status"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# TENANT ACTIVATION/DEACTIVATION OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
@router.post(route_builder.build_base_route("{tenant_id}/deactivate", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("tenant_deactivate")
|
||||
async def deactivate_tenant(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Deactivate a tenant (owner only) with enhanced validation"""
|
||||
|
||||
try:
|
||||
success = await tenant_service.deactivate_tenant(
|
||||
str(tenant_id),
|
||||
current_user["user_id"]
|
||||
)
|
||||
|
||||
if success:
|
||||
return {"success": True, "message": "Tenant deactivated successfully"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to deactivate tenant"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Tenant deactivation failed",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to deactivate tenant"
|
||||
)
|
||||
|
||||
@router.post(route_builder.build_base_route("{tenant_id}/activate", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("tenant_activate")
|
||||
async def activate_tenant(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Activate a previously deactivated tenant (owner only) with enhanced validation"""
|
||||
|
||||
try:
|
||||
success = await tenant_service.activate_tenant(
|
||||
str(tenant_id),
|
||||
current_user["user_id"]
|
||||
)
|
||||
|
||||
if success:
|
||||
return {"success": True, "message": "Tenant activated successfully"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to activate tenant"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Tenant activation failed",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to activate tenant"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# TENANT STATISTICS & ANALYTICS
|
||||
# ============================================================================
|
||||
|
||||
@router.get(route_builder.build_base_route("statistics", include_tenant_prefix=False), dependencies=[Depends(require_admin_role_dep)])
|
||||
@track_endpoint_metrics("tenant_get_statistics")
|
||||
async def get_tenant_statistics(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get comprehensive tenant statistics (admin only) with enhanced analytics"""
|
||||
|
||||
try:
|
||||
stats = await tenant_service.get_tenant_statistics()
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Get tenant statistics failed", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get tenant statistics"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# SUBSCRIPTION OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
@router.get(route_builder.build_base_route("subscriptions/{tenant_id}/limits", include_tenant_prefix=False))
|
||||
async def get_subscription_limits(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Get current subscription limits for a tenant"""
|
||||
|
||||
try:
|
||||
limits = await limit_service.get_tenant_subscription_limits(str(tenant_id))
|
||||
return limits
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get subscription limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get subscription limits"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("subscriptions/{tenant_id}/usage", include_tenant_prefix=False))
|
||||
async def get_usage_summary(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Get usage summary vs limits for a tenant"""
|
||||
|
||||
try:
|
||||
usage = await limit_service.get_usage_summary(str(tenant_id))
|
||||
return usage
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get usage summary",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get usage summary"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("subscriptions/{tenant_id}/can-add-location", include_tenant_prefix=False))
|
||||
async def can_add_location(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant can add another location"""
|
||||
|
||||
try:
|
||||
result = await limit_service.can_add_location(str(tenant_id))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check location limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check location limits"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("subscriptions/{tenant_id}/can-add-product", include_tenant_prefix=False))
|
||||
async def can_add_product(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant can add another product"""
|
||||
|
||||
try:
|
||||
result = await limit_service.can_add_product(str(tenant_id))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check product limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check product limits"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("subscriptions/{tenant_id}/can-add-user", include_tenant_prefix=False))
|
||||
async def can_add_user(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant can add another user/member"""
|
||||
|
||||
try:
|
||||
result = await limit_service.can_add_user(str(tenant_id))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check user limits",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check user limits"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("subscriptions/{tenant_id}/features/{feature}", include_tenant_prefix=False))
|
||||
async def has_feature(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
feature: str = Path(..., description="Feature name"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Check if tenant has access to a specific feature"""
|
||||
|
||||
try:
|
||||
result = await limit_service.has_feature(str(tenant_id), feature)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check feature access",
|
||||
tenant_id=str(tenant_id),
|
||||
feature=feature,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to check feature access"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("subscriptions/{tenant_id}/validate-upgrade/{new_plan}", include_tenant_prefix=False))
|
||||
async def validate_plan_upgrade(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
new_plan: str = Path(..., description="New plan name"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Validate if tenant can upgrade to a new plan"""
|
||||
|
||||
try:
|
||||
result = await limit_service.validate_plan_upgrade(str(tenant_id), new_plan)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to validate plan upgrade",
|
||||
tenant_id=str(tenant_id),
|
||||
new_plan=new_plan,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to validate plan upgrade"
|
||||
)
|
||||
|
||||
@router.post(route_builder.build_base_route("subscriptions/{tenant_id}/upgrade", include_tenant_prefix=False))
|
||||
async def upgrade_subscription_plan(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
new_plan: str = Query(..., description="New plan name"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
|
||||
):
|
||||
"""Upgrade subscription plan for a tenant"""
|
||||
|
||||
try:
|
||||
# First validate the upgrade
|
||||
validation = await limit_service.validate_plan_upgrade(str(tenant_id), new_plan)
|
||||
if not validation.get("can_upgrade", False):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=validation.get("reason", "Cannot upgrade to this plan")
|
||||
)
|
||||
|
||||
# TODO: Implement actual plan upgrade logic
|
||||
# This would involve:
|
||||
# 1. Update subscription in database
|
||||
# 2. Process payment changes
|
||||
# 3. Update billing cycle
|
||||
# 4. Send notifications
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Plan upgrade to {new_plan} initiated",
|
||||
"validation": validation
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to upgrade subscription plan",
|
||||
tenant_id=str(tenant_id),
|
||||
new_plan=new_plan,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to upgrade subscription plan"
|
||||
)
|
||||
|
||||
@router.get("/api/v1/plans")
|
||||
async def get_available_plans():
|
||||
"""Get all available subscription plans with features and pricing - Public endpoint"""
|
||||
|
||||
try:
|
||||
# This could be moved to a config service or database
|
||||
plans = {
|
||||
"starter": {
|
||||
"name": "Starter",
|
||||
"description": "Ideal para panaderías pequeñas o nuevas",
|
||||
"monthly_price": 49.0,
|
||||
"max_users": 5,
|
||||
"max_locations": 1,
|
||||
"max_products": 50,
|
||||
"features": {
|
||||
"inventory_management": "basic",
|
||||
"demand_prediction": "basic",
|
||||
"production_reports": "basic",
|
||||
"analytics": "basic",
|
||||
"support": "email",
|
||||
"trial_days": 14,
|
||||
"locations": "1_location",
|
||||
"ai_model_configuration": "basic"
|
||||
},
|
||||
"trial_available": True
|
||||
},
|
||||
"professional": {
|
||||
"name": "Professional",
|
||||
"description": "Ideal para panaderías y cadenas en crecimiento",
|
||||
"monthly_price": 129.0,
|
||||
"max_users": 15,
|
||||
"max_locations": 2,
|
||||
"max_products": -1, # Unlimited
|
||||
"features": {
|
||||
"inventory_management": "advanced",
|
||||
"demand_prediction": "ai_92_percent",
|
||||
"production_management": "complete",
|
||||
"pos_integrated": True,
|
||||
"logistics": "basic",
|
||||
"analytics": "advanced",
|
||||
"support": "priority_24_7",
|
||||
"trial_days": 14,
|
||||
"locations": "1_2_locations",
|
||||
"ai_model_configuration": "advanced"
|
||||
},
|
||||
"trial_available": True,
|
||||
"popular": True
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
"description": "Ideal para cadenas con obradores centrales",
|
||||
"monthly_price": 399.0,
|
||||
"max_users": -1, # Unlimited
|
||||
"max_locations": -1, # Unlimited
|
||||
"max_products": -1, # Unlimited
|
||||
"features": {
|
||||
"inventory_management": "multi_location",
|
||||
"demand_prediction": "ai_personalized",
|
||||
"production_optimization": "capacity",
|
||||
"erp_integration": True,
|
||||
"logistics": "advanced",
|
||||
"analytics": "predictive",
|
||||
"api_access": "personalized",
|
||||
"account_manager": True,
|
||||
"demo": "personalized",
|
||||
"locations": "unlimited_obradores",
|
||||
"ai_model_configuration": "enterprise"
|
||||
},
|
||||
"trial_available": False,
|
||||
"contact_sales": True
|
||||
}
|
||||
}
|
||||
|
||||
return {"plans": plans}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get available plans", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get available plans"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# PAYMENT OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
@router.post(route_builder.build_base_route("subscriptions/register-with-subscription", include_tenant_prefix=False))
|
||||
async def register_with_subscription(
|
||||
user_data: Dict[str, Any],
|
||||
plan_id: str = Query(..., description="Plan ID to subscribe to"),
|
||||
payment_method_id: str = Query(..., description="Payment method ID from frontend"),
|
||||
use_trial: bool = Query(False, description="Whether to use trial period for pilot users"),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Process user registration with subscription creation"""
|
||||
|
||||
try:
|
||||
result = await payment_service.process_registration_with_subscription(
|
||||
user_data,
|
||||
plan_id,
|
||||
payment_method_id,
|
||||
use_trial
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Registration and subscription created successfully",
|
||||
"data": result
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to register with subscription", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to register with subscription"
|
||||
)
|
||||
|
||||
@router.post(route_builder.build_base_route("subscriptions/{tenant_id}/cancel", include_tenant_prefix=False))
|
||||
async def cancel_subscription(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Cancel subscription for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user is owner/admin of tenant
|
||||
# In a real implementation, you would need to retrieve the subscription ID from the database
|
||||
# For now, this is a placeholder
|
||||
subscription_id = "sub_test" # This would come from the database
|
||||
|
||||
result = await payment_service.cancel_subscription(subscription_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Subscription cancelled successfully",
|
||||
"data": {
|
||||
"subscription_id": result.id,
|
||||
"status": result.status
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to cancel subscription", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to cancel subscription"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("subscriptions/{tenant_id}/invoices", include_tenant_prefix=False))
|
||||
async def get_invoices(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Get invoices for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
# In a real implementation, you would need to retrieve the customer ID from the database
|
||||
# For now, this is a placeholder
|
||||
customer_id = "cus_test" # This would come from the database
|
||||
|
||||
invoices = await payment_service.get_invoices(customer_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": invoices
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get invoices", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get invoices"
|
||||
)
|
||||
@@ -1,28 +1,23 @@
|
||||
"""
|
||||
Enhanced Tenant API endpoints using repository pattern and dependency injection
|
||||
Tenant API - ATOMIC operations
|
||||
Handles basic CRUD operations for tenants
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Path, Query
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Path
|
||||
from typing import Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.tenants import (
|
||||
BakeryRegistration, TenantResponse, TenantAccessResponse,
|
||||
TenantUpdate, TenantMemberResponse, TenantSearchRequest
|
||||
)
|
||||
from app.schemas.tenants import TenantResponse, TenantUpdate
|
||||
from app.services.tenant_service import EnhancedTenantService
|
||||
from shared.auth.decorators import (
|
||||
get_current_user_dep,
|
||||
require_admin_role,
|
||||
require_admin_role_dep
|
||||
)
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing.route_builder import RouteBuilder
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.monitoring.metrics import track_endpoint_metrics
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
route_builder = RouteBuilder("tenants")
|
||||
|
||||
# Dependency injection for enhanced tenant service
|
||||
def get_enhanced_tenant_service():
|
||||
@@ -34,223 +29,41 @@ def get_enhanced_tenant_service():
|
||||
logger.error("Failed to create enhanced tenant service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Service initialization failed")
|
||||
|
||||
@router.post("/tenants/register", response_model=TenantResponse)
|
||||
async def register_bakery_enhanced(
|
||||
bakery_data: BakeryRegistration,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Register a new bakery/tenant with enhanced validation and features"""
|
||||
|
||||
try:
|
||||
result = await tenant_service.create_bakery(
|
||||
bakery_data,
|
||||
current_user["user_id"]
|
||||
)
|
||||
|
||||
logger.info("Bakery registered successfully",
|
||||
name=bakery_data.name,
|
||||
owner_email=current_user.get('email'),
|
||||
tenant_id=result.id)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Bakery registration failed",
|
||||
name=bakery_data.name,
|
||||
owner_id=current_user["user_id"],
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Bakery registration failed"
|
||||
)
|
||||
|
||||
@router.get("/tenants/{tenant_id}/my-access", response_model=TenantAccessResponse)
|
||||
async def get_current_user_tenant_access(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep)
|
||||
):
|
||||
"""Get current user's access to tenant with role and permissions"""
|
||||
|
||||
try:
|
||||
# Create tenant service directly
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
tenant_service = EnhancedTenantService(database_manager)
|
||||
|
||||
access_info = await tenant_service.verify_user_access(current_user["user_id"], str(tenant_id))
|
||||
return access_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Current user access verification failed",
|
||||
user_id=current_user["user_id"],
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Access verification failed"
|
||||
)
|
||||
|
||||
@router.get("/tenants/{tenant_id}/access/{user_id}", response_model=TenantAccessResponse)
|
||||
async def verify_tenant_access_enhanced(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
user_id: str = Path(..., description="User ID")
|
||||
):
|
||||
"""Verify if user has access to tenant - Enhanced version with detailed permissions"""
|
||||
|
||||
# Check if this is a service request
|
||||
if user_id in ["training-service", "data-service", "forecasting-service", "auth-service"]:
|
||||
# Services have access to all tenants for their operations
|
||||
return TenantAccessResponse(
|
||||
has_access=True,
|
||||
role="service",
|
||||
permissions=["read", "write"]
|
||||
)
|
||||
|
||||
try:
|
||||
# Create tenant service directly
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
tenant_service = EnhancedTenantService(database_manager)
|
||||
|
||||
access_info = await tenant_service.verify_user_access(user_id, str(tenant_id))
|
||||
return access_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Access verification failed",
|
||||
user_id=user_id,
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Access verification failed"
|
||||
)
|
||||
|
||||
@router.get("/tenants/{tenant_id}", response_model=TenantResponse)
|
||||
@router.get(route_builder.build_base_route("{tenant_id}", include_tenant_prefix=False), response_model=TenantResponse)
|
||||
@track_endpoint_metrics("tenant_get")
|
||||
async def get_tenant_enhanced(
|
||||
async def get_tenant(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get tenant by ID with enhanced data and access control"""
|
||||
|
||||
"""Get tenant by ID - ATOMIC operation"""
|
||||
|
||||
tenant = await tenant_service.get_tenant_by_id(str(tenant_id))
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
|
||||
return tenant
|
||||
|
||||
@router.get("/tenants/subdomain/{subdomain}", response_model=TenantResponse)
|
||||
@track_endpoint_metrics("tenant_get_by_subdomain")
|
||||
async def get_tenant_by_subdomain_enhanced(
|
||||
subdomain: str = Path(..., description="Tenant subdomain"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get tenant by subdomain with enhanced validation"""
|
||||
|
||||
tenant = await tenant_service.get_tenant_by_subdomain(subdomain)
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Verify user has access to this tenant
|
||||
access = await tenant_service.verify_user_access(current_user["user_id"], tenant.id)
|
||||
if not access.has_access:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant"
|
||||
)
|
||||
|
||||
return tenant
|
||||
|
||||
@router.get("/tenants/user/{user_id}/owned", response_model=List[TenantResponse])
|
||||
# @track_endpoint_metrics("tenant_get_user_owned") # Temporarily disabled
|
||||
async def get_user_owned_tenants_enhanced(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get all tenants owned by a user with enhanced data"""
|
||||
|
||||
# Users can only get their own tenants unless they're admin
|
||||
user_role = current_user.get('role', '').lower()
|
||||
if user_id != current_user["user_id"] and user_role != 'admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Can only access your own tenants"
|
||||
)
|
||||
|
||||
tenants = await tenant_service.get_user_tenants(user_id)
|
||||
return tenants
|
||||
|
||||
@router.get("/tenants/search", response_model=List[TenantResponse])
|
||||
@track_endpoint_metrics("tenant_search")
|
||||
async def search_tenants_enhanced(
|
||||
search_term: str = Query(..., description="Search term"),
|
||||
business_type: Optional[str] = Query(None, description="Business type filter"),
|
||||
city: Optional[str] = Query(None, description="City filter"),
|
||||
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
limit: int = Query(50, ge=1, le=100, description="Maximum number of records to return"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Search tenants with advanced filters and pagination"""
|
||||
|
||||
tenants = await tenant_service.search_tenants(
|
||||
search_term=search_term,
|
||||
business_type=business_type,
|
||||
city=city,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
return tenants
|
||||
|
||||
@router.get("/tenants/nearby", response_model=List[TenantResponse])
|
||||
@track_endpoint_metrics("tenant_get_nearby")
|
||||
async def get_nearby_tenants_enhanced(
|
||||
latitude: float = Query(..., description="Latitude coordinate"),
|
||||
longitude: float = Query(..., description="Longitude coordinate"),
|
||||
radius_km: float = Query(10.0, ge=0.1, le=100.0, description="Search radius in kilometers"),
|
||||
limit: int = Query(50, ge=1, le=100, description="Maximum number of results"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get tenants near a geographic location with enhanced geospatial search"""
|
||||
|
||||
tenants = await tenant_service.get_tenants_near_location(
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
radius_km=radius_km,
|
||||
limit=limit
|
||||
)
|
||||
return tenants
|
||||
|
||||
@router.put("/tenants/{tenant_id}", response_model=TenantResponse)
|
||||
async def update_tenant_enhanced(
|
||||
@router.put(route_builder.build_base_route("{tenant_id}", include_tenant_prefix=False), response_model=TenantResponse)
|
||||
async def update_tenant(
|
||||
update_data: TenantUpdate,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Update tenant information with enhanced validation and permission checks"""
|
||||
"""Update tenant information - ATOMIC operation"""
|
||||
|
||||
try:
|
||||
result = await tenant_service.update_tenant(
|
||||
str(tenant_id),
|
||||
update_data,
|
||||
str(tenant_id),
|
||||
update_data,
|
||||
current_user["user_id"]
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -262,293 +75,3 @@ async def update_tenant_enhanced(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Tenant update failed"
|
||||
)
|
||||
|
||||
@router.put("/tenants/{tenant_id}/model-status")
|
||||
@track_endpoint_metrics("tenant_update_model_status")
|
||||
async def update_tenant_model_status_enhanced(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
ml_model_trained: bool = Query(..., description="Whether model is trained"),
|
||||
last_training_date: Optional[datetime] = Query(None, description="Last training date"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Update tenant model training status with enhanced tracking"""
|
||||
|
||||
try:
|
||||
result = await tenant_service.update_model_status(
|
||||
str(tenant_id),
|
||||
ml_model_trained,
|
||||
current_user["user_id"],
|
||||
last_training_date
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Model status update failed",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update model status"
|
||||
)
|
||||
|
||||
@router.post("/tenants/{tenant_id}/members", response_model=TenantMemberResponse)
|
||||
@track_endpoint_metrics("tenant_add_member")
|
||||
async def add_team_member_enhanced(
|
||||
user_id: str,
|
||||
role: str,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Add a team member to tenant with enhanced validation and role management"""
|
||||
|
||||
try:
|
||||
result = await tenant_service.add_team_member(
|
||||
str(tenant_id),
|
||||
user_id,
|
||||
role,
|
||||
current_user["user_id"]
|
||||
)
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Add team member failed",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=user_id,
|
||||
role=role,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to add team member"
|
||||
)
|
||||
|
||||
@router.get("/tenants/{tenant_id}/members", response_model=List[TenantMemberResponse])
|
||||
async def get_team_members_enhanced(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
active_only: bool = Query(True, description="Only return active members"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get all team members for a tenant with enhanced filtering"""
|
||||
|
||||
try:
|
||||
members = await tenant_service.get_team_members(
|
||||
str(tenant_id),
|
||||
current_user["user_id"],
|
||||
active_only=active_only
|
||||
)
|
||||
return members
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Get team members failed",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get team members"
|
||||
)
|
||||
|
||||
@router.put("/tenants/{tenant_id}/members/{member_user_id}/role", response_model=TenantMemberResponse)
|
||||
@track_endpoint_metrics("tenant_update_member_role")
|
||||
async def update_member_role_enhanced(
|
||||
new_role: str,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
member_user_id: str = Path(..., description="Member user ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Update team member role with enhanced permission validation"""
|
||||
|
||||
try:
|
||||
result = await tenant_service.update_member_role(
|
||||
str(tenant_id),
|
||||
member_user_id,
|
||||
new_role,
|
||||
current_user["user_id"]
|
||||
)
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Update member role failed",
|
||||
tenant_id=str(tenant_id),
|
||||
member_user_id=member_user_id,
|
||||
new_role=new_role,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update member role"
|
||||
)
|
||||
|
||||
@router.delete("/tenants/{tenant_id}/members/{member_user_id}")
|
||||
@track_endpoint_metrics("tenant_remove_member")
|
||||
async def remove_team_member_enhanced(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
member_user_id: str = Path(..., description="Member user ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Remove team member from tenant with enhanced validation"""
|
||||
|
||||
try:
|
||||
success = await tenant_service.remove_team_member(
|
||||
str(tenant_id),
|
||||
member_user_id,
|
||||
current_user["user_id"]
|
||||
)
|
||||
|
||||
if success:
|
||||
return {"success": True, "message": "Team member removed successfully"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to remove team member"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Remove team member failed",
|
||||
tenant_id=str(tenant_id),
|
||||
member_user_id=member_user_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to remove team member"
|
||||
)
|
||||
|
||||
@router.post("/tenants/{tenant_id}/deactivate")
|
||||
@track_endpoint_metrics("tenant_deactivate")
|
||||
async def deactivate_tenant_enhanced(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Deactivate a tenant (owner only) with enhanced validation"""
|
||||
|
||||
try:
|
||||
success = await tenant_service.deactivate_tenant(
|
||||
str(tenant_id),
|
||||
current_user["user_id"]
|
||||
)
|
||||
|
||||
if success:
|
||||
return {"success": True, "message": "Tenant deactivated successfully"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to deactivate tenant"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Tenant deactivation failed",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to deactivate tenant"
|
||||
)
|
||||
|
||||
@router.post("/tenants/{tenant_id}/activate")
|
||||
@track_endpoint_metrics("tenant_activate")
|
||||
async def activate_tenant_enhanced(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Activate a previously deactivated tenant (owner only) with enhanced validation"""
|
||||
|
||||
try:
|
||||
success = await tenant_service.activate_tenant(
|
||||
str(tenant_id),
|
||||
current_user["user_id"]
|
||||
)
|
||||
|
||||
if success:
|
||||
return {"success": True, "message": "Tenant activated successfully"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to activate tenant"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Tenant activation failed",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to activate tenant"
|
||||
)
|
||||
|
||||
@router.get("/tenants/users/{user_id}", response_model=List[TenantResponse])
|
||||
@track_endpoint_metrics("tenant_get_user_tenants")
|
||||
async def get_user_tenants_enhanced(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get all tenants owned by a user - Fixed endpoint for frontend"""
|
||||
|
||||
try:
|
||||
tenants = await tenant_service.get_user_tenants(user_id)
|
||||
logger.info("Retrieved user tenants", user_id=user_id, tenant_count=len(tenants))
|
||||
return tenants
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Get user tenants failed", user_id=user_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user tenants"
|
||||
)
|
||||
|
||||
@router.get("/tenants/members/user/{user_id}")
|
||||
@track_endpoint_metrics("tenant_get_user_memberships")
|
||||
async def get_user_memberships(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get all tenant memberships for a user (for authentication service)"""
|
||||
|
||||
try:
|
||||
memberships = await tenant_service.get_user_memberships(user_id)
|
||||
logger.info("Retrieved user memberships", user_id=user_id, membership_count=len(memberships))
|
||||
return memberships
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Get user memberships failed", user_id=user_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user memberships"
|
||||
)
|
||||
|
||||
@router.get("/tenants/statistics", dependencies=[Depends(require_admin_role_dep)])
|
||||
@track_endpoint_metrics("tenant_get_statistics")
|
||||
async def get_tenant_statistics_enhanced(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
"""Get comprehensive tenant statistics (admin only) with enhanced analytics"""
|
||||
|
||||
try:
|
||||
stats = await tenant_service.get_tenant_statistics()
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Get tenant statistics failed", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get tenant statistics"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ from fastapi import FastAPI
|
||||
from sqlalchemy import text
|
||||
from app.core.config import settings
|
||||
from app.core.database import database_manager
|
||||
from app.api import tenants, subscriptions, webhooks
|
||||
from app.api import tenants, tenant_members, tenant_operations, webhooks
|
||||
from shared.service_base import StandardFastAPIService
|
||||
|
||||
|
||||
@@ -52,14 +52,14 @@ class TenantService(StandardFastAPIService):
|
||||
def __init__(self):
|
||||
# Define expected database tables for health checks
|
||||
tenant_expected_tables = ['tenants', 'tenant_members', 'subscriptions']
|
||||
|
||||
# Note: api_prefix is empty because RouteBuilder already includes /api/v1
|
||||
super().__init__(
|
||||
service_name="tenant-service",
|
||||
app_name="Tenant Management Service",
|
||||
description="Multi-tenant bakery management service",
|
||||
version="1.0.0",
|
||||
log_level=settings.LOG_LEVEL,
|
||||
api_prefix="/api/v1",
|
||||
api_prefix="",
|
||||
database_manager=database_manager,
|
||||
expected_tables=tenant_expected_tables
|
||||
)
|
||||
@@ -112,7 +112,8 @@ service.setup_custom_endpoints()
|
||||
|
||||
# Include routers
|
||||
service.add_router(tenants.router, tags=["tenants"])
|
||||
service.add_router(subscriptions.router, tags=["subscriptions"])
|
||||
service.add_router(tenant_members.router, tags=["tenant-members"])
|
||||
service.add_router(tenant_operations.router, tags=["tenant-operations"])
|
||||
service.add_router(webhooks.router, tags=["webhooks"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
"""add_demo_columns
|
||||
|
||||
Revision ID: 2a9b3c4d5e6f
|
||||
Revises: 1e8aebb4d9ce
|
||||
Create Date: 2025-10-02 17:00:00.000000+02:00
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2a9b3c4d5e6f'
|
||||
down_revision: Union[str, None] = '1e8aebb4d9ce'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add demo-related columns to tenants table
|
||||
op.add_column('tenants', sa.Column('is_demo', sa.Boolean(), nullable=False, server_default='false'))
|
||||
op.add_column('tenants', sa.Column('is_demo_template', sa.Boolean(), nullable=False, server_default='false'))
|
||||
op.add_column('tenants', sa.Column('base_demo_tenant_id', sa.UUID(), nullable=True))
|
||||
op.add_column('tenants', sa.Column('demo_session_id', sa.String(length=100), nullable=True))
|
||||
op.add_column('tenants', sa.Column('demo_expires_at', sa.DateTime(timezone=True), nullable=True))
|
||||
|
||||
# Create indexes for demo columns
|
||||
op.create_index(op.f('ix_tenants_is_demo'), 'tenants', ['is_demo'], unique=False)
|
||||
op.create_index(op.f('ix_tenants_is_demo_template'), 'tenants', ['is_demo_template'], unique=False)
|
||||
op.create_index(op.f('ix_tenants_base_demo_tenant_id'), 'tenants', ['base_demo_tenant_id'], unique=False)
|
||||
op.create_index(op.f('ix_tenants_demo_session_id'), 'tenants', ['demo_session_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop indexes
|
||||
op.drop_index(op.f('ix_tenants_demo_session_id'), table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_base_demo_tenant_id'), table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_is_demo_template'), table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_is_demo'), table_name='tenants')
|
||||
|
||||
# Drop columns
|
||||
op.drop_column('tenants', 'demo_expires_at')
|
||||
op.drop_column('tenants', 'demo_session_id')
|
||||
op.drop_column('tenants', 'base_demo_tenant_id')
|
||||
op.drop_column('tenants', 'is_demo_template')
|
||||
op.drop_column('tenants', 'is_demo')
|
||||
@@ -1,8 +1,8 @@
|
||||
"""initial_schema_20251001_1119
|
||||
"""initial_schema_20251006_1516
|
||||
|
||||
Revision ID: 1e8aebb4d9ce
|
||||
Revision ID: 964ef5a3ac09
|
||||
Revises:
|
||||
Create Date: 2025-10-01 11:19:18.038250+02:00
|
||||
Create Date: 2025-10-06 15:16:42.493219+02:00
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
@@ -12,7 +12,7 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1e8aebb4d9ce'
|
||||
revision: str = '964ef5a3ac09'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
@@ -35,6 +35,11 @@ def upgrade() -> None:
|
||||
sa.Column('email', sa.String(length=255), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('subscription_tier', sa.String(length=50), nullable=True),
|
||||
sa.Column('is_demo', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_demo_template', sa.Boolean(), nullable=True),
|
||||
sa.Column('base_demo_tenant_id', sa.UUID(), nullable=True),
|
||||
sa.Column('demo_session_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('demo_expires_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('ml_model_trained', sa.Boolean(), nullable=True),
|
||||
sa.Column('last_training_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('owner_id', sa.UUID(), nullable=False),
|
||||
@@ -43,6 +48,10 @@ def upgrade() -> None:
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('subdomain')
|
||||
)
|
||||
op.create_index(op.f('ix_tenants_base_demo_tenant_id'), 'tenants', ['base_demo_tenant_id'], unique=False)
|
||||
op.create_index(op.f('ix_tenants_demo_session_id'), 'tenants', ['demo_session_id'], unique=False)
|
||||
op.create_index(op.f('ix_tenants_is_demo'), 'tenants', ['is_demo'], unique=False)
|
||||
op.create_index(op.f('ix_tenants_is_demo_template'), 'tenants', ['is_demo_template'], unique=False)
|
||||
op.create_index(op.f('ix_tenants_owner_id'), 'tenants', ['owner_id'], unique=False)
|
||||
op.create_table('subscriptions',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
@@ -86,5 +95,9 @@ def downgrade() -> None:
|
||||
op.drop_table('tenant_members')
|
||||
op.drop_table('subscriptions')
|
||||
op.drop_index(op.f('ix_tenants_owner_id'), table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_is_demo_template'), table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_is_demo'), table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_demo_session_id'), table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_base_demo_tenant_id'), table_name='tenants')
|
||||
op.drop_table('tenants')
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user