|
|
|
|
@ -261,12 +261,12 @@ class OrderFlowHarness(OrderFlowMixin):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RentalFlowHarness(RentalFlowMixin):
|
|
|
|
|
def __init__(self, state, registry):
|
|
|
|
|
def __init__(self, state, registry, rental_now_provider=None):
|
|
|
|
|
self.state = state
|
|
|
|
|
self.registry = registry
|
|
|
|
|
self.tool_executor = registry
|
|
|
|
|
self.normalizer = EntityNormalizer()
|
|
|
|
|
|
|
|
|
|
self._rental_now_provider = rental_now_provider
|
|
|
|
|
def _get_user_context(self, user_id: int | None):
|
|
|
|
|
return self.state.get_user_context(user_id)
|
|
|
|
|
|
|
|
|
|
@ -1105,9 +1105,17 @@ class CreateOrderFlowWithVehicleTests(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
|
|
|
|
|
async def test_order_flow_accepts_turn_decision_without_legacy_intents(self):
|
|
|
|
|
state = FakeState(
|
|
|
|
|
entries={
|
|
|
|
|
"pending_order_drafts": {
|
|
|
|
|
10: {
|
|
|
|
|
"payload": {"cpf": "12345678909"},
|
|
|
|
|
"expires_at": utc_now() + timedelta(minutes=30),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
contexts={
|
|
|
|
|
10: {
|
|
|
|
|
"generic_memory": {"cpf": "12345678909"},
|
|
|
|
|
"generic_memory": {},
|
|
|
|
|
"last_stock_results": [
|
|
|
|
|
{"id": 1, "modelo": "Honda Civic 2021", "categoria": "sedan", "preco": 51524.0},
|
|
|
|
|
],
|
|
|
|
|
@ -1143,9 +1151,17 @@ class CreateOrderFlowWithVehicleTests(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
|
|
|
|
|
async def test_order_flow_accepts_model_intent_without_keyword_trigger(self):
|
|
|
|
|
state = FakeState(
|
|
|
|
|
entries={
|
|
|
|
|
"pending_order_drafts": {
|
|
|
|
|
10: {
|
|
|
|
|
"payload": {"cpf": "12345678909"},
|
|
|
|
|
"expires_at": utc_now() + timedelta(minutes=30),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
contexts={
|
|
|
|
|
10: {
|
|
|
|
|
"generic_memory": {"cpf": "12345678909"},
|
|
|
|
|
"generic_memory": {},
|
|
|
|
|
"last_stock_results": [
|
|
|
|
|
{"id": 1, "modelo": "Honda Civic 2021", "categoria": "sedan", "preco": 51524.0},
|
|
|
|
|
],
|
|
|
|
|
@ -1201,9 +1217,166 @@ class CreateOrderFlowWithVehicleTests(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
intents={"order_create": True},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertIn("escolha primeiro qual veiculo", response.lower())
|
|
|
|
|
self.assertIn("Honda Civic 2021", response)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
response,
|
|
|
|
|
"Pode me dizer a faixa de preco, o modelo ou o tipo de carro que voce procura.",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(registry.calls, [])
|
|
|
|
|
self.assertEqual(state.get_user_context(10)["last_stock_results"], [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_order_flow_generic_request_asks_for_price_range_even_with_previous_search_context(self):
|
|
|
|
|
state = FakeState(
|
|
|
|
|
contexts={
|
|
|
|
|
10: {
|
|
|
|
|
"generic_memory": {"cpf": "12345678909", "orcamento_max": 70000, "perfil_veiculo": ["suv"]},
|
|
|
|
|
"shared_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]},
|
|
|
|
|
"last_stock_results": [
|
|
|
|
|
{"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0},
|
|
|
|
|
{"id": 3, "modelo": "Chevrolet Onix 2022", "categoria": "suv", "preco": 51809.0},
|
|
|
|
|
],
|
|
|
|
|
"selected_vehicle": {"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0},
|
|
|
|
|
"pending_single_vehicle_confirmation": {"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = OrderFlowHarness(state=state, registry=registry)
|
|
|
|
|
|
|
|
|
|
response = await flow._try_collect_and_create_order(
|
|
|
|
|
message="Quero fazer um pedido de veiculo",
|
|
|
|
|
user_id=10,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={"order_create": True},
|
|
|
|
|
turn_decision={"intent": "order_create", "domain": "sales", "action": "collect_order_create"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
draft = state.get_entry("pending_order_drafts", 10)
|
|
|
|
|
context = state.get_user_context(10)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
response,
|
|
|
|
|
"Pode me dizer a faixa de preco, o modelo ou o tipo de carro que voce procura.",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(registry.calls, [])
|
|
|
|
|
self.assertIsNotNone(draft)
|
|
|
|
|
self.assertEqual(draft["payload"], {})
|
|
|
|
|
self.assertEqual(context["last_stock_results"], [])
|
|
|
|
|
self.assertIsNone(context["selected_vehicle"])
|
|
|
|
|
self.assertIsNone(context.get("pending_single_vehicle_confirmation"))
|
|
|
|
|
self.assertNotIn("orcamento_max", context["generic_memory"])
|
|
|
|
|
self.assertNotIn("perfil_veiculo", context["generic_memory"])
|
|
|
|
|
self.assertNotIn("orcamento_max", context["shared_memory"])
|
|
|
|
|
self.assertNotIn("perfil_veiculo", context["shared_memory"])
|
|
|
|
|
|
|
|
|
|
async def test_order_flow_requires_confirmation_before_using_known_cpf(self):
|
|
|
|
|
state = FakeState(
|
|
|
|
|
contexts={
|
|
|
|
|
10: {
|
|
|
|
|
"generic_memory": {"cpf": "12345678909"},
|
|
|
|
|
"last_stock_results": [
|
|
|
|
|
{"id": 1, "modelo": "Honda Civic 2021", "categoria": "sedan", "preco": 51524.0},
|
|
|
|
|
],
|
|
|
|
|
"selected_vehicle": None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = OrderFlowHarness(state=state, registry=registry)
|
|
|
|
|
hydrated = []
|
|
|
|
|
|
|
|
|
|
async def fake_hydrate_mock_customer_from_cpf(cpf: str, user_id: int | None = None):
|
|
|
|
|
hydrated.append((cpf, user_id))
|
|
|
|
|
return {"cpf": cpf, "user_id": user_id}
|
|
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
|
"app.services.flows.order_flow.hydrate_mock_customer_from_cpf",
|
|
|
|
|
new=fake_hydrate_mock_customer_from_cpf,
|
|
|
|
|
):
|
|
|
|
|
first_response = await flow._try_collect_and_create_order(
|
|
|
|
|
message="1",
|
|
|
|
|
user_id=10,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
draft = state.get_entry("pending_order_drafts", 10)
|
|
|
|
|
self.assertIsNotNone(draft)
|
|
|
|
|
self.assertEqual(draft["payload"].get("vehicle_id"), 1)
|
|
|
|
|
self.assertEqual(draft["payload"].get("cpf"), "12345678909")
|
|
|
|
|
self.assertIs(draft["payload"].get("cpf_confirmed"), False)
|
|
|
|
|
self.assertIn("cpf informado anteriormente", first_response.lower())
|
|
|
|
|
self.assertIn("continua correto", first_response.lower())
|
|
|
|
|
self.assertEqual(registry.calls, [])
|
|
|
|
|
self.assertEqual(hydrated, [])
|
|
|
|
|
|
|
|
|
|
second_response = await flow._try_collect_and_create_order(
|
|
|
|
|
message="sim",
|
|
|
|
|
user_id=10,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(registry.calls), 1)
|
|
|
|
|
self.assertEqual(registry.calls[0][0], "realizar_pedido")
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["vehicle_id"], 1)
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["cpf"], "12345678909")
|
|
|
|
|
self.assertEqual(hydrated, [("12345678909", 10)])
|
|
|
|
|
self.assertIn("Pedido criado com sucesso.", second_response)
|
|
|
|
|
|
|
|
|
|
async def test_order_flow_updates_known_cpf_after_negative_confirmation_and_new_value(self):
|
|
|
|
|
state = FakeState(
|
|
|
|
|
contexts={
|
|
|
|
|
10: {
|
|
|
|
|
"generic_memory": {"cpf": "12345678909"},
|
|
|
|
|
"last_stock_results": [
|
|
|
|
|
{"id": 1, "modelo": "Honda Civic 2021", "categoria": "sedan", "preco": 51524.0},
|
|
|
|
|
],
|
|
|
|
|
"selected_vehicle": None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = OrderFlowHarness(state=state, registry=registry)
|
|
|
|
|
hydrated = []
|
|
|
|
|
|
|
|
|
|
async def fake_hydrate_mock_customer_from_cpf(cpf: str, user_id: int | None = None):
|
|
|
|
|
hydrated.append((cpf, user_id))
|
|
|
|
|
return {"cpf": cpf, "user_id": user_id}
|
|
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
|
"app.services.flows.order_flow.hydrate_mock_customer_from_cpf",
|
|
|
|
|
new=fake_hydrate_mock_customer_from_cpf,
|
|
|
|
|
):
|
|
|
|
|
await flow._try_collect_and_create_order(
|
|
|
|
|
message="1",
|
|
|
|
|
user_id=10,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
second_response = await flow._try_collect_and_create_order(
|
|
|
|
|
message="nao",
|
|
|
|
|
user_id=10,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
third_response = await flow._try_collect_and_create_order(
|
|
|
|
|
message="52998224725",
|
|
|
|
|
user_id=10,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertIn("me informe o cpf correto", second_response.lower())
|
|
|
|
|
self.assertEqual(len(registry.calls), 1)
|
|
|
|
|
self.assertEqual(registry.calls[0][0], "realizar_pedido")
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["vehicle_id"], 1)
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["cpf"], "52998224725")
|
|
|
|
|
self.assertEqual(hydrated, [("52998224725", 10)])
|
|
|
|
|
self.assertEqual(state.get_user_context(10)["generic_memory"]["cpf"], "52998224725")
|
|
|
|
|
self.assertEqual(state.get_user_context(10)["shared_memory"]["cpf"], "52998224725")
|
|
|
|
|
self.assertIn("Pedido criado com sucesso.", third_response)
|
|
|
|
|
|
|
|
|
|
async def test_order_flow_single_stock_result_requires_explicit_confirmation(self):
|
|
|
|
|
state = FakeState(
|
|
|
|
|
@ -2003,6 +2176,186 @@ class RentalFlowDraftTests(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
self.assertIsNone(state.get_entry("pending_rental_drafts", 21))
|
|
|
|
|
self.assertIn("LOC-TESTE-123", response)
|
|
|
|
|
|
|
|
|
|
async def test_rental_flow_preserves_relative_dates_from_initial_request_until_vehicle_selection(self):
|
|
|
|
|
fixed_now = lambda: datetime(2026, 3, 19, 9, 0)
|
|
|
|
|
state = FakeState(contexts={21: self._base_context()})
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = RentalFlowHarness(state=state, registry=registry, rental_now_provider=fixed_now)
|
|
|
|
|
|
|
|
|
|
list_response = await flow._try_collect_and_open_rental(
|
|
|
|
|
message="Quero alugar um hatch amanha 10h ate depois de amanha 10h",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "rental_create", "domain": "rental", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(registry.calls[0][0], "consultar_frota_aluguel")
|
|
|
|
|
self.assertNotIn("modelo", registry.calls[0][1])
|
|
|
|
|
draft = state.get_entry("pending_rental_drafts", 21)
|
|
|
|
|
self.assertIsNotNone(draft)
|
|
|
|
|
self.assertEqual(draft["payload"]["data_inicio"], "20/03/2026 10:00")
|
|
|
|
|
self.assertEqual(draft["payload"]["data_fim_prevista"], "21/03/2026 10:00")
|
|
|
|
|
self.assertEqual(draft["payload"]["categoria"], "hatch")
|
|
|
|
|
self.assertIn("veiculo(s) para locacao", list_response)
|
|
|
|
|
|
|
|
|
|
open_response = await flow._try_collect_and_open_rental(
|
|
|
|
|
message="1",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "rental_create", "domain": "rental", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(registry.calls[1][0], "abrir_locacao_aluguel")
|
|
|
|
|
self.assertEqual(registry.calls[1][1]["data_inicio"], "20/03/2026 10:00")
|
|
|
|
|
self.assertEqual(registry.calls[1][1]["data_fim_prevista"], "21/03/2026 10:00")
|
|
|
|
|
self.assertIn("LOC-TESTE-123", open_response)
|
|
|
|
|
|
|
|
|
|
async def test_rental_flow_preserves_relative_dates_even_when_day_words_arrive_truncated(self):
|
|
|
|
|
fixed_now = lambda: datetime(2026, 3, 19, 9, 0)
|
|
|
|
|
state = FakeState(contexts={21: self._base_context()})
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = RentalFlowHarness(state=state, registry=registry, rental_now_provider=fixed_now)
|
|
|
|
|
|
|
|
|
|
list_response = await flow._try_collect_and_open_rental(
|
|
|
|
|
message="Quero alugar um hatch amanh 10h at depois de amanh 10h",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "rental_create", "domain": "rental", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(registry.calls[0][0], "consultar_frota_aluguel")
|
|
|
|
|
draft = state.get_entry("pending_rental_drafts", 21)
|
|
|
|
|
self.assertIsNotNone(draft)
|
|
|
|
|
self.assertEqual(draft["payload"]["data_inicio"], "20/03/2026 10:00")
|
|
|
|
|
self.assertEqual(draft["payload"]["data_fim_prevista"], "21/03/2026 10:00")
|
|
|
|
|
self.assertNotIn("modelo", registry.calls[0][1])
|
|
|
|
|
self.assertIn("veiculo(s) para locacao", list_response)
|
|
|
|
|
|
|
|
|
|
open_response = await flow._try_collect_and_open_rental(
|
|
|
|
|
message="1",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "rental_create", "domain": "rental", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(registry.calls[1][0], "abrir_locacao_aluguel")
|
|
|
|
|
self.assertEqual(registry.calls[1][1]["data_inicio"], "20/03/2026 10:00")
|
|
|
|
|
self.assertEqual(registry.calls[1][1]["data_fim_prevista"], "21/03/2026 10:00")
|
|
|
|
|
self.assertIn("LOC-TESTE-123", open_response)
|
|
|
|
|
|
|
|
|
|
async def test_rental_flow_preserves_relative_dates_even_when_day_words_arrive_with_question_marks(self):
|
|
|
|
|
fixed_now = lambda: datetime(2026, 3, 19, 9, 0)
|
|
|
|
|
state = FakeState(contexts={21: self._base_context()})
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = RentalFlowHarness(state=state, registry=registry, rental_now_provider=fixed_now)
|
|
|
|
|
|
|
|
|
|
list_response = await flow._try_collect_and_open_rental(
|
|
|
|
|
message="Quero alugar um hatch amanh? 10h at? depois de amanh? 10h",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "rental_create", "domain": "rental", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(registry.calls[0][0], "consultar_frota_aluguel")
|
|
|
|
|
draft = state.get_entry("pending_rental_drafts", 21)
|
|
|
|
|
self.assertIsNotNone(draft)
|
|
|
|
|
self.assertEqual(draft["payload"]["data_inicio"], "20/03/2026 10:00")
|
|
|
|
|
self.assertEqual(draft["payload"]["data_fim_prevista"], "21/03/2026 10:00")
|
|
|
|
|
self.assertNotIn("modelo", registry.calls[0][1])
|
|
|
|
|
self.assertIn("veiculo(s) para locacao", list_response)
|
|
|
|
|
|
|
|
|
|
open_response = await flow._try_collect_and_open_rental(
|
|
|
|
|
message="1",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "rental_create", "domain": "rental", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(registry.calls[1][0], "abrir_locacao_aluguel")
|
|
|
|
|
self.assertEqual(registry.calls[1][1]["data_inicio"], "20/03/2026 10:00")
|
|
|
|
|
self.assertEqual(registry.calls[1][1]["data_fim_prevista"], "21/03/2026 10:00")
|
|
|
|
|
self.assertIn("LOC-TESTE-123", open_response)
|
|
|
|
|
|
|
|
|
|
async def test_rental_flow_rehydrates_search_payload_from_context_when_selection_survives_without_draft(self):
|
|
|
|
|
state = FakeState(
|
|
|
|
|
entries={
|
|
|
|
|
"pending_rental_selections": {
|
|
|
|
|
21: {
|
|
|
|
|
"payload": [
|
|
|
|
|
{"id": 1, "placa": "RAA1A01", "modelo": "Chevrolet Tracker", "categoria": "hatch", "ano": 2024, "valor_diaria": 219.9, "status": "disponivel"},
|
|
|
|
|
{"id": 2, "placa": "RAA1A02", "modelo": "Fiat Pulse", "categoria": "hatch", "ano": 2024, "valor_diaria": 189.9, "status": "disponivel"},
|
|
|
|
|
],
|
|
|
|
|
"expires_at": utc_now() + timedelta(minutes=15),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
contexts={
|
|
|
|
|
21: self._base_context() | {
|
|
|
|
|
"active_domain": "rental",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
state.get_entry("pending_rental_selections", 21)["search_payload"] = {
|
|
|
|
|
"categoria": "hatch",
|
|
|
|
|
"data_inicio": "20/03/2026 10:00",
|
|
|
|
|
"data_fim_prevista": "21/03/2026 10:00",
|
|
|
|
|
}
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = RentalFlowHarness(state=state, registry=registry)
|
|
|
|
|
|
|
|
|
|
response = await flow._try_collect_and_open_rental(
|
|
|
|
|
message="2",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "rental_create", "domain": "rental", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(registry.calls[0][0], "abrir_locacao_aluguel")
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["rental_vehicle_id"], 2)
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["data_inicio"], "20/03/2026 10:00")
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["data_fim_prevista"], "21/03/2026 10:00")
|
|
|
|
|
self.assertIn("LOC-TESTE-123", response)
|
|
|
|
|
|
|
|
|
|
async def test_rental_flow_opens_contract_after_collecting_relative_dates_follow_up(self):
|
|
|
|
|
fixed_now = lambda: datetime(2026, 3, 19, 9, 0)
|
|
|
|
|
state = FakeState(
|
|
|
|
|
entries={
|
|
|
|
|
"pending_rental_drafts": {
|
|
|
|
|
21: {
|
|
|
|
|
"payload": {
|
|
|
|
|
"rental_vehicle_id": 1,
|
|
|
|
|
"placa": "RAA1A01",
|
|
|
|
|
"modelo_veiculo": "Chevrolet Tracker",
|
|
|
|
|
},
|
|
|
|
|
"expires_at": utc_now() + timedelta(minutes=15),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
contexts={21: self._base_context() | {"active_domain": "rental", "selected_rental_vehicle": {"id": 1, "placa": "RAA1A01", "modelo": "Chevrolet Tracker", "categoria": "suv", "ano": 2024, "valor_diaria": 219.9, "status": "disponivel"}}},
|
|
|
|
|
)
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = RentalFlowHarness(state=state, registry=registry, rental_now_provider=fixed_now)
|
|
|
|
|
|
|
|
|
|
response = await flow._try_collect_and_open_rental(
|
|
|
|
|
message="amanha 10h ate depois de amanha 10h",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "rental_create", "domain": "rental", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(registry.calls[0][0], "abrir_locacao_aluguel")
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["data_inicio"], "20/03/2026 10:00")
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["data_fim_prevista"], "21/03/2026 10:00")
|
|
|
|
|
self.assertIsNone(state.get_entry("pending_rental_drafts", 21))
|
|
|
|
|
self.assertIn("LOC-TESTE-123", response)
|
|
|
|
|
|
|
|
|
|
class ReviewFlowDraftTests(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
async def test_review_flow_extracts_relative_datetime_from_followup_message(self):
|
|
|
|
|
@ -2066,6 +2419,36 @@ class ReviewFlowDraftTests(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
self.assertIn("- o modelo do veiculo", response)
|
|
|
|
|
self.assertNotIn("- a data e hora desejada para a revisao", response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_review_flow_date_only_supports_day_after_tomorrow(self):
|
|
|
|
|
fixed_now = lambda: datetime(2026, 3, 12, 9, 0)
|
|
|
|
|
state = FakeState(
|
|
|
|
|
entries={
|
|
|
|
|
"pending_review_drafts": {
|
|
|
|
|
21: {
|
|
|
|
|
"payload": {"placa": "ABC1269"},
|
|
|
|
|
"expires_at": utc_now() + timedelta(minutes=30),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = ReviewFlowHarness(state=state, registry=registry, review_now_provider=fixed_now)
|
|
|
|
|
|
|
|
|
|
response = await flow._try_collect_and_schedule_review(
|
|
|
|
|
message="depois de amanha",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "review_schedule", "domain": "review", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
draft = state.get_entry("pending_review_drafts", 21)
|
|
|
|
|
self.assertIsNotNone(draft)
|
|
|
|
|
self.assertEqual(draft["payload"].get("data_hora_base"), "14/03/2026")
|
|
|
|
|
self.assertIn("Perfeito. Tenho a data 14/03/2026.", response)
|
|
|
|
|
self.assertIn("- o horario desejado para a revisao", response)
|
|
|
|
|
|
|
|
|
|
async def test_review_flow_keeps_review_draft_when_time_follow_up_is_misclassified_as_sales(self):
|
|
|
|
|
state = FakeState(
|
|
|
|
|
entries={
|
|
|
|
|
@ -2862,6 +3245,37 @@ class ReviewFlowDraftTests(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["nova_data_hora"], "13/03/2026 11:00")
|
|
|
|
|
self.assertIn("13/03/2026 11:00", response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_review_management_reschedule_consumes_day_after_tomorrow_relative_datetime_follow_up(self):
|
|
|
|
|
fixed_now = lambda: datetime(2026, 3, 12, 9, 0)
|
|
|
|
|
state = FakeState(
|
|
|
|
|
entries={
|
|
|
|
|
"pending_review_management_drafts": {
|
|
|
|
|
21: {
|
|
|
|
|
"action": "reschedule",
|
|
|
|
|
"payload": {"protocolo": "REV-20260313-F754AF27"},
|
|
|
|
|
"expires_at": utc_now() + timedelta(minutes=30),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
flow = ReviewFlowHarness(state=state, registry=registry, review_now_provider=fixed_now)
|
|
|
|
|
|
|
|
|
|
response = await flow._try_handle_review_management(
|
|
|
|
|
message="depois de amanha 11h",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "review_reschedule", "domain": "review", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertIsNone(state.get_entry("pending_review_management_drafts", 21))
|
|
|
|
|
self.assertEqual(registry.calls[0][0], "editar_data_revisao")
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["protocolo"], "REV-20260313-F754AF27")
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["nova_data_hora"], "14/03/2026 11:00")
|
|
|
|
|
self.assertIn("14/03/2026 11:00", response)
|
|
|
|
|
|
|
|
|
|
async def test_review_management_reschedule_date_only_then_time_follow_up(self):
|
|
|
|
|
fixed_now = lambda: datetime(2026, 3, 12, 9, 0)
|
|
|
|
|
state = FakeState(
|
|
|
|
|
@ -2904,6 +3318,50 @@ class ReviewFlowDraftTests(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
self.assertEqual(registry.calls[0][1]["nova_data_hora"], "13/03/2026 11:00")
|
|
|
|
|
self.assertIn("13/03/2026 11:00", second_response)
|
|
|
|
|
|
|
|
|
|
async def test_review_management_reschedule_conflict_stores_pending_confirmation_suggestion(self):
|
|
|
|
|
fixed_now = lambda: datetime(2026, 3, 12, 9, 0)
|
|
|
|
|
state = FakeState(
|
|
|
|
|
entries={
|
|
|
|
|
"pending_review_management_drafts": {
|
|
|
|
|
21: {
|
|
|
|
|
"action": "reschedule",
|
|
|
|
|
"payload": {"protocolo": "REV-20260313-F754AF27"},
|
|
|
|
|
"expires_at": utc_now() + timedelta(minutes=30),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
registry.raise_http_exception = HTTPException(
|
|
|
|
|
status_code=409,
|
|
|
|
|
detail={
|
|
|
|
|
"code": "review_reschedule_conflict",
|
|
|
|
|
"message": "O horario 14/03/2026 as 11:00 ja esta ocupado. Posso agendar em 14/03/2026 as 12:00.",
|
|
|
|
|
"retryable": True,
|
|
|
|
|
"field": "nova_data_hora",
|
|
|
|
|
"suggested_iso": "2026-03-14T12:00:00-03:00",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
flow = ReviewFlowHarness(state=state, registry=registry, review_now_provider=fixed_now)
|
|
|
|
|
|
|
|
|
|
response = await flow._try_handle_review_management(
|
|
|
|
|
message="depois de amanha 11h",
|
|
|
|
|
user_id=21,
|
|
|
|
|
extracted_fields={},
|
|
|
|
|
intents={},
|
|
|
|
|
turn_decision={"intent": "review_reschedule", "domain": "review", "action": "answer_user"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
draft = state.get_entry("pending_review_management_drafts", 21)
|
|
|
|
|
self.assertIsNotNone(draft)
|
|
|
|
|
self.assertNotIn("nova_data_hora", draft["payload"])
|
|
|
|
|
self.assertEqual(len(flow.captured_suggestions), 1)
|
|
|
|
|
suggestion = flow.captured_suggestions[0]
|
|
|
|
|
self.assertEqual(suggestion["tool_name"], "editar_data_revisao")
|
|
|
|
|
self.assertEqual(suggestion["arguments"]["protocolo"], "REV-20260313-F754AF27")
|
|
|
|
|
self.assertEqual(suggestion["arguments"]["nova_data_hora"], "14/03/2026 11:00")
|
|
|
|
|
self.assertIn("ocupado", response)
|
|
|
|
|
|
|
|
|
|
async def test_review_management_infers_listing_intent_from_agendamentos_message(self):
|
|
|
|
|
state = FakeState()
|
|
|
|
|
registry = FakeRegistry()
|
|
|
|
|
@ -3192,7 +3650,3 @@ class ToolRegistryExecutionTests(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|