""" Message generator for creating i18n message keys and parameters. Converts minimal event metadata into structured i18n format for frontend translation. """ from typing import Dict, Any from datetime import datetime from app.utils.message_templates import ALERT_TEMPLATES, NOTIFICATION_TEMPLATES, RECOMMENDATION_TEMPLATES import structlog logger = structlog.get_logger() class MessageGenerator: """Generates i18n message keys and parameters from event metadata""" def generate_message(self, event_type: str, metadata: Dict[str, Any], event_class: str = "alert") -> dict: """ Generate i18n structure for frontend. Args: event_type: Alert/notification/recommendation type metadata: Event metadata dictionary event_class: One of: alert, notification, recommendation Returns: Dictionary with title_key, title_params, message_key, message_params """ # Select appropriate template collection if event_class == "notification": templates = NOTIFICATION_TEMPLATES elif event_class == "recommendation": templates = RECOMMENDATION_TEMPLATES else: templates = ALERT_TEMPLATES template = templates.get(event_type) if not template: logger.warning("no_template_found", event_type=event_type, event_class=event_class) return self._generate_fallback(event_type, metadata) # Build parameters from metadata title_params = self._build_params(template["title_params"], metadata) message_params = self._build_params(template["message_params"], metadata) # Select message variant based on context message_key = self._select_message_variant( template["message_variants"], metadata ) return { "title_key": template["title_key"], "title_params": title_params, "message_key": message_key, "message_params": message_params } def _generate_fallback(self, event_type: str, metadata: Dict[str, Any]) -> dict: """Generate fallback message structure when template not found""" return { "title_key": "alerts.generic.title", "title_params": {}, "message_key": "alerts.generic.message", "message_params": { "event_type": event_type, "metadata_summary": self._summarize_metadata(metadata) } } def _summarize_metadata(self, metadata: Dict[str, Any]) -> str: """Create human-readable summary of metadata""" # Take first 3 fields items = list(metadata.items())[:3] summary_parts = [f"{k}: {v}" for k, v in items] return ", ".join(summary_parts) def _build_params(self, param_mapping: dict, metadata: dict) -> dict: """ Extract and transform parameters from metadata. param_mapping format: {"display_param_name": "metadata_key"} """ params = {} for param_key, metadata_key in param_mapping.items(): if metadata_key in metadata: value = metadata[metadata_key] # Apply transformations based on parameter suffix if param_key.endswith("_kg"): value = round(float(value), 1) elif param_key.endswith("_eur"): value = round(float(value), 2) elif param_key.endswith("_percentage"): value = round(float(value), 1) elif param_key.endswith("_date"): value = self._format_date(value) elif param_key.endswith("_day_name"): value = self._format_day_name(value) elif param_key.endswith("_datetime"): value = self._format_datetime(value) params[param_key] = value return params def _select_message_variant(self, variants: dict, metadata: dict) -> str: """ Select appropriate message variant based on metadata context. Checks for specific conditions in priority order. """ # Check for PO-related variants if "po_id" in metadata: if metadata.get("po_status") == "pending_approval": variant = variants.get("with_po_pending") if variant: return variant else: variant = variants.get("with_po_created") if variant: return variant # Check for time-based variants if "hours_until" in metadata: variant = variants.get("with_hours") if variant: return variant if "production_date" in metadata or "planned_date" in metadata: variant = variants.get("with_date") if variant: return variant # Check for customer-related variants if "customer_names" in metadata and metadata.get("customer_names"): variant = variants.get("with_customers") if variant: return variant # Check for order-related variants if "affected_orders" in metadata and metadata.get("affected_orders", 0) > 0: variant = variants.get("with_orders") if variant: return variant # Check for supplier contact variants if "supplier_contact" in metadata: variant = variants.get("with_supplier") if variant: return variant # Check for batch-related variants if "affected_batches" in metadata and metadata.get("affected_batches", 0) > 0: variant = variants.get("with_batches") if variant: return variant # Check for product names list variants if "product_names" in metadata and metadata.get("product_names"): variant = variants.get("with_names") if variant: return variant # Check for time duration variants if "hours_overdue" in metadata: variant = variants.get("with_hours") if variant: return variant if "days_overdue" in metadata: variant = variants.get("with_days") if variant: return variant # Default to generic variant return variants.get("generic", variants[list(variants.keys())[0]]) def _format_date(self, date_value: Any) -> str: """ Format date for display. Accepts: - ISO string: "2025-12-10" - datetime object - date object Returns: ISO format "YYYY-MM-DD" """ if isinstance(date_value, str): # Already a string, might be ISO format try: dt = datetime.fromisoformat(date_value.replace('Z', '+00:00')) return dt.date().isoformat() except: return date_value if isinstance(date_value, datetime): return date_value.date().isoformat() if hasattr(date_value, 'isoformat'): return date_value.isoformat() return str(date_value) def _format_day_name(self, date_value: Any) -> str: """ Format day name with date. Example: "miƩrcoles 10 de diciembre" Note: Frontend will handle localization. For now, return ISO date and let frontend format. """ iso_date = self._format_date(date_value) try: dt = datetime.fromisoformat(iso_date) # Frontend will use this to format in user's language return iso_date except: return iso_date def _format_datetime(self, datetime_value: Any) -> str: """ Format datetime for display. Returns: ISO 8601 format with timezone """ if isinstance(datetime_value, str): return datetime_value if isinstance(datetime_value, datetime): return datetime_value.isoformat() if hasattr(datetime_value, 'isoformat'): return datetime_value.isoformat() return str(datetime_value)