mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 23:39:34 +00:00
Builds the full Saudi Autonomous Revenue OS surface as 10 deterministic
modules + a 16-endpoint router under /api/v1/growth-operator/.
Approval-first: every outbound is draft. No live send / charge / calendar
insert from this layer.
MODULES (auto_client_acquisition/growth_operator/)
1. client_profile.py — ClientGrowthProfile + Saudi-default approval
+ compliance rules (no cold WhatsApp, blocked keywords, weekly cap,
quiet_hours_riyadh)
2. contact_importer.py — normalize_phone (Saudi E.164),
dedupe_contacts (richer-record-wins), classify_contact_source
(existing/inbound/event/referral/old_lead/cold/unknown), detect_opt_out
(Arabic + English markers), summarize_import (dashboard report)
3. contactability.py — score_contactability returns
safe/needs_review/blocked with Arabic reasons; default policy:
no cold WhatsApp without lawful basis (PDPL Art.5)
4. targeting.py — segment_contacts, rank_targets (filters unsafe),
recommend_top_10, why_now_stub (deterministic, sector-aware)
5. message_planner.py — draft_arabic_message (Saudi tone, 4-sector
opener bank, no overhyped phrases, always pending_approval),
draft_followup (4 outcome modes), draft_objection_response
(6 indexed Saudi B2B objections with score_delta + next_action)
6. partnership_planner.py — 6 partner types catalog
(agency / consultant / integrator / crm / community / influencer)
+ suggest_partner_types (size/sector aware) + draft_partner_outreach
+ partner_scorecard (platinum/gold/silver/bronze)
7. meeting_planner.py — build_meeting_agenda (15/20-30/45+ min slot
plans), build_calendar_draft (Google Calendar shape, live_inserted=False,
conferenceData for Meet, Asia/Riyadh timezone), build_post_meeting_followup
8. payment_offer.py — sar_to_halalas, build_moyasar_payment_link_draft
(full payload + in-chat message + 4-plan catalog, live_charged=False)
9. proof_pack.py — build_weekly_proof_pack with grade A+/A/B/C/D,
activity/money/quality/best-of sections, dynamic next_week_plan_ar,
markdown export
10. mission_planner.py — 6 GROWTH_MISSIONS (first_10_opportunities ⭐
kill feature, recover_stalled_deals, partnership_sprint,
safe_whatsapp_campaign, meeting_booking_sprint, list_cleanup);
list_missions() + run_mission()
ROUTER (api/routers/growth_operator.py) — 16 endpoints
POST /contacts/import-preview · POST /contactability/score
POST /targets/top-10 · POST /messages/draft · POST /messages/followup
POST /messages/objection-response · POST /partners/suggest
POST /partners/outreach · POST /partners/scorecard
POST /meetings/draft · POST /meetings/post-followup
POST /payment-offer/draft · GET /missions · POST /missions/{id}/run
GET /proof-pack/demo · POST /profile
WIRING: api/main.py adds growth_operator import + router include
(positioned after personal_operator, before public).
DOCS
- docs/ARABIC_GROWTH_OPERATOR_FULL_SPEC.md (NEW): 20-section vision +
customer-type table + upload flow + contactability rules +
WhatsApp/Gmail/Calendar/Moyasar drafts + 6 missions + 16-endpoint
catalog + competitive comparison + beta readiness checklist
TESTS — 50 passing on Python 3.10 venv
tests/unit/test_growth_operator.py covers:
- Phone normalization across 5 input formats including invalid
- Dedupe richer-record invariant
- Source classification (existing/inbound/event/cold/unknown)
- Opt-out detection (Arabic + English notes + status)
- Import summary aggregation
- Contactability: opt-out blocked, cold WhatsApp blocked,
unknown→needs_review, existing→safe, inbound→safe
- Bulk contactability summary
- Top-10 filtering (unsafe excluded), max-cap enforcement
- Segment buckets
- Arabic message: pending_approval invariant + Arabic content
+ no overhyped phrases (banned list)
- Followup approval invariant
- Objection response: known + unknown→diagnostic
- Partner suggestions size-aware (SMB→agency/consultant/community)
- Partner outreach approval invariant
- Partner unknown type returns error
- Partner scorecard tier ordering
- Meeting agenda + calendar draft (live_inserted=False) +
Asia/Riyadh timezone + post-followup pending
- Payment: halalas conversion (1 SAR=100), negative raises,
draft NEVER charges (live_charged=False), unknown plan→error
- Proof pack: grade range + structure + markdown export
- Missions: first_10_opportunities present + kill feature ID
+ run mission known/unknown
- Profile: demo specialized + partial not specialized
+ default compliance blocks 'ضمان 100' + no_cold_whatsapp_without_lawful_basis
VERIFICATION
- 527 unit tests pass (was 477; +50 growth_operator)
- 2 skipped (provider smoke needs API keys)
- AST green on all 13 new files
- Approval invariant holds across every drafting function
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
402 lines
14 KiB
Python
402 lines
14 KiB
Python
"""Unit tests for the Arabic Growth Operator layer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from auto_client_acquisition.growth_operator import (
|
|
build_calendar_draft,
|
|
build_demo_profile,
|
|
build_meeting_agenda,
|
|
build_moyasar_payment_link_draft,
|
|
build_post_meeting_followup,
|
|
build_weekly_proof_pack,
|
|
classify_contact_source,
|
|
contactability_summary,
|
|
dedupe_contacts,
|
|
detect_opt_out,
|
|
draft_arabic_message,
|
|
draft_followup,
|
|
draft_objection_response,
|
|
draft_partner_outreach,
|
|
list_missions,
|
|
normalize_phone,
|
|
partner_scorecard,
|
|
profile_from_dict,
|
|
rank_targets,
|
|
recommend_top_10,
|
|
run_mission,
|
|
sar_to_halalas,
|
|
score_contactability,
|
|
segment_contacts,
|
|
suggest_partner_types,
|
|
summarize_import,
|
|
)
|
|
|
|
|
|
# ── 1. Phone normalization ───────────────────────────────────────
|
|
def test_normalize_phone_country_prefix_kept():
|
|
assert normalize_phone("+966500000001") == "966500000001"
|
|
|
|
|
|
def test_normalize_phone_local_zero_to_country():
|
|
assert normalize_phone("0500000001") == "966500000001"
|
|
|
|
|
|
def test_normalize_phone_bare_9_digits():
|
|
assert normalize_phone("500000001") == "966500000001"
|
|
|
|
|
|
def test_normalize_phone_double_zero():
|
|
assert normalize_phone("00966500000001") == "966500000001"
|
|
|
|
|
|
def test_normalize_phone_strips_punctuation():
|
|
assert normalize_phone("+966 (50) 000-0001") == "966500000001"
|
|
|
|
|
|
def test_normalize_phone_invalid_returns_empty():
|
|
assert normalize_phone("") == ""
|
|
assert normalize_phone("abc") == ""
|
|
|
|
|
|
# ── 2. Dedupe ────────────────────────────────────────────────────
|
|
def test_dedupe_drops_exact_phone_duplicates():
|
|
out = dedupe_contacts([
|
|
{"name": "X", "phone": "0500000001"},
|
|
{"name": "X duplicate", "phone": "+966 50 000 0001"},
|
|
])
|
|
assert len(out) == 1
|
|
|
|
|
|
def test_dedupe_keeps_richer_record():
|
|
out = dedupe_contacts([
|
|
{"name": "X", "phone": "0500000001"},
|
|
{"name": "X full", "phone": "0500000001", "email": "x@example.sa", "company": "Co"},
|
|
])
|
|
assert len(out) == 1
|
|
assert out[0].get("email") == "x@example.sa"
|
|
|
|
|
|
# ── 3. Source classification ─────────────────────────────────────
|
|
def test_classify_existing_customer():
|
|
assert classify_contact_source({"relationship_status": "customer"}) == "existing_customer"
|
|
|
|
|
|
def test_classify_inbound():
|
|
assert classify_contact_source({"source": "website_form"}) == "inbound_lead"
|
|
|
|
|
|
def test_classify_event():
|
|
assert classify_contact_source({"source": "exhibition"}) == "event_lead"
|
|
|
|
|
|
def test_classify_cold():
|
|
assert classify_contact_source({"source": "cold"}) == "cold_list"
|
|
|
|
|
|
def test_classify_unknown_default():
|
|
assert classify_contact_source({}) == "unknown"
|
|
|
|
|
|
# ── 4. Opt-out detection ─────────────────────────────────────────
|
|
def test_detect_opt_out_via_status():
|
|
assert detect_opt_out({"opt_in_status": "opted_out"}) is True
|
|
|
|
|
|
def test_detect_opt_out_via_arabic_notes():
|
|
assert detect_opt_out({"notes": "العميل طلب إيقاف الرسائل"}) is True
|
|
|
|
|
|
def test_detect_opt_out_clean():
|
|
assert detect_opt_out({"name": "X"}) is False
|
|
|
|
|
|
# ── 5. Summarize import ──────────────────────────────────────────
|
|
def test_summarize_import_aggregates():
|
|
contacts = [
|
|
{"name": "A", "phone": "0500000001", "source": "customer"},
|
|
{"name": "A dup", "phone": "0500000001", "source": "customer"}, # dup
|
|
{"name": "B", "phone": "0500000002", "source": "cold"},
|
|
{"name": "C", "phone": "0500000003", "opt_in_status": "opted_out"},
|
|
]
|
|
s = summarize_import(contacts)
|
|
assert s["raw_total"] == 4
|
|
assert s["after_dedupe"] == 3
|
|
assert s["duplicates_removed"] == 1
|
|
assert s["opt_out_count"] == 1
|
|
|
|
|
|
# ── 6. Contactability — core safety rules ───────────────────────
|
|
def test_contactability_blocks_opt_out():
|
|
out = score_contactability({"opt_in_status": "opted_out", "phone": "0500000001"})
|
|
assert out["label"] == "blocked"
|
|
|
|
|
|
def test_contactability_blocks_cold_whatsapp_by_default():
|
|
"""No cold WhatsApp without lawful basis — that's the policy."""
|
|
out = score_contactability(
|
|
{"phone": "0500000001", "source": "cold", "name": "X"},
|
|
channel="whatsapp",
|
|
)
|
|
assert out["label"] == "blocked"
|
|
assert any("PDPL" in r or "lawful" in r or "بدون" in r for r in out["reasons"])
|
|
|
|
|
|
def test_contactability_unknown_source_needs_review():
|
|
out = score_contactability(
|
|
{"phone": "0500000001", "name": "X"},
|
|
channel="whatsapp",
|
|
)
|
|
assert out["label"] == "needs_review"
|
|
|
|
|
|
def test_contactability_existing_customer_safe():
|
|
out = score_contactability(
|
|
{"phone": "0500000001", "name": "X", "relationship_status": "customer"},
|
|
channel="whatsapp",
|
|
)
|
|
assert out["label"] == "safe"
|
|
|
|
|
|
def test_contactability_inbound_lead_safe():
|
|
out = score_contactability(
|
|
{"phone": "0500000001", "name": "X", "source": "website_form"},
|
|
channel="whatsapp",
|
|
)
|
|
assert out["label"] == "safe"
|
|
|
|
|
|
def test_contactability_summary_aggregates():
|
|
s = contactability_summary(
|
|
[
|
|
{"phone": "0500000001", "relationship_status": "customer"},
|
|
{"phone": "0500000002", "source": "cold"},
|
|
{"phone": "0500000003", "opt_in_status": "opted_out"},
|
|
],
|
|
channel="whatsapp",
|
|
)
|
|
assert s["by_label"]["safe"] >= 1
|
|
assert s["by_label"]["blocked"] >= 2 # cold + opt-out
|
|
|
|
|
|
# ── 7. Targeting + ranking ──────────────────────────────────────
|
|
def test_rank_targets_filters_unsafe():
|
|
contacts = [
|
|
{"phone": "0500000001", "relationship_status": "customer"},
|
|
{"phone": "0500000002", "source": "cold"},
|
|
]
|
|
ranked = rank_targets(contacts, sector_hint="real_estate", channel="whatsapp")
|
|
assert len(ranked) == 1 # only the safe customer survives
|
|
|
|
|
|
def test_recommend_top_10_returns_at_most_10():
|
|
contacts = [
|
|
{"phone": f"05000000{i:02d}", "relationship_status": "customer", "name": f"X{i}"}
|
|
for i in range(15)
|
|
]
|
|
out = recommend_top_10(contacts, sector_hint="real_estate")
|
|
assert out["candidates_evaluated"] == 15
|
|
assert len(out["top"]) == 10
|
|
|
|
|
|
def test_segment_contacts_buckets():
|
|
segs = segment_contacts([
|
|
{"phone": "0500000001", "relationship_status": "customer"},
|
|
{"phone": "0500000002", "source": "exhibition"},
|
|
{"phone": "0500000003", "source": "cold"},
|
|
{"phone": "0500000004", "opt_in_status": "opted_out"},
|
|
])
|
|
assert len(segs["existing_customer"]) == 1
|
|
assert len(segs["event_lead"]) == 1
|
|
assert len(segs["cold_list"]) == 1
|
|
assert len(segs["blocked_or_invalid"]) == 1
|
|
|
|
|
|
# ── 8. Message planner — Arabic + approval invariant ────────────
|
|
def test_arabic_message_always_pending_approval():
|
|
out = draft_arabic_message(
|
|
{"phone": "0500000001", "name": "سامي", "city": "الرياض", "sector": "real_estate"},
|
|
)
|
|
assert out["approval_required"] is True
|
|
assert out["approval_status"] == "pending_approval"
|
|
|
|
|
|
def test_arabic_message_contains_arabic():
|
|
out = draft_arabic_message({"phone": "0500000001", "name": "X", "sector": "clinics"})
|
|
assert any("" <= ch <= "ۿ" for ch in out["body_ar"]), "body must contain Arabic"
|
|
|
|
|
|
def test_arabic_message_no_overhyped_phrases():
|
|
"""The default templates must not contain 'ضمان 100%' / 'مضمونة' etc."""
|
|
out = draft_arabic_message({"phone": "0500000001", "name": "X"})
|
|
body = out["body_ar"]
|
|
for banned in ("ضمان 100", "نتائج مضمونة", "آخر فرصة", "اضغط هنا فوراً"):
|
|
assert banned not in body
|
|
|
|
|
|
def test_followup_returns_pending_approval():
|
|
out = draft_followup({"phone": "0500000001", "name": "X"}, days_since_last=3)
|
|
assert out["approval_required"] is True
|
|
|
|
|
|
def test_objection_response_known():
|
|
out = draft_objection_response("send_offer_whatsapp")
|
|
assert "next_action" in out
|
|
assert out["approval_required"] is True
|
|
|
|
|
|
def test_objection_response_unknown_diagnostic():
|
|
out = draft_objection_response("totally_unknown_objection")
|
|
assert out["next_action"] == "diagnostic_question"
|
|
|
|
|
|
# ── 9. Partnership planner ──────────────────────────────────────
|
|
def test_partnership_suggestions_smb_emphasizes_agencies():
|
|
out = suggest_partner_types(customer_size="smb")
|
|
top_keys = [s["key"] for s in out["suggestions"][:3]]
|
|
# SMB should prioritize agency / consultant / community
|
|
assert any(k in ("marketing_agency", "sales_consultant", "founder_community") for k in top_keys)
|
|
|
|
|
|
def test_partnership_outreach_pending():
|
|
out = draft_partner_outreach(partner_type_key="marketing_agency", partner_name="Test Agency")
|
|
assert out["approval_required"] is True
|
|
|
|
|
|
def test_partnership_outreach_unknown_type():
|
|
out = draft_partner_outreach(partner_type_key="bogus_type")
|
|
assert "error" in out
|
|
|
|
|
|
def test_partner_scorecard_grading():
|
|
high = partner_scorecard(
|
|
partner_id="p1", intros_made=10, deals_influenced=4,
|
|
revenue_share_paid_sar=50_000, relationship_age_months=6,
|
|
)
|
|
low = partner_scorecard(partner_id="p2")
|
|
assert high["overall_score"] > low["overall_score"]
|
|
assert high["tier"] in ("platinum", "gold")
|
|
assert low["tier"] == "bronze"
|
|
|
|
|
|
# ── 10. Meeting planner — no live event ─────────────────────────
|
|
def test_meeting_agenda_returns_slots():
|
|
out = build_meeting_agenda(
|
|
contact_name="سامي", company="Test Co.",
|
|
purpose_ar="تأهيل أولي", duration_minutes=20,
|
|
)
|
|
assert out["agenda_ar"]
|
|
assert out["approval_required"] is True
|
|
|
|
|
|
def test_meeting_calendar_draft_not_inserted():
|
|
out = build_calendar_draft(
|
|
contact_email="x@test.sa", contact_name="X", company="Test Co.",
|
|
duration_minutes=30,
|
|
)
|
|
assert out["live_inserted"] is False
|
|
assert out["approval_required"] is True
|
|
# Required Google Calendar shape
|
|
assert "summary" in out and "start" in out and "end" in out
|
|
assert out["start"]["timeZone"] == "Asia/Riyadh"
|
|
|
|
|
|
def test_post_meeting_followup_pending():
|
|
out = build_post_meeting_followup(
|
|
contact_name="X", company="Test", summary_ar="مهتم في pilot.",
|
|
)
|
|
assert out["approval_required"] is True
|
|
|
|
|
|
# ── 11. Payment offer — no live charge ──────────────────────────
|
|
def test_sar_to_halalas_basic():
|
|
assert sar_to_halalas(1) == 100
|
|
assert sar_to_halalas(2999) == 299_900
|
|
|
|
|
|
def test_sar_to_halalas_negative_raises():
|
|
with pytest.raises(ValueError):
|
|
sar_to_halalas(-5)
|
|
|
|
|
|
def test_payment_draft_does_not_charge():
|
|
out = build_moyasar_payment_link_draft(
|
|
plan_key="growth_os", customer_id="c1", contact_email="x@test.sa",
|
|
)
|
|
assert out["live_charged"] is False
|
|
assert out["approval_required"] is True
|
|
# Moyasar payload uses halalas
|
|
assert out["moyasar_request_draft"]["amount"] == 299_900
|
|
assert out["moyasar_request_draft"]["currency"] == "SAR"
|
|
|
|
|
|
def test_payment_draft_unknown_plan():
|
|
out = build_moyasar_payment_link_draft(plan_key="bogus", customer_id="c1")
|
|
assert "error" in out
|
|
assert out["live_charged"] is False
|
|
|
|
|
|
# ── 12. Proof pack ──────────────────────────────────────────────
|
|
def test_proof_pack_structure():
|
|
out = build_weekly_proof_pack(
|
|
customer_id="c1", customer_name="Test Co.", week_label="W18-2026",
|
|
plan_cost_weekly_sar=750,
|
|
opportunities_discovered=42, messages_drafted=38,
|
|
messages_approved=33, messages_sent=33,
|
|
replies_received=11, positive_replies=4,
|
|
meetings_booked=3, meetings_held=2,
|
|
proposals_sent=1, deals_won=0,
|
|
pipeline_added_sar=185_000, revenue_won_sar=0,
|
|
risky_drafts_blocked=5, revenue_leaks_recovered=2,
|
|
avg_response_minutes=42,
|
|
)
|
|
assert out["grade"] in ("A+", "A", "B", "C", "D")
|
|
assert "activity" in out and "money" in out and "quality" in out
|
|
assert out["next_week_plan_ar"]
|
|
assert "Dealix Proof Pack" in out["markdown_export"]
|
|
|
|
|
|
# ── 13. Missions ────────────────────────────────────────────────
|
|
def test_missions_include_first_10_opportunities():
|
|
out = list_missions()
|
|
ids = {m["id"] for m in out["missions"]}
|
|
assert "first_10_opportunities" in ids
|
|
assert "recover_stalled_deals" in ids
|
|
assert "partnership_sprint" in ids
|
|
assert "safe_whatsapp_campaign" in ids
|
|
assert out["kill_feature_id"] == "first_10_opportunities"
|
|
|
|
|
|
def test_run_mission_known():
|
|
out = run_mission("first_10_opportunities", payload={"sector": "real_estate"})
|
|
assert out["mission_id"] == "first_10_opportunities"
|
|
assert out["next_step_ar"]
|
|
assert out["primary_endpoint"]
|
|
assert out["approval_required"] is True
|
|
|
|
|
|
def test_run_mission_unknown():
|
|
out = run_mission("bogus_mission")
|
|
assert "error" in out
|
|
|
|
|
|
# ── 14. Profile ─────────────────────────────────────────────────
|
|
def test_demo_profile_specialized():
|
|
p = build_demo_profile()
|
|
assert p.is_specialized()
|
|
assert "compliance_rules" in p.to_dict()
|
|
|
|
|
|
def test_profile_from_dict_partial_not_specialized():
|
|
p = profile_from_dict({"customer_id": "c1", "company_name": "X"})
|
|
assert not p.is_specialized()
|
|
|
|
|
|
def test_profile_default_compliance_blocks_keywords():
|
|
p = profile_from_dict({"customer_id": "c1"})
|
|
rules = p.compliance_rules
|
|
assert "blocked_keywords" in rules
|
|
assert "ضمان 100" in rules["blocked_keywords"]
|
|
assert rules["no_cold_whatsapp_without_lawful_basis"] is True
|