Add equipment fail feature
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user