Improve the frontend

This commit is contained in:
Urtzi Alfaro
2025-10-21 19:50:07 +02:00
parent 05da20357d
commit 8d30172483
105 changed files with 14699 additions and 4630 deletions

View File

@@ -62,7 +62,7 @@ async def get_orders_service(db = Depends(get_db)) -> OrdersService:
# ===== Order CRUD Endpoints =====
@router.post(
route_builder.build_base_route("orders"),
route_builder.build_base_route("").rstrip("/"),
response_model=OrderResponse,
status_code=status.HTTP_201_CREATED
)
@@ -106,7 +106,7 @@ async def create_order(
@router.get(
route_builder.build_base_route("orders"),
route_builder.build_base_route("").rstrip("/"),
response_model=List[OrderResponse]
)
async def get_orders(

View File

@@ -57,6 +57,7 @@ class CustomerSegment(enum.Enum):
class PriorityLevel(enum.Enum):
"""Priority levels for orders and customers"""
URGENT = "urgent"
HIGH = "high"
NORMAL = "normal"
LOW = "low"

View File

@@ -0,0 +1,200 @@
# services/orders/app/services/approval_rules_service.py
"""
Approval Rules Service - Smart auto-approval logic for purchase orders
Evaluates POs against configurable business rules to determine if auto-approval is appropriate
"""
from typing import Dict, List, Any, Optional, Tuple
from decimal import Decimal
from uuid import UUID
import structlog
from shared.config.base import BaseServiceSettings
logger = structlog.get_logger()
class ApprovalRulesService:
"""
Service for evaluating purchase orders against approval rules
Implements smart auto-approval logic based on multiple criteria
"""
def __init__(self, config: BaseServiceSettings):
self.config = config
async def evaluate_po_for_auto_approval(
self,
po_data: Dict[str, Any],
supplier_data: Optional[Dict[str, Any]] = None,
requirements_data: Optional[List[Dict[str, Any]]] = None
) -> Tuple[bool, List[str]]:
"""
Evaluate if a PO should be auto-approved
Returns:
Tuple of (should_auto_approve, reasons)
"""
if not self.config.AUTO_APPROVE_ENABLED:
return False, ["Auto-approval is disabled in configuration"]
reasons = []
should_approve = True
# Rule 1: Amount threshold check
total_amount = self._calculate_po_total(po_data)
if total_amount > self.config.AUTO_APPROVE_THRESHOLD_EUR:
should_approve = False
reasons.append(
f"PO amount €{total_amount:.2f} exceeds threshold €{self.config.AUTO_APPROVE_THRESHOLD_EUR:.2f}"
)
else:
reasons.append(
f"PO amount €{total_amount:.2f} within threshold €{self.config.AUTO_APPROVE_THRESHOLD_EUR:.2f}"
)
# Rule 2: Supplier trust score check
if supplier_data and self.config.AUTO_APPROVE_TRUSTED_SUPPLIERS:
supplier_score = supplier_data.get('trust_score', 0.0)
is_preferred = supplier_data.get('is_preferred_supplier', False)
auto_approve_enabled = supplier_data.get('auto_approve_enabled', False)
if supplier_score < self.config.AUTO_APPROVE_MIN_SUPPLIER_SCORE:
should_approve = False
reasons.append(
f"Supplier trust score {supplier_score:.2f} below minimum {self.config.AUTO_APPROVE_MIN_SUPPLIER_SCORE:.2f}"
)
else:
reasons.append(f"Supplier trust score {supplier_score:.2f} meets minimum requirements")
if not is_preferred:
should_approve = False
reasons.append("Supplier is not marked as preferred")
else:
reasons.append("Supplier is a preferred supplier")
if not auto_approve_enabled:
should_approve = False
reasons.append("Auto-approve is disabled for this supplier")
else:
reasons.append("Auto-approve is enabled for this supplier")
elif supplier_data is None:
should_approve = False
reasons.append("No supplier data available")
# Rule 3: New supplier check
if supplier_data and self.config.REQUIRE_APPROVAL_NEW_SUPPLIERS:
total_pos = supplier_data.get('total_pos_count', 0)
if total_pos < 5:
should_approve = False
reasons.append(f"New supplier with only {total_pos} previous orders (minimum 5 required)")
else:
reasons.append(f"Established supplier with {total_pos} previous orders")
# Rule 4: Critical/urgent items check
if requirements_data and self.config.REQUIRE_APPROVAL_CRITICAL_ITEMS:
critical_count = sum(
1 for req in requirements_data
if req.get('priority') in ['critical', 'urgent', 'CRITICAL', 'URGENT']
)
if critical_count > 0:
should_approve = False
reasons.append(f"Contains {critical_count} critical/urgent items requiring manual review")
else:
reasons.append("No critical/urgent items detected")
# Rule 5: Historical approval rate check
if supplier_data:
total_pos = supplier_data.get('total_pos_count', 0)
approved_pos = supplier_data.get('approved_pos_count', 0)
if total_pos > 0:
approval_rate = approved_pos / total_pos
if approval_rate < 0.95:
should_approve = False
reasons.append(
f"Historical approval rate {approval_rate:.1%} below 95% threshold"
)
else:
reasons.append(f"High historical approval rate {approval_rate:.1%}")
# Rule 6: PO priority check
priority = po_data.get('priority', 'normal')
if priority in ['urgent', 'critical', 'URGENT', 'CRITICAL']:
should_approve = False
reasons.append(f"PO priority is '{priority}' - requires manual review")
logger.info(
"PO auto-approval evaluation completed",
should_auto_approve=should_approve,
total_amount=float(total_amount),
supplier_id=supplier_data.get('id') if supplier_data else None,
reasons_count=len(reasons),
po_data=po_data.get('id') if isinstance(po_data, dict) else str(po_data)
)
return should_approve, reasons
def _calculate_po_total(self, po_data: Dict[str, Any]) -> Decimal:
"""Calculate total PO amount including tax and shipping"""
subtotal = Decimal(str(po_data.get('subtotal', 0)))
tax = Decimal(str(po_data.get('tax_amount', 0)))
shipping = Decimal(str(po_data.get('shipping_cost', 0)))
discount = Decimal(str(po_data.get('discount_amount', 0)))
total = subtotal + tax + shipping - discount
return total
def get_approval_summary(
self,
should_approve: bool,
reasons: List[str]
) -> Dict[str, Any]:
"""
Generate a human-readable approval summary
Returns:
Dict with summary data for UI display
"""
return {
"auto_approved": should_approve,
"decision": "APPROVED" if should_approve else "REQUIRES_MANUAL_APPROVAL",
"reasons": reasons,
"reason_count": len(reasons),
"summary": self._format_summary(should_approve, reasons)
}
def _format_summary(self, should_approve: bool, reasons: List[str]) -> str:
"""Format approval decision summary"""
if should_approve:
return f"Auto-approved: {', '.join(reasons[:2])}"
else:
failing_reasons = [r for r in reasons if any(
keyword in r.lower()
for keyword in ['exceeds', 'below', 'not', 'disabled', 'new', 'critical']
)]
if failing_reasons:
return f"Manual approval required: {failing_reasons[0]}"
return "Manual approval required"
def validate_approval_override(
self,
override_reason: str,
user_role: str
) -> Tuple[bool, Optional[str]]:
"""
Validate if a user can override auto-approval decision
Returns:
Tuple of (is_valid, error_message)
"""
# Only admin/owner can override
if user_role not in ['admin', 'owner']:
return False, "Insufficient permissions to override approval rules"
# Require a reason
if not override_reason or len(override_reason.strip()) < 10:
return False, "Override reason must be at least 10 characters"
return True, None

View File

@@ -0,0 +1,257 @@
# services/orders/app/services/procurement_notification_service.py
"""
Procurement Notification Service - Send alerts and notifications for procurement events
Handles PO approval notifications, reminders, escalations, and summaries
"""
from typing import Dict, List, Any, Optional
from uuid import UUID
from datetime import datetime, timedelta, timezone
import structlog
from shared.config.base import BaseServiceSettings
from shared.alerts.base_service import AlertServiceMixin
logger = structlog.get_logger()
class ProcurementNotificationService(AlertServiceMixin):
"""Service for sending procurement-related notifications and alerts"""
def __init__(self, config: BaseServiceSettings):
self.config = config
async def send_pos_pending_approval_alert(
self,
tenant_id: UUID,
pos_data: List[Dict[str, Any]]
):
"""
Send alert when new POs are created and need approval
Groups POs and sends a summary notification
"""
try:
if not pos_data:
return
# Calculate totals
total_amount = sum(float(po.get('total_amount', 0)) for po in pos_data)
critical_count = sum(1 for po in pos_data if po.get('priority') in ['high', 'critical', 'urgent'])
# Determine severity based on amount and urgency
severity = "medium"
if critical_count > 0 or total_amount > 5000:
severity = "high"
elif total_amount > 10000:
severity = "critical"
alert_data = {
"type": "procurement_pos_pending_approval",
"severity": severity,
"title": f"{len(pos_data)} Pedidos Pendientes de Aprobación",
"message": f"Se han creado {len(pos_data)} pedidos de compra que requieren tu aprobación. Total: €{total_amount:.2f}",
"metadata": {
"tenant_id": str(tenant_id),
"pos_count": len(pos_data),
"total_amount": total_amount,
"critical_count": critical_count,
"pos": [
{
"po_id": po.get("po_id"),
"po_number": po.get("po_number"),
"supplier_id": po.get("supplier_id"),
"total_amount": po.get("total_amount"),
"auto_approved": po.get("auto_approved", False)
}
for po in pos_data
],
"action_required": True,
"action_url": "/app/comprar"
}
}
await self.publish_item(tenant_id, alert_data, item_type='alert')
logger.info("POs pending approval alert sent",
tenant_id=str(tenant_id),
pos_count=len(pos_data),
total_amount=total_amount)
except Exception as e:
logger.error("Error sending POs pending approval alert",
tenant_id=str(tenant_id),
error=str(e))
async def send_approval_reminder(
self,
tenant_id: UUID,
po_data: Dict[str, Any],
hours_pending: int
):
"""
Send reminder for POs that haven't been approved within threshold
"""
try:
alert_data = {
"type": "procurement_approval_reminder",
"severity": "medium" if hours_pending < 36 else "high",
"title": f"Recordatorio: Pedido {po_data.get('po_number')} Pendiente",
"message": f"El pedido {po_data.get('po_number')} lleva {hours_pending} horas sin aprobarse. Total: €{po_data.get('total_amount', 0):.2f}",
"metadata": {
"tenant_id": str(tenant_id),
"po_id": po_data.get("po_id"),
"po_number": po_data.get("po_number"),
"supplier_name": po_data.get("supplier_name"),
"total_amount": po_data.get("total_amount"),
"hours_pending": hours_pending,
"created_at": po_data.get("created_at"),
"action_required": True,
"action_url": f"/app/comprar?po={po_data.get('po_id')}"
}
}
await self.publish_item(tenant_id, alert_data, item_type='alert')
logger.info("Approval reminder sent",
tenant_id=str(tenant_id),
po_id=po_data.get("po_id"),
hours_pending=hours_pending)
except Exception as e:
logger.error("Error sending approval reminder",
tenant_id=str(tenant_id),
po_id=po_data.get("po_id"),
error=str(e))
async def send_critical_po_escalation(
self,
tenant_id: UUID,
po_data: Dict[str, Any],
hours_pending: int
):
"""
Send escalation alert for critical/urgent POs not approved in time
"""
try:
alert_data = {
"type": "procurement_critical_po",
"severity": "critical",
"title": f"🚨 URGENTE: Pedido Crítico {po_data.get('po_number')}",
"message": f"El pedido crítico {po_data.get('po_number')} lleva {hours_pending} horas sin aprobar. Se requiere acción inmediata.",
"metadata": {
"tenant_id": str(tenant_id),
"po_id": po_data.get("po_id"),
"po_number": po_data.get("po_number"),
"supplier_name": po_data.get("supplier_name"),
"total_amount": po_data.get("total_amount"),
"priority": po_data.get("priority"),
"required_delivery_date": po_data.get("required_delivery_date"),
"hours_pending": hours_pending,
"escalated": True,
"action_required": True,
"action_url": f"/app/comprar?po={po_data.get('po_id')}"
}
}
await self.publish_item(tenant_id, alert_data, item_type='alert')
logger.warning("Critical PO escalation sent",
tenant_id=str(tenant_id),
po_id=po_data.get("po_id"),
hours_pending=hours_pending)
except Exception as e:
logger.error("Error sending critical PO escalation",
tenant_id=str(tenant_id),
po_id=po_data.get("po_id"),
error=str(e))
async def send_auto_approval_summary(
self,
tenant_id: UUID,
summary_data: Dict[str, Any]
):
"""
Send daily summary of auto-approved POs
"""
try:
auto_approved_count = summary_data.get("auto_approved_count", 0)
total_amount = summary_data.get("total_auto_approved_amount", 0)
manual_approval_count = summary_data.get("manual_approval_count", 0)
if auto_approved_count == 0 and manual_approval_count == 0:
# No activity, skip notification
return
alert_data = {
"type": "procurement_auto_approval_summary",
"severity": "low",
"title": "Resumen Diario de Pedidos",
"message": f"Hoy se aprobaron automáticamente {auto_approved_count} pedidos (€{total_amount:.2f}). {manual_approval_count} requieren aprobación manual.",
"metadata": {
"tenant_id": str(tenant_id),
"auto_approved_count": auto_approved_count,
"total_auto_approved_amount": total_amount,
"manual_approval_count": manual_approval_count,
"summary_date": summary_data.get("date"),
"auto_approved_pos": summary_data.get("auto_approved_pos", []),
"pending_approval_pos": summary_data.get("pending_approval_pos", []),
"action_url": "/app/comprar"
}
}
await self.publish_item(tenant_id, alert_data, item_type='notification')
logger.info("Auto-approval summary sent",
tenant_id=str(tenant_id),
auto_approved_count=auto_approved_count,
manual_count=manual_approval_count)
except Exception as e:
logger.error("Error sending auto-approval summary",
tenant_id=str(tenant_id),
error=str(e))
async def send_po_approved_confirmation(
self,
tenant_id: UUID,
po_data: Dict[str, Any],
approved_by: str,
auto_approved: bool = False
):
"""
Send confirmation when a PO is approved
"""
try:
approval_type = "automáticamente" if auto_approved else f"por {approved_by}"
alert_data = {
"type": "procurement_po_approved",
"severity": "low",
"title": f"Pedido {po_data.get('po_number')} Aprobado",
"message": f"El pedido {po_data.get('po_number')} ha sido aprobado {approval_type}. Total: €{po_data.get('total_amount', 0):.2f}",
"metadata": {
"tenant_id": str(tenant_id),
"po_id": po_data.get("po_id"),
"po_number": po_data.get("po_number"),
"supplier_name": po_data.get("supplier_name"),
"total_amount": po_data.get("total_amount"),
"approved_by": approved_by,
"auto_approved": auto_approved,
"approved_at": datetime.now(timezone.utc).isoformat(),
"action_url": f"/app/comprar?po={po_data.get('po_id')}"
}
}
await self.publish_item(tenant_id, alert_data, item_type='notification')
logger.info("PO approved confirmation sent",
tenant_id=str(tenant_id),
po_id=po_data.get("po_id"),
auto_approved=auto_approved)
except Exception as e:
logger.error("Error sending PO approved confirmation",
tenant_id=str(tenant_id),
po_id=po_data.get("po_id"),
error=str(e))

View File

@@ -297,16 +297,24 @@ class ProcurementSchedulerService(BaseAlertService, AlertServiceMixin):
result = await procurement_service.generate_procurement_plan(tenant_id, request)
if result.success and result.plan:
# Send notification about new plan
await self.send_procurement_notification(
tenant_id, result.plan, "plan_created"
)
logger.info("🎉 Procurement plan created successfully",
tenant_id=str(tenant_id),
plan_id=str(result.plan.id),
plan_date=str(planning_date),
total_requirements=result.plan.total_requirements)
# Auto-create POs from the plan (NEW FEATURE)
if self.config.AUTO_CREATE_POS_FROM_PLAN:
await self._auto_create_purchase_orders_from_plan(
procurement_service,
tenant_id,
result.plan.id
)
# Send notification about new plan
await self.send_procurement_notification(
tenant_id, result.plan, "plan_created"
)
else:
logger.warning("⚠️ Failed to generate procurement plan",
tenant_id=str(tenant_id),
@@ -400,6 +408,70 @@ class ProcurementSchedulerService(BaseAlertService, AlertServiceMixin):
notification_type=notification_type,
error=str(e))
async def _auto_create_purchase_orders_from_plan(
self,
procurement_service,
tenant_id: UUID,
plan_id: UUID
):
"""
Automatically create purchase orders from procurement plan
Integrates with auto-approval rules
"""
try:
logger.info("🛒 Auto-creating purchase orders from plan",
tenant_id=str(tenant_id),
plan_id=str(plan_id))
# Create POs with auto-approval evaluation enabled
po_result = await procurement_service.create_purchase_orders_from_plan(
tenant_id=tenant_id,
plan_id=plan_id,
auto_approve=True # Enable auto-approval evaluation
)
if po_result.get("success"):
total_created = po_result.get("total_created", 0)
auto_approved = po_result.get("total_auto_approved", 0)
pending_approval = po_result.get("total_pending_approval", 0)
logger.info("✅ Purchase orders created from plan",
tenant_id=str(tenant_id),
plan_id=str(plan_id),
total_created=total_created,
auto_approved=auto_approved,
pending_approval=pending_approval)
# Send notifications
from app.services.procurement_notification_service import ProcurementNotificationService
notification_service = ProcurementNotificationService(self.config)
# Notify about pending approvals
if pending_approval > 0:
await notification_service.send_pos_pending_approval_alert(
tenant_id=tenant_id,
pos_data=po_result.get("pending_approval_pos", [])
)
# Log auto-approved POs for summary
if auto_approved > 0:
logger.info("🤖 Auto-approved POs",
tenant_id=str(tenant_id),
count=auto_approved,
pos=po_result.get("auto_approved_pos", []))
else:
logger.error("❌ Failed to create purchase orders from plan",
tenant_id=str(tenant_id),
plan_id=str(plan_id),
error=po_result.get("error"))
except Exception as e:
logger.error("💥 Error auto-creating purchase orders",
tenant_id=str(tenant_id),
plan_id=str(plan_id),
error=str(e))
async def test_procurement_generation(self):
"""Test method to manually trigger procurement planning"""
# Get the first available tenant for testing

View File

@@ -482,8 +482,9 @@ class ProcurementService:
auto_approve: bool = False
) -> Dict[str, Any]:
"""
Create purchase orders from procurement plan requirements (Feature #1)
Create purchase orders from procurement plan requirements with smart auto-approval
Groups requirements by supplier and creates POs automatically
Evaluates auto-approval rules and approves qualifying POs
"""
try:
plan = await self.plan_repo.get_plan_by_id(plan_id, tenant_id)
@@ -493,6 +494,10 @@ class ProcurementService:
if not plan.requirements:
return {"success": False, "error": "No requirements in plan"}
# Import approval rules service
from app.services.approval_rules_service import ApprovalRulesService
approval_service = ApprovalRulesService(self.config)
# Group requirements by supplier
supplier_requirements = {}
for req in plan.requirements:
@@ -507,6 +512,8 @@ class ProcurementService:
# Create PO for each supplier
created_pos = []
failed_pos = []
auto_approved_pos = []
pending_approval_pos = []
for supplier_id, requirements in supplier_requirements.items():
if supplier_id == 'no_supplier':
@@ -530,7 +537,19 @@ class ProcurementService:
for item in po_items
)
# Create PO via suppliers service
# Get supplier data for approval evaluation
supplier_data = await self._get_supplier_performance_data(tenant_id, supplier_id)
# Prepare requirements data for approval evaluation
requirements_data = [
{
"priority": req.priority,
"risk_level": req.risk_level
}
for req in requirements
]
# Create PO data structure
po_data = {
"supplier_id": supplier_id,
"items": po_items,
@@ -539,9 +558,26 @@ class ProcurementService:
"notes": f"Auto-generated from procurement plan {plan.plan_number}",
"tax_amount": subtotal * 0.1, # 10% tax estimation
"shipping_cost": 0,
"discount_amount": 0
"discount_amount": 0,
"subtotal": subtotal
}
# Evaluate auto-approval rules
should_auto_approve = False
approval_reasons = []
if auto_approve and self.config.AUTO_CREATE_POS_FROM_PLAN:
should_auto_approve, approval_reasons = await approval_service.evaluate_po_for_auto_approval(
po_data=po_data,
supplier_data=supplier_data,
requirements_data=requirements_data
)
# Add approval metadata to PO
po_data["auto_approval_evaluated"] = True
po_data["auto_approval_decision"] = "APPROVED" if should_auto_approve else "REQUIRES_MANUAL_APPROVAL"
po_data["auto_approval_reasons"] = approval_reasons
# Call suppliers service to create PO
po_response = await self.suppliers_client.create_purchase_order(
str(tenant_id),
@@ -563,15 +599,48 @@ class ProcurementService:
expected_delivery_date=req.required_by_date
)
# Auto-approve PO if rules pass
if should_auto_approve:
await self._auto_approve_purchase_order(
tenant_id=tenant_id,
po_id=po_id,
approval_reasons=approval_reasons
)
auto_approved_pos.append({
"po_id": po_id,
"po_number": po_number,
"supplier_id": supplier_id,
"items_count": len(requirements),
"total_amount": subtotal,
"auto_approved": True,
"approval_reasons": approval_reasons
})
logger.info("PO auto-approved", po_id=po_id, supplier_id=supplier_id)
else:
pending_approval_pos.append({
"po_id": po_id,
"po_number": po_number,
"supplier_id": supplier_id,
"items_count": len(requirements),
"total_amount": subtotal,
"auto_approved": False,
"requires_manual_approval": True,
"approval_reasons": approval_reasons
})
created_pos.append({
"po_id": po_id,
"po_number": po_number,
"supplier_id": supplier_id,
"items_count": len(requirements),
"total_amount": subtotal
"total_amount": subtotal,
"auto_approved": should_auto_approve
})
logger.info("PO created from plan", po_id=po_id, supplier_id=supplier_id)
logger.info("PO created from plan",
po_id=po_id,
supplier_id=supplier_id,
auto_approved=should_auto_approve)
else:
failed_pos.append({
"supplier_id": supplier_id,
@@ -589,11 +658,21 @@ class ProcurementService:
await self.db.commit()
logger.info("PO creation from plan completed",
total_created=len(created_pos),
auto_approved=len(auto_approved_pos),
pending_approval=len(pending_approval_pos),
failed=len(failed_pos))
return {
"success": True,
"created_pos": created_pos,
"failed_pos": failed_pos,
"auto_approved_pos": auto_approved_pos,
"pending_approval_pos": pending_approval_pos,
"total_created": len(created_pos),
"total_auto_approved": len(auto_approved_pos),
"total_pending_approval": len(pending_approval_pos),
"total_failed": len(failed_pos)
}
@@ -1612,3 +1691,75 @@ class ProcurementService:
"plan_completion_rate": 0.0,
"supplier_performance": 0.0
}
async def _get_supplier_performance_data(self, tenant_id: uuid.UUID, supplier_id: str) -> Optional[Dict[str, Any]]:
"""
Get supplier performance data from suppliers service
Used for auto-approval evaluation
"""
try:
# Call suppliers service to get supplier with performance metrics
supplier_data = await self.suppliers_client.get_supplier(str(tenant_id), supplier_id)
if not supplier_data:
logger.warning("Supplier not found", supplier_id=supplier_id)
return None
# Extract relevant performance fields
return {
"id": supplier_data.get("id"),
"name": supplier_data.get("name"),
"trust_score": supplier_data.get("trust_score", 0.0),
"is_preferred_supplier": supplier_data.get("is_preferred_supplier", False),
"auto_approve_enabled": supplier_data.get("auto_approve_enabled", False),
"total_pos_count": supplier_data.get("total_pos_count", 0),
"approved_pos_count": supplier_data.get("approved_pos_count", 0),
"on_time_delivery_rate": supplier_data.get("on_time_delivery_rate", 0.0),
"fulfillment_rate": supplier_data.get("fulfillment_rate", 0.0),
"quality_rating": supplier_data.get("quality_rating", 0.0),
"delivery_rating": supplier_data.get("delivery_rating", 0.0),
"status": supplier_data.get("status")
}
except Exception as e:
logger.error("Error getting supplier performance data",
supplier_id=supplier_id,
error=str(e))
return None
async def _auto_approve_purchase_order(
self,
tenant_id: uuid.UUID,
po_id: str,
approval_reasons: List[str]
):
"""
Auto-approve a purchase order via suppliers service
Updates PO status to 'approved' with system approval
"""
try:
# Call suppliers service to approve the PO
approval_data = {
"approved_by": "system", # System auto-approval
"approval_notes": f"Auto-approved: {', '.join(approval_reasons[:2])}",
"auto_approved": True,
"approval_reasons": approval_reasons
}
await self.suppliers_client.approve_purchase_order(
str(tenant_id),
po_id,
approval_data
)
logger.info("PO auto-approved successfully",
po_id=po_id,
tenant_id=str(tenant_id),
reasons_count=len(approval_reasons))
except Exception as e:
logger.error("Error auto-approving PO",
po_id=po_id,
tenant_id=str(tenant_id),
error=str(e))
raise