Add user delete process 2
This commit is contained in:
@@ -26,6 +26,7 @@ export interface PurchaseOrderItem {
|
|||||||
id: string;
|
id: string;
|
||||||
inventory_product_id: string;
|
inventory_product_id: string;
|
||||||
product_code?: string;
|
product_code?: string;
|
||||||
|
product_name?: string;
|
||||||
ordered_quantity: number;
|
ordered_quantity: number;
|
||||||
unit_of_measure: string;
|
unit_of_measure: string;
|
||||||
unit_price: string; // Decimal as string
|
unit_price: string; // Decimal as string
|
||||||
|
|||||||
@@ -390,12 +390,12 @@ const ProcurementPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
value: po.supplier?.contact_email || 'N/A',
|
value: po.supplier?.email || 'N/A',
|
||||||
type: 'text' as const
|
type: 'text' as const
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Teléfono',
|
label: 'Teléfono',
|
||||||
value: po.supplier?.contact_phone || 'N/A',
|
value: po.supplier?.phone || 'N/A',
|
||||||
type: 'text' as const
|
type: 'text' as const
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -557,7 +557,18 @@ const ProcurementPage: React.FC = () => {
|
|||||||
|
|
||||||
const totalAmount = items.reduce((sum, item) => {
|
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 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);
|
return sum + (price * quantity);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
@@ -566,9 +577,20 @@ const ProcurementPage: React.FC = () => {
|
|||||||
{/* Items as cards */}
|
{/* Items as cards */}
|
||||||
{items.map((item, index) => {
|
{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 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 itemTotal = unitPrice * quantity;
|
||||||
const productName = item.product_name || `Producto ${index + 1}`;
|
const productName = item.product_name || item.ingredient_name || `Producto ${index + 1}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ NC='\033[0m'
|
|||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
TENANT_ID="${1:-dbc2128a-7539-470c-94b9-c1e37031bd77}"
|
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
|
# Results
|
||||||
TOTAL_SERVICES=12
|
TOTAL_SERVICES=12
|
||||||
@@ -44,6 +51,7 @@ print_info() {
|
|||||||
test_service() {
|
test_service() {
|
||||||
local service_name=$1
|
local service_name=$1
|
||||||
local endpoint_path=$2
|
local endpoint_path=$2
|
||||||
|
local port=${3:-8000} # Default to 8000 if not specified
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}Testing ${service_name}...${NC}"
|
echo -e "${BLUE}Testing ${service_name}...${NC}"
|
||||||
@@ -62,7 +70,7 @@ test_service() {
|
|||||||
# Execute request
|
# Execute request
|
||||||
local result=$(kubectl exec -n bakery-ia "$pod" -- curl -s -w "\nHTTP_CODE:%{http_code}" \
|
local result=$(kubectl exec -n bakery-ia "$pod" -- curl -s -w "\nHTTP_CODE:%{http_code}" \
|
||||||
-H "Authorization: Bearer ${SERVICE_TOKEN}" \
|
-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 http_code=$(echo "$result" | grep "HTTP_CODE" | cut -d':' -f2)
|
||||||
local body=$(echo "$result" | sed '/HTTP_CODE/d')
|
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 "production-service" "/api/v1/production"
|
||||||
test_service "suppliers-service" "/api/v1/suppliers"
|
test_service "suppliers-service" "/api/v1/suppliers"
|
||||||
test_service "pos-service" "/api/v1/pos"
|
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 "forecasting-service" "/api/v1/forecasting"
|
||||||
test_service "training-service" "/api/v1/training"
|
test_service "training-service" "/api/v1/training"
|
||||||
test_service "alert-processor-service" "/api/v1/analytics"
|
test_service "alert-processor-api" "/api/v1/alerts" 8010
|
||||||
test_service "notification-service" "/api/v1/notifications"
|
test_service "notification-service" "/api/v1/notification"
|
||||||
|
|
||||||
# Summary
|
# Summary
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class AlertProcessorTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
# Count alerts (CASCADE will delete alert_interactions)
|
# Count alerts (CASCADE will delete alert_interactions)
|
||||||
alert_count = await self.db.scalar(
|
alert_count = await self.db.scalar(
|
||||||
select(func.count(Alert.id)).where(
|
select(func.count(Alert.id)).where(
|
||||||
Alert.tenant_id == UUID(tenant_id)
|
Alert.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
preview["alerts"] = alert_count or 0
|
preview["alerts"] = alert_count or 0
|
||||||
@@ -53,7 +53,7 @@ class AlertProcessorTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
from app.models.alerts import AlertInteraction
|
from app.models.alerts import AlertInteraction
|
||||||
interaction_count = await self.db.scalar(
|
interaction_count = await self.db.scalar(
|
||||||
select(func.count(AlertInteraction.id)).where(
|
select(func.count(AlertInteraction.id)).where(
|
||||||
AlertInteraction.tenant_id == UUID(tenant_id)
|
AlertInteraction.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
preview["alert_interactions"] = interaction_count or 0
|
preview["alert_interactions"] = interaction_count or 0
|
||||||
@@ -61,7 +61,7 @@ class AlertProcessorTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
# Count audit logs
|
# Count audit logs
|
||||||
audit_count = await self.db.scalar(
|
audit_count = await self.db.scalar(
|
||||||
select(func.count(AuditLog.id)).where(
|
select(func.count(AuditLog.id)).where(
|
||||||
AuditLog.tenant_id == UUID(tenant_id)
|
AuditLog.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
preview["audit_logs"] = audit_count or 0
|
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)
|
logger.info("alert_processor.tenant_deletion.deleting_interactions", tenant_id=tenant_id)
|
||||||
interactions_result = await self.db.execute(
|
interactions_result = await self.db.execute(
|
||||||
delete(AlertInteraction).where(
|
delete(AlertInteraction).where(
|
||||||
AlertInteraction.tenant_id == UUID(tenant_id)
|
AlertInteraction.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.deleted_counts["alert_interactions"] = interactions_result.rowcount
|
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)
|
logger.info("alert_processor.tenant_deletion.deleting_alerts", tenant_id=tenant_id)
|
||||||
alerts_result = await self.db.execute(
|
alerts_result = await self.db.execute(
|
||||||
delete(Alert).where(
|
delete(Alert).where(
|
||||||
Alert.tenant_id == UUID(tenant_id)
|
Alert.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.deleted_counts["alerts"] = alerts_result.rowcount
|
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)
|
logger.info("alert_processor.tenant_deletion.deleting_audit_logs", tenant_id=tenant_id)
|
||||||
audit_result = await self.db.execute(
|
audit_result = await self.db.execute(
|
||||||
delete(AuditLog).where(
|
delete(AuditLog).where(
|
||||||
AuditLog.tenant_id == UUID(tenant_id)
|
AuditLog.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.deleted_counts["audit_logs"] = audit_result.rowcount
|
result.deleted_counts["audit_logs"] = audit_result.rowcount
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class ExternalTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
# Count tenant-specific weather data (if any)
|
# Count tenant-specific weather data (if any)
|
||||||
weather_count = await self.db.scalar(
|
weather_count = await self.db.scalar(
|
||||||
select(func.count(WeatherData.id)).where(
|
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
|
preview["tenant_weather_data"] = weather_count or 0
|
||||||
@@ -69,7 +69,7 @@ class ExternalTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
# Count audit logs
|
# Count audit logs
|
||||||
audit_count = await self.db.scalar(
|
audit_count = await self.db.scalar(
|
||||||
select(func.count(AuditLog.id)).where(
|
select(func.count(AuditLog.id)).where(
|
||||||
AuditLog.tenant_id == UUID(tenant_id)
|
AuditLog.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
preview["audit_logs"] = audit_count or 0
|
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)
|
logger.info("external.tenant_deletion.deleting_weather_data", tenant_id=tenant_id)
|
||||||
weather_result = await self.db.execute(
|
weather_result = await self.db.execute(
|
||||||
delete(WeatherData).where(
|
delete(WeatherData).where(
|
||||||
WeatherData.tenant_id == UUID(tenant_id)
|
WeatherData.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.deleted_counts["tenant_weather_data"] = weather_result.rowcount
|
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)
|
logger.info("external.tenant_deletion.deleting_audit_logs", tenant_id=tenant_id)
|
||||||
audit_result = await self.db.execute(
|
audit_result = await self.db.execute(
|
||||||
delete(AuditLog).where(
|
delete(AuditLog).where(
|
||||||
AuditLog.tenant_id == UUID(tenant_id)
|
AuditLog.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.deleted_counts["audit_logs"] = audit_result.rowcount
|
result.deleted_counts["audit_logs"] = audit_result.rowcount
|
||||||
|
|||||||
@@ -889,84 +889,3 @@ async def preview_tenant_data_deletion(
|
|||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"Failed to preview tenant data deletion: {str(e)}"
|
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)}")
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
# Count notifications
|
# Count notifications
|
||||||
notification_count = await self.db.scalar(
|
notification_count = await self.db.scalar(
|
||||||
select(func.count(Notification.id)).where(
|
select(func.count(Notification.id)).where(
|
||||||
Notification.tenant_id == UUID(tenant_id)
|
Notification.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
preview["notifications"] = notification_count or 0
|
preview["notifications"] = notification_count or 0
|
||||||
@@ -57,7 +57,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
# Count tenant-specific notification templates
|
# Count tenant-specific notification templates
|
||||||
template_count = await self.db.scalar(
|
template_count = await self.db.scalar(
|
||||||
select(func.count(NotificationTemplate.id)).where(
|
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
|
NotificationTemplate.is_system == False # Don't delete system templates
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -66,15 +66,17 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
# Count notification preferences
|
# Count notification preferences
|
||||||
preference_count = await self.db.scalar(
|
preference_count = await self.db.scalar(
|
||||||
select(func.count(NotificationPreference.id)).where(
|
select(func.count(NotificationPreference.id)).where(
|
||||||
NotificationPreference.tenant_id == UUID(tenant_id)
|
NotificationPreference.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
preview["notification_preferences"] = preference_count or 0
|
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(
|
log_count = await self.db.scalar(
|
||||||
select(func.count(NotificationLog.id)).where(
|
select(func.count(NotificationLog.id)).select_from(NotificationLog).join(
|
||||||
NotificationLog.tenant_id == UUID(tenant_id)
|
Notification, NotificationLog.notification_id == Notification.id
|
||||||
|
).where(
|
||||||
|
Notification.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
preview["notification_logs"] = log_count or 0
|
preview["notification_logs"] = log_count or 0
|
||||||
@@ -82,7 +84,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
# Count audit logs
|
# Count audit logs
|
||||||
audit_count = await self.db.scalar(
|
audit_count = await self.db.scalar(
|
||||||
select(func.count(AuditLog.id)).where(
|
select(func.count(AuditLog.id)).where(
|
||||||
AuditLog.tenant_id == UUID(tenant_id)
|
AuditLog.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
preview["audit_logs"] = audit_count or 0
|
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)
|
result = TenantDataDeletionResult(tenant_id=tenant_id, service_name=self.service_name)
|
||||||
|
|
||||||
try:
|
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)
|
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(
|
logs_result = await self.db.execute(
|
||||||
delete(NotificationLog).where(
|
delete(NotificationLog).where(
|
||||||
NotificationLog.tenant_id == UUID(tenant_id)
|
NotificationLog.notification_id.in_(notification_ids_subquery)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.deleted_counts["notification_logs"] = logs_result.rowcount
|
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)
|
logger.info("notification.tenant_deletion.deleting_preferences", tenant_id=tenant_id)
|
||||||
preferences_result = await self.db.execute(
|
preferences_result = await self.db.execute(
|
||||||
delete(NotificationPreference).where(
|
delete(NotificationPreference).where(
|
||||||
NotificationPreference.tenant_id == UUID(tenant_id)
|
NotificationPreference.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.deleted_counts["notification_preferences"] = preferences_result.rowcount
|
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)
|
logger.info("notification.tenant_deletion.deleting_notifications", tenant_id=tenant_id)
|
||||||
notifications_result = await self.db.execute(
|
notifications_result = await self.db.execute(
|
||||||
delete(Notification).where(
|
delete(Notification).where(
|
||||||
Notification.tenant_id == UUID(tenant_id)
|
Notification.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.deleted_counts["notifications"] = notifications_result.rowcount
|
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)
|
logger.info("notification.tenant_deletion.deleting_templates", tenant_id=tenant_id)
|
||||||
templates_result = await self.db.execute(
|
templates_result = await self.db.execute(
|
||||||
delete(NotificationTemplate).where(
|
delete(NotificationTemplate).where(
|
||||||
NotificationTemplate.tenant_id == UUID(tenant_id),
|
NotificationTemplate.tenant_id == tenant_id,
|
||||||
NotificationTemplate.is_system == False
|
NotificationTemplate.is_system == False
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -189,7 +192,7 @@ class NotificationTenantDeletionService(BaseTenantDataDeletionService):
|
|||||||
logger.info("notification.tenant_deletion.deleting_audit_logs", tenant_id=tenant_id)
|
logger.info("notification.tenant_deletion.deleting_audit_logs", tenant_id=tenant_id)
|
||||||
audit_result = await self.db.execute(
|
audit_result = await self.db.execute(
|
||||||
delete(AuditLog).where(
|
delete(AuditLog).where(
|
||||||
AuditLog.tenant_id == UUID(tenant_id)
|
AuditLog.tenant_id == tenant_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.deleted_counts["audit_logs"] = audit_result.rowcount
|
result.deleted_counts["audit_logs"] = audit_result.rowcount
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from app.schemas.purchase_order_schemas import (
|
|||||||
PurchaseOrderCreate,
|
PurchaseOrderCreate,
|
||||||
PurchaseOrderUpdate,
|
PurchaseOrderUpdate,
|
||||||
PurchaseOrderResponse,
|
PurchaseOrderResponse,
|
||||||
|
PurchaseOrderWithSupplierResponse,
|
||||||
PurchaseOrderApproval,
|
PurchaseOrderApproval,
|
||||||
DeliveryCreate,
|
DeliveryCreate,
|
||||||
DeliveryResponse,
|
DeliveryResponse,
|
||||||
@@ -75,7 +76,7 @@ async def create_purchase_order(
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
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(
|
async def get_purchase_order(
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
po_id: str,
|
po_id: str,
|
||||||
@@ -91,7 +92,7 @@ async def get_purchase_order(
|
|||||||
if not po:
|
if not po:
|
||||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||||
|
|
||||||
return PurchaseOrderResponse.model_validate(po)
|
return PurchaseOrderWithSupplierResponse.model_validate(po)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class PurchaseOrderItemResponse(PurchaseOrderBase):
|
|||||||
tenant_id: uuid.UUID
|
tenant_id: uuid.UUID
|
||||||
purchase_order_id: uuid.UUID
|
purchase_order_id: uuid.UUID
|
||||||
inventory_product_id: uuid.UUID # Changed from ingredient_id to match model
|
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
|
ordered_quantity: Decimal
|
||||||
received_quantity: Decimal
|
received_quantity: Decimal
|
||||||
unit_price: Decimal
|
unit_price: Decimal
|
||||||
@@ -109,6 +109,29 @@ class PurchaseOrderApproval(PurchaseOrderBase):
|
|||||||
approved_by: Optional[uuid.UUID] = None
|
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):
|
class PurchaseOrderResponse(PurchaseOrderBase):
|
||||||
"""Schema for purchase order responses"""
|
"""Schema for purchase order responses"""
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
@@ -155,6 +178,11 @@ class PurchaseOrderResponse(PurchaseOrderBase):
|
|||||||
items: List[PurchaseOrderItemResponse] = []
|
items: List[PurchaseOrderItemResponse] = []
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderWithSupplierResponse(PurchaseOrderResponse):
|
||||||
|
"""Schema for purchase order responses with supplier information"""
|
||||||
|
supplier: Optional[SupplierSummary] = None
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderSummary(PurchaseOrderBase):
|
class PurchaseOrderSummary(PurchaseOrderBase):
|
||||||
"""Schema for purchase order summary (list view)"""
|
"""Schema for purchase order summary (list view)"""
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
|
|||||||
@@ -613,9 +613,37 @@ class PurchaseOrderService:
|
|||||||
if supplier:
|
if supplier:
|
||||||
# Set supplier_name as a dynamic attribute on the model instance
|
# Set supplier_name as a dynamic attribute on the model instance
|
||||||
po.supplier_name = supplier.get('name', 'Unknown Supplier')
|
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:
|
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)
|
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_name = None
|
||||||
|
po.supplier = None
|
||||||
|
|
||||||
def _requires_approval(self, total_amount: Decimal, priority: str) -> bool:
|
def _requires_approval(self, total_amount: Decimal, priority: str) -> bool:
|
||||||
"""Determine if PO requires approval"""
|
"""Determine if PO requires approval"""
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
|
# services/production/app/api/production_orders_operations.py
|
||||||
|
"""
|
||||||
|
Tenant Data Deletion Operations (Internal Service Only)
|
||||||
|
"""
|
||||||
|
|
||||||
# ============================================================================
|
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||||
# Tenant Data Deletion Operations (Internal Service Only)
|
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.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
|
from app.services.tenant_deletion_service import ProductionTenantDeletionService
|
||||||
|
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
route_builder = RouteBuilder('production')
|
||||||
|
router = APIRouter(tags=["production-tenant-deletion"])
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
route_builder.build_base_route("tenant/{tenant_id}", include_tenant_prefix=False),
|
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)
|
logger.info("production.tenant_deletion.preview_called", tenant_id=tenant_id)
|
||||||
|
|
||||||
deletion_service = ProductionTenantDeletionService(db)
|
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:
|
if not result.success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ from app.api import (
|
|||||||
quality_templates,
|
quality_templates,
|
||||||
equipment,
|
equipment,
|
||||||
internal_demo,
|
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
|
# Include standardized routers
|
||||||
# NOTE: Register more specific routes before generic parameterized routes
|
# NOTE: Register more specific routes before generic parameterized routes
|
||||||
service.add_router(orchestrator.router) # NEW: Orchestrator integration endpoint
|
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(quality_templates.router) # Register first to avoid route conflicts
|
||||||
service.add_router(equipment.router)
|
service.add_router(equipment.router)
|
||||||
service.add_router(production_batches.router)
|
service.add_router(production_batches.router)
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ async def get_recipe_count(
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
from shared.auth.access_control import service_only_access
|
from shared.auth.access_control import service_only_access
|
||||||
|
from shared.services.tenant_deletion import TenantDataDeletionResult
|
||||||
from app.services.tenant_deletion_service import RecipesTenantDeletionService
|
from app.services.tenant_deletion_service import RecipesTenantDeletionService
|
||||||
|
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@ async def delete_tenant_data(
|
|||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
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)}")
|
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)
|
Preview what data would be deleted for a tenant (dry-run)
|
||||||
"""
|
"""
|
||||||
try:
|
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)
|
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:
|
if not result.success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -298,5 +302,5 @@ async def preview_tenant_data_deletion(
|
|||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
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)}")
|
raise HTTPException(status_code=500, detail=f"Failed to preview tenant data deletion: {str(e)}")
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ from ..schemas.recipes import (
|
|||||||
)
|
)
|
||||||
from ..models import AuditLog
|
from ..models import AuditLog
|
||||||
from shared.routing import RouteBuilder, RouteCategory
|
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.auth.decorators import get_current_user_dep
|
||||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||||
|
from shared.services.tenant_deletion import TenantDataDeletionResult
|
||||||
|
|
||||||
route_builder = RouteBuilder('recipes')
|
route_builder = RouteBuilder('recipes')
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -395,6 +396,7 @@ async def get_recipe_deletion_summary(
|
|||||||
# ===== Tenant Data Deletion Endpoints =====
|
# ===== Tenant Data Deletion Endpoints =====
|
||||||
|
|
||||||
@router.delete("/tenant/{tenant_id}")
|
@router.delete("/tenant/{tenant_id}")
|
||||||
|
@service_only_access
|
||||||
async def delete_tenant_data(
|
async def delete_tenant_data(
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
current_user: dict = Depends(get_current_user_dep),
|
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}")
|
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:
|
try:
|
||||||
from app.services.tenant_deletion_service import RecipesTenantDeletionService
|
from app.services.tenant_deletion_service import RecipesTenantDeletionService
|
||||||
|
|
||||||
@@ -434,6 +429,7 @@ async def delete_tenant_data(
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/tenant/{tenant_id}/deletion-preview")
|
@router.get("/tenant/{tenant_id}/deletion-preview")
|
||||||
|
@service_only_access
|
||||||
async def preview_tenant_data_deletion(
|
async def preview_tenant_data_deletion(
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
current_user: dict = Depends(get_current_user_dep),
|
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
|
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:
|
try:
|
||||||
from app.services.tenant_deletion_service import RecipesTenantDeletionService
|
from app.services.tenant_deletion_service import RecipesTenantDeletionService
|
||||||
|
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ async def get_import_template(
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
from shared.auth.access_control import service_only_access
|
from shared.auth.access_control import service_only_access
|
||||||
|
from shared.services.tenant_deletion import TenantDataDeletionResult
|
||||||
from app.services.tenant_deletion_service import SalesTenantDeletionService
|
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)
|
logger.info("sales.tenant_deletion.preview_called", tenant_id=tenant_id)
|
||||||
|
|
||||||
deletion_service = SalesTenantDeletionService(db)
|
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:
|
if not result.success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
Reference in New Issue
Block a user