2025-10-19 19:22:37 +02:00
|
|
|
# services/production/app/api/equipment.py
|
|
|
|
|
"""
|
|
|
|
|
Equipment API - CRUD operations on Equipment model
|
|
|
|
|
"""
|
|
|
|
|
|
2025-12-13 23:57:54 +01:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
|
2025-10-19 19:22:37 +02:00
|
|
|
from typing import Optional
|
|
|
|
|
from uuid import UUID
|
2026-01-11 17:03:46 +01:00
|
|
|
from datetime import datetime, timezone
|
2025-10-19 19:22:37 +02:00
|
|
|
import structlog
|
|
|
|
|
|
|
|
|
|
from shared.auth.decorators import get_current_user_dep
|
|
|
|
|
from shared.auth.access_control import require_user_role
|
|
|
|
|
from shared.routing import RouteBuilder
|
|
|
|
|
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
|
|
|
|
from app.core.database import get_db
|
|
|
|
|
from app.services.production_service import ProductionService
|
2025-10-29 06:58:05 +01:00
|
|
|
from app.models import AuditLog
|
2025-10-19 19:22:37 +02:00
|
|
|
from app.schemas.equipment import (
|
|
|
|
|
EquipmentCreate,
|
|
|
|
|
EquipmentUpdate,
|
|
|
|
|
EquipmentResponse,
|
2025-10-29 06:58:05 +01:00
|
|
|
EquipmentListResponse,
|
|
|
|
|
EquipmentDeletionSummary
|
2025-10-19 19:22:37 +02:00
|
|
|
)
|
|
|
|
|
from app.models.production import EquipmentStatus, EquipmentType
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
route_builder = RouteBuilder('production')
|
|
|
|
|
router = APIRouter(tags=["production-equipment"])
|
|
|
|
|
|
2025-10-29 06:58:05 +01:00
|
|
|
# Initialize audit logger with the production service's AuditLog model
|
|
|
|
|
audit_logger = create_audit_logger("production-service", AuditLog)
|
2025-10-19 19:22:37 +02:00
|
|
|
|
|
|
|
|
|
2025-12-13 23:57:54 +01:00
|
|
|
def get_production_service(request: Request) -> ProductionService:
|
2025-10-19 19:22:37 +02:00
|
|
|
"""Dependency injection for production service"""
|
|
|
|
|
from app.core.database import database_manager
|
2025-12-13 23:57:54 +01:00
|
|
|
notification_service = getattr(request.app.state, 'notification_service', None)
|
|
|
|
|
return ProductionService(database_manager, settings, notification_service)
|
2025-10-19 19:22:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
route_builder.build_base_route("equipment"),
|
|
|
|
|
response_model=EquipmentListResponse
|
|
|
|
|
)
|
|
|
|
|
async def list_equipment(
|
|
|
|
|
tenant_id: UUID = Path(...),
|
|
|
|
|
status: Optional[EquipmentStatus] = Query(None, description="Filter by status"),
|
|
|
|
|
type: Optional[EquipmentType] = Query(None, description="Filter by equipment type"),
|
|
|
|
|
is_active: Optional[bool] = Query(None, description="Filter by active status"),
|
|
|
|
|
page: int = Query(1, ge=1, description="Page number"),
|
|
|
|
|
page_size: int = Query(50, ge=1, le=100, description="Page size"),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
|
|
|
):
|
|
|
|
|
"""List equipment with filters: status, type, active status"""
|
|
|
|
|
try:
|
|
|
|
|
filters = {
|
|
|
|
|
"status": status,
|
|
|
|
|
"type": type,
|
|
|
|
|
"is_active": is_active
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
equipment_list = await production_service.get_equipment_list(tenant_id, filters, page, page_size)
|
|
|
|
|
|
|
|
|
|
logger.info("Retrieved equipment list",
|
|
|
|
|
tenant_id=str(tenant_id), filters=filters)
|
|
|
|
|
|
|
|
|
|
return equipment_list
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error listing equipment",
|
|
|
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to list equipment")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
route_builder.build_base_route("equipment"),
|
|
|
|
|
response_model=EquipmentResponse
|
|
|
|
|
)
|
|
|
|
|
async def create_equipment(
|
|
|
|
|
equipment_data: EquipmentCreate,
|
|
|
|
|
tenant_id: UUID = Path(...),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
2025-10-29 06:58:05 +01:00
|
|
|
production_service: ProductionService = Depends(get_production_service),
|
|
|
|
|
db = Depends(get_db)
|
2025-10-19 19:22:37 +02:00
|
|
|
):
|
|
|
|
|
"""Create a new equipment item"""
|
|
|
|
|
try:
|
|
|
|
|
equipment = await production_service.create_equipment(tenant_id, equipment_data)
|
|
|
|
|
|
|
|
|
|
logger.info("Created equipment",
|
|
|
|
|
equipment_id=str(equipment.id), tenant_id=str(tenant_id))
|
|
|
|
|
|
2025-10-29 06:58:05 +01:00
|
|
|
# Audit log the equipment creation
|
|
|
|
|
await audit_logger.log_event(
|
|
|
|
|
db_session=db,
|
|
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
user_id=current_user.get('user_id'),
|
|
|
|
|
action=AuditAction.CREATE.value,
|
2025-10-19 19:22:37 +02:00
|
|
|
resource_type="equipment",
|
|
|
|
|
resource_id=str(equipment.id),
|
2025-10-29 06:58:05 +01:00
|
|
|
severity=AuditSeverity.INFO.value,
|
|
|
|
|
audit_metadata={"equipment_name": equipment.name, "equipment_type": equipment.type.value}
|
2025-10-19 19:22:37 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return EquipmentResponse.model_validate(equipment)
|
|
|
|
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
logger.warning("Validation error creating equipment",
|
|
|
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error creating equipment",
|
|
|
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to create equipment")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
route_builder.build_base_route("equipment/{equipment_id}"),
|
|
|
|
|
response_model=EquipmentResponse
|
|
|
|
|
)
|
|
|
|
|
async def get_equipment(
|
|
|
|
|
tenant_id: UUID = Path(...),
|
|
|
|
|
equipment_id: UUID = Path(...),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
|
|
|
):
|
|
|
|
|
"""Get a specific equipment item"""
|
|
|
|
|
try:
|
|
|
|
|
equipment = await production_service.get_equipment(tenant_id, equipment_id)
|
|
|
|
|
|
|
|
|
|
if not equipment:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Equipment not found")
|
|
|
|
|
|
|
|
|
|
logger.info("Retrieved equipment",
|
|
|
|
|
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
|
|
|
|
|
return EquipmentResponse.model_validate(equipment)
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error retrieving equipment",
|
|
|
|
|
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to retrieve equipment")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put(
|
|
|
|
|
route_builder.build_base_route("equipment/{equipment_id}"),
|
|
|
|
|
response_model=EquipmentResponse
|
|
|
|
|
)
|
|
|
|
|
async def update_equipment(
|
|
|
|
|
equipment_data: EquipmentUpdate,
|
|
|
|
|
tenant_id: UUID = Path(...),
|
|
|
|
|
equipment_id: UUID = Path(...),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
2025-10-29 06:58:05 +01:00
|
|
|
production_service: ProductionService = Depends(get_production_service),
|
|
|
|
|
db = Depends(get_db)
|
2025-10-19 19:22:37 +02:00
|
|
|
):
|
|
|
|
|
"""Update an equipment item"""
|
|
|
|
|
try:
|
|
|
|
|
equipment = await production_service.update_equipment(tenant_id, equipment_id, equipment_data)
|
|
|
|
|
|
|
|
|
|
if not equipment:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Equipment not found")
|
|
|
|
|
|
|
|
|
|
logger.info("Updated equipment",
|
|
|
|
|
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
|
2025-10-29 06:58:05 +01:00
|
|
|
# Audit log the equipment update
|
|
|
|
|
await audit_logger.log_event(
|
|
|
|
|
db_session=db,
|
|
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
user_id=current_user.get('user_id'),
|
|
|
|
|
action=AuditAction.UPDATE.value,
|
2025-10-19 19:22:37 +02:00
|
|
|
resource_type="equipment",
|
|
|
|
|
resource_id=str(equipment_id),
|
2025-10-29 06:58:05 +01:00
|
|
|
severity=AuditSeverity.INFO.value,
|
|
|
|
|
audit_metadata={"updates": equipment_data.model_dump(exclude_unset=True)}
|
2025-10-19 19:22:37 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return EquipmentResponse.model_validate(equipment)
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
logger.warning("Validation error updating equipment",
|
|
|
|
|
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error updating equipment",
|
|
|
|
|
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to update equipment")
|
|
|
|
|
|
|
|
|
|
|
2025-10-29 06:58:05 +01:00
|
|
|
@router.get(
|
|
|
|
|
route_builder.build_base_route("equipment/{equipment_id}/deletion-summary"),
|
|
|
|
|
response_model=EquipmentDeletionSummary
|
|
|
|
|
)
|
|
|
|
|
async def get_equipment_deletion_summary(
|
|
|
|
|
tenant_id: UUID = Path(...),
|
|
|
|
|
equipment_id: UUID = Path(...),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
|
|
|
):
|
|
|
|
|
"""Get deletion summary for equipment (dependency check)"""
|
|
|
|
|
try:
|
|
|
|
|
summary = await production_service.get_equipment_deletion_summary(tenant_id, equipment_id)
|
|
|
|
|
|
|
|
|
|
logger.info("Retrieved equipment deletion summary",
|
|
|
|
|
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
|
|
|
|
|
return EquipmentDeletionSummary(**summary)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting equipment deletion summary",
|
|
|
|
|
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to get deletion summary")
|
|
|
|
|
|
|
|
|
|
|
2026-01-11 17:03:46 +01:00
|
|
|
@router.post(
|
|
|
|
|
route_builder.build_base_route("equipment/{equipment_id}/report-failure"),
|
|
|
|
|
response_model=EquipmentResponse
|
|
|
|
|
)
|
|
|
|
|
async def report_equipment_failure(
|
|
|
|
|
failure_data: dict,
|
|
|
|
|
request: Request,
|
|
|
|
|
tenant_id: UUID = Path(...),
|
|
|
|
|
equipment_id: UUID = Path(...),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
production_service: ProductionService = Depends(get_production_service),
|
|
|
|
|
db = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Report equipment failure and trigger maintenance workflow"""
|
|
|
|
|
try:
|
|
|
|
|
# Update equipment status and add failure record
|
|
|
|
|
equipment = await production_service.report_equipment_failure(
|
|
|
|
|
tenant_id,
|
|
|
|
|
equipment_id,
|
|
|
|
|
failure_data
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not equipment:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Equipment not found")
|
|
|
|
|
|
|
|
|
|
logger.info("Reported equipment failure",
|
|
|
|
|
equipment_id=str(equipment_id),
|
|
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
failure_type=failure_data.get('failureType'))
|
|
|
|
|
|
|
|
|
|
# Audit log the failure report
|
|
|
|
|
await audit_logger.log_event(
|
|
|
|
|
db_session=db,
|
|
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
user_id=current_user.get('user_id'),
|
|
|
|
|
action=AuditAction.UPDATE.value,
|
|
|
|
|
resource_type="equipment",
|
|
|
|
|
resource_id=str(equipment_id),
|
|
|
|
|
severity=AuditSeverity.WARNING.value,
|
|
|
|
|
audit_metadata={
|
|
|
|
|
"action": "report_failure",
|
|
|
|
|
"failure_type": failure_data.get('failureType'),
|
|
|
|
|
"severity": failure_data.get('severity')
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Get notification service from app state
|
|
|
|
|
notification_service = getattr(request.app.state, 'notification_service', None)
|
|
|
|
|
|
|
|
|
|
# Trigger notifications if notification service is available
|
|
|
|
|
if notification_service:
|
|
|
|
|
try:
|
|
|
|
|
await trigger_failure_notifications(
|
|
|
|
|
notification_service,
|
|
|
|
|
tenant_id,
|
|
|
|
|
equipment,
|
|
|
|
|
failure_data
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Send primary notification to equipment support contact if available
|
|
|
|
|
if equipment.support_contact and equipment.support_contact.get('email'):
|
|
|
|
|
await send_support_contact_notification(
|
|
|
|
|
notification_service,
|
|
|
|
|
tenant_id,
|
|
|
|
|
equipment,
|
|
|
|
|
failure_data,
|
|
|
|
|
equipment.support_contact['email']
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("Failed to send notifications", error=str(e), equipment_id=str(equipment_id))
|
|
|
|
|
# Continue even if notifications fail
|
|
|
|
|
|
|
|
|
|
return EquipmentResponse.model_validate(equipment)
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error reporting equipment failure",
|
|
|
|
|
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to report equipment failure")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
route_builder.build_base_route("equipment/{equipment_id}/mark-repaired"),
|
|
|
|
|
response_model=EquipmentResponse
|
|
|
|
|
)
|
|
|
|
|
async def mark_equipment_repaired(
|
|
|
|
|
repair_data: dict,
|
|
|
|
|
request: Request,
|
|
|
|
|
tenant_id: UUID = Path(...),
|
|
|
|
|
equipment_id: UUID = Path(...),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
production_service: ProductionService = Depends(get_production_service),
|
|
|
|
|
db = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Mark equipment as repaired and update maintenance records"""
|
|
|
|
|
try:
|
|
|
|
|
# Update equipment status and add repair record
|
|
|
|
|
equipment = await production_service.mark_equipment_repaired(
|
|
|
|
|
tenant_id,
|
|
|
|
|
equipment_id,
|
|
|
|
|
repair_data
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not equipment:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Equipment not found")
|
|
|
|
|
|
|
|
|
|
logger.info("Marked equipment as repaired",
|
|
|
|
|
equipment_id=str(equipment_id),
|
|
|
|
|
tenant_id=str(tenant_id))
|
|
|
|
|
|
|
|
|
|
# Audit log the repair completion
|
|
|
|
|
await audit_logger.log_event(
|
|
|
|
|
db_session=db,
|
|
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
user_id=current_user.get('user_id'),
|
|
|
|
|
action=AuditAction.UPDATE.value,
|
|
|
|
|
resource_type="equipment",
|
|
|
|
|
resource_id=str(equipment_id),
|
|
|
|
|
severity=AuditSeverity.INFO.value,
|
|
|
|
|
audit_metadata={
|
|
|
|
|
"action": "mark_repaired",
|
|
|
|
|
"technician": repair_data.get('technicianName'),
|
|
|
|
|
"cost": repair_data.get('cost')
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Get notification service from app state
|
|
|
|
|
notification_service = getattr(request.app.state, 'notification_service', None)
|
|
|
|
|
|
|
|
|
|
# Trigger notifications if notification service is available
|
|
|
|
|
if notification_service:
|
|
|
|
|
try:
|
|
|
|
|
# Calculate downtime for notifications
|
|
|
|
|
last_maintenance_date = equipment.last_maintenance_date or datetime.now(timezone.utc)
|
|
|
|
|
repair_date_str = repair_data.get('repairDate')
|
|
|
|
|
if repair_date_str:
|
|
|
|
|
if 'T' in repair_date_str:
|
|
|
|
|
repair_date = datetime.fromisoformat(repair_date_str.replace('Z', '+00:00'))
|
|
|
|
|
else:
|
|
|
|
|
repair_date = datetime.fromisoformat(f"{repair_date_str}T00:00:00+00:00")
|
|
|
|
|
else:
|
|
|
|
|
repair_date = datetime.now(timezone.utc)
|
|
|
|
|
|
|
|
|
|
downtime_hours = int((repair_date - last_maintenance_date).total_seconds() / 3600)
|
|
|
|
|
|
|
|
|
|
# Add downtime to repair_data for notification
|
|
|
|
|
repair_data_with_downtime = {**repair_data, 'downtime': downtime_hours}
|
|
|
|
|
|
|
|
|
|
await trigger_repair_notifications(
|
|
|
|
|
notification_service,
|
|
|
|
|
tenant_id,
|
|
|
|
|
equipment,
|
|
|
|
|
repair_data_with_downtime
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("Failed to send notifications", error=str(e), equipment_id=str(equipment_id))
|
|
|
|
|
# Continue even if notifications fail
|
|
|
|
|
|
|
|
|
|
return EquipmentResponse.model_validate(equipment)
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error marking equipment as repaired",
|
|
|
|
|
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to mark equipment as repaired")
|
|
|
|
|
|
|
|
|
|
|
2025-10-19 19:22:37 +02:00
|
|
|
@router.delete(
|
|
|
|
|
route_builder.build_base_route("equipment/{equipment_id}")
|
|
|
|
|
)
|
|
|
|
|
async def delete_equipment(
|
|
|
|
|
tenant_id: UUID = Path(...),
|
|
|
|
|
equipment_id: UUID = Path(...),
|
2025-10-29 06:58:05 +01:00
|
|
|
permanent: bool = Query(False, description="Permanent delete (hard delete) if true"),
|
2025-10-19 19:22:37 +02:00
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
2025-10-29 06:58:05 +01:00
|
|
|
production_service: ProductionService = Depends(get_production_service),
|
|
|
|
|
db = Depends(get_db)
|
2025-10-19 19:22:37 +02:00
|
|
|
):
|
2025-10-29 06:58:05 +01:00
|
|
|
"""Delete an equipment item. Use permanent=true for hard delete (requires admin role)"""
|
2025-10-19 19:22:37 +02:00
|
|
|
try:
|
2025-10-29 06:58:05 +01:00
|
|
|
# Hard delete requires admin role
|
|
|
|
|
if permanent:
|
|
|
|
|
user_role = current_user.get('role', '').lower()
|
|
|
|
|
if user_role not in ['admin', 'owner']:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=403,
|
|
|
|
|
detail="Hard delete requires admin or owner role"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
success = await production_service.hard_delete_equipment(tenant_id, equipment_id)
|
|
|
|
|
delete_type = "hard_delete"
|
|
|
|
|
severity = AuditSeverity.CRITICAL.value
|
|
|
|
|
else:
|
|
|
|
|
success = await production_service.delete_equipment(tenant_id, equipment_id)
|
|
|
|
|
delete_type = "soft_delete"
|
|
|
|
|
severity = AuditSeverity.WARNING.value
|
2025-10-19 19:22:37 +02:00
|
|
|
|
|
|
|
|
if not success:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Equipment not found")
|
|
|
|
|
|
2025-10-29 06:58:05 +01:00
|
|
|
logger.info(f"{'Hard' if permanent else 'Soft'} deleted equipment",
|
2025-10-19 19:22:37 +02:00
|
|
|
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
|
2025-10-29 06:58:05 +01:00
|
|
|
# Audit log the equipment deletion
|
|
|
|
|
await audit_logger.log_event(
|
|
|
|
|
db_session=db,
|
|
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
user_id=current_user.get('user_id'),
|
|
|
|
|
action=AuditAction.DELETE.value,
|
2025-10-19 19:22:37 +02:00
|
|
|
resource_type="equipment",
|
|
|
|
|
resource_id=str(equipment_id),
|
2025-10-29 06:58:05 +01:00
|
|
|
severity=severity,
|
|
|
|
|
audit_metadata={"action": delete_type, "permanent": permanent}
|
2025-10-19 19:22:37 +02:00
|
|
|
)
|
|
|
|
|
|
2025-10-29 06:58:05 +01:00
|
|
|
return {"message": f"Equipment {'permanently deleted' if permanent else 'deleted'} successfully"}
|
2025-10-19 19:22:37 +02:00
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error deleting equipment",
|
|
|
|
|
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to delete equipment")
|
2026-01-11 17:03:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# Helper functions for notifications
|
|
|
|
|
async def trigger_failure_notifications(notification_service: any, tenant_id: UUID, equipment: any, failure_data: dict):
|
|
|
|
|
"""Trigger failure notifications via email - sends to bakery managers"""
|
|
|
|
|
try:
|
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
# Load template from file
|
|
|
|
|
template_dir = Path(__file__).parent.parent.parent / "notification" / "app" / "templates"
|
|
|
|
|
env = Environment(loader=FileSystemLoader(str(template_dir)))
|
|
|
|
|
template = env.get_template('equipment_failure_email.html')
|
|
|
|
|
|
|
|
|
|
# Prepare template variables
|
|
|
|
|
template_vars = {
|
|
|
|
|
"equipment_name": equipment.name,
|
|
|
|
|
"equipment_type": equipment.type.value if hasattr(equipment.type, 'value') else equipment.type,
|
|
|
|
|
"equipment_model": equipment.model or "N/A",
|
|
|
|
|
"equipment_serial_number": equipment.serial_number or "N/A",
|
|
|
|
|
"equipment_location": equipment.location or "N/A",
|
|
|
|
|
"failure_type": failure_data.get('failureType', 'Unknown'),
|
|
|
|
|
"severity": failure_data.get('severity', 'high'),
|
|
|
|
|
"description": failure_data.get('description', ''),
|
|
|
|
|
"reported_time": datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC'),
|
|
|
|
|
"estimated_impact": "SÍ - Afecta producción" if failure_data.get('estimatedImpact') else "NO - Sin impacto en producción",
|
|
|
|
|
"support_contact": equipment.support_contact or {},
|
|
|
|
|
"equipment_link": f"https://app.bakeryia.com/equipment/{equipment.id}",
|
|
|
|
|
"bakery_name": "BakeryIA",
|
|
|
|
|
"current_year": datetime.now().year
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html_content = template.render(**template_vars)
|
|
|
|
|
|
|
|
|
|
# Send via notification service (which will handle the actual email sending)
|
|
|
|
|
# This is a simplified approach - in production you'd want to get manager emails from DB
|
|
|
|
|
logger.info("Failure notifications triggered (template rendered)",
|
|
|
|
|
equipment_id=str(equipment.id),
|
|
|
|
|
tenant_id=str(tenant_id))
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error triggering failure notifications",
|
|
|
|
|
error=str(e), equipment_id=str(equipment.id), tenant_id=str(tenant_id))
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def trigger_repair_notifications(notification_service: any, tenant_id: UUID, equipment: any, repair_data: dict):
|
|
|
|
|
"""Trigger repair completion notifications via email - sends to bakery managers"""
|
|
|
|
|
try:
|
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
# Load template from file
|
|
|
|
|
template_dir = Path(__file__).parent.parent.parent / "notification" / "app" / "templates"
|
|
|
|
|
env = Environment(loader=FileSystemLoader(str(template_dir)))
|
|
|
|
|
template = env.get_template('equipment_repaired_email.html')
|
|
|
|
|
|
|
|
|
|
# Prepare template variables
|
|
|
|
|
template_vars = {
|
|
|
|
|
"equipment_name": equipment.name,
|
|
|
|
|
"equipment_type": equipment.type.value if hasattr(equipment.type, 'value') else equipment.type,
|
|
|
|
|
"equipment_model": equipment.model or "N/A",
|
|
|
|
|
"equipment_location": equipment.location or "N/A",
|
|
|
|
|
"repair_date": repair_data.get('repairDate', datetime.now(timezone.utc).strftime('%Y-%m-%d')),
|
|
|
|
|
"technician_name": repair_data.get('technicianName', 'Unknown'),
|
|
|
|
|
"repair_description": repair_data.get('repairDescription', ''),
|
|
|
|
|
"parts_replaced": repair_data.get('partsReplaced', []),
|
|
|
|
|
"cost": repair_data.get('cost', 0),
|
|
|
|
|
"downtime_hours": repair_data.get('downtime', 0),
|
|
|
|
|
"test_results": repair_data.get('testResults', False),
|
|
|
|
|
"equipment_link": f"https://app.bakeryia.com/equipment/{equipment.id}",
|
|
|
|
|
"bakery_name": "BakeryIA",
|
|
|
|
|
"current_year": datetime.now().year
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html_content = template.render(**template_vars)
|
|
|
|
|
|
|
|
|
|
# Send via notification service
|
|
|
|
|
logger.info("Repair notifications triggered (template rendered)",
|
|
|
|
|
equipment_id=str(equipment.id),
|
|
|
|
|
tenant_id=str(tenant_id))
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error triggering repair notifications",
|
|
|
|
|
error=str(e), equipment_id=str(equipment.id), tenant_id=str(tenant_id))
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_support_contact_notification(notification_service: any, tenant_id: UUID, equipment: any, failure_data: dict, support_email: str):
|
|
|
|
|
"""Send direct notification to equipment support contact for repair request"""
|
|
|
|
|
try:
|
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
# Load template from file
|
|
|
|
|
template_dir = Path(__file__).parent.parent.parent / "notification" / "app" / "templates"
|
|
|
|
|
env = Environment(loader=FileSystemLoader(str(template_dir)))
|
|
|
|
|
template = env.get_template('equipment_failure_email.html')
|
|
|
|
|
|
|
|
|
|
# Prepare template variables
|
|
|
|
|
template_vars = {
|
|
|
|
|
"equipment_name": equipment.name,
|
|
|
|
|
"equipment_type": equipment.type.value if hasattr(equipment.type, 'value') else equipment.type,
|
|
|
|
|
"equipment_model": equipment.model or "N/A",
|
|
|
|
|
"equipment_serial_number": equipment.serial_number or "N/A",
|
|
|
|
|
"equipment_location": equipment.location or "N/A",
|
|
|
|
|
"failure_type": failure_data.get('failureType', 'Unknown'),
|
|
|
|
|
"severity": failure_data.get('severity', 'high'),
|
|
|
|
|
"description": failure_data.get('description', ''),
|
|
|
|
|
"reported_time": datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC'),
|
|
|
|
|
"estimated_impact": "SÍ - Afecta producción" if failure_data.get('estimatedImpact') else "NO - Sin impacto en producción",
|
|
|
|
|
"support_contact": equipment.support_contact or {},
|
|
|
|
|
"equipment_link": f"https://app.bakeryia.com/equipment/{equipment.id}",
|
|
|
|
|
"bakery_name": "BakeryIA",
|
|
|
|
|
"current_year": datetime.now().year
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html_content = template.render(**template_vars)
|
|
|
|
|
|
|
|
|
|
# TODO: Actually send email via notification service
|
|
|
|
|
# For now, just log that we would send to the support email
|
|
|
|
|
logger.info("Support contact notification prepared (would send to support)",
|
|
|
|
|
equipment_id=str(equipment.id),
|
|
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
support_email=support_email,
|
|
|
|
|
subject=f"🚨 URGENTE: Fallo de Equipo - {equipment.name}")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error sending support contact notification",
|
|
|
|
|
error=str(e), equipment_id=str(equipment.id), tenant_id=str(tenant_id))
|
|
|
|
|
raise
|