Add user delete process 2

This commit is contained in:
Urtzi Alfaro
2025-10-31 18:57:58 +01:00
parent 269d3b5032
commit f44d235c6d
15 changed files with 166 additions and 145 deletions

View File

@@ -26,6 +26,7 @@ export interface PurchaseOrderItem {
id: string;
inventory_product_id: string;
product_code?: string;
product_name?: string;
ordered_quantity: number;
unit_of_measure: string;
unit_price: string; // Decimal as string

View File

@@ -390,12 +390,12 @@ const ProcurementPage: React.FC = () => {
},
{
label: 'Email',
value: po.supplier?.contact_email || 'N/A',
value: po.supplier?.email || 'N/A',
type: 'text' as const
},
{
label: 'Teléfono',
value: po.supplier?.contact_phone || 'N/A',
value: po.supplier?.phone || 'N/A',
type: 'text' as const
}
]
@@ -557,7 +557,18 @@ const ProcurementPage: React.FC = () => {
const totalAmount = items.reduce((sum, item) => {
const price = typeof item.unit_price === 'string' ? parseFloat(item.unit_price) : typeof item.unit_price === 'number' ? item.unit_price : 0;
const quantity = typeof item.ordered_quantity === 'number' ? item.ordered_quantity : 0;
const quantity = (() => {
if (typeof item.ordered_quantity === 'number') {
return item.ordered_quantity;
} else if (typeof item.ordered_quantity === 'string') {
const parsed = parseFloat(item.ordered_quantity);
return isNaN(parsed) ? 0 : parsed;
} else if (typeof item.ordered_quantity === 'object' && item.ordered_quantity !== null) {
// Handle if it's a decimal object or similar
return parseFloat(item.ordered_quantity.toString()) || 0;
}
return 0;
})();
return sum + (price * quantity);
}, 0);
@@ -566,9 +577,20 @@ const ProcurementPage: React.FC = () => {
{/* Items as cards */}
{items.map((item, index) => {
const unitPrice = typeof item.unit_price === 'string' ? parseFloat(item.unit_price) : typeof item.unit_price === 'number' ? item.unit_price : 0;
const quantity = typeof item.ordered_quantity === 'number' ? item.ordered_quantity : 0;
const quantity = (() => {
if (typeof item.ordered_quantity === 'number') {
return item.ordered_quantity;
} else if (typeof item.ordered_quantity === 'string') {
const parsed = parseFloat(item.ordered_quantity);
return isNaN(parsed) ? 0 : parsed;
} else if (typeof item.ordered_quantity === 'object' && item.ordered_quantity !== null) {
// Handle if it's a decimal object or similar
return parseFloat(item.ordered_quantity.toString()) || 0;
}
return 0;
})();
const itemTotal = unitPrice * quantity;
const productName = item.product_name || `Producto ${index + 1}`;
const productName = item.product_name || item.ingredient_name || `Producto ${index + 1}`;
return (
<div

View File

@@ -14,7 +14,14 @@ NC='\033[0m'
# Configuration
TENANT_ID="${1:-dbc2128a-7539-470c-94b9-c1e37031bd77}"
SERVICE_TOKEN="${SERVICE_TOKEN}"
# Generate or use provided SERVICE_TOKEN
if [ -z "${SERVICE_TOKEN}" ]; then
SERVICE_TOKEN=$(python3 scripts/generate_service_token.py tenant-deletion-orchestrator 2>&1 | grep -A1 "Token:" | tail -1 | sed 's/^[[:space:]]*//' | tr -d '\n')
else
# Clean the token if provided (remove whitespace and newlines)
SERVICE_TOKEN=$(echo "${SERVICE_TOKEN}" | tr -d '[:space:]')
fi
# Results
TOTAL_SERVICES=12
@@ -44,6 +51,7 @@ print_info() {
test_service() {
local service_name=$1
local endpoint_path=$2
local port=${3:-8000} # Default to 8000 if not specified
echo ""
echo -e "${BLUE}Testing ${service_name}...${NC}"
@@ -62,7 +70,7 @@ test_service() {
# Execute request
local result=$(kubectl exec -n bakery-ia "$pod" -- curl -s -w "\nHTTP_CODE:%{http_code}" \
-H "Authorization: Bearer ${SERVICE_TOKEN}" \
"http://localhost:8000${endpoint_path}/tenant/${TENANT_ID}/deletion-preview" 2>&1)
"http://localhost:${port}${endpoint_path}/tenant/${TENANT_ID}/deletion-preview" 2>&1)
local http_code=$(echo "$result" | grep "HTTP_CODE" | cut -d':' -f2)
local body=$(echo "$result" | sed '/HTTP_CODE/d')
@@ -114,11 +122,11 @@ test_service "sales-service" "/api/v1/sales"
test_service "production-service" "/api/v1/production"
test_service "suppliers-service" "/api/v1/suppliers"
test_service "pos-service" "/api/v1/pos"
test_service "city-service" "/api/v1/nominatim"
test_service "external-service" "/api/v1/external"
test_service "forecasting-service" "/api/v1/forecasting"
test_service "training-service" "/api/v1/training"
test_service "alert-processor-service" "/api/v1/analytics"
test_service "notification-service" "/api/v1/notifications"
test_service "alert-processor-api" "/api/v1/alerts" 8010
test_service "notification-service" "/api/v1/notification"
# Summary
echo ""

View File

@@ -43,7 +43,7 @@ class AlertProcessorTenantDeletionService(BaseTenantDataDeletionService):
# Count alerts (CASCADE will delete alert_interactions)
alert_count = await self.db.scalar(
select(func.count(Alert.id)).where(
Alert.tenant_id == UUID(tenant_id)
Alert.tenant_id == tenant_id
)
)
preview["alerts"] = alert_count or 0
@@ -53,7 +53,7 @@ class AlertProcessorTenantDeletionService(BaseTenantDataDeletionService):
from app.models.alerts import AlertInteraction
interaction_count = await self.db.scalar(
select(func.count(AlertInteraction.id)).where(
AlertInteraction.tenant_id == UUID(tenant_id)
AlertInteraction.tenant_id == tenant_id
)
)
preview["alert_interactions"] = interaction_count or 0
@@ -61,7 +61,7 @@ class AlertProcessorTenantDeletionService(BaseTenantDataDeletionService):
# Count audit logs
audit_count = await self.db.scalar(
select(func.count(AuditLog.id)).where(
AuditLog.tenant_id == UUID(tenant_id)
AuditLog.tenant_id == tenant_id
)
)
preview["audit_logs"] = audit_count or 0
@@ -113,7 +113,7 @@ class AlertProcessorTenantDeletionService(BaseTenantDataDeletionService):
logger.info("alert_processor.tenant_deletion.deleting_interactions", tenant_id=tenant_id)
interactions_result = await self.db.execute(
delete(AlertInteraction).where(
AlertInteraction.tenant_id == UUID(tenant_id)
AlertInteraction.tenant_id == tenant_id
)
)
result.deleted_counts["alert_interactions"] = interactions_result.rowcount
@@ -127,7 +127,7 @@ class AlertProcessorTenantDeletionService(BaseTenantDataDeletionService):
logger.info("alert_processor.tenant_deletion.deleting_alerts", tenant_id=tenant_id)
alerts_result = await self.db.execute(
delete(Alert).where(
Alert.tenant_id == UUID(tenant_id)
Alert.tenant_id == tenant_id
)
)
result.deleted_counts["alerts"] = alerts_result.rowcount
@@ -141,7 +141,7 @@ class AlertProcessorTenantDeletionService(BaseTenantDataDeletionService):
logger.info("alert_processor.tenant_deletion.deleting_audit_logs", tenant_id=tenant_id)
audit_result = await self.db.execute(
delete(AuditLog).where(
AuditLog.tenant_id == UUID(tenant_id)
AuditLog.tenant_id == tenant_id
)
)
result.deleted_counts["audit_logs"] = audit_result.rowcount

View File

@@ -61,7 +61,7 @@ class ExternalTenantDeletionService(BaseTenantDataDeletionService):
# Count tenant-specific weather data (if any)
weather_count = await self.db.scalar(
select(func.count(WeatherData.id)).where(
WeatherData.tenant_id == UUID(tenant_id)
WeatherData.tenant_id == tenant_id
)
)
preview["tenant_weather_data"] = weather_count or 0
@@ -69,7 +69,7 @@ class ExternalTenantDeletionService(BaseTenantDataDeletionService):
# Count audit logs
audit_count = await self.db.scalar(
select(func.count(AuditLog.id)).where(
AuditLog.tenant_id == UUID(tenant_id)
AuditLog.tenant_id == tenant_id
)
)
preview["audit_logs"] = audit_count or 0
@@ -119,7 +119,7 @@ class ExternalTenantDeletionService(BaseTenantDataDeletionService):
logger.info("external.tenant_deletion.deleting_weather_data", tenant_id=tenant_id)
weather_result = await self.db.execute(
delete(WeatherData).where(
WeatherData.tenant_id == UUID(tenant_id)
WeatherData.tenant_id == tenant_id
)
)
result.deleted_counts["tenant_weather_data"] = weather_result.rowcount
@@ -133,7 +133,7 @@ class ExternalTenantDeletionService(BaseTenantDataDeletionService):
logger.info("external.tenant_deletion.deleting_audit_logs", tenant_id=tenant_id)
audit_result = await self.db.execute(
delete(AuditLog).where(
AuditLog.tenant_id == UUID(tenant_id)
AuditLog.tenant_id == tenant_id
)
)
result.deleted_counts["audit_logs"] = audit_result.rowcount

View File

@@ -889,84 +889,3 @@ async def preview_tenant_data_deletion(
status_code=500,
detail=f"Failed to preview tenant data deletion: {str(e)}"
)
# ============================================================================
# Tenant Data Deletion Operations (Internal Service Only)
# ============================================================================
from shared.auth.access_control import service_only_access
from app.services.tenant_deletion_service import NotificationTenantDeletionService
@router.delete(
route_builder.build_base_route("tenant/{tenant_id}", include_tenant_prefix=False),
response_model=dict
)
@service_only_access
async def delete_tenant_data(
tenant_id: str = Path(..., description="Tenant ID to delete data for"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Delete all notification data for a tenant (Internal service only)
"""
try:
logger.info("notification.tenant_deletion.api_called", tenant_id=tenant_id)
deletion_service = NotificationTenantDeletionService(db)
result = await deletion_service.safe_delete_tenant_data(tenant_id)
if not result.success:
raise HTTPException(
status_code=500,
detail=f"Tenant data deletion failed: {', '.join(result.errors)}"
)
return {
"message": "Tenant data deletion completed successfully",
"summary": result.to_dict()
}
except HTTPException:
raise
except Exception as e:
logger.error("notification.tenant_deletion.api_error", tenant_id=tenant_id, error=str(e), exc_info=True)
raise HTTPException(status_code=500, 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),
response_model=dict
)
@service_only_access
async def preview_tenant_data_deletion(
tenant_id: str = Path(..., description="Tenant ID to preview deletion for"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Preview what data would be deleted for a tenant (dry-run)
"""
try:
logger.info("notification.tenant_deletion.preview_called", tenant_id=tenant_id)
deletion_service = NotificationTenantDeletionService(db)
result = await deletion_service.preview_deletion(tenant_id)
if not result.success:
raise HTTPException(
status_code=500,
detail=f"Tenant deletion preview failed: {', '.join(result.errors)}"
)
return {
"tenant_id": tenant_id,
"service": "notification-service",
"data_counts": result.deleted_counts,
"total_items": sum(result.deleted_counts.values())
}
except HTTPException:
raise
except Exception as e:
logger.error("notification.tenant_deletion.preview_error", tenant_id=tenant_id, error=str(e), exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to preview tenant data deletion: {str(e)}")

View File

@@ -49,7 +49,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
# Count notifications
notification_count = await self.db.scalar(
select(func.count(Notification.id)).where(
Notification.tenant_id == UUID(tenant_id)
Notification.tenant_id == tenant_id
)
)
preview["notifications"] = notification_count or 0
@@ -57,7 +57,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
# Count tenant-specific notification templates
template_count = await self.db.scalar(
select(func.count(NotificationTemplate.id)).where(
NotificationTemplate.tenant_id == UUID(tenant_id),
NotificationTemplate.tenant_id == tenant_id,
NotificationTemplate.is_system == False # Don't delete system templates
)
)
@@ -66,15 +66,17 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
# Count notification preferences
preference_count = await self.db.scalar(
select(func.count(NotificationPreference.id)).where(
NotificationPreference.tenant_id == UUID(tenant_id)
NotificationPreference.tenant_id == tenant_id
)
)
preview["notification_preferences"] = preference_count or 0
# Count notification logs
# Count notification logs (join with Notification to get tenant_id)
log_count = await self.db.scalar(
select(func.count(NotificationLog.id)).where(
NotificationLog.tenant_id == UUID(tenant_id)
select(func.count(NotificationLog.id)).select_from(NotificationLog).join(
Notification, NotificationLog.notification_id == Notification.id
).where(
Notification.tenant_id == tenant_id
)
)
preview["notification_logs"] = log_count or 0
@@ -82,7 +84,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
# Count audit logs
audit_count = await self.db.scalar(
select(func.count(AuditLog.id)).where(
AuditLog.tenant_id == UUID(tenant_id)
AuditLog.tenant_id == tenant_id
)
)
preview["audit_logs"] = audit_count or 0
@@ -127,11 +129,12 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
result = TenantDataDeletionResult(tenant_id=tenant_id, service_name=self.service_name)
try:
# Step 1: Delete notification logs
# Step 1: Delete notification logs (via subquery to get notification_ids for this tenant)
logger.info("notification.tenant_deletion.deleting_logs", tenant_id=tenant_id)
notification_ids_subquery = select(Notification.id).where(Notification.tenant_id == tenant_id)
logs_result = await self.db.execute(
delete(NotificationLog).where(
NotificationLog.tenant_id == UUID(tenant_id)
NotificationLog.notification_id.in_(notification_ids_subquery)
)
)
result.deleted_counts["notification_logs"] = logs_result.rowcount
@@ -145,7 +148,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
logger.info("notification.tenant_deletion.deleting_preferences", tenant_id=tenant_id)
preferences_result = await self.db.execute(
delete(NotificationPreference).where(
NotificationPreference.tenant_id == UUID(tenant_id)
NotificationPreference.tenant_id == tenant_id
)
)
result.deleted_counts["notification_preferences"] = preferences_result.rowcount
@@ -159,7 +162,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
logger.info("notification.tenant_deletion.deleting_notifications", tenant_id=tenant_id)
notifications_result = await self.db.execute(
delete(Notification).where(
Notification.tenant_id == UUID(tenant_id)
Notification.tenant_id == tenant_id
)
)
result.deleted_counts["notifications"] = notifications_result.rowcount
@@ -173,7 +176,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
logger.info("notification.tenant_deletion.deleting_templates", tenant_id=tenant_id)
templates_result = await self.db.execute(
delete(NotificationTemplate).where(
NotificationTemplate.tenant_id == UUID(tenant_id),
NotificationTemplate.tenant_id == tenant_id,
NotificationTemplate.is_system == False
)
)
@@ -189,7 +192,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
logger.info("notification.tenant_deletion.deleting_audit_logs", tenant_id=tenant_id)
audit_result = await self.db.execute(
delete(AuditLog).where(
AuditLog.tenant_id == UUID(tenant_id)
AuditLog.tenant_id == tenant_id
)
)
result.deleted_counts["audit_logs"] = audit_result.rowcount

View File

@@ -17,6 +17,7 @@ from app.schemas.purchase_order_schemas import (
PurchaseOrderCreate,
PurchaseOrderUpdate,
PurchaseOrderResponse,
PurchaseOrderWithSupplierResponse,
PurchaseOrderApproval,
DeliveryCreate,
DeliveryResponse,
@@ -75,7 +76,7 @@ async def create_purchase_order(
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{po_id}", response_model=PurchaseOrderResponse)
@router.get("/{po_id}", response_model=PurchaseOrderWithSupplierResponse)
async def get_purchase_order(
tenant_id: str,
po_id: str,
@@ -91,7 +92,7 @@ async def get_purchase_order(
if not po:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.model_validate(po)
return PurchaseOrderWithSupplierResponse.model_validate(po)
except HTTPException:
raise

View File

@@ -50,7 +50,7 @@ class PurchaseOrderItemResponse(PurchaseOrderBase):
tenant_id: uuid.UUID
purchase_order_id: uuid.UUID
inventory_product_id: uuid.UUID # Changed from ingredient_id to match model
ingredient_name: Optional[str] = None
product_name: Optional[str] = None
ordered_quantity: Decimal
received_quantity: Decimal
unit_price: Decimal
@@ -109,6 +109,29 @@ class PurchaseOrderApproval(PurchaseOrderBase):
approved_by: Optional[uuid.UUID] = None
class SupplierSummary(PurchaseOrderBase):
"""Schema for supplier summary - matches the structure returned by suppliers service"""
id: str
name: str
supplier_code: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
contact_person: Optional[str] = None
address_line1: Optional[str] = None
city: Optional[str] = None
country: Optional[str] = None
supplier_type: Optional[str] = None
status: Optional[str] = None
mobile: Optional[str] = None
website: Optional[str] = None
payment_terms: Optional[str] = None
standard_lead_time: Optional[int] = None
quality_rating: Optional[float] = None
delivery_rating: Optional[float] = None
total_orders: Optional[int] = None
total_amount: Optional[float] = None
class PurchaseOrderResponse(PurchaseOrderBase):
"""Schema for purchase order responses"""
id: uuid.UUID
@@ -155,6 +178,11 @@ class PurchaseOrderResponse(PurchaseOrderBase):
items: List[PurchaseOrderItemResponse] = []
class PurchaseOrderWithSupplierResponse(PurchaseOrderResponse):
"""Schema for purchase order responses with supplier information"""
supplier: Optional[SupplierSummary] = None
class PurchaseOrderSummary(PurchaseOrderBase):
"""Schema for purchase order summary (list view)"""
id: uuid.UUID

View File

@@ -613,9 +613,37 @@ class PurchaseOrderService:
if supplier:
# Set supplier_name as a dynamic attribute on the model instance
po.supplier_name = supplier.get('name', 'Unknown Supplier')
# Create a supplier summary object with the required fields for the frontend
# Using the same structure as the suppliers service SupplierSummary schema
supplier_summary = {
'id': supplier.get('id'),
'name': supplier.get('name', 'Unknown Supplier'),
'supplier_code': supplier.get('supplier_code'),
'email': supplier.get('email'),
'phone': supplier.get('phone'),
'contact_person': supplier.get('contact_person'),
'address_line1': supplier.get('address_line1'),
'city': supplier.get('city'),
'country': supplier.get('country'),
'supplier_type': supplier.get('supplier_type', 'raw_material'),
'status': supplier.get('status', 'active'),
'mobile': supplier.get('mobile'),
'website': supplier.get('website'),
'payment_terms': supplier.get('payment_terms', 'NET_30'),
'standard_lead_time': supplier.get('standard_lead_time', 3),
'quality_rating': supplier.get('quality_rating'),
'delivery_rating': supplier.get('delivery_rating'),
'total_orders': supplier.get('total_orders', 0),
'total_amount': supplier.get('total_amount', 0)
}
# Set the full supplier object as a dynamic attribute
po.supplier = supplier_summary
except Exception as e:
logger.warning("Failed to enrich PO with supplier info", error=str(e), po_id=po.id, supplier_id=po.supplier_id)
po.supplier_name = None
po.supplier = None
def _requires_approval(self, total_amount: Decimal, priority: str) -> bool:
"""Determine if PO requires approval"""

View File

@@ -1,11 +1,23 @@
# services/production/app/api/production_orders_operations.py
"""
Tenant Data Deletion Operations (Internal Service Only)
"""
# ============================================================================
# Tenant Data Deletion Operations (Internal Service Only)
# ============================================================================
from fastapi import APIRouter, Depends, HTTPException, Path
from sqlalchemy.ext.asyncio import AsyncSession
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import service_only_access
from shared.routing import RouteBuilder
from shared.services.tenant_deletion import TenantDataDeletionResult
from app.core.database import get_db
from app.services.tenant_deletion_service import ProductionTenantDeletionService
logger = structlog.get_logger()
route_builder = RouteBuilder('production')
router = APIRouter(tags=["production-tenant-deletion"])
@router.delete(
route_builder.build_base_route("tenant/{tenant_id}", include_tenant_prefix=False),
@@ -60,7 +72,10 @@ async def preview_tenant_data_deletion(
logger.info("production.tenant_deletion.preview_called", tenant_id=tenant_id)
deletion_service = ProductionTenantDeletionService(db)
result = await deletion_service.preview_deletion(tenant_id)
preview_data = await deletion_service.get_tenant_data_preview(tenant_id)
result = TenantDataDeletionResult(tenant_id=tenant_id, service_name=deletion_service.service_name)
result.deleted_counts = preview_data
result.success = True
if not result.success:
raise HTTPException(

View File

@@ -24,7 +24,8 @@ from app.api import (
quality_templates,
equipment,
internal_demo,
orchestrator # NEW: Orchestrator integration endpoint
orchestrator, # NEW: Orchestrator integration endpoint
production_orders_operations # Tenant deletion endpoints
)
@@ -151,6 +152,7 @@ service.setup_custom_middleware()
# Include standardized routers
# NOTE: Register more specific routes before generic parameterized routes
service.add_router(orchestrator.router) # NEW: Orchestrator integration endpoint
service.add_router(production_orders_operations.router) # Tenant deletion endpoints
service.add_router(quality_templates.router) # Register first to avoid route conflicts
service.add_router(equipment.router)
service.add_router(production_batches.router)

View File

@@ -225,6 +225,7 @@ async def get_recipe_count(
# ============================================================================
from shared.auth.access_control import service_only_access
from shared.services.tenant_deletion import TenantDataDeletionResult
from app.services.tenant_deletion_service import RecipesTenantDeletionService
@@ -260,7 +261,7 @@ async def delete_tenant_data(
except HTTPException:
raise
except Exception as e:
logger.error("recipes.tenant_deletion.api_error", tenant_id=tenant_id, error=str(e), exc_info=True)
logger.error(f"recipes.tenant_deletion.api_error - tenant_id: {tenant_id}, error: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to delete tenant data: {str(e)}")
@@ -278,10 +279,13 @@ async def preview_tenant_data_deletion(
Preview what data would be deleted for a tenant (dry-run)
"""
try:
logger.info("recipes.tenant_deletion.preview_called", tenant_id=tenant_id)
logger.info(f"recipes.tenant_deletion.preview_called - tenant_id: {tenant_id}")
deletion_service = RecipesTenantDeletionService(db)
result = await deletion_service.preview_deletion(tenant_id)
preview_data = await deletion_service.get_tenant_data_preview(tenant_id)
result = TenantDataDeletionResult(tenant_id=tenant_id, service_name=deletion_service.service_name)
result.deleted_counts = preview_data
result.success = True
if not result.success:
raise HTTPException(
@@ -298,5 +302,5 @@ async def preview_tenant_data_deletion(
except HTTPException:
raise
except Exception as e:
logger.error("recipes.tenant_deletion.preview_error", tenant_id=tenant_id, error=str(e), exc_info=True)
logger.error(f"recipes.tenant_deletion.preview_error - tenant_id: {tenant_id}, error: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to preview tenant data deletion: {str(e)}")

View File

@@ -18,9 +18,10 @@ from ..schemas.recipes import (
)
from ..models import AuditLog
from shared.routing import RouteBuilder, RouteCategory
from shared.auth.access_control import require_user_role
from shared.auth.access_control import require_user_role, service_only_access
from shared.auth.decorators import get_current_user_dep
from shared.security import create_audit_logger, AuditSeverity, AuditAction
from shared.services.tenant_deletion import TenantDataDeletionResult
route_builder = RouteBuilder('recipes')
logger = logging.getLogger(__name__)
@@ -395,6 +396,7 @@ async def get_recipe_deletion_summary(
# ===== Tenant Data Deletion Endpoints =====
@router.delete("/tenant/{tenant_id}")
@service_only_access
async def delete_tenant_data(
tenant_id: str,
current_user: dict = Depends(get_current_user_dep),
@@ -407,13 +409,6 @@ async def delete_tenant_data(
logger.info(f"Tenant data deletion request received for tenant: {tenant_id}")
# Only allow internal service calls
if current_user.get("type") != "service":
raise HTTPException(
status_code=403,
detail="This endpoint is only accessible to internal services"
)
try:
from app.services.tenant_deletion_service import RecipesTenantDeletionService
@@ -434,6 +429,7 @@ async def delete_tenant_data(
@router.get("/tenant/{tenant_id}/deletion-preview")
@service_only_access
async def preview_tenant_data_deletion(
tenant_id: str,
current_user: dict = Depends(get_current_user_dep),
@@ -444,16 +440,6 @@ async def preview_tenant_data_deletion(
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=403,
detail="Insufficient permissions"
)
try:
from app.services.tenant_deletion_service import RecipesTenantDeletionService

View File

@@ -439,6 +439,7 @@ async def get_import_template(
# ============================================================================
from shared.auth.access_control import service_only_access
from shared.services.tenant_deletion import TenantDataDeletionResult
from app.services.tenant_deletion_service import SalesTenantDeletionService
@@ -495,7 +496,10 @@ async def preview_tenant_data_deletion(
logger.info("sales.tenant_deletion.preview_called", tenant_id=tenant_id)
deletion_service = SalesTenantDeletionService(db)
result = await deletion_service.preview_deletion(tenant_id)
preview_data = await deletion_service.get_tenant_data_preview(tenant_id)
result = TenantDataDeletionResult(tenant_id=tenant_id, service_name=deletion_service.service_name)
result.deleted_counts = preview_data
result.success = True
if not result.success:
raise HTTPException(