Add user delete process
This commit is contained in:
@@ -13,8 +13,10 @@ from sqlalchemy import select
|
||||
from shared.auth.decorators import get_current_user_dep, require_admin_role_dep
|
||||
from shared.routing import RouteBuilder
|
||||
from app.core.database import get_db
|
||||
from app.models.tenants import Subscription
|
||||
from app.models.tenants import Subscription, Tenant
|
||||
from app.services.subscription_limit_service import SubscriptionLimitService
|
||||
from shared.clients.stripe_client import StripeProvider
|
||||
from app.core.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
@@ -65,6 +67,18 @@ class SubscriptionStatusResponse(BaseModel):
|
||||
days_until_inactive: int | None
|
||||
|
||||
|
||||
class InvoiceResponse(BaseModel):
|
||||
"""Response model for an invoice"""
|
||||
id: str
|
||||
date: str
|
||||
amount: float
|
||||
currency: str
|
||||
status: str
|
||||
description: str | None = None
|
||||
invoice_pdf: str | None = None
|
||||
hosted_invoice_url: str | None = None
|
||||
|
||||
|
||||
@router.post("/api/v1/subscriptions/cancel", response_model=SubscriptionCancellationResponse)
|
||||
async def cancel_subscription(
|
||||
request: SubscriptionCancellationRequest,
|
||||
@@ -251,3 +265,65 @@ async def get_subscription_status(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get subscription status"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/subscriptions/{tenant_id}/invoices", response_model=list[InvoiceResponse])
|
||||
async def get_tenant_invoices(
|
||||
tenant_id: str,
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get invoice history for a tenant from Stripe
|
||||
"""
|
||||
try:
|
||||
# Verify tenant exists
|
||||
query = select(Tenant).where(Tenant.id == UUID(tenant_id))
|
||||
result = await db.execute(query)
|
||||
tenant = result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Check if tenant has a Stripe customer ID
|
||||
if not tenant.stripe_customer_id:
|
||||
logger.info("no_stripe_customer_id", tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
# Initialize Stripe provider
|
||||
stripe_provider = StripeProvider(
|
||||
api_key=settings.STRIPE_SECRET_KEY,
|
||||
webhook_secret=settings.STRIPE_WEBHOOK_SECRET
|
||||
)
|
||||
|
||||
# Fetch invoices from Stripe
|
||||
stripe_invoices = await stripe_provider.get_invoices(tenant.stripe_customer_id)
|
||||
|
||||
# Transform to response format
|
||||
invoices = []
|
||||
for invoice in stripe_invoices:
|
||||
invoices.append(InvoiceResponse(
|
||||
id=invoice.id,
|
||||
date=invoice.created_at.strftime('%Y-%m-%d'),
|
||||
amount=invoice.amount,
|
||||
currency=invoice.currency.upper(),
|
||||
status=invoice.status,
|
||||
description=invoice.description,
|
||||
invoice_pdf=invoice.invoice_pdf,
|
||||
hosted_invoice_url=invoice.hosted_invoice_url
|
||||
))
|
||||
|
||||
logger.info("invoices_retrieved", tenant_id=tenant_id, count=len(invoices))
|
||||
return invoices
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("get_invoices_failed", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve invoices"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ 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, AddMemberWithUserCreate
|
||||
from app.schemas.tenants import TenantMemberResponse, AddMemberWithUserCreate, TenantResponse
|
||||
from app.services.tenant_service import EnhancedTenantService
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing.route_builder import RouteBuilder
|
||||
@@ -269,3 +269,157 @@ async def remove_team_member(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to remove team member"
|
||||
)
|
||||
|
||||
@router.delete(route_builder.build_base_route("user/{user_id}/memberships", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("user_memberships_delete")
|
||||
async def delete_user_memberships(
|
||||
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)
|
||||
):
|
||||
"""
|
||||
Delete all tenant memberships for a user.
|
||||
Used by auth service when deleting a user account.
|
||||
Only accessible by internal services.
|
||||
"""
|
||||
|
||||
logger.info(
|
||||
"Delete user memberships request received",
|
||||
user_id=user_id,
|
||||
requesting_service=current_user.get("service", "unknown"),
|
||||
is_service=current_user.get("type") == "service"
|
||||
)
|
||||
|
||||
# Only allow internal service calls
|
||||
if current_user.get("type") != "service":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="This endpoint is only accessible to internal services"
|
||||
)
|
||||
|
||||
try:
|
||||
result = await tenant_service.delete_user_memberships(user_id)
|
||||
|
||||
logger.info(
|
||||
"User memberships deleted successfully",
|
||||
user_id=user_id,
|
||||
deleted_count=result.get("deleted_count"),
|
||||
total_memberships=result.get("total_memberships")
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "User memberships deleted successfully",
|
||||
"summary": result
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Delete user memberships failed",
|
||||
user_id=user_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to delete user memberships"
|
||||
)
|
||||
|
||||
@router.post(route_builder.build_base_route("{tenant_id}/transfer-ownership", include_tenant_prefix=False), response_model=TenantResponse)
|
||||
@track_endpoint_metrics("tenant_transfer_ownership")
|
||||
async def transfer_ownership(
|
||||
new_owner_id: 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)
|
||||
):
|
||||
"""
|
||||
Transfer tenant ownership to another admin.
|
||||
Only the current owner or internal services can perform this action.
|
||||
"""
|
||||
|
||||
logger.info(
|
||||
"Transfer ownership request received",
|
||||
tenant_id=str(tenant_id),
|
||||
new_owner_id=new_owner_id,
|
||||
requesting_user=current_user.get("user_id"),
|
||||
is_service=current_user.get("type") == "service"
|
||||
)
|
||||
|
||||
try:
|
||||
# Get current tenant to find current owner
|
||||
tenant_info = await tenant_service.get_tenant_by_id(str(tenant_id))
|
||||
if not tenant_info:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
current_owner_id = tenant_info.owner_id
|
||||
|
||||
result = await tenant_service.transfer_tenant_ownership(
|
||||
str(tenant_id),
|
||||
current_owner_id,
|
||||
new_owner_id,
|
||||
requesting_user_id=current_user.get("user_id")
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Ownership transferred successfully",
|
||||
tenant_id=str(tenant_id),
|
||||
from_owner=current_owner_id,
|
||||
to_owner=new_owner_id
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Transfer ownership failed",
|
||||
tenant_id=str(tenant_id),
|
||||
new_owner_id=new_owner_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to transfer ownership"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("{tenant_id}/admins", include_tenant_prefix=False), response_model=List[TenantMemberResponse])
|
||||
@track_endpoint_metrics("tenant_get_admins")
|
||||
async def get_tenant_admins(
|
||||
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 all admins (owner + admins) for a tenant.
|
||||
Used by auth service to check for other admins before tenant deletion.
|
||||
"""
|
||||
|
||||
logger.info(
|
||||
"Get tenant admins request received",
|
||||
tenant_id=str(tenant_id),
|
||||
requesting_user=current_user.get("user_id"),
|
||||
is_service=current_user.get("type") == "service"
|
||||
)
|
||||
|
||||
try:
|
||||
admins = await tenant_service.get_tenant_admins(str(tenant_id))
|
||||
|
||||
logger.info(
|
||||
"Retrieved tenant admins",
|
||||
tenant_id=str(tenant_id),
|
||||
admin_count=len(admins)
|
||||
)
|
||||
|
||||
return admins
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Get tenant admins failed",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get tenant admins"
|
||||
)
|
||||
|
||||
@@ -98,3 +98,56 @@ async def update_tenant(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Tenant update failed"
|
||||
)
|
||||
|
||||
@router.delete(route_builder.build_base_route("{tenant_id}", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("tenant_delete")
|
||||
async def delete_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)
|
||||
):
|
||||
"""Delete tenant and all associated data - ATOMIC operation (Owner/Admin or System only)"""
|
||||
|
||||
logger.info(
|
||||
"Tenant DELETE request received",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id"),
|
||||
user_type=current_user.get("type", "user"),
|
||||
is_service=current_user.get("type") == "service",
|
||||
role=current_user.get("role"),
|
||||
service_name=current_user.get("service", "none")
|
||||
)
|
||||
|
||||
try:
|
||||
# Allow internal service calls to bypass admin check
|
||||
skip_admin_check = current_user.get("type") == "service"
|
||||
|
||||
result = await tenant_service.delete_tenant(
|
||||
str(tenant_id),
|
||||
requesting_user_id=current_user.get("user_id"),
|
||||
skip_admin_check=skip_admin_check
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Tenant DELETE request successful",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id"),
|
||||
deleted_items=result.get("deleted_items")
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Tenant deleted successfully",
|
||||
"summary": result
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Tenant deletion failed",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id"),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Tenant deletion failed"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user