from pathlib import Path from app.followup_service import FOLLOW_UP_CADENCES, follow_up_suggested_message def read(path: str) -> str: return Path(path).read_text(encoding="utf-8") def test_quote_cadence_starts_with_commercial_followup(): quote = FOLLOW_UP_CADENCES["quote"] assert [step["delay_business_days"] for step in quote] == [2, 4, 5] assert quote[0]["action_code"] == "FOLLOW_UP_QUOTE" assert quote[0]["purpose"] == "objection_check" assert quote[-1]["purpose"] == "final_attempt" assert all(step["action_code"] != "CONFIRM_DELIVERY" for step in quote) def test_payment_cadence_is_short_and_progressive_without_delivery_gate(): payment = FOLLOW_UP_CADENCES["payment"] assert [step["delay_business_days"] for step in payment] == [2, 3, 3, 5] assert payment[0]["purpose"] == "payment_date" assert payment[-1]["purpose"] == "final_attempt" assert all(step["action_code"] != "CONFIRM_DELIVERY" for step in payment) def test_delivery_confirmation_message_remains_available_for_manual_use(): message = follow_up_suggested_message( action_code="CONFIRM_DELIVERY", customer_name="Maria Silva", follow_up_stage=1, follow_up_family="quote", ) assert "confirmar se recebeu o orçamento" in message assert "reenvio de imediato" in message def test_lifecycle_schema_and_safe_reopen_are_additive(): service = read("app/opportunity_service.py") for field in [ "lifecycle_state", "last_customer_activity_at", "last_operator_activity_at", "last_outbound_sent_at", "last_delivery_checked_at", "last_delivery_status", "last_commercial_activity_at", "next_follow_up_at", "follow_up_attempts", "nurture_until", "lost_reason", ]: assert f"ADD COLUMN IF NOT EXISTS {field}" in service assert "_find_reopenable_opportunity_by_conversation" in service assert "stage IN ('LOST', 'NO_INTEREST')" in service assert "DELIVERED" not in service.split("def _find_reopenable_opportunity_by_conversation", 1)[1].split("def _open_opportunities_for_contact", 1)[0] def test_opportunity_ui_has_recovery_nurture_and_mandatory_loss_reason(): page = read("app/admin_ui/pages/opportunities.py") for label in [ "A aguardar cliente", "Follow-up vencido", "Recuperação", "Acompanhamento futuro", "Por valorizar", ]: assert label in page assert '@router.post("/opportunities/{opportunity_id}/lost")' in page assert "Motivo de perda obrigatório" in read("app/opportunity_service.py") assert "Timing futuro deve usar acompanhamento futuro" in read("app/opportunity_service.py") def test_backfill_script_is_dry_run_safe_and_skips_delivery_confirmation(): script = read("scripts/sync_opportunity_followup_lifecycle.py") assert 'parser.add_argument("--apply", action="store_true"' in script assert '"--include-zero-value"' in script assert "Dry-run only" in script assert "materialize_initial_follow_up_for_opportunity" in script assert 'due_at=None' in script assert "migration_v4928_1_5_127" in script assert "send_email" not in script