Add equipment fail feature

This commit is contained in:
Urtzi Alfaro
2026-01-11 17:03:46 +01:00
parent b66bfda100
commit ce4f3aff8c
19 changed files with 2101 additions and 51 deletions

View File

@@ -6,6 +6,7 @@ Equipment API - CRUD operations on Equipment model
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
from typing import Optional
from uuid import UUID
from datetime import datetime, timezone
import structlog
from shared.auth.decorators import get_current_user_dep
@@ -221,6 +222,175 @@ async def get_equipment_deletion_summary(
raise HTTPException(status_code=500, detail="Failed to get deletion summary")
@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")
@router.delete(
route_builder.build_base_route("equipment/{equipment_id}")
)
@@ -277,3 +447,134 @@ async def delete_equipment(
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")
# 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