Add user delete process 2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -390,12 +390,12 @@ const ProcurementPage: React.FC = () => {
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
value: po.supplier?.contact_email || 'N/A',
|
||||
value: po.supplier?.email || 'N/A',
|
||||
type: 'text' as const
|
||||
},
|
||||
{
|
||||
label: 'Teléfono',
|
||||
value: po.supplier?.contact_phone || 'N/A',
|
||||
value: po.supplier?.phone || 'N/A',
|
||||
type: 'text' as const
|
||||
}
|
||||
]
|
||||
@@ -557,7 +557,18 @@ const ProcurementPage: React.FC = () => {
|
||||
|
||||
const totalAmount = items.reduce((sum, item) => {
|
||||
const price = typeof item.unit_price === 'string' ? parseFloat(item.unit_price) : typeof item.unit_price === 'number' ? item.unit_price : 0;
|
||||
const quantity = typeof item.ordered_quantity === 'number' ? item.ordered_quantity : 0;
|
||||
const quantity = (() => {
|
||||
if (typeof item.ordered_quantity === 'number') {
|
||||
return item.ordered_quantity;
|
||||
} else if (typeof item.ordered_quantity === 'string') {
|
||||
const parsed = parseFloat(item.ordered_quantity);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
} else if (typeof item.ordered_quantity === 'object' && item.ordered_quantity !== null) {
|
||||
// Handle if it's a decimal object or similar
|
||||
return parseFloat(item.ordered_quantity.toString()) || 0;
|
||||
}
|
||||
return 0;
|
||||
})();
|
||||
return sum + (price * quantity);
|
||||
}, 0);
|
||||
|
||||
@@ -566,9 +577,20 @@ const ProcurementPage: React.FC = () => {
|
||||
{/* Items as cards */}
|
||||
{items.map((item, index) => {
|
||||
const unitPrice = typeof item.unit_price === 'string' ? parseFloat(item.unit_price) : typeof item.unit_price === 'number' ? item.unit_price : 0;
|
||||
const quantity = typeof item.ordered_quantity === 'number' ? item.ordered_quantity : 0;
|
||||
const quantity = (() => {
|
||||
if (typeof item.ordered_quantity === 'number') {
|
||||
return item.ordered_quantity;
|
||||
} else if (typeof item.ordered_quantity === 'string') {
|
||||
const parsed = parseFloat(item.ordered_quantity);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
} else if (typeof item.ordered_quantity === 'object' && item.ordered_quantity !== null) {
|
||||
// Handle if it's a decimal object or similar
|
||||
return parseFloat(item.ordered_quantity.toString()) || 0;
|
||||
}
|
||||
return 0;
|
||||
})();
|
||||
const itemTotal = unitPrice * quantity;
|
||||
const productName = item.product_name || `Producto ${index + 1}`;
|
||||
const productName = item.product_name || item.ingredient_name || `Producto ${index + 1}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -14,7 +14,14 @@ NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
TENANT_ID="${1:-dbc2128a-7539-470c-94b9-c1e37031bd77}"
|
||||
SERVICE_TOKEN="${SERVICE_TOKEN}"
|
||||
|
||||
# Generate or use provided SERVICE_TOKEN
|
||||
if [ -z "${SERVICE_TOKEN}" ]; then
|
||||
SERVICE_TOKEN=$(python3 scripts/generate_service_token.py tenant-deletion-orchestrator 2>&1 | grep -A1 "Token:" | tail -1 | sed 's/^[[:space:]]*//' | tr -d '\n')
|
||||
else
|
||||
# Clean the token if provided (remove whitespace and newlines)
|
||||
SERVICE_TOKEN=$(echo "${SERVICE_TOKEN}" | tr -d '[:space:]')
|
||||
fi
|
||||
|
||||
# Results
|
||||
TOTAL_SERVICES=12
|
||||
@@ -44,6 +51,7 @@ print_info() {
|
||||
test_service() {
|
||||
local service_name=$1
|
||||
local endpoint_path=$2
|
||||
local port=${3:-8000} # Default to 8000 if not specified
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}Testing ${service_name}...${NC}"
|
||||
@@ -62,7 +70,7 @@ test_service() {
|
||||
# Execute request
|
||||
local result=$(kubectl exec -n bakery-ia "$pod" -- curl -s -w "\nHTTP_CODE:%{http_code}" \
|
||||
-H "Authorization: Bearer ${SERVICE_TOKEN}" \
|
||||
"http://localhost:8000${endpoint_path}/tenant/${TENANT_ID}/deletion-preview" 2>&1)
|
||||
"http://localhost:${port}${endpoint_path}/tenant/${TENANT_ID}/deletion-preview" 2>&1)
|
||||
|
||||
local http_code=$(echo "$result" | grep "HTTP_CODE" | cut -d':' -f2)
|
||||
local body=$(echo "$result" | sed '/HTTP_CODE/d')
|
||||
@@ -114,11 +122,11 @@ test_service "sales-service" "/api/v1/sales"
|
||||
test_service "production-service" "/api/v1/production"
|
||||
test_service "suppliers-service" "/api/v1/suppliers"
|
||||
test_service "pos-service" "/api/v1/pos"
|
||||
test_service "city-service" "/api/v1/nominatim"
|
||||
test_service "external-service" "/api/v1/external"
|
||||
test_service "forecasting-service" "/api/v1/forecasting"
|
||||
test_service "training-service" "/api/v1/training"
|
||||
test_service "alert-processor-service" "/api/v1/analytics"
|
||||
test_service "notification-service" "/api/v1/notifications"
|
||||
test_service "alert-processor-api" "/api/v1/alerts" 8010
|
||||
test_service "notification-service" "/api/v1/notification"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user