From f44d235c6d9ba6e9b826e991795382c92d5f0b34 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Fri, 31 Oct 2025 18:57:58 +0100 Subject: [PATCH] Add user delete process 2 --- frontend/src/api/services/purchase_orders.ts | 1 + .../procurement/ProcurementPage.tsx | 32 ++++++-- scripts/functional_test_deletion_simple.sh | 18 +++-- .../app/services/tenant_deletion_service.py | 12 +-- .../app/services/tenant_deletion_service.py | 8 +- .../app/api/notification_operations.py | 81 ------------------- .../app/services/tenant_deletion_service.py | 29 ++++--- .../procurement/app/api/purchase_orders.py | 5 +- .../app/schemas/purchase_order_schemas.py | 30 ++++++- .../app/services/purchase_order_service.py | 28 +++++++ .../app/api/production_orders_operations.py | 23 +++++- services/production/app/main.py | 4 +- services/recipes/app/api/recipe_operations.py | 12 ++- services/recipes/app/api/recipes.py | 22 +---- services/sales/app/api/sales_operations.py | 6 +- 15 files changed, 166 insertions(+), 145 deletions(-) diff --git a/frontend/src/api/services/purchase_orders.ts b/frontend/src/api/services/purchase_orders.ts index efb3757f..5ab1500a 100644 --- a/frontend/src/api/services/purchase_orders.ts +++ b/frontend/src/api/services/purchase_orders.ts @@ -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 diff --git a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx index c66c9ab2..92a10eb0 100644 --- a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx +++ b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx @@ -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 (
&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 "" diff --git a/services/alert_processor/app/services/tenant_deletion_service.py b/services/alert_processor/app/services/tenant_deletion_service.py index e917a757..d9b24e65 100644 --- a/services/alert_processor/app/services/tenant_deletion_service.py +++ b/services/alert_processor/app/services/tenant_deletion_service.py @@ -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 diff --git a/services/external/app/services/tenant_deletion_service.py b/services/external/app/services/tenant_deletion_service.py index ce5d4077..6bf4a77b 100644 --- a/services/external/app/services/tenant_deletion_service.py +++ b/services/external/app/services/tenant_deletion_service.py @@ -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 diff --git a/services/notification/app/api/notification_operations.py b/services/notification/app/api/notification_operations.py index c1091c36..7dc449f2 100644 --- a/services/notification/app/api/notification_operations.py +++ b/services/notification/app/api/notification_operations.py @@ -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)}") diff --git a/services/notification/app/services/tenant_deletion_service.py b/services/notification/app/services/tenant_deletion_service.py index 6189270e..0a84d87c 100644 --- a/services/notification/app/services/tenant_deletion_service.py +++ b/services/notification/app/services/tenant_deletion_service.py @@ -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 diff --git a/services/procurement/app/api/purchase_orders.py b/services/procurement/app/api/purchase_orders.py index 6ee8f01c..53664fe0 100644 --- a/services/procurement/app/api/purchase_orders.py +++ b/services/procurement/app/api/purchase_orders.py @@ -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 diff --git a/services/procurement/app/schemas/purchase_order_schemas.py b/services/procurement/app/schemas/purchase_order_schemas.py index 061fe770..d4504808 100644 --- a/services/procurement/app/schemas/purchase_order_schemas.py +++ b/services/procurement/app/schemas/purchase_order_schemas.py @@ -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 diff --git a/services/procurement/app/services/purchase_order_service.py b/services/procurement/app/services/purchase_order_service.py index bda26743..f3184932 100644 --- a/services/procurement/app/services/purchase_order_service.py +++ b/services/procurement/app/services/purchase_order_service.py @@ -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""" diff --git a/services/production/app/api/production_orders_operations.py b/services/production/app/api/production_orders_operations.py index cad505a6..426fb501 100644 --- a/services/production/app/api/production_orders_operations.py +++ b/services/production/app/api/production_orders_operations.py @@ -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( diff --git a/services/production/app/main.py b/services/production/app/main.py index d330b38f..83353d57 100644 --- a/services/production/app/main.py +++ b/services/production/app/main.py @@ -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) diff --git a/services/recipes/app/api/recipe_operations.py b/services/recipes/app/api/recipe_operations.py index 39c6b026..1eb787f8 100644 --- a/services/recipes/app/api/recipe_operations.py +++ b/services/recipes/app/api/recipe_operations.py @@ -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)}") diff --git a/services/recipes/app/api/recipes.py b/services/recipes/app/api/recipes.py index 1133bda9..ef372d5c 100644 --- a/services/recipes/app/api/recipes.py +++ b/services/recipes/app/api/recipes.py @@ -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 diff --git a/services/sales/app/api/sales_operations.py b/services/sales/app/api/sales_operations.py index d3fe5ac8..2ab3cf4c 100644 --- a/services/sales/app/api/sales_operations.py +++ b/services/sales/app/api/sales_operations.py @@ -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(