import re from datetime import datetime, timedelta from fastapi import HTTPException from app.db.mock_database import SessionMockLocal from app.db.mock_models import User from app.services.orchestration.orchestrator_config import ( CANCEL_ORDER_REQUIRED_FIELDS, ORDER_REQUIRED_FIELDS, PENDING_CANCEL_ORDER_DRAFT_TTL_MINUTES, PENDING_ORDER_DRAFT_TTL_MINUTES, ) from app.services.user.mock_customer_service import hydrate_mock_customer_from_cpf class OrderFlowMixin: def _is_valid_cpf(self, cpf: str) -> bool: digits = re.sub(r"\D", "", cpf or "") if len(digits) != 11: return False if digits == digits[0] * 11: return False numbers = [int(d) for d in digits] sum_first = sum(n * w for n, w in zip(numbers[:9], range(10, 1, -1))) first_digit = 11 - (sum_first % 11) first_digit = 0 if first_digit >= 10 else first_digit if first_digit != numbers[9]: return False sum_second = sum(n * w for n, w in zip(numbers[:10], range(11, 1, -1))) second_digit = 11 - (sum_second % 11) second_digit = 0 if second_digit >= 10 else second_digit return second_digit == numbers[10] def _try_prefill_order_value_from_memory(self, user_id: int | None, payload: dict) -> None: if user_id is None or payload.get("valor_veiculo") is not None: return context = self._get_user_context(user_id) if not context: return memory = context.get("generic_memory", {}) budget = memory.get("orcamento_max") if isinstance(budget, (int, float)) and budget > 0: payload["valor_veiculo"] = float(budget) def _try_prefill_order_cpf_from_memory(self, user_id: int | None, payload: dict) -> None: if user_id is None or payload.get("cpf"): return context = self._get_user_context(user_id) if not context: return memory = context.get("generic_memory", {}) cpf = memory.get("cpf") if isinstance(cpf, str) and self._is_valid_cpf(cpf): payload["cpf"] = cpf def _try_prefill_order_cpf_from_user_profile(self, user_id: int | None, payload: dict) -> None: if user_id is None or payload.get("cpf"): return db = SessionMockLocal() try: user = db.query(User).filter(User.id == user_id).first() if user and isinstance(user.cpf, str) and self._is_valid_cpf(user.cpf): payload["cpf"] = user.cpf finally: db.close() def _render_missing_order_fields_prompt(self, missing_fields: list[str]) -> str: labels = { "cpf": "o CPF do cliente", "valor_veiculo": "o valor do veiculo (R$)", } itens = [f"- {labels[field]}" for field in missing_fields] return "Para realizar o pedido, preciso dos dados abaixo:\n" + "\n".join(itens) def _render_missing_cancel_order_fields_prompt(self, missing_fields: list[str]) -> str: labels = { "numero_pedido": "o numero do pedido (ex.: PED-20260305123456-ABC123)", "motivo": "o motivo do cancelamento", } itens = [f"- {labels[field]}" for field in missing_fields] return "Para cancelar o pedido, preciso dos dados abaixo:\n" + "\n".join(itens) async def _try_collect_and_create_order( self, message: str, user_id: int | None, extracted_fields: dict | None = None, intents: dict | None = None, ) -> str | None: if user_id is None: return None normalized_intents = self._normalize_intents(intents) draft = self.state.get_entry("pending_order_drafts", user_id, expire=True) extracted = self._normalize_order_fields(extracted_fields) has_intent = normalized_intents.get("order_create", False) if ( draft and not has_intent and ( normalized_intents.get("review_schedule", False) or normalized_intents.get("review_list", False) or normalized_intents.get("review_cancel", False) or normalized_intents.get("review_reschedule", False) or normalized_intents.get("order_cancel", False) ) and not extracted ): self.state.pop_entry("pending_order_drafts", user_id) return None if not has_intent and draft is None: return None if draft is None: draft = { "payload": {}, "expires_at": datetime.utcnow() + timedelta(minutes=PENDING_ORDER_DRAFT_TTL_MINUTES), } draft["payload"].update(extracted) self._try_prefill_order_cpf_from_memory(user_id=user_id, payload=draft["payload"]) self._try_prefill_order_cpf_from_user_profile(user_id=user_id, payload=draft["payload"]) self._try_prefill_order_value_from_memory(user_id=user_id, payload=draft["payload"]) cpf_value = draft["payload"].get("cpf") if cpf_value and not self._is_valid_cpf(str(cpf_value)): draft["payload"].pop("cpf", None) self.state.set_entry("pending_order_drafts", user_id, draft) return "Para seguir com o pedido, preciso de um CPF valido com 11 digitos." if cpf_value: try: await hydrate_mock_customer_from_cpf( cpf=str(cpf_value), user_id=user_id, ) except ValueError: draft["payload"].pop("cpf", None) self.state.set_entry("pending_order_drafts", user_id, draft) return "Para seguir com o pedido, preciso de um CPF valido com 11 digitos." valor = draft["payload"].get("valor_veiculo") if valor is not None: try: parsed = float(valor) if parsed <= 0: draft["payload"].pop("valor_veiculo", None) else: draft["payload"]["valor_veiculo"] = round(parsed, 2) except (TypeError, ValueError): draft["payload"].pop("valor_veiculo", None) draft["expires_at"] = datetime.utcnow() + timedelta(minutes=PENDING_ORDER_DRAFT_TTL_MINUTES) self.state.set_entry("pending_order_drafts", user_id, draft) missing = [field for field in ORDER_REQUIRED_FIELDS if field not in draft["payload"]] if missing: return self._render_missing_order_fields_prompt(missing) try: tool_result = await self.registry.execute( "realizar_pedido", draft["payload"], user_id=user_id, ) except HTTPException as exc: return self._http_exception_detail(exc) finally: self.state.pop_entry("pending_order_drafts", user_id) return self._fallback_format_tool_result("realizar_pedido", tool_result) async def _try_collect_and_cancel_order( self, message: str, user_id: int | None, extracted_fields: dict | None = None, intents: dict | None = None, ) -> str | None: if user_id is None: return None normalized_intents = self._normalize_intents(intents) draft = self.state.get_entry("pending_cancel_order_drafts", user_id, expire=True) extracted = self._normalize_cancel_order_fields(extracted_fields) has_intent = normalized_intents.get("order_cancel", False) if ( draft and not has_intent and ( normalized_intents.get("review_schedule", False) or normalized_intents.get("review_list", False) or normalized_intents.get("review_cancel", False) or normalized_intents.get("review_reschedule", False) or normalized_intents.get("order_create", False) ) and not extracted ): self.state.pop_entry("pending_cancel_order_drafts", user_id) return None if not has_intent and draft is None: return None if draft is None: draft = { "payload": {}, "expires_at": datetime.utcnow() + timedelta(minutes=PENDING_CANCEL_ORDER_DRAFT_TTL_MINUTES), } if ( "motivo" not in extracted and draft["payload"].get("numero_pedido") and not has_intent ): free_text = (message or "").strip() if free_text and len(free_text) >= 4: extracted["motivo"] = free_text draft["payload"].update(extracted) draft["expires_at"] = datetime.utcnow() + timedelta(minutes=PENDING_CANCEL_ORDER_DRAFT_TTL_MINUTES) self.state.set_entry("pending_cancel_order_drafts", user_id, draft) missing = [field for field in CANCEL_ORDER_REQUIRED_FIELDS if field not in draft["payload"]] if missing: return self._render_missing_cancel_order_fields_prompt(missing) try: tool_result = await self.registry.execute( "cancelar_pedido", draft["payload"], user_id=user_id, ) except HTTPException as exc: return self._http_exception_detail(exc) finally: self.state.pop_entry("pending_cancel_order_drafts", user_id) return self._fallback_format_tool_result("cancelar_pedido", tool_result)