Import ClientFlow production v4928.1.5.132.4

This commit is contained in:
plx
2026-07-29 13:11:01 +00:00
parent 6445044ac6
commit 261d342057
405 changed files with 48373 additions and 1401 deletions

View File

@@ -0,0 +1,82 @@
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