diff --git a/app/api/routes/mock.py b/app/api/routes/mock.py index 675b240..13774a1 100644 --- a/app/api/routes/mock.py +++ b/app/api/routes/mock.py @@ -7,15 +7,21 @@ from app.api.routes.dependencies import db_error_detail from app.api.schemas import ( AgendarRevisaoRequest, AvaliarVeiculoTrocaRequest, + CancelarAgendamentoRevisaoRequest, CancelarPedidoRequest, ConsultarEstoqueRequest, + EditarDataRevisaoRequest, + ListarAgendamentosRevisaoRequest, RealizarPedidoRequest, ValidarClienteVendaRequest, ) from app.services.handlers import ( agendar_revisao, avaliar_veiculo_troca, + cancelar_agendamento_revisao, cancelar_pedido, + editar_data_revisao, + listar_agendamentos_revisao, consultar_estoque, realizar_pedido, validar_cliente_venda, @@ -85,6 +91,52 @@ async def agendar_revisao_endpoint( raise HTTPException(status_code=503, detail=db_error_detail(exc)) +@router.post("/listar-agendamentos-revisao") +async def listar_agendamentos_revisao_endpoint( + body: ListarAgendamentosRevisaoRequest, +) -> List[Dict[str, Any]]: + """Lista os agendamentos de revisao do usuario autenticado.""" + try: + return await listar_agendamentos_revisao( + user_id=body.user_id, + placa=body.placa, + status=body.status, + limite=body.limite, + ) + except SQLAlchemyError as exc: + raise HTTPException(status_code=503, detail=db_error_detail(exc)) + + +@router.post("/cancelar-agendamento-revisao") +async def cancelar_agendamento_revisao_endpoint( + body: CancelarAgendamentoRevisaoRequest, +) -> Dict[str, Any]: + """Cancela agendamento de revisao usando o protocolo.""" + try: + return await cancelar_agendamento_revisao( + protocolo=body.protocolo, + motivo=body.motivo, + user_id=body.user_id, + ) + except SQLAlchemyError as exc: + raise HTTPException(status_code=503, detail=db_error_detail(exc)) + + +@router.post("/editar-data-revisao") +async def editar_data_revisao_endpoint( + body: EditarDataRevisaoRequest, +) -> Dict[str, Any]: + """Edita data e hora de um agendamento de revisao existente.""" + try: + return await editar_data_revisao( + protocolo=body.protocolo, + nova_data_hora=body.nova_data_hora, + user_id=body.user_id, + ) + except SQLAlchemyError as exc: + raise HTTPException(status_code=503, detail=db_error_detail(exc)) + + @router.post("/cancelar-pedido") async def cancelar_pedido_endpoint( body: CancelarPedidoRequest, diff --git a/app/api/schemas.py b/app/api/schemas.py index 1a667b8..71c27df 100644 --- a/app/api/schemas.py +++ b/app/api/schemas.py @@ -55,6 +55,25 @@ class AgendarRevisaoRequest(BaseModel): user_id: Optional[int] = None +class ListarAgendamentosRevisaoRequest(BaseModel): + user_id: Optional[int] = None + placa: Optional[str] = None + status: Optional[str] = None + limite: Optional[int] = 20 + + +class CancelarAgendamentoRevisaoRequest(BaseModel): + protocolo: str + motivo: Optional[str] = None + user_id: Optional[int] = None + + +class EditarDataRevisaoRequest(BaseModel): + protocolo: str + nova_data_hora: str + user_id: Optional[int] = None + + class CancelarPedidoRequest(BaseModel): numero_pedido: str motivo: str diff --git a/app/db/tool_seed.py b/app/db/tool_seed.py index e1ba772..f48fae7 100644 --- a/app/db/tool_seed.py +++ b/app/db/tool_seed.py @@ -139,6 +139,80 @@ def get_tools_definitions(): ], }, }, + { + "name": "listar_agendamentos_revisao", + "description": ( + "Use esta ferramenta quando o cliente quiser ver os agendamentos de " + "revisao que ele possui. Permite filtrar por placa, status e quantidade " + "maxima de itens retornados." + ), + "parameters": { + "type": "object", + "properties": { + "placa": { + "type": "string", + "description": "Placa do veiculo para filtrar os agendamentos. Opcional.", + }, + "status": { + "type": "string", + "description": "Status para filtrar (por exemplo: agendado, cancelado). Opcional.", + }, + "limite": { + "type": "integer", + "description": "Quantidade maxima de agendamentos retornados. Opcional.", + }, + }, + "required": [], + }, + }, + { + "name": "cancelar_agendamento_revisao", + "description": ( + "Use esta ferramenta quando o cliente quiser cancelar um agendamento de " + "revisao existente. Ela recebe o protocolo do agendamento e opcionalmente " + "um motivo informado pelo cliente." + ), + "parameters": { + "type": "object", + "properties": { + "protocolo": { + "type": "string", + "description": "Protocolo do agendamento de revisao que sera cancelado.", + }, + "motivo": { + "type": "string", + "description": "Motivo do cancelamento informado pelo cliente. Opcional.", + }, + }, + "required": ["protocolo"], + }, + }, + { + "name": "editar_data_revisao", + "description": ( + "Use esta ferramenta quando o cliente quiser remarcar a data/hora de um " + "agendamento de revisao. Se o horario estiver ocupado, retorna sugestao " + "do proximo horario disponivel." + ), + "parameters": { + "type": "object", + "properties": { + "protocolo": { + "type": "string", + "description": "Protocolo do agendamento de revisao que sera remarcado.", + }, + "nova_data_hora": { + "type": "string", + "description": ( + "Nova data e hora desejada para a revisao. Aceita formatos como " + "'2026-03-10T09:00:00-03:00', '2026-03-10 09:00', " + "'10/03/2026 09:00' e '10/03/2026 as 09:00'." + ), + }, + }, + "required": ["protocolo", "nova_data_hora"], + }, + }, { "name": "realizar_pedido", "description": ( diff --git a/app/services/handlers.py b/app/services/handlers.py index 1fb1fb8..aa6c325 100644 --- a/app/services/handlers.py +++ b/app/services/handlers.py @@ -10,7 +10,7 @@ from sqlalchemy import func from app.db.mock_database import SessionMockLocal from app.db.mock_models import Customer, Order, ReviewSchedule, Vehicle -# Nesse arquivo eu faço a limpeza dos dados para persisti-los no DB +# Nesse arquivo eu faço a normalização dos dados para persisti-los no DB def normalize_cpf(value: str) -> str: @@ -385,6 +385,176 @@ async def agendar_revisao( db.close() +async def listar_agendamentos_revisao( + user_id: Optional[int] = None, + placa: Optional[str] = None, + status: Optional[str] = None, + limite: Optional[int] = 20, +) -> List[Dict[str, Any]]: + """Lista agendamentos de revisao do usuario autenticado com filtros opcionais.""" + if user_id is None: + raise HTTPException(status_code=400, detail="Informe user_id para listar seus agendamentos de revisao.") + + placa_normalizada = placa.upper().strip() if placa else None + status_normalizado = status.lower().strip() if status else None + + try: + limite_int = int(limite) if limite is not None else 20 + except (TypeError, ValueError): + limite_int = 20 + limite_int = max(1, min(limite_int, 100)) + + db = SessionMockLocal() + try: + query = db.query(ReviewSchedule).filter(ReviewSchedule.user_id == user_id) + if placa_normalizada: + query = query.filter(ReviewSchedule.placa == placa_normalizada) + if status_normalizado: + query = query.filter(func.lower(ReviewSchedule.status) == status_normalizado) + + agendamentos = ( + query.order_by(ReviewSchedule.data_hora.asc()) + .limit(limite_int) + .all() + ) + + return [ + { + "protocolo": row.protocolo, + "user_id": row.user_id, + "placa": row.placa, + "data_hora": row.data_hora.isoformat(), + "status": row.status, + "created_at": row.created_at.isoformat() if row.created_at else None, + } + for row in agendamentos + ] + finally: + db.close() + + +async def cancelar_agendamento_revisao( + protocolo: str, + motivo: Optional[str] = None, + user_id: Optional[int] = None, +) -> Dict[str, Any]: + """Cancela um agendamento de revisao existente pelo protocolo.""" + if user_id is None: + raise HTTPException(status_code=400, detail="Informe user_id para cancelar seu agendamento de revisao.") + + db = SessionMockLocal() + try: + agendamento = ( + db.query(ReviewSchedule) + .filter(ReviewSchedule.protocolo == protocolo) + .filter(ReviewSchedule.user_id == user_id) + .first() + ) + if not agendamento: + raise HTTPException(status_code=404, detail="Agendamento de revisao nao encontrado para este usuario.") + + if agendamento.status.lower() == "cancelado": + return { + "protocolo": agendamento.protocolo, + "user_id": agendamento.user_id, + "placa": agendamento.placa, + "data_hora": agendamento.data_hora.isoformat(), + "status": agendamento.status, + "motivo": motivo, + } + + agendamento.status = "cancelado" + db.commit() + db.refresh(agendamento) + + return { + "protocolo": agendamento.protocolo, + "user_id": agendamento.user_id, + "placa": agendamento.placa, + "data_hora": agendamento.data_hora.isoformat(), + "status": agendamento.status, + "motivo": motivo, + } + finally: + db.close() + + +async def editar_data_revisao( + protocolo: str, + nova_data_hora: str, + user_id: Optional[int] = None, +) -> Dict[str, Any]: + """Edita a data/hora de um agendamento de revisao existente.""" + if user_id is None: + raise HTTPException(status_code=400, detail="Informe user_id para editar seu agendamento de revisao.") + + try: + nova_data = _normalize_review_slot(_parse_data_hora_revisao(nova_data_hora)) + except ValueError: + raise HTTPException( + status_code=400, + detail=( + "nova_data_hora invalida. Exemplos aceitos: " + "2026-03-10T09:00:00-03:00, 2026-03-10 09:00, 10/03/2026 09:00, " + "10/03/2026 as 09:00." + ), + ) + + db = SessionMockLocal() + try: + agendamento = ( + db.query(ReviewSchedule) + .filter(ReviewSchedule.protocolo == protocolo) + .filter(ReviewSchedule.user_id == user_id) + .first() + ) + if not agendamento: + raise HTTPException(status_code=404, detail="Agendamento de revisao nao encontrado para este usuario.") + + if agendamento.status.lower() == "cancelado": + raise HTTPException(status_code=400, detail="Nao e possivel editar um agendamento ja cancelado.") + + conflito = ( + db.query(ReviewSchedule) + .filter(ReviewSchedule.id != agendamento.id) + .filter(ReviewSchedule.data_hora == nova_data) + .filter(func.lower(ReviewSchedule.status) != "cancelado") + .first() + ) + if conflito: + proximo_horario = _find_next_available_review_slot(db=db, requested_dt=nova_data) + if proximo_horario: + raise HTTPException( + status_code=409, + detail=( + f"O horario {_format_datetime_pt_br(nova_data)} ja esta ocupado. " + f"Sugestao: {_format_datetime_pt_br(proximo_horario)} " + f"(ISO: {proximo_horario.isoformat()})." + ), + ) + raise HTTPException( + status_code=409, + detail=( + f"O horario {_format_datetime_pt_br(nova_data)} ja esta ocupado e nao encontrei " + "disponibilidade nas proximas 8 horas." + ), + ) + + agendamento.data_hora = nova_data + db.commit() + db.refresh(agendamento) + + return { + "protocolo": agendamento.protocolo, + "user_id": agendamento.user_id, + "placa": agendamento.placa, + "data_hora": agendamento.data_hora.isoformat(), + "status": agendamento.status, + } + finally: + db.close() + + async def cancelar_pedido(numero_pedido: str, motivo: str, user_id: Optional[int] = None) -> Dict[str, Any]: """Cancela pedido existente e registra motivo e data de cancelamento.""" db = SessionMockLocal() diff --git a/app/services/tool_registry.py b/app/services/tool_registry.py index da40367..bdae591 100644 --- a/app/services/tool_registry.py +++ b/app/services/tool_registry.py @@ -8,7 +8,10 @@ from app.repositories.tool_repository import ToolRepository from app.services.handlers import ( agendar_revisao, avaliar_veiculo_troca, + cancelar_agendamento_revisao, cancelar_pedido, + editar_data_revisao, + listar_agendamentos_revisao, consultar_estoque, realizar_pedido, validar_cliente_venda, @@ -20,6 +23,9 @@ HANDLERS: Dict[str, Callable] = { "validar_cliente_venda": validar_cliente_venda, "avaliar_veiculo_troca": avaliar_veiculo_troca, "agendar_revisao": agendar_revisao, + "listar_agendamentos_revisao": listar_agendamentos_revisao, + "cancelar_agendamento_revisao": cancelar_agendamento_revisao, + "editar_data_revisao": editar_data_revisao, "cancelar_pedido": cancelar_pedido, "realizar_pedido": realizar_pedido, }