83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
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
|