feat(review-api): adicionar fluxos de listar, cancelar e remarcar revisao

main
parent f818258fec
commit f09081150f

@ -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,

@ -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

@ -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": (

@ -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()

@ -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,
}

Loading…
Cancel
Save