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(