Improve the frontend and repository layer
This commit is contained in:
@@ -426,3 +426,102 @@ async def get_predictive_maintenance_insights(
|
||||
status_code=500,
|
||||
detail="Failed to generate predictive maintenance insights"
|
||||
)
|
||||
|
||||
|
||||
# ===== SUSTAINABILITY / WASTE ANALYTICS ENDPOINT =====
|
||||
# Called by Inventory Service for sustainability metrics
|
||||
|
||||
@router.get(
|
||||
"/api/v1/tenants/{tenant_id}/production/waste-analytics",
|
||||
response_model=dict
|
||||
)
|
||||
async def get_waste_analytics_for_sustainability(
|
||||
tenant_id: UUID = Path(...),
|
||||
start_date: datetime = Query(..., description="Start date for waste analysis"),
|
||||
end_date: datetime = Query(..., description="End date for waste analysis"),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
):
|
||||
"""
|
||||
Get production waste analytics for sustainability tracking
|
||||
|
||||
This endpoint is called by the Inventory Service's sustainability module
|
||||
to calculate environmental impact and SDG 12.3 compliance.
|
||||
|
||||
Does NOT require analytics tier - this is core sustainability data.
|
||||
|
||||
Returns:
|
||||
- total_production_waste: Sum of waste_quantity from all batches
|
||||
- total_defects: Sum of defect_quantity from all batches
|
||||
- total_planned: Sum of planned_quantity
|
||||
- total_actual: Sum of actual_quantity
|
||||
"""
|
||||
try:
|
||||
waste_data = await production_service.get_waste_analytics(
|
||||
tenant_id,
|
||||
start_date,
|
||||
end_date
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Production waste analytics retrieved for sustainability",
|
||||
tenant_id=str(tenant_id),
|
||||
total_waste=waste_data.get('total_production_waste', 0),
|
||||
start_date=start_date.isoformat(),
|
||||
end_date=end_date.isoformat()
|
||||
)
|
||||
|
||||
return waste_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error getting waste analytics for sustainability",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to retrieve waste analytics: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/tenants/{tenant_id}/production/baseline",
|
||||
response_model=dict
|
||||
)
|
||||
async def get_baseline_metrics(
|
||||
tenant_id: UUID = Path(...),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
):
|
||||
"""
|
||||
Get baseline production metrics from first 90 days
|
||||
|
||||
Used by sustainability service to establish waste baseline
|
||||
for SDG 12.3 compliance tracking.
|
||||
|
||||
Returns:
|
||||
- waste_percentage: Baseline waste percentage from first 90 days
|
||||
- total_production_kg: Total production in first 90 days
|
||||
- total_waste_kg: Total waste in first 90 days
|
||||
- period: Date range of baseline period
|
||||
"""
|
||||
try:
|
||||
baseline_data = await production_service.get_baseline_metrics(tenant_id)
|
||||
|
||||
logger.info(
|
||||
"Baseline metrics retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
baseline_percentage=baseline_data.get('waste_percentage', 0)
|
||||
)
|
||||
|
||||
return baseline_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error getting baseline metrics",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to retrieve baseline metrics: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -20,8 +20,6 @@ from app.models.production import (
|
||||
EquipmentStatus, EquipmentType
|
||||
)
|
||||
from shared.utils.demo_dates import adjust_date_for_demo, BASE_REFERENCE_DATE
|
||||
from shared.utils.alert_generator import generate_equipment_alerts
|
||||
from shared.messaging.rabbitmq import RabbitMQClient
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(prefix="/internal/demo", tags=["internal"])
|
||||
@@ -430,44 +428,18 @@ async def clone_demo_data(
|
||||
db.add(new_capacity)
|
||||
stats["production_capacity"] += 1
|
||||
|
||||
# Commit cloned data first
|
||||
# Commit cloned data
|
||||
await db.commit()
|
||||
|
||||
# Generate equipment maintenance and status alerts with RabbitMQ publishing
|
||||
rabbitmq_client = None
|
||||
try:
|
||||
# Initialize RabbitMQ client for alert publishing
|
||||
rabbitmq_host = os.getenv("RABBITMQ_HOST", "rabbitmq-service")
|
||||
rabbitmq_user = os.getenv("RABBITMQ_USER", "bakery")
|
||||
rabbitmq_password = os.getenv("RABBITMQ_PASSWORD", "forecast123")
|
||||
rabbitmq_port = os.getenv("RABBITMQ_PORT", "5672")
|
||||
rabbitmq_vhost = os.getenv("RABBITMQ_VHOST", "/")
|
||||
rabbitmq_url = f"amqp://{rabbitmq_user}:{rabbitmq_password}@{rabbitmq_host}:{rabbitmq_port}{rabbitmq_vhost}"
|
||||
# NOTE: Alert generation removed - alerts are now generated automatically by the
|
||||
# production alert service which runs scheduled checks at appropriate intervals.
|
||||
# This eliminates duplicate alerts and provides a more realistic demo experience.
|
||||
stats["alerts_generated"] = 0
|
||||
|
||||
rabbitmq_client = RabbitMQClient(rabbitmq_url, service_name="production")
|
||||
await rabbitmq_client.connect()
|
||||
|
||||
# Generate alerts and publish to RabbitMQ
|
||||
alerts_count = await generate_equipment_alerts(
|
||||
db,
|
||||
virtual_uuid,
|
||||
session_time,
|
||||
rabbitmq_client=rabbitmq_client
|
||||
)
|
||||
stats["alerts_generated"] += alerts_count
|
||||
await db.commit()
|
||||
logger.info(f"Generated {alerts_count} equipment alerts")
|
||||
except Exception as alert_error:
|
||||
logger.warning(f"Alert generation failed: {alert_error}", exc_info=True)
|
||||
finally:
|
||||
# Clean up RabbitMQ connection
|
||||
if rabbitmq_client:
|
||||
try:
|
||||
await rabbitmq_client.disconnect()
|
||||
except Exception as cleanup_error:
|
||||
logger.warning(f"Error disconnecting RabbitMQ: {cleanup_error}")
|
||||
|
||||
total_records = sum(stats.values())
|
||||
# Calculate total from non-alert stats
|
||||
total_records = (stats["equipment"] + stats["batches"] + stats["schedules"] +
|
||||
stats["quality_templates"] + stats["quality_checks"] +
|
||||
stats["production_capacity"])
|
||||
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -12,7 +12,7 @@ from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.routing import RouteBuilder, RouteCategory
|
||||
from app.core.database import get_db
|
||||
from app.repositories.quality_template_repository import QualityTemplateRepository
|
||||
from app.services.quality_template_service import QualityTemplateService
|
||||
from app.models.production import ProcessStage, QualityCheckTemplate
|
||||
from app.schemas.quality_templates import (
|
||||
QualityCheckTemplateCreate,
|
||||
@@ -52,9 +52,9 @@ async def list_quality_templates(
|
||||
- is_active: Filter by active status (default: True)
|
||||
"""
|
||||
try:
|
||||
repo = QualityTemplateRepository(db)
|
||||
service = QualityTemplateService(db)
|
||||
|
||||
templates, total = await repo.get_templates_by_tenant(
|
||||
templates, total = await service.get_templates(
|
||||
tenant_id=str(tenant_id),
|
||||
stage=stage,
|
||||
check_type=check_type.value if check_type else None,
|
||||
@@ -98,29 +98,18 @@ async def create_quality_template(
|
||||
):
|
||||
"""Create a new quality check template"""
|
||||
try:
|
||||
repo = QualityTemplateRepository(db)
|
||||
service = QualityTemplateService(db)
|
||||
|
||||
# Check if template code already exists (if provided)
|
||||
if template_data.template_code:
|
||||
code_exists = await repo.check_template_code_exists(
|
||||
tenant_id=str(tenant_id),
|
||||
template_code=template_data.template_code
|
||||
)
|
||||
if code_exists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Template code '{template_data.template_code}' already exists"
|
||||
)
|
||||
|
||||
# Create template
|
||||
# Add created_by from current user
|
||||
template_dict = template_data.dict()
|
||||
template_dict['tenant_id'] = str(tenant_id)
|
||||
template_dict['created_by'] = UUID(current_user["sub"])
|
||||
template_create = QualityCheckTemplateCreate(**template_dict)
|
||||
|
||||
template = QualityCheckTemplate(**template_dict)
|
||||
db.add(template)
|
||||
await db.commit()
|
||||
await db.refresh(template)
|
||||
# Create template via service (handles validation and business rules)
|
||||
template = await service.create_template(
|
||||
tenant_id=str(tenant_id),
|
||||
template_data=template_create
|
||||
)
|
||||
|
||||
logger.info("Created quality template",
|
||||
template_id=str(template.id),
|
||||
@@ -129,10 +118,13 @@ async def create_quality_template(
|
||||
|
||||
return QualityCheckTemplateResponse.from_orm(template)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
# Business rule validation errors
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error("Error creating quality template",
|
||||
error=str(e), tenant_id=str(tenant_id))
|
||||
raise HTTPException(
|
||||
@@ -153,9 +145,9 @@ async def get_quality_template(
|
||||
):
|
||||
"""Get a specific quality check template"""
|
||||
try:
|
||||
repo = QualityTemplateRepository(db)
|
||||
service = QualityTemplateService(db)
|
||||
|
||||
template = await repo.get_by_tenant_and_id(
|
||||
template = await service.get_template(
|
||||
tenant_id=str(tenant_id),
|
||||
template_id=template_id
|
||||
)
|
||||
@@ -195,12 +187,13 @@ async def update_quality_template(
|
||||
):
|
||||
"""Update a quality check template"""
|
||||
try:
|
||||
repo = QualityTemplateRepository(db)
|
||||
service = QualityTemplateService(db)
|
||||
|
||||
# Get existing template
|
||||
template = await repo.get_by_tenant_and_id(
|
||||
# Update template via service (handles validation and business rules)
|
||||
template = await service.update_template(
|
||||
tenant_id=str(tenant_id),
|
||||
template_id=template_id
|
||||
template_id=template_id,
|
||||
template_data=template_data
|
||||
)
|
||||
|
||||
if not template:
|
||||
@@ -209,37 +202,21 @@ async def update_quality_template(
|
||||
detail="Quality template not found"
|
||||
)
|
||||
|
||||
# Check if template code already exists (if being updated)
|
||||
if template_data.template_code and template_data.template_code != template.template_code:
|
||||
code_exists = await repo.check_template_code_exists(
|
||||
tenant_id=str(tenant_id),
|
||||
template_code=template_data.template_code,
|
||||
exclude_id=template_id
|
||||
)
|
||||
if code_exists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Template code '{template_data.template_code}' already exists"
|
||||
)
|
||||
|
||||
# Update template fields
|
||||
update_data = template_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(template, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(template)
|
||||
|
||||
logger.info("Updated quality template",
|
||||
template_id=str(template_id),
|
||||
tenant_id=str(tenant_id))
|
||||
|
||||
return QualityCheckTemplateResponse.from_orm(template)
|
||||
|
||||
except ValueError as e:
|
||||
# Business rule validation errors
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error("Error updating quality template",
|
||||
error=str(e),
|
||||
template_id=str(template_id),
|
||||
@@ -262,31 +239,27 @@ async def delete_quality_template(
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Delete a quality check template (soft delete by setting is_active to False)
|
||||
Delete a quality check template
|
||||
|
||||
Note: For safety, this performs a soft delete. Hard deletes would require
|
||||
checking for dependencies in recipes and production batches.
|
||||
Note: Service layer determines whether to use soft or hard delete
|
||||
based on business rules (checking dependencies, etc.)
|
||||
"""
|
||||
try:
|
||||
repo = QualityTemplateRepository(db)
|
||||
service = QualityTemplateService(db)
|
||||
|
||||
# Get existing template
|
||||
template = await repo.get_by_tenant_and_id(
|
||||
# Delete template via service (handles business rules)
|
||||
success = await service.delete_template(
|
||||
tenant_id=str(tenant_id),
|
||||
template_id=template_id
|
||||
)
|
||||
|
||||
if not template:
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Quality template not found"
|
||||
)
|
||||
|
||||
# Soft delete by marking as inactive
|
||||
template.is_active = False
|
||||
await db.commit()
|
||||
|
||||
logger.info("Deleted quality template (soft delete)",
|
||||
logger.info("Deleted quality template",
|
||||
template_id=str(template_id),
|
||||
tenant_id=str(tenant_id))
|
||||
|
||||
@@ -322,9 +295,9 @@ async def get_templates_for_stage(
|
||||
):
|
||||
"""Get all quality templates applicable to a specific process stage"""
|
||||
try:
|
||||
repo = QualityTemplateRepository(db)
|
||||
service = QualityTemplateService(db)
|
||||
|
||||
templates = await repo.get_templates_for_stage(
|
||||
templates = await service.get_templates_for_stage(
|
||||
tenant_id=str(tenant_id),
|
||||
stage=stage,
|
||||
is_active=is_active
|
||||
@@ -367,50 +340,20 @@ async def duplicate_quality_template(
|
||||
):
|
||||
"""Duplicate an existing quality check template"""
|
||||
try:
|
||||
repo = QualityTemplateRepository(db)
|
||||
service = QualityTemplateService(db)
|
||||
|
||||
# Get existing template
|
||||
original = await repo.get_by_tenant_and_id(
|
||||
# Duplicate template via service (handles business rules)
|
||||
duplicate = await service.duplicate_template(
|
||||
tenant_id=str(tenant_id),
|
||||
template_id=template_id
|
||||
)
|
||||
|
||||
if not original:
|
||||
if not duplicate:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Quality template not found"
|
||||
)
|
||||
|
||||
# Create duplicate
|
||||
duplicate_data = {
|
||||
'tenant_id': original.tenant_id,
|
||||
'name': f"{original.name} (Copy)",
|
||||
'template_code': f"{original.template_code}_copy" if original.template_code else None,
|
||||
'check_type': original.check_type,
|
||||
'category': original.category,
|
||||
'description': original.description,
|
||||
'instructions': original.instructions,
|
||||
'parameters': original.parameters,
|
||||
'thresholds': original.thresholds,
|
||||
'scoring_criteria': original.scoring_criteria,
|
||||
'is_active': original.is_active,
|
||||
'is_required': original.is_required,
|
||||
'is_critical': original.is_critical,
|
||||
'weight': original.weight,
|
||||
'min_value': original.min_value,
|
||||
'max_value': original.max_value,
|
||||
'target_value': original.target_value,
|
||||
'unit': original.unit,
|
||||
'tolerance_percentage': original.tolerance_percentage,
|
||||
'applicable_stages': original.applicable_stages,
|
||||
'created_by': UUID(current_user["sub"])
|
||||
}
|
||||
|
||||
duplicate = QualityCheckTemplate(**duplicate_data)
|
||||
db.add(duplicate)
|
||||
await db.commit()
|
||||
await db.refresh(duplicate)
|
||||
|
||||
logger.info("Duplicated quality template",
|
||||
original_id=str(template_id),
|
||||
duplicate_id=str(duplicate.id),
|
||||
@@ -421,7 +364,6 @@ async def duplicate_quality_template(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error("Error duplicating quality template",
|
||||
error=str(e),
|
||||
template_id=str(template_id),
|
||||
|
||||
Reference in New Issue
Block a user