REFACTOR production scheduler

This commit is contained in:
Urtzi Alfaro
2025-10-09 18:01:24 +02:00
parent 3c689b4f98
commit b420af32c5
13 changed files with 4046 additions and 6 deletions

View File

@@ -309,6 +309,9 @@ class ProcurementService:
elif status == "cancelled":
updates["execution_completed_at"] = datetime.utcnow()
# Handle plan rejection workflow - trigger notification and potential regeneration
await self._handle_plan_rejection(tenant_id, plan_id, approval_notes, updated_by)
plan = await self.plan_repo.update_plan(plan_id, tenant_id, updates)
if plan:
await self.db.commit()
@@ -1238,6 +1241,168 @@ class ProcurementService:
except Exception as e:
logger.warning("Failed to publish event", error=str(e))
async def _handle_plan_rejection(
self,
tenant_id: uuid.UUID,
plan_id: uuid.UUID,
rejection_notes: Optional[str],
rejected_by: Optional[uuid.UUID]
) -> None:
"""
Handle plan rejection workflow with notifications and optional regeneration
When a plan is rejected:
1. Send notifications to stakeholders
2. Analyze rejection reason
3. Offer regeneration option
4. Publish rejection event
"""
try:
logger.info("Processing plan rejection",
tenant_id=str(tenant_id),
plan_id=str(plan_id),
rejected_by=str(rejected_by) if rejected_by else None)
# Get plan details
plan = await self.plan_repo.get_plan_by_id(plan_id, tenant_id)
if not plan:
logger.error("Plan not found for rejection handling", plan_id=plan_id)
return
# Send notification to stakeholders
await self._send_plan_rejection_notification(
tenant_id, plan_id, plan.plan_number, rejection_notes, rejected_by
)
# Publish rejection event with details
await self._publish_plan_rejection_event(
tenant_id, plan_id, rejection_notes, rejected_by
)
# Check if we should auto-regenerate (e.g., if rejection due to stale data)
should_regenerate = self._should_auto_regenerate_plan(rejection_notes)
if should_regenerate:
logger.info("Auto-regenerating plan after rejection",
plan_id=plan_id, reason="stale data detected")
# Schedule regeneration (async task to not block rejection)
await self._schedule_plan_regeneration(tenant_id, plan.plan_date)
except Exception as e:
logger.error("Error handling plan rejection",
error=str(e),
plan_id=plan_id,
tenant_id=str(tenant_id))
def _should_auto_regenerate_plan(self, rejection_notes: Optional[str]) -> bool:
"""Determine if plan should be auto-regenerated based on rejection reason"""
if not rejection_notes:
return False
# Auto-regenerate if rejection mentions stale data or outdated forecasts
auto_regenerate_keywords = [
"stale", "outdated", "old data", "datos antiguos",
"desactualizado", "obsoleto"
]
rejection_lower = rejection_notes.lower()
return any(keyword in rejection_lower for keyword in auto_regenerate_keywords)
async def _send_plan_rejection_notification(
self,
tenant_id: uuid.UUID,
plan_id: uuid.UUID,
plan_number: str,
rejection_notes: Optional[str],
rejected_by: Optional[uuid.UUID]
) -> None:
"""Send notifications about plan rejection"""
try:
notification_data = {
"type": "procurement_plan_rejected",
"severity": "medium",
"title": f"Plan de Aprovisionamiento Rechazado: {plan_number}",
"message": f"El plan {plan_number} ha sido rechazado. {rejection_notes or 'Sin motivo especificado.'}",
"metadata": {
"tenant_id": str(tenant_id),
"plan_id": str(plan_id),
"plan_number": plan_number,
"rejection_notes": rejection_notes,
"rejected_by": str(rejected_by) if rejected_by else None,
"rejected_at": datetime.utcnow().isoformat(),
"action_required": "review_and_regenerate"
}
}
await self.rabbitmq_client.publish_event(
exchange_name="bakery_events",
routing_key="procurement.plan.rejected",
event_data=notification_data
)
logger.info("Plan rejection notification sent",
tenant_id=str(tenant_id),
plan_id=str(plan_id))
except Exception as e:
logger.error("Failed to send plan rejection notification", error=str(e))
async def _publish_plan_rejection_event(
self,
tenant_id: uuid.UUID,
plan_id: uuid.UUID,
rejection_notes: Optional[str],
rejected_by: Optional[uuid.UUID]
) -> None:
"""Publish plan rejection event for downstream systems"""
try:
event_data = {
"tenant_id": str(tenant_id),
"plan_id": str(plan_id),
"rejection_notes": rejection_notes,
"rejected_by": str(rejected_by) if rejected_by else None,
"timestamp": datetime.utcnow().isoformat(),
"event_type": "procurement.plan.rejected"
}
await self.rabbitmq_client.publish_event(
exchange_name="procurement.events",
routing_key="procurement.plan.rejected",
event_data=event_data
)
except Exception as e:
logger.warning("Failed to publish plan rejection event", error=str(e))
async def _schedule_plan_regeneration(
self,
tenant_id: uuid.UUID,
plan_date: date
) -> None:
"""Schedule automatic plan regeneration after rejection"""
try:
logger.info("Scheduling plan regeneration",
tenant_id=str(tenant_id),
plan_date=str(plan_date))
# Publish regeneration request event
event_data = {
"tenant_id": str(tenant_id),
"plan_date": plan_date.isoformat(),
"trigger": "rejection_auto_regenerate",
"timestamp": datetime.utcnow().isoformat(),
"event_type": "procurement.plan.regeneration_requested"
}
await self.rabbitmq_client.publish_event(
exchange_name="procurement.events",
routing_key="procurement.plan.regeneration_requested",
event_data=event_data
)
except Exception as e:
logger.error("Failed to schedule plan regeneration", error=str(e))
async def _publish_plan_status_changed_event(
self,
tenant_id: uuid.UUID,