You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
365 lines
12 KiB
Python
365 lines
12 KiB
Python
from app.core.time_utils import utc_now
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from sqlalchemy import or_, text
|
|
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
|
|
|
from app.db.mock_database import SessionMockLocal
|
|
from app.db.mock_models import Order, User, Vehicle
|
|
from app.services.domain.common import is_legacy_schema_issue
|
|
from app.services.domain.credit_service import validar_cliente_venda
|
|
from app.services.domain.tool_errors import raise_tool_http_error
|
|
from app.services.integrations.events import ORDER_CANCELLED_EVENT, ORDER_CREATED_EVENT
|
|
from app.services.integrations.service import publish_business_event_safely
|
|
from app.services.orchestration.technical_normalizer import normalize_cpf
|
|
from app.services.user.mock_customer_service import hydrate_mock_customer_from_cpf
|
|
|
|
# Responsabilidade: regra de pedido.
|
|
|
|
|
|
def _get_vehicle_for_update(db, vehicle_id: int) -> Vehicle | None:
|
|
return (
|
|
db.query(Vehicle)
|
|
.filter(Vehicle.id == vehicle_id)
|
|
.with_for_update()
|
|
.first()
|
|
)
|
|
|
|
|
|
def _get_active_order_for_vehicle(db, vehicle_id: int) -> Order | None:
|
|
return (
|
|
db.query(Order)
|
|
.filter(Order.vehicle_id == vehicle_id)
|
|
.filter(Order.status != "Cancelado")
|
|
.first()
|
|
)
|
|
|
|
|
|
def _acquire_vehicle_reservation_lock(db, vehicle_id: int, timeout_seconds: int = 5) -> str | None:
|
|
lock_name = f"orquestrador:vehicle_reservation:{vehicle_id}"
|
|
try:
|
|
acquired = db.execute(
|
|
text("SELECT GET_LOCK(:lock_name, :timeout_seconds)"),
|
|
{"lock_name": lock_name, "timeout_seconds": timeout_seconds},
|
|
).scalar()
|
|
except (OperationalError, SQLAlchemyError):
|
|
return None
|
|
|
|
if int(acquired or 0) != 1:
|
|
raise_tool_http_error(
|
|
status_code=409,
|
|
code="vehicle_reservation_busy",
|
|
message="Outro atendimento esta finalizando a reserva deste veiculo. Tente novamente.",
|
|
retryable=True,
|
|
field="vehicle_id",
|
|
)
|
|
return lock_name
|
|
|
|
|
|
def _release_vehicle_reservation_lock(db, lock_name: str | None) -> None:
|
|
if not lock_name:
|
|
return
|
|
try:
|
|
db.execute(
|
|
text("SELECT RELEASE_LOCK(:lock_name)"),
|
|
{"lock_name": lock_name},
|
|
)
|
|
except (OperationalError, SQLAlchemyError):
|
|
pass
|
|
|
|
|
|
async def listar_pedidos(
|
|
user_id: int | None = None,
|
|
cpf: str | None = None,
|
|
status: str | None = None,
|
|
limite: int = 20,
|
|
) -> list[dict[str, Any]]:
|
|
cpf_norm = normalize_cpf(cpf) if cpf else None
|
|
db = SessionMockLocal()
|
|
try:
|
|
user = None
|
|
if user_id is not None:
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if user and not cpf_norm:
|
|
cpf_norm = normalize_cpf(user.cpf)
|
|
|
|
query = db.query(Order)
|
|
if user_id is not None and cpf_norm:
|
|
query = query.filter(
|
|
or_(
|
|
Order.user_id == user_id,
|
|
(Order.user_id.is_(None) & (Order.cpf == cpf_norm)),
|
|
)
|
|
)
|
|
elif user_id is not None:
|
|
query = query.filter(Order.user_id == user_id)
|
|
elif cpf_norm:
|
|
query = query.filter(Order.cpf == cpf_norm)
|
|
else:
|
|
raise_tool_http_error(
|
|
status_code=400,
|
|
code="order_list_missing_identity",
|
|
message="Preciso identificar o cliente para listar os pedidos.",
|
|
retryable=True,
|
|
field="cpf",
|
|
)
|
|
|
|
normalized_status = str(status or "").strip()
|
|
if normalized_status:
|
|
query = query.filter(Order.status == normalized_status)
|
|
|
|
safe_limit = max(1, min(int(limite or 20), 50))
|
|
pedidos = query.order_by(Order.created_at.desc()).limit(safe_limit).all()
|
|
|
|
if user_id is not None and pedidos:
|
|
attached_legacy_orders = False
|
|
for pedido in pedidos:
|
|
if pedido.user_id is None:
|
|
pedido.user_id = user_id
|
|
attached_legacy_orders = True
|
|
if attached_legacy_orders:
|
|
db.commit()
|
|
for pedido in pedidos:
|
|
db.refresh(pedido)
|
|
|
|
return [
|
|
{
|
|
"numero_pedido": pedido.numero_pedido,
|
|
"user_id": pedido.user_id,
|
|
"cpf": pedido.cpf,
|
|
"vehicle_id": pedido.vehicle_id,
|
|
"modelo_veiculo": pedido.modelo_veiculo,
|
|
"valor_veiculo": pedido.valor_veiculo,
|
|
"status": pedido.status,
|
|
"motivo": pedido.motivo_cancelamento,
|
|
"data_cancelamento": pedido.data_cancelamento.isoformat() if pedido.data_cancelamento else None,
|
|
"created_at": pedido.created_at.isoformat() if pedido.created_at else None,
|
|
}
|
|
for pedido in pedidos
|
|
]
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
async def cancelar_pedido(
|
|
numero_pedido: str,
|
|
motivo: str,
|
|
user_id: int | None = None,
|
|
) -> dict[str, Any]:
|
|
db = SessionMockLocal()
|
|
try:
|
|
query = db.query(Order).filter(Order.numero_pedido == numero_pedido)
|
|
if user_id is not None:
|
|
query = query.filter(Order.user_id == user_id)
|
|
|
|
pedido = query.first()
|
|
if not pedido and user_id is not None:
|
|
legado = (
|
|
db.query(Order)
|
|
.filter(Order.numero_pedido == numero_pedido)
|
|
.filter(Order.user_id.is_(None))
|
|
.first()
|
|
)
|
|
if legado:
|
|
legado.user_id = user_id
|
|
db.commit()
|
|
db.refresh(legado)
|
|
pedido = legado
|
|
|
|
if not pedido:
|
|
raise_tool_http_error(
|
|
status_code=404,
|
|
code="order_not_found",
|
|
message=(
|
|
"Pedido nao encontrado para este usuario."
|
|
if user_id is not None
|
|
else "Pedido nao encontrado na base ficticia."
|
|
),
|
|
retryable=True,
|
|
field="numero_pedido",
|
|
)
|
|
|
|
if pedido.status.lower() == "cancelado":
|
|
return {
|
|
"numero_pedido": pedido.numero_pedido,
|
|
"user_id": pedido.user_id,
|
|
"status": pedido.status,
|
|
"motivo": pedido.motivo_cancelamento,
|
|
"data_cancelamento": pedido.data_cancelamento.isoformat() if pedido.data_cancelamento else None,
|
|
}
|
|
|
|
pedido.status = "Cancelado"
|
|
pedido.motivo_cancelamento = motivo
|
|
pedido.data_cancelamento = utc_now()
|
|
db.commit()
|
|
db.refresh(pedido)
|
|
|
|
result = {
|
|
"numero_pedido": pedido.numero_pedido,
|
|
"user_id": pedido.user_id,
|
|
"status": pedido.status,
|
|
"motivo": pedido.motivo_cancelamento,
|
|
"data_cancelamento": pedido.data_cancelamento.isoformat() if pedido.data_cancelamento else None,
|
|
}
|
|
await publish_business_event_safely(ORDER_CANCELLED_EVENT, result)
|
|
return result
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
async def realizar_pedido(
|
|
cpf: str,
|
|
vehicle_id: int,
|
|
user_id: int | None = None,
|
|
) -> dict[str, Any]:
|
|
cpf_norm = normalize_cpf(cpf)
|
|
db = SessionMockLocal()
|
|
reservation_lock_name: str | None = None
|
|
try:
|
|
vehicle = db.query(Vehicle).filter(Vehicle.id == vehicle_id).first()
|
|
if not vehicle:
|
|
raise_tool_http_error(
|
|
status_code=404,
|
|
code="vehicle_not_found",
|
|
message="Veiculo nao encontrado no estoque.",
|
|
retryable=True,
|
|
field="vehicle_id",
|
|
)
|
|
|
|
valor_veiculo = float(vehicle.preco)
|
|
modelo_veiculo = str(vehicle.modelo)
|
|
|
|
try:
|
|
await hydrate_mock_customer_from_cpf(cpf=cpf_norm, user_id=user_id)
|
|
except ValueError as exc:
|
|
if str(exc) == "cpf_already_linked":
|
|
raise_tool_http_error(
|
|
status_code=409,
|
|
code="cpf_already_linked",
|
|
message="Este CPF ja esta vinculado a outro usuario.",
|
|
retryable=True,
|
|
field="cpf",
|
|
)
|
|
raise_tool_http_error(
|
|
status_code=400,
|
|
code="invalid_cpf",
|
|
message="CPF invalido para realizar o pedido.",
|
|
retryable=True,
|
|
field="cpf",
|
|
)
|
|
|
|
avaliacao = await validar_cliente_venda(cpf=cpf_norm, valor_veiculo=valor_veiculo)
|
|
if not avaliacao.get("aprovado"):
|
|
raise_tool_http_error(
|
|
status_code=400,
|
|
code="credit_not_approved",
|
|
message=(
|
|
"Cliente nao aprovado para este valor. "
|
|
f"Limite disponivel: R$ {avaliacao.get('limite_credito', 0):.2f}."
|
|
),
|
|
retryable=False,
|
|
field="cpf",
|
|
)
|
|
|
|
reservation_lock_name = _acquire_vehicle_reservation_lock(db, vehicle_id)
|
|
|
|
try:
|
|
locked_vehicle = _get_vehicle_for_update(db, vehicle_id)
|
|
except (OperationalError, SQLAlchemyError) as exc:
|
|
db.rollback()
|
|
if not is_legacy_schema_issue(exc):
|
|
raise
|
|
locked_vehicle = db.query(Vehicle).filter(Vehicle.id == vehicle_id).first()
|
|
|
|
if not locked_vehicle:
|
|
raise_tool_http_error(
|
|
status_code=404,
|
|
code="vehicle_not_found",
|
|
message="Veiculo nao encontrado no estoque.",
|
|
retryable=True,
|
|
field="vehicle_id",
|
|
)
|
|
|
|
existing_order = None
|
|
try:
|
|
existing_order = _get_active_order_for_vehicle(db, vehicle_id)
|
|
except (OperationalError, SQLAlchemyError) as exc:
|
|
if not is_legacy_schema_issue(exc):
|
|
raise
|
|
db.rollback()
|
|
if existing_order:
|
|
raise_tool_http_error(
|
|
status_code=409,
|
|
code="vehicle_already_reserved",
|
|
message="Este veiculo ja esta reservado e nao aparece mais no estoque disponivel.",
|
|
retryable=True,
|
|
field="vehicle_id",
|
|
)
|
|
|
|
numero_pedido = f"PED-{utc_now().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6].upper()}"
|
|
|
|
pedido = Order(
|
|
numero_pedido=numero_pedido,
|
|
user_id=user_id,
|
|
cpf=cpf_norm,
|
|
vehicle_id=locked_vehicle.id,
|
|
modelo_veiculo=modelo_veiculo,
|
|
valor_veiculo=valor_veiculo,
|
|
status="Ativo",
|
|
)
|
|
db.add(pedido)
|
|
try:
|
|
db.commit()
|
|
db.refresh(pedido)
|
|
except (OperationalError, SQLAlchemyError) as exc:
|
|
db.rollback()
|
|
if not is_legacy_schema_issue(exc):
|
|
raise
|
|
|
|
db.execute(
|
|
text(
|
|
"INSERT INTO orders (numero_pedido, user_id, cpf, status) "
|
|
"VALUES (:numero_pedido, :user_id, :cpf, :status)"
|
|
),
|
|
{
|
|
"numero_pedido": numero_pedido,
|
|
"user_id": user_id,
|
|
"cpf": cpf_norm,
|
|
"status": "Ativo",
|
|
},
|
|
)
|
|
db.commit()
|
|
result = {
|
|
"numero_pedido": numero_pedido,
|
|
"user_id": user_id,
|
|
"cpf": cpf_norm,
|
|
"vehicle_id": locked_vehicle.id,
|
|
"modelo_veiculo": modelo_veiculo,
|
|
"status": "Ativo",
|
|
"status_veiculo": "Reservado",
|
|
"valor_veiculo": valor_veiculo,
|
|
"aprovado_credito": True,
|
|
}
|
|
await publish_business_event_safely(ORDER_CREATED_EVENT, result)
|
|
return result
|
|
|
|
result = {
|
|
"numero_pedido": pedido.numero_pedido,
|
|
"user_id": pedido.user_id,
|
|
"cpf": pedido.cpf,
|
|
"vehicle_id": pedido.vehicle_id,
|
|
"modelo_veiculo": pedido.modelo_veiculo,
|
|
"status": pedido.status,
|
|
"status_veiculo": "Reservado",
|
|
"valor_veiculo": pedido.valor_veiculo,
|
|
"aprovado_credito": True,
|
|
}
|
|
await publish_business_event_safely(ORDER_CREATED_EVENT, result)
|
|
return result
|
|
finally:
|
|
_release_vehicle_reservation_lock(db, reservation_lock_name)
|
|
db.close()
|
|
|
|
|