Add user delete process

This commit is contained in:
Urtzi Alfaro
2025-10-31 11:54:19 +01:00
parent 63f5c6d512
commit 269d3b5032
74 changed files with 16783 additions and 213 deletions

View File

@@ -9,6 +9,7 @@ from datetime import date
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
import structlog
from shared.auth.decorators import get_current_user_dep
@@ -307,3 +308,98 @@ async def delete_order(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete order"
)
# ===== Tenant Data Deletion Endpoint =====
@router.delete(
route_builder.build_base_route("tenant/{tenant_id}", include_tenant_prefix=False),
status_code=status.HTTP_200_OK
)
async def delete_tenant_data(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Delete all order-related data for a tenant
Only accessible by internal services (called during tenant deletion)
"""
logger.info("Tenant data deletion request received",
tenant_id=tenant_id,
requesting_service=current_user.get("service", "unknown"))
# 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:
from app.services.tenant_deletion_service import OrdersTenantDeletionService
deletion_service = OrdersTenantDeletionService(db)
result = await deletion_service.safe_delete_tenant_data(tenant_id)
return {
"message": "Tenant data deletion completed in orders-service",
"summary": result.to_dict()
}
except Exception as e:
logger.error("Tenant data deletion failed",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete tenant data: {str(e)}"
)
@router.get(
route_builder.build_base_route("tenant/{tenant_id}/deletion-preview", include_tenant_prefix=False),
status_code=status.HTTP_200_OK
)
async def preview_tenant_data_deletion(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Preview what data would be deleted for a tenant (dry-run)
Accessible by internal services and tenant admins
"""
# Allow internal services and admins
is_service = current_user.get("type") == "service"
is_admin = current_user.get("role") in ["owner", "admin"]
if not (is_service or is_admin):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions"
)
try:
from app.services.tenant_deletion_service import OrdersTenantDeletionService
deletion_service = OrdersTenantDeletionService(db)
preview = await deletion_service.get_tenant_data_preview(tenant_id)
return {
"tenant_id": tenant_id,
"service": "orders-service",
"data_counts": preview,
"total_items": sum(preview.values())
}
except Exception as e:
logger.error("Deletion preview failed",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get deletion preview: {str(e)}"
)

View File

@@ -0,0 +1,140 @@
"""
Orders Service - Tenant Data Deletion
Handles deletion of all order-related data for a tenant
"""
from typing import Dict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete, func
import structlog
from shared.services.tenant_deletion import BaseTenantDataDeletionService, TenantDataDeletionResult
from app.models.order import CustomerOrder, OrderItem, OrderStatusHistory
from app.models.customer import Customer, CustomerContact
logger = structlog.get_logger()
class OrdersTenantDeletionService(BaseTenantDataDeletionService):
"""Service for deleting all orders-related data for a tenant"""
def __init__(self, db_session: AsyncSession):
super().__init__("orders-service")
self.db = db_session
async def get_tenant_data_preview(self, tenant_id: str) -> Dict[str, int]:
"""Get counts of what would be deleted"""
try:
preview = {}
# Count orders
order_count = await self.db.scalar(
select(func.count(CustomerOrder.id)).where(CustomerOrder.tenant_id == tenant_id)
)
preview["orders"] = order_count or 0
# Count order items (will be deleted via CASCADE)
order_item_count = await self.db.scalar(
select(func.count(OrderItem.id))
.join(CustomerOrder)
.where(CustomerOrder.tenant_id == tenant_id)
)
preview["order_items"] = order_item_count or 0
# Count order status history (will be deleted via CASCADE)
status_history_count = await self.db.scalar(
select(func.count(OrderStatusHistory.id))
.join(CustomerOrder)
.where(CustomerOrder.tenant_id == tenant_id)
)
preview["order_status_history"] = status_history_count or 0
# Count customers
customer_count = await self.db.scalar(
select(func.count(Customer.id)).where(Customer.tenant_id == tenant_id)
)
preview["customers"] = customer_count or 0
# Count customer contacts (will be deleted via CASCADE)
contact_count = await self.db.scalar(
select(func.count(CustomerContact.id))
.join(Customer)
.where(Customer.tenant_id == tenant_id)
)
preview["customer_contacts"] = contact_count or 0
return preview
except Exception as e:
logger.error("Error getting deletion preview",
tenant_id=tenant_id,
error=str(e))
return {}
async def delete_tenant_data(self, tenant_id: str) -> TenantDataDeletionResult:
"""Delete all data for a tenant"""
result = TenantDataDeletionResult(tenant_id, self.service_name)
try:
# Get preview before deletion for reporting
preview = await self.get_tenant_data_preview(tenant_id)
# Delete customers (CASCADE will delete customer_contacts)
try:
customer_delete = await self.db.execute(
delete(Customer).where(Customer.tenant_id == tenant_id)
)
deleted_customers = customer_delete.rowcount
result.add_deleted_items("customers", deleted_customers)
# Customer contacts are deleted via CASCADE
result.add_deleted_items("customer_contacts", preview.get("customer_contacts", 0))
logger.info("Deleted customers for tenant",
tenant_id=tenant_id,
count=deleted_customers)
except Exception as e:
logger.error("Error deleting customers",
tenant_id=tenant_id,
error=str(e))
result.add_error(f"Customer deletion: {str(e)}")
# Delete orders (CASCADE will delete order_items and order_status_history)
try:
order_delete = await self.db.execute(
delete(CustomerOrder).where(CustomerOrder.tenant_id == tenant_id)
)
deleted_orders = order_delete.rowcount
result.add_deleted_items("orders", deleted_orders)
# Order items and status history are deleted via CASCADE
result.add_deleted_items("order_items", preview.get("order_items", 0))
result.add_deleted_items("order_status_history", preview.get("order_status_history", 0))
logger.info("Deleted orders for tenant",
tenant_id=tenant_id,
count=deleted_orders)
except Exception as e:
logger.error("Error deleting orders",
tenant_id=tenant_id,
error=str(e))
result.add_error(f"Order deletion: {str(e)}")
# Commit all deletions
await self.db.commit()
logger.info("Tenant data deletion completed",
tenant_id=tenant_id,
deleted_counts=result.deleted_counts)
except Exception as e:
logger.error("Fatal error during tenant data deletion",
tenant_id=tenant_id,
error=str(e))
await self.db.rollback()
result.add_error(f"Fatal error: {str(e)}")
return result