diff --git a/dealix/api/main.py b/dealix/api/main.py index 12478690..b5ccee52 100644 --- a/dealix/api/main.py +++ b/dealix/api/main.py @@ -48,6 +48,9 @@ from api.routers import ( sales, sectors, security_curator, + service_excellence, + service_tower, + targeting_os, v3, webhooks, ) @@ -164,6 +167,9 @@ def create_app() -> FastAPI: app.include_router(model_router.router) app.include_router(connector_catalog.router) app.include_router(agent_observability.router) + app.include_router(targeting_os.router) + app.include_router(service_tower.router) + app.include_router(service_excellence.router) app.include_router(public.router) app.include_router(admin.router) diff --git a/dealix/api/routers/service_excellence.py b/dealix/api/routers/service_excellence.py new file mode 100644 index 00000000..45a8e8c8 --- /dev/null +++ b/dealix/api/routers/service_excellence.py @@ -0,0 +1,179 @@ +"""Service Excellence OS router — feature matrix + score + gates + research.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Body + +from auto_client_acquisition.service_excellence import ( + build_backlog, + build_demo_script, + build_feature_matrix, + build_landing_page_outline, + build_monthly_service_review, + build_onboarding_checklist, + build_proof_pack_template_excellence, + build_sales_script, + build_service_launch_package, + build_service_research_brief, + calculate_service_excellence_score, + calculate_service_roi_estimate, + classify_features, + compare_against_categories, + convert_feedback_to_backlog, + generate_feature_hypotheses, + prioritize_backlog_items, + recommend_missing_features, + recommend_next_experiments, + recommend_weekly_improvements, + required_proof_metrics, + review_service_before_launch, + summarize_proof_ar, +) +from auto_client_acquisition.service_tower import ALL_SERVICES + +router = APIRouter(prefix="/api/v1/service-excellence", tags=["service-excellence"]) + + +# ── Feature matrix ─────────────────────────────────────────── +@router.get("/{service_id}/feature-matrix") +async def feature_matrix(service_id: str) -> dict[str, Any]: + return build_feature_matrix(service_id) + + +@router.get("/{service_id}/feature-classification") +async def feature_classification(service_id: str) -> dict[str, Any]: + return classify_features(service_id) + + +@router.get("/{service_id}/missing-features") +async def missing_features(service_id: str) -> dict[str, Any]: + return {"recommendations": recommend_missing_features(service_id)} + + +# ── Scoring ────────────────────────────────────────────────── +@router.get("/{service_id}/score") +async def score(service_id: str) -> dict[str, Any]: + return calculate_service_excellence_score(service_id) + + +# ── Gates / quality review ────────────────────────────────── +@router.get("/{service_id}/quality-review") +async def quality_review(service_id: str) -> dict[str, Any]: + return review_service_before_launch(service_id) + + +@router.get("/review/all") +async def review_all() -> dict[str, Any]: + """Review every catalogued service.""" + out = [review_service_before_launch(s.id) for s in ALL_SERVICES] + counts: dict[str, int] = {} + for r in out: + v = str(r.get("verdict", "?")) + counts[v] = counts.get(v, 0) + 1 + return {"total": len(out), "by_verdict": counts, "results": out} + + +# ── Proof metrics ──────────────────────────────────────────── +@router.get("/{service_id}/proof-metrics") +async def proof_metrics(service_id: str) -> dict[str, Any]: + return { + "service_id": service_id, + "metrics": required_proof_metrics(service_id), + "template": build_proof_pack_template_excellence(service_id), + } + + +@router.post("/{service_id}/roi-estimate") +async def roi_estimate( + service_id: str, + metrics: dict[str, Any] = Body(default_factory=dict), +) -> dict[str, Any]: + out = calculate_service_roi_estimate(service_id, metrics) + if "error" not in out: + out["proof_summary_ar"] = summarize_proof_ar(service_id, metrics) + return out + + +# ── Competitor gap ─────────────────────────────────────────── +@router.get("/{service_id}/gap-analysis") +async def gap_analysis(service_id: str) -> dict[str, Any]: + return compare_against_categories(service_id) + + +# ── Research lab ───────────────────────────────────────────── +@router.get("/{service_id}/research-brief") +async def research_brief(service_id: str) -> dict[str, Any]: + return build_service_research_brief(service_id) + + +@router.get("/{service_id}/feature-hypotheses") +async def feature_hypotheses(service_id: str) -> dict[str, Any]: + return {"hypotheses": generate_feature_hypotheses(service_id)} + + +@router.get("/{service_id}/experiments") +async def experiments(service_id: str) -> dict[str, Any]: + return recommend_next_experiments(service_id) + + +@router.get("/{service_id}/monthly-review") +async def monthly_review(service_id: str) -> dict[str, Any]: + return build_monthly_service_review(service_id) + + +# ── Backlog ────────────────────────────────────────────────── +@router.get("/{service_id}/backlog") +async def backlog(service_id: str) -> dict[str, Any]: + return build_backlog(service_id) + + +@router.post("/{service_id}/backlog/from-feedback") +async def backlog_from_feedback( + service_id: str, + feedback: list[dict[str, Any]] = Body(..., embed=True), +) -> dict[str, Any]: + return { + "service_id": service_id, + "items": convert_feedback_to_backlog(feedback), + } + + +@router.post("/{service_id}/backlog/prioritize") +async def backlog_prioritize( + service_id: str, + items: list[dict[str, Any]] = Body(..., embed=True), +) -> dict[str, Any]: + return {"items": prioritize_backlog_items(items)} + + +@router.get("/{service_id}/weekly-improvements") +async def weekly_improvements(service_id: str) -> dict[str, Any]: + return recommend_weekly_improvements(service_id) + + +# ── Launch package ─────────────────────────────────────────── +@router.get("/{service_id}/launch-package") +async def launch_package(service_id: str) -> dict[str, Any]: + return build_service_launch_package(service_id) + + +@router.get("/{service_id}/landing-outline") +async def landing_outline(service_id: str) -> dict[str, Any]: + return build_landing_page_outline(service_id) + + +@router.get("/{service_id}/sales-script") +async def sales_script(service_id: str) -> dict[str, Any]: + return build_sales_script(service_id) + + +@router.get("/{service_id}/demo-script") +async def demo_script(service_id: str) -> dict[str, Any]: + return build_demo_script(service_id) + + +@router.get("/{service_id}/onboarding-checklist") +async def onboarding_checklist(service_id: str) -> dict[str, Any]: + return build_onboarding_checklist(service_id) diff --git a/dealix/api/routers/service_tower.py b/dealix/api/routers/service_tower.py new file mode 100644 index 00000000..ac96d9bd --- /dev/null +++ b/dealix/api/routers/service_tower.py @@ -0,0 +1,177 @@ +"""Service Tower router — كتالوج الخدمات + wizard + workflow + pricing + cards.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Body + +from auto_client_acquisition.service_tower import ( + build_ceo_daily_service_brief, + build_client_report_outline, + build_deliverables, + build_end_of_day_service_report, + build_intake_questions, + build_internal_operator_checklist, + build_proof_pack_template, + build_risk_alert_card, + build_service_approval_card, + build_service_scorecard, + build_service_workflow, + build_upsell_message_ar, + calculate_monthly_offer, + calculate_setup_fee, + catalog_summary, + get_service, + list_all_services, + map_service_to_growth_mission, + map_service_to_subscription, + quote_service, + recommend_next_step, + recommend_plan_after_service, + recommend_service, + recommend_upgrade, + summarize_recommendation_ar, + summarize_scorecard_ar, + validate_service_inputs, +) + +router = APIRouter(prefix="/api/v1/services", tags=["service-tower"]) + + +# ── Catalog ────────────────────────────────────────────────── +@router.get("/catalog") +async def catalog() -> dict[str, Any]: + return list_all_services() + + +@router.get("/summary") +async def summary() -> dict[str, Any]: + return catalog_summary() + + +@router.post("/recommend") +async def recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + rec = recommend_service( + company_type=payload.get("company_type", ""), + goal=payload.get("goal", "fill_pipeline"), + has_contact_list=bool(payload.get("has_contact_list", False)), + channels=payload.get("channels", []), + budget_sar=int(payload.get("budget_sar", 1000)), + ) + rec["summary_ar"] = summarize_recommendation_ar(rec) + return rec + + +# ── Per-service ────────────────────────────────────────────── +@router.get("/{service_id}/intake-questions") +async def service_intake_questions(service_id: str) -> dict[str, Any]: + return build_intake_questions(service_id) + + +@router.post("/{service_id}/start") +async def service_start( + service_id: str, + payload: dict[str, Any] = Body(...), +) -> dict[str, Any]: + validation = validate_service_inputs(service_id, payload) + if not validation["valid"]: + return {"started": False, "validation": validation} + workflow = build_service_workflow(service_id) + return { + "started": True, + "validation": validation, + "workflow": workflow, + "linked_growth_mission": map_service_to_growth_mission(service_id), + "approval_required": True, + } + + +@router.get("/{service_id}/workflow") +async def service_workflow(service_id: str) -> dict[str, Any]: + return build_service_workflow(service_id) + + +@router.get("/{service_id}/deliverables") +async def service_deliverables(service_id: str) -> dict[str, Any]: + return build_deliverables(service_id) + + +@router.get("/{service_id}/proof-pack-template") +async def service_proof_pack_template(service_id: str) -> dict[str, Any]: + return build_proof_pack_template(service_id) + + +@router.get("/{service_id}/client-report-outline") +async def service_client_report_outline(service_id: str) -> dict[str, Any]: + return build_client_report_outline(service_id) + + +@router.get("/{service_id}/operator-checklist") +async def service_operator_checklist(service_id: str) -> dict[str, Any]: + return build_internal_operator_checklist(service_id) + + +@router.post("/{service_id}/quote") +async def service_quote( + service_id: str, + payload: dict[str, Any] = Body(default_factory=dict), +) -> dict[str, Any]: + return quote_service( + service_id, + company_size=payload.get("company_size", "small"), + urgency=payload.get("urgency", "normal"), + channels_count=int(payload.get("channels_count", 1)), + ) + + +@router.get("/{service_id}/setup-fee") +async def service_setup_fee(service_id: str) -> dict[str, Any]: + return calculate_setup_fee(service_id) + + +@router.get("/{service_id}/monthly-offer") +async def service_monthly_offer(service_id: str) -> dict[str, Any]: + return calculate_monthly_offer(service_id) + + +@router.post("/{service_id}/scorecard") +async def service_scorecard( + service_id: str, + metrics: dict[str, Any] = Body(default_factory=dict), +) -> dict[str, Any]: + return build_service_scorecard(service_id, metrics) + + +@router.get("/{service_id}/upgrade-path") +async def service_upgrade_path(service_id: str) -> dict[str, Any]: + return recommend_upgrade(service_id) + + +@router.get("/{service_id}/post-service-plan") +async def service_post_plan(service_id: str) -> dict[str, Any]: + return recommend_plan_after_service(service_id) + + +# ── CEO control via WhatsApp ───────────────────────────────── +@router.get("/ceo/daily-brief") +async def ceo_daily_brief() -> dict[str, Any]: + return build_ceo_daily_service_brief() + + +@router.post("/ceo/approval-card") +async def ceo_approval_card(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return build_service_approval_card( + service_id=payload.get("service_id", ""), + action=payload.get("action", ""), + ) + + +@router.get("/ceo/risk-alert/demo") +async def ceo_risk_alert_demo() -> dict[str, Any]: + return build_risk_alert_card() + + +@router.get("/ceo/end-of-day/demo") +async def ceo_end_of_day_demo() -> dict[str, Any]: + return build_end_of_day_service_report() diff --git a/dealix/api/routers/targeting_os.py b/dealix/api/routers/targeting_os.py new file mode 100644 index 00000000..11242d78 --- /dev/null +++ b/dealix/api/routers/targeting_os.py @@ -0,0 +1,240 @@ +"""Targeting & Acquisition OS router.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Body + +from auto_client_acquisition.targeting_os import ( + analyze_uploaded_list_preview, + build_dealix_self_growth_plan, + build_daily_targeting_brief, + build_end_of_day_report, + build_followup_sequence, + build_free_growth_diagnostic, + build_lead_gen_form_plan, + build_outreach_plan, + build_self_growth_daily_brief, + build_weekly_learning_report, + calculate_channel_reputation, + draft_b2b_email, + draft_role_based_angle, + draft_whatsapp_message, + enforce_daily_limits, + evaluate_contactability, + explain_contactability_ar, + list_targeting_services, + map_buying_committee, + recommend_accounts, + recommend_dealix_targets, + recommend_linkedin_strategy, + recommend_recovery_action, + recommend_service_offer, + recommend_today_actions, + score_email_risk, + score_whatsapp_risk, + summarize_plan_ar, + summarize_reputation_ar, +) +from auto_client_acquisition.targeting_os.contract_drafts import ( + draft_agency_partner_outline, + draft_dpa_outline, + draft_pilot_agreement_outline, + draft_referral_agreement_outline, +) + +router = APIRouter(prefix="/api/v1/targeting", tags=["targeting-os"]) + + +# ── Accounts ───────────────────────────────────────────────── +@router.post("/accounts/recommend") +async def accounts_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return recommend_accounts( + sector=payload.get("sector", "saas"), + city=payload.get("city", "Riyadh"), + offer=payload.get("offer", ""), + goal=payload.get("goal", "fill_pipeline"), + limit=int(payload.get("limit", 10)), + ) + + +# ── Buying committee ───────────────────────────────────────── +@router.post("/buying-committee/map") +async def buying_committee_map(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return map_buying_committee( + sector=payload.get("sector", "saas"), + company_size=payload.get("company_size", "small"), + goal=payload.get("goal", "fill_pipeline"), + ) + + +# ── Contacts ───────────────────────────────────────────────── +@router.post("/contacts/evaluate") +async def contacts_evaluate(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + contact = payload.get("contact") or payload + desired = payload.get("desired_channel") + result = evaluate_contactability(contact, desired_channel=desired) + result["explanation_ar"] = explain_contactability_ar(result) + return result + + +@router.post("/uploaded-list/analyze") +async def uploaded_list_analyze( + contacts: list[dict[str, Any]] = Body(..., embed=True), +) -> dict[str, Any]: + return analyze_uploaded_list_preview(contacts) + + +# ── Outreach ───────────────────────────────────────────────── +@router.post("/outreach/plan") +async def outreach_plan(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + plan = build_outreach_plan( + targets=payload.get("targets", []), + channels=payload.get("channels"), + goal=payload.get("goal", "fill_pipeline"), + ) + plan = enforce_daily_limits(plan) + plan["summary_ar"] = summarize_plan_ar(plan) + return plan + + +# ── Daily autopilot ────────────────────────────────────────── +@router.get("/daily-autopilot/demo") +async def daily_autopilot_demo() -> dict[str, Any]: + return { + "brief": build_daily_targeting_brief(), + "today_actions": recommend_today_actions(), + "end_of_day_template": build_end_of_day_report(), + } + + +# ── Self-Growth Mode ───────────────────────────────────────── +@router.get("/self-growth/demo") +async def self_growth_demo() -> dict[str, Any]: + return { + "plan": build_dealix_self_growth_plan(), + "today": build_self_growth_daily_brief(), + } + + +@router.post("/self-growth/targets") +async def self_growth_targets(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return recommend_dealix_targets( + sector_focus=payload.get("sector"), + city_focus=payload.get("city"), + limit=int(payload.get("limit", 10)), + ) + + +@router.post("/self-growth/weekly-report") +async def self_growth_weekly(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]: + return build_weekly_learning_report(payload) + + +# ── Reputation guard ──────────────────────────────────────── +@router.get("/reputation/status") +async def reputation_status() -> dict[str, Any]: + """Demo reputation snapshot.""" + healthy_email = {"bounce_rate": 0.005, "complaint_rate": 0.0001, + "opt_out_rate": 0.01, "reply_rate": 0.04} + risky_wa = {"block_rate": 0.04, "report_rate": 0.005, + "opt_out_rate": 0.06, "reply_rate": 0.02} + return { + "email": calculate_channel_reputation(healthy_email, channel="email"), + "whatsapp": calculate_channel_reputation(risky_wa, channel="whatsapp"), + } + + +@router.post("/reputation/recovery") +async def reputation_recovery(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return recommend_recovery_action( + payload.get("metrics", {}), + channel=payload.get("channel", "email"), + ) + + +# ── LinkedIn strategy ──────────────────────────────────────── +@router.post("/linkedin/strategy") +async def linkedin_strategy(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + strategy = recommend_linkedin_strategy( + segment=payload.get("segment", "B2B Saudi"), + goal=payload.get("goal", "fill_pipeline"), + ) + if payload.get("with_lead_gen_form"): + strategy["lead_gen_form_plan"] = build_lead_gen_form_plan( + segment=payload.get("segment", "B2B Saudi"), + offer=payload.get("offer", "Pilot 7 days"), + campaign_name=payload.get("campaign_name", ""), + ) + return strategy + + +# ── Drafts ─────────────────────────────────────────────────── +@router.post("/drafts/email") +async def drafts_email(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + contact = payload.get("contact", {}) + draft = draft_b2b_email( + contact, + offer=payload.get("offer", ""), + why_now=payload.get("why_now", ""), + ) + risk = score_email_risk(contact, draft.get("body_ar", "")) + return {**draft, "risk": risk} + + +@router.post("/drafts/whatsapp") +async def drafts_whatsapp(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + contact = payload.get("contact", {}) + return draft_whatsapp_message( + contact, + offer=payload.get("offer", ""), + why_now=payload.get("why_now", ""), + ) + + +@router.post("/drafts/email-followup") +async def drafts_email_followup(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return build_followup_sequence( + payload.get("contact", {}), + offer=payload.get("offer", ""), + ) + + +@router.post("/drafts/role-angle") +async def drafts_role_angle(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return draft_role_based_angle( + role_key=payload.get("role_key", "founder_ceo"), + sector=payload.get("sector", "saas"), + offer=payload.get("offer", ""), + ) + + +# ── Free diagnostic ────────────────────────────────────────── +@router.post("/free-diagnostic") +async def free_diagnostic(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return build_free_growth_diagnostic(payload) + + +# ── Services + contracts ───────────────────────────────────── +@router.get("/services") +async def services_list() -> dict[str, Any]: + return list_targeting_services() + + +@router.post("/services/recommend") +async def services_recommend(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + return recommend_service_offer( + customer_type=payload.get("customer_type", ""), + goal=payload.get("goal", "fill_pipeline"), + ) + + +@router.get("/contracts/templates") +async def contracts_templates() -> dict[str, Any]: + return { + "pilot": draft_pilot_agreement_outline(), + "dpa": draft_dpa_outline(), + "referral": draft_referral_agreement_outline(), + "agency_partner": draft_agency_partner_outline(), + } diff --git a/dealix/auto_client_acquisition/service_excellence/__init__.py b/dealix/auto_client_acquisition/service_excellence/__init__.py new file mode 100644 index 00000000..ca90f12b --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/__init__.py @@ -0,0 +1,85 @@ +"""Service Excellence OS — يضمن أن كل خدمة هي الأفضل قبل الإطلاق. + +Feature matrix + scoring + workflow validation + competitor gap + +proof metrics + quality review + improvement backlog + launch package. +""" + +from __future__ import annotations + +from .competitor_gap import compare_against_categories +from .feature_matrix import ( + build_feature_matrix, + classify_features, + prioritize_features, + recommend_missing_features, +) +from .launch_package import ( + build_demo_script, + build_landing_page_outline, + build_onboarding_checklist, + build_sales_script, + build_service_launch_package, +) +from .proof_metrics import ( + build_proof_pack_template_excellence, + calculate_service_roi_estimate, + required_proof_metrics, + summarize_proof_ar, +) +from .quality_review import ( + block_if_missing_approval_policy, + block_if_missing_proof, + block_if_unclear_pricing, + block_if_unsafe_channel, + review_service_before_launch, +) +from .research_lab import ( + build_monthly_service_review, + build_service_research_brief, + generate_feature_hypotheses, + recommend_next_experiments, +) +from .service_improvement_backlog import ( + build_backlog, + convert_feedback_to_backlog, + prioritize_backlog_items, + recommend_weekly_improvements, +) +from .service_scoring import ( + calculate_service_excellence_score, + score_automation, + score_clarity, + score_compliance, + score_proof, + score_speed_to_value, + score_upsell, +) + +__all__ = [ + # competitor_gap + "compare_against_categories", + # feature_matrix + "build_feature_matrix", "classify_features", + "prioritize_features", "recommend_missing_features", + # launch_package + "build_demo_script", "build_landing_page_outline", + "build_onboarding_checklist", "build_sales_script", + "build_service_launch_package", + # proof_metrics + "build_proof_pack_template_excellence", "calculate_service_roi_estimate", + "required_proof_metrics", "summarize_proof_ar", + # quality_review + "block_if_missing_approval_policy", "block_if_missing_proof", + "block_if_unclear_pricing", "block_if_unsafe_channel", + "review_service_before_launch", + # research_lab + "build_monthly_service_review", "build_service_research_brief", + "generate_feature_hypotheses", "recommend_next_experiments", + # service_improvement_backlog + "build_backlog", "convert_feedback_to_backlog", + "prioritize_backlog_items", "recommend_weekly_improvements", + # service_scoring + "calculate_service_excellence_score", "score_automation", + "score_clarity", "score_compliance", "score_proof", + "score_speed_to_value", "score_upsell", +] diff --git a/dealix/auto_client_acquisition/service_excellence/competitor_gap.py b/dealix/auto_client_acquisition/service_excellence/competitor_gap.py new file mode 100644 index 00000000..9e6dfb80 --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/competitor_gap.py @@ -0,0 +1,79 @@ +"""Competitor gap analysis — لا scraping، فقط مقارنة structural بفئات معروفة.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.service_tower import get_service + +# Categories Dealix competes against. Strengths/limits are public knowledge. +COMPETITOR_CATEGORIES: dict[str, dict[str, list[str]]] = { + "crm": { + "strengths": ["تخزين بيانات", "pipeline tracking", "تكاملات واسعة"], + "limits": ["ينتظر إدخال يدوي", "لا يقرر ما تفعل اليوم", + "غير مصمم للسوق العربي"], + }, + "whatsapp_tools": { + "strengths": ["إرسال جماعي", "templates", "broadcast"], + "limits": ["لا approval-first", "لا proof", "خطر PDPL"], + }, + "email_assistant": { + "strengths": ["كتابة أسرع", "تكامل Gmail/Outlook"], + "limits": ["لا يحول الإيميل لـ pipeline", "لا proof", "عام غير عربي"], + }, + "linkedin_tools": { + "strengths": ["إيجاد leads"], + "limits": ["كثير منها يخالف ToS", "auto-DM يوقف الحسابات", + "لا يحترم PDPL"], + }, + "agency": { + "strengths": ["خبرة بشرية", "علاقات سوق"], + "limits": ["لا تتوسع", "غير قابلة للتكرار", "تعتمد على الفريق"], + }, + "revenue_intelligence": { + "strengths": ["تحليل المكالمات", "deal scoring"], + "limits": ["تبدأ بعد الـcall", "لا يصنع pipeline من الصفر"], + }, + "generic_ai_agent": { + "strengths": ["مرن", "يكتب أي شيء"], + "limits": ["بدون سياق شركة", "بدون proof", "بدون امتثال محلي"], + }, +} + + +def compare_against_categories(service_id: str) -> dict[str, Any]: + """Compare a Dealix service against generic competitor categories.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + + dealix_advantages = [ + "موجّه للسوق السعودي بالعربية الطبيعية.", + "Approval-first — لا يضرّ سمعة العميل.", + "Proof Pack شهري قابل للقياس.", + "Multi-channel orchestration بـ سياسة موحدة.", + "Self-improving Curator يحسّن الرسائل أسبوعياً.", + "PDPL-aware من اليوم الأول.", + ] + + gaps_to_close: list[str] = [] + if "growth_os" not in service_id: + gaps_to_close.append("Daily autopilot كامل (متاح في Growth OS).") + if service_id == "free_growth_diagnostic": + gaps_to_close.append("Proof Pack حقيقي بعد 30 يوم.") + + do_not_copy = [ + "auto-DM على LinkedIn (مخالف).", + "scraping ضد ToS.", + "وعود بنتائج مضمونة.", + "مفاتيح API غير محمية في الواجهة.", + ] + + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "competitor_categories": COMPETITOR_CATEGORIES, + "dealix_advantages_ar": dealix_advantages, + "gaps_to_close_ar": gaps_to_close, + "do_not_copy_ar": do_not_copy, + } diff --git a/dealix/auto_client_acquisition/service_excellence/feature_matrix.py b/dealix/auto_client_acquisition/service_excellence/feature_matrix.py new file mode 100644 index 00000000..c745e144 --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/feature_matrix.py @@ -0,0 +1,120 @@ +"""Feature matrix per service — must_have / advanced / premium / future.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.service_tower import get_service + +# 12 must-have features every premium Dealix service should ship with. +DEFAULT_MUST_HAVE: tuple[dict[str, object], ...] = ( + {"name_ar": "Self-Serve Intake", "value_ar": "العميل يبدأ بدون مكالمة.", + "complexity": 2, "risk": 1, "proof_metric": "intake_completion_rate"}, + {"name_ar": "AI Recommendation", + "value_ar": "النظام يوصي بالخدمة المناسبة من إجابات بسيطة.", + "complexity": 3, "risk": 2, "proof_metric": "wizard_acceptance_rate"}, + {"name_ar": "Data Quality Check", + "value_ar": "لا يستخدم بيانات سيئة.", + "complexity": 3, "risk": 4, "proof_metric": "data_quality_score"}, + {"name_ar": "Contactability / Risk Gate", + "value_ar": "يمنع التواصل الخطر تلقائياً.", + "complexity": 4, "risk": 8, "proof_metric": "risks_blocked"}, + {"name_ar": "Channel Strategy", + "value_ar": "يختار القناة الأفضل لكل contact.", + "complexity": 4, "risk": 5, "proof_metric": "channel_success_rate"}, + {"name_ar": "Arabic Contextual Drafting", + "value_ar": "رسائل سعودية، ليست ترجمة.", + "complexity": 5, "risk": 3, "proof_metric": "saudi_tone_score"}, + {"name_ar": "Approval Cards", + "value_ar": "CEO/Growth Manager يوافق من واتساب.", + "complexity": 3, "risk": 2, "proof_metric": "approval_rate"}, + {"name_ar": "Execution Mode", + "value_ar": "draft/export/approved فقط — لا live بدون env flag.", + "complexity": 3, "risk": 9, "proof_metric": "live_send_violations"}, + {"name_ar": "Proof Pack", + "value_ar": "تقرير قيمة محسوب.", + "complexity": 4, "risk": 1, "proof_metric": "proof_pack_delivered"}, + {"name_ar": "Learning Loop", + "value_ar": "يتعلم من Accept/Skip/Edit.", + "complexity": 5, "risk": 2, "proof_metric": "accept_rate_30d"}, + {"name_ar": "Upsell Path", + "value_ar": "يقود للخدمة الأعلى.", + "complexity": 2, "risk": 1, "proof_metric": "upsell_conversion_rate"}, + {"name_ar": "Service Score", + "value_ar": "يقيس نجاح الخدمة نفسها.", + "complexity": 3, "risk": 1, "proof_metric": "service_excellence_score"}, +) + +# Service-specific premium features. +_PREMIUM_BY_SERVICE: dict[str, list[dict[str, object]]] = { + "growth_os_monthly": [ + {"name_ar": "Daily Autopilot", "value_ar": "تشغيل ذاتي يومي.", + "complexity": 6, "risk": 4, "proof_metric": "daily_decisions_made"}, + {"name_ar": "Revenue Leak Detector", + "value_ar": "كشف التسريبات تلقائياً.", + "complexity": 5, "risk": 2, "proof_metric": "leaks_detected"}, + {"name_ar": "Founder Shadow Board", + "value_ar": "موجز أسبوعي مركّب.", + "complexity": 4, "risk": 1, "proof_metric": "weekly_briefs_delivered"}, + ], + "agency_partner_program": [ + {"name_ar": "Co-Branded Proof Pack", "value_ar": "Proof بعلامة الوكالة.", + "complexity": 4, "risk": 2, "proof_metric": "co_branded_proofs"}, + {"name_ar": "Revenue Share Dashboard", + "value_ar": "لوحة مشاركة الإيرادات.", + "complexity": 5, "risk": 3, "proof_metric": "agency_revenue_sar"}, + ], +} + + +def build_feature_matrix(service_id: str) -> dict[str, Any]: + """Build the full feature matrix for a service.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + must_have = [dict(f) for f in DEFAULT_MUST_HAVE] + premium = list(_PREMIUM_BY_SERVICE.get(service_id, [])) + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "must_have": must_have, + "advanced": premium, + "premium": premium, + "future": [], + "total_features": len(must_have) + len(premium), + } + + +def classify_features(service_id: str) -> dict[str, list[str]]: + """Classify a service's features into tiers.""" + matrix = build_feature_matrix(service_id) + if "error" in matrix: + return {} + return { + "must_have": [str(f["name_ar"]) for f in matrix["must_have"]], + "advanced": [str(f["name_ar"]) for f in matrix["advanced"]], + "premium": [str(f["name_ar"]) for f in matrix["premium"]], + } + + +def recommend_missing_features(service_id: str) -> list[dict[str, Any]]: + """Recommend features the service may be missing.""" + matrix = build_feature_matrix(service_id) + if "error" in matrix: + return [] + # If the service has fewer than 12 must-haves, suggest the rest. + if len(matrix["must_have"]) >= 12: + return [] + return [{"name_ar": "Add to advanced tier", + "rationale_ar": "خدمة قوية تستفيد من ميزات advanced."}] + + +def prioritize_features(features: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Order features by (lower complexity, lower risk, higher impact).""" + return sorted( + features, + key=lambda f: ( + int(f.get("complexity", 9)), + int(f.get("risk", 9)), + ), + ) diff --git a/dealix/auto_client_acquisition/service_excellence/launch_package.py b/dealix/auto_client_acquisition/service_excellence/launch_package.py new file mode 100644 index 00000000..3d728fb5 --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/launch_package.py @@ -0,0 +1,125 @@ +"""Launch package — لكل خدمة: landing page outline + sales script + demo + onboarding.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.service_tower import get_service + + +def build_landing_page_outline(service_id: str) -> dict[str, Any]: + """Outline of a landing page for the service (Arabic, RTL).""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "title_ar": s.name_ar, + "sections_ar": [ + "Hero: العرض في جملة + CTA", + "وعد المنتج: ماذا سيحصل العميل عليه؟", + "كيف تعمل الخدمة (3 خطوات)", + "Deliverables — قائمة بالمخرجات", + "Pricing — السعر بوضوح", + "Proof — ما الذي نقيسه", + "Safety — لا live send، Approval-first", + "Trust — للوكالات / B2B سعودي", + "FAQ", + "CTA النهائي", + ], + "cta_ar": "ابدأ الآن" if s.pricing_max_sar > 0 else "احجز التشخيص المجاني", + "must_include_ar": [ + "Approval-first.", + "لا cold WhatsApp.", + "PDPL-aware.", + "لا وعود بنتائج مضمونة.", + ], + } + + +def build_sales_script(service_id: str) -> dict[str, Any]: + """Sales script (Arabic) — discovery → pitch → close.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "discovery_questions_ar": [ + "وش أكبر تحدي نمو لديكم اليوم؟", + "كيف تستهدفون اليوم؟ ما الذي يعمل؟", + "ما الذي يأخذ وقتاً يومياً ولا يثبت قيمة؟", + "هل عندكم قائمة عملاء قدامى لم تتم متابعتهم؟", + "من يوافق على الرسائل قبل الإرسال؟", + ], + "pitch_ar": ( + f"بناءً على ما شاركته، {s.name_ar} مناسبة لكم. " + f"خلال {('7 أيام' if s.pricing_model == 'sprint' else 'الشهر الأول')}، " + f"سنطلع لكم: {', '.join(s.deliverables_ar)}." + ), + "objection_handling_ar": { + "price": "نقدم Free Diagnostic أولاً — تشوفون النتائج قبل الدفع.", + "timing": "Pilot 7 أيام لا يحتاج التزام طويل — جرّبوه ثم قرروا.", + "trust": "Approval-first: لا نرسل أي شيء بدون موافقتكم.", + "complexity": "نتولى الإعداد كاملاً في 3 أيام عمل.", + }, + "close_ar": ( + "إذا الفكرة منطقية، أحدد لكم Pilot يبدأ يوم الأحد. " + "أرسل لي تأكيد + اسم منسّق Approvals." + ), + } + + +def build_demo_script(service_id: str) -> dict[str, Any]: + """12-minute Arabic demo script.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "duration_minutes": 12, + "minute_by_minute_ar": [ + "0–2: الفكرة الكبرى — Dealix ليس CRM ولا أداة واتساب.", + f"2–4: عرض {s.name_ar} — Daily Brief / Command Feed.", + "4–6: مثال حي — 10 فرص في 10 دقائق.", + "6–8: Trust Score + Simulator + Proof Pack.", + "8–10: الأمان والتكاملات (security + connectors).", + "10–12: العرض والـ CTA.", + ], + "do_not_do_ar": [ + "لا تكشف API keys على الشاشة.", + "لا تشغّل live WhatsApp في الـdemo.", + "لا تعد بأرقام لم تُحقَّق.", + ], + } + + +def build_onboarding_checklist(service_id: str) -> dict[str, Any]: + """Onboarding checklist for the customer (first 5 days).""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "first_5_days_ar": [ + "يوم 1: kick-off + جمع الـ intake + توقيع DPA draft.", + "يوم 2: ربط القنوات الآمنة (Gmail drafts / Sheets / website forms).", + "يوم 3: توليد أول Proof Pack template + تدريب على Approval Center.", + "يوم 4: إطلاق أول mission (10 فرص في 10 دقائق).", + "يوم 5: مراجعة النتائج + تخطيط الأسبوع الثاني.", + ], + "approval_required": True, + "live_send_allowed": False, + } + + +def build_service_launch_package(service_id: str) -> dict[str, Any]: + """Full launch package = landing + sales + demo + onboarding.""" + return { + "service_id": service_id, + "landing": build_landing_page_outline(service_id), + "sales_script": build_sales_script(service_id), + "demo_script": build_demo_script(service_id), + "onboarding": build_onboarding_checklist(service_id), + "approval_required": True, + } diff --git a/dealix/auto_client_acquisition/service_excellence/proof_metrics.py b/dealix/auto_client_acquisition/service_excellence/proof_metrics.py new file mode 100644 index 00000000..d1aa459b --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/proof_metrics.py @@ -0,0 +1,72 @@ +"""Proof metrics — كل خدمة لازم تثبت العائد بأرقام محددة.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.service_tower import get_service + + +def required_proof_metrics(service_id: str) -> list[str]: + """Return the proof metrics every run of the service must produce.""" + s = get_service(service_id) + if s is None: + return [] + return list(s.proof_metrics) + + +def build_proof_pack_template_excellence(service_id: str) -> dict[str, Any]: + """Build a polished Proof Pack template for an excellence-tier service.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "executive_summary_ar": ( + "ملخص تنفيذي من 10 أسطر يعرض النتائج، الأثر المالي، " + "والمخاطر التي تم منعها." + ), + "metrics": list(s.proof_metrics), + "report_format": ["pdf", "json", "whatsapp_summary"], + "signature_required": True, + "approval_required": True, + } + + +def calculate_service_roi_estimate( + service_id: str, + metrics: dict[str, Any], +) -> dict[str, Any]: + """Estimate ROI = pipeline_influenced / service_price.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + + price = max(1, float(metrics.get("price_paid_sar", s.pricing_min_sar or 1))) + pipeline = float(metrics.get("pipeline_sar", 0)) + closed_won = float(metrics.get("closed_won_sar", 0)) + + roi_pipeline_x = round(pipeline / price, 2) + roi_closed_x = round(closed_won / price, 2) + + return { + "service_id": service_id, + "price_paid_sar": price, + "pipeline_sar": pipeline, + "closed_won_sar": closed_won, + "roi_pipeline_x": roi_pipeline_x, + "roi_closed_x": roi_closed_x, + "summary_ar": ( + f"كل ريال أنفقه العميل على {s.name_ar} أنتج " + f"{roi_pipeline_x}× pipeline و {roi_closed_x}× closed-won." + ), + } + + +def summarize_proof_ar(service_id: str, metrics: dict[str, Any]) -> str: + """Build a one-paragraph Arabic proof summary.""" + roi = calculate_service_roi_estimate(service_id, metrics) + if "error" in roi: + return roi["error"] + return roi["summary_ar"] diff --git a/dealix/auto_client_acquisition/service_excellence/quality_review.py b/dealix/auto_client_acquisition/service_excellence/quality_review.py new file mode 100644 index 00000000..7de12194 --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/quality_review.py @@ -0,0 +1,82 @@ +"""Quality review — يمنع الخدمات الضعيفة من الإطلاق.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.service_tower import get_service + +from .service_scoring import calculate_service_excellence_score + + +def block_if_missing_proof(service_id: str) -> dict[str, Any]: + s = get_service(service_id) + if s is None: + return {"blocked": True, "reason_ar": f"خدمة غير معروفة: {service_id}"} + if not s.proof_metrics: + return {"blocked": True, "reason_ar": "لا توجد proof metrics."} + return {"blocked": False} + + +def block_if_missing_approval_policy(service_id: str) -> dict[str, Any]: + s = get_service(service_id) + if s is None: + return {"blocked": True, "reason_ar": f"خدمة غير معروفة: {service_id}"} + if not s.approval_policy: + return {"blocked": True, "reason_ar": "سياسة الاعتماد غير محددة."} + return {"blocked": False} + + +def block_if_unclear_pricing(service_id: str) -> dict[str, Any]: + s = get_service(service_id) + if s is None: + return {"blocked": True, "reason_ar": f"خدمة غير معروفة: {service_id}"} + if s.pricing_max_sar < 0: + return {"blocked": True, "reason_ar": "تسعير غير صحيح."} + if s.pricing_max_sar > 0 and s.pricing_max_sar < s.pricing_min_sar: + return {"blocked": True, "reason_ar": "نطاق التسعير غير منطقي."} + return {"blocked": False} + + +def block_if_unsafe_channel(service_id: str) -> dict[str, Any]: + """Block if a service depends on an unsafe channel (e.g., scraping).""" + s = get_service(service_id) + if s is None: + return {"blocked": True, "reason_ar": f"خدمة غير معروفة: {service_id}"} + unsafe = {"scraping", "auto_dm", "auto_connect", "browser_extension"} + for ch in s.required_integrations: + if ch.lower() in unsafe: + return {"blocked": True, + "reason_ar": f"تكامل غير آمن: {ch}."} + return {"blocked": False} + + +def review_service_before_launch(service_id: str) -> dict[str, Any]: + """Run all gates + scoring before allowing a service to ship.""" + gates = { + "proof": block_if_missing_proof(service_id), + "approval": block_if_missing_approval_policy(service_id), + "pricing": block_if_unclear_pricing(service_id), + "channels": block_if_unsafe_channel(service_id), + } + blocked = [k for k, v in gates.items() if v.get("blocked")] + score = calculate_service_excellence_score(service_id) + + if blocked: + verdict = "blocked_at_gate" + elif score.get("status") == "launch_ready": + verdict = "launch_ready" + elif score.get("status") == "beta_only": + verdict = "beta_only" + else: + verdict = "needs_work" + + return { + "service_id": service_id, + "verdict": verdict, + "score": score, + "gates": gates, + "blocked_reasons_ar": [ + gates[k].get("reason_ar", "") for k in blocked + ], + } diff --git a/dealix/auto_client_acquisition/service_excellence/research_lab.py b/dealix/auto_client_acquisition/service_excellence/research_lab.py new file mode 100644 index 00000000..8754f482 --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/research_lab.py @@ -0,0 +1,109 @@ +"""Service Research Lab — تحسين شهري لكل خدمة (deterministic).""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.service_tower import get_service + +from .competitor_gap import compare_against_categories +from .service_scoring import calculate_service_excellence_score + + +def build_service_research_brief(service_id: str) -> dict[str, Any]: + """Research brief: questions to answer about a service this month.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "questions_to_answer_ar": [ + "من أكثر فئة عميل اشترت هذه الخدمة آخر 30 يوم؟", + "ما متوسط الـ time-to-value الفعلي؟", + "ما أعلى اعتراض ظهر في الـonboarding؟", + "ما أكثر deliverable يطلبه العميل بالاسم؟", + "ما أضعف proof_metric لم يُحقَّق هذا الشهر؟", + "ما أكثر سعر يقبله العميل بدون تردد؟", + ], + "data_sources_ar": [ + "Action Ledger.", + "Proof Ledger.", + "Approval Center.", + "Decision Memory.", + "Customer feedback.", + ], + "approval_required": True, + } + + +def generate_feature_hypotheses(service_id: str) -> list[dict[str, Any]]: + """Generate hypotheses for feature additions/improvements.""" + s = get_service(service_id) + if s is None: + return [] + base = [ + { + "hypothesis_ar": "إضافة exit survey بعد كل deliverable يرفع NPS بـ20%.", + "effort": "low", "impact": "medium", + }, + { + "hypothesis_ar": "اقتراح 3 رسائل بدل 1 في الـapproval card يرفع approval rate 30%.", + "effort": "medium", "impact": "high", + }, + { + "hypothesis_ar": "إضافة Saudi-tone-score مرئية في الواجهة يقلل الرسائل المرفوضة 40%.", + "effort": "medium", "impact": "high", + }, + { + "hypothesis_ar": "ربط Proof Pack بـ Moyasar invoice draft يرفع conversion 25%.", + "effort": "medium", "impact": "high", + }, + ] + if s.pricing_model == "monthly": + base.append({ + "hypothesis_ar": "تقرير شهري بصيغة فيديو 60 ثانية يرفع retention 15%.", + "effort": "high", "impact": "medium", + }) + return base + + +def recommend_next_experiments(service_id: str) -> dict[str, Any]: + """Recommend the next 3 experiments to run on a service.""" + hypotheses = generate_feature_hypotheses(service_id) + # Pick top-3 by impact desc, effort asc. + impact_rank = {"high": 0, "medium": 1, "low": 2} + effort_rank = {"low": 0, "medium": 1, "high": 2} + sorted_h = sorted( + hypotheses, + key=lambda h: (impact_rank.get(str(h.get("impact")), 9), + effort_rank.get(str(h.get("effort")), 9)), + ) + return { + "service_id": service_id, + "experiments": sorted_h[:3], + "approval_required": True, + } + + +def build_monthly_service_review(service_id: str) -> dict[str, Any]: + """Build a structured monthly review of a service's performance.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + score = calculate_service_excellence_score(service_id) + gaps = compare_against_categories(service_id) + experiments = recommend_next_experiments(service_id) + + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "current_excellence_score": score, + "competitor_gap_summary": { + "advantages": gaps.get("dealix_advantages_ar", []), + "gaps_to_close": gaps.get("gaps_to_close_ar", []), + }, + "next_experiments": experiments.get("experiments", []), + "research_brief": build_service_research_brief(service_id), + "approval_required": True, + } diff --git a/dealix/auto_client_acquisition/service_excellence/service_improvement_backlog.py b/dealix/auto_client_acquisition/service_excellence/service_improvement_backlog.py new file mode 100644 index 00000000..89e71563 --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/service_improvement_backlog.py @@ -0,0 +1,67 @@ +"""Improvement backlog — يحوّل الفيدباك إلى bands prioritized.""" + +from __future__ import annotations + +from typing import Any + + +def build_backlog(service_id: str) -> dict[str, Any]: + """Build an empty backlog skeleton for a service.""" + return { + "service_id": service_id, + "items": [], + "policies_ar": [ + "كل بند يتضمن: title_ar, impact, effort, owner.", + "بند بدون proof_metric يُرفض.", + "بند يخالف PDPL/ToS يُرفض فوراً.", + ], + } + + +def prioritize_backlog_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Sort backlog items by impact desc, effort asc.""" + impact_rank = {"high": 0, "medium": 1, "low": 2} + effort_rank = {"low": 0, "medium": 1, "high": 2} + return sorted( + items, + key=lambda i: ( + impact_rank.get(str(i.get("impact", "low")), 9), + effort_rank.get(str(i.get("effort", "high")), 9), + ), + ) + + +def convert_feedback_to_backlog( + feedback: list[dict[str, Any]], +) -> list[dict[str, Any]]: + """Convert customer feedback items into prioritized backlog items.""" + out: list[dict[str, Any]] = [] + for f in feedback or []: + text = str(f.get("text", "")).strip() + if not text: + continue + # Heuristic prioritization (deterministic). + sentiment = f.get("sentiment", "neutral") + impact = "high" if sentiment == "negative" else "medium" + effort = "medium" + out.append({ + "title_ar": text[:120], + "impact": impact, + "effort": effort, + "source": f.get("source", "feedback"), + "owner": f.get("owner", "service_lead"), + }) + return prioritize_backlog_items(out) + + +def recommend_weekly_improvements(service_id: str) -> dict[str, Any]: + """Recommend 3 weekly improvements for a service.""" + return { + "service_id": service_id, + "weekly_plan_ar": [ + "حسّن الرسالة الأولى — اختبر زاوية جديدة لقطاع واحد.", + "أضف proof_metric حقيقي لو يوجد فجوة.", + "نظّف backlog: ادمج أو احذف بنود متشابهة.", + ], + "approval_required": True, + } diff --git a/dealix/auto_client_acquisition/service_excellence/service_scoring.py b/dealix/auto_client_acquisition/service_excellence/service_scoring.py new file mode 100644 index 00000000..98401c7a --- /dev/null +++ b/dealix/auto_client_acquisition/service_excellence/service_scoring.py @@ -0,0 +1,151 @@ +"""Service Excellence scoring — every service must score ≥80 to ship.""" + +from __future__ import annotations + +from typing import Any + +from auto_client_acquisition.service_tower import Service, get_service + + +def score_clarity(service: Service | dict[str, Any]) -> int: + """0..10. هل العميل يفهم ما الذي سيحصل عليه؟""" + if isinstance(service, dict): + outcome = service.get("outcome_ar", "") + deliverables = service.get("deliverables_ar", []) + else: + outcome = service.outcome_ar + deliverables = list(service.deliverables_ar) + score = 5 + if len(outcome or "") >= 30: + score += 3 + if len(deliverables) >= 3: + score += 2 + return min(10, score) + + +def score_speed_to_value(service: Service | dict[str, Any]) -> int: + """0..10. هل النتيجة خلال 7 أيام؟""" + if isinstance(service, dict): + model = service.get("pricing_model", "") + else: + model = service.pricing_model + if model == "sprint": + return 10 + if model == "monthly": + return 6 + return 8 # one_time + + +def score_automation(service: Service | dict[str, Any]) -> int: + """0..10. هل قابلة للأتمتة؟""" + if isinstance(service, dict): + steps = service.get("workflow_steps", []) + else: + steps = list(service.workflow_steps) + auto_steps = sum(1 for s in steps + if s in {"intake", "data_check", "targeting", + "contactability", "strategy", "drafting", + "tracking", "proof", "upsell"}) + return min(10, auto_steps) + + +def score_compliance(service: Service | dict[str, Any]) -> int: + """0..10. هل فيها opt-in/approval/audit؟""" + if isinstance(service, dict): + policy = service.get("approval_policy", "") + else: + policy = service.approval_policy + if "approval_required" in policy: + return 10 + if "draft_only" in policy: + return 9 + if policy: + return 6 + return 3 + + +def score_proof(service: Service | dict[str, Any]) -> int: + """0..10. هل لها proof metrics؟""" + if isinstance(service, dict): + metrics = service.get("proof_metrics", []) + else: + metrics = list(service.proof_metrics) + return min(10, len(metrics) * 3) + + +def score_upsell(service: Service | dict[str, Any]) -> int: + """0..10. هل لها upgrade path؟""" + if isinstance(service, dict): + upgrade = service.get("upgrade_path", []) + else: + upgrade = list(service.upgrade_path) + return 10 if upgrade else 5 + + +def calculate_service_excellence_score( + service: Service | dict[str, Any] | str, +) -> dict[str, Any]: + """Compute the full excellence score (0..100) + verdict.""" + if isinstance(service, str): + s = get_service(service) + if s is None: + return {"error": f"unknown service: {service}"} + service_obj: Service | dict[str, Any] = s + else: + service_obj = service + + clarity = score_clarity(service_obj) + speed = score_speed_to_value(service_obj) + automation = score_automation(service_obj) + compliance = score_compliance(service_obj) + proof = score_proof(service_obj) + upsell = score_upsell(service_obj) + + # Each dimension max=10; we have 6 dimensions → max=60. + # Add 4 baseline dimensions (uniqueness, scalability, ops, proof_data) + # at fixed values for now (can become real signals later). + uniqueness = 8 # deterministic — Dealix is Saudi-first + scalability = 8 # multi-sector ready + ops_daily = 7 # daily autopilot integration + proof_data = min(10, proof + 2) + + total = (clarity + speed + automation + compliance + + proof + upsell + uniqueness + scalability + + ops_daily + proof_data) + total = max(0, min(100, total)) + + if total >= 80: + status = "launch_ready" + elif total >= 60: + status = "beta_only" + else: + status = "needs_work" + + reasons: list[str] = [] + fixes: list[str] = [] + if compliance < 8: + reasons.append("سياسة الاعتماد غير واضحة.") + fixes.append("اضبط approval_policy على 'approval_required' أو 'draft_only'.") + if proof < 6: + reasons.append("Proof metrics قليلة.") + fixes.append("أضف ≥3 proof metrics محددة.") + if not upsell: + reasons.append("لا يوجد upgrade path.") + fixes.append("اربط الخدمة بخدمة أعلى عبر upgrade_path.") + + return { + "service_id": ( + service_obj.get("id") if isinstance(service_obj, dict) else service_obj.id + ), + "total_score": total, + "dimensions": { + "clarity": clarity, "speed_to_value": speed, + "automation": automation, "compliance": compliance, + "proof": proof, "upsell": upsell, + "uniqueness": uniqueness, "scalability": scalability, + "ops_daily": ops_daily, "proof_data": proof_data, + }, + "status": status, + "reasons_ar": reasons, + "required_fixes_ar": fixes, + } diff --git a/dealix/auto_client_acquisition/service_tower/__init__.py b/dealix/auto_client_acquisition/service_tower/__init__.py new file mode 100644 index 00000000..3c2117bc --- /dev/null +++ b/dealix/auto_client_acquisition/service_tower/__init__.py @@ -0,0 +1,82 @@ +"""Service Tower — كل قدرات Dealix كخدمات قابلة للبيع والتشغيل الذاتي. + +العميل يختار هدفه → النظام يوصي بالخدمة → يجمع البيانات → يقيّم المخاطر → +يكتب الخطة → يطلب الموافقات → يشغّل القنوات → يطلع Proof Pack. +""" + +from __future__ import annotations + +from .deliverables import ( + build_client_report_outline, + build_deliverables, + build_internal_operator_checklist, + build_proof_pack_template, +) +from .mission_templates import ( + build_service_workflow, + get_default_mission_steps, + map_service_to_growth_mission, +) +from .pricing_engine import ( + calculate_monthly_offer, + calculate_setup_fee, + quote_service, + recommend_plan_after_service, +) +from .service_catalog import ( + ALL_SERVICES, + Service, + catalog_summary, + get_service, + list_all_services, +) +from .service_scorecard import ( + build_service_scorecard, + calculate_service_success_score, + recommend_next_step, + summarize_scorecard_ar, +) +from .service_wizard import ( + build_intake_questions, + recommend_service, + summarize_recommendation_ar, + validate_service_inputs, +) +from .upgrade_paths import ( + build_upsell_message_ar, + map_service_to_subscription, + recommend_upgrade, +) +from .whatsapp_ceo_control import ( + build_ceo_daily_service_brief, + build_end_of_day_service_report, + build_risk_alert_card, + build_service_approval_card, +) + +__all__ = [ + # service_catalog + "ALL_SERVICES", "Service", "catalog_summary", + "get_service", "list_all_services", + # service_wizard + "build_intake_questions", "recommend_service", + "summarize_recommendation_ar", "validate_service_inputs", + # mission_templates + "build_service_workflow", "get_default_mission_steps", + "map_service_to_growth_mission", + # pricing_engine + "calculate_monthly_offer", "calculate_setup_fee", + "quote_service", "recommend_plan_after_service", + # deliverables + "build_client_report_outline", "build_deliverables", + "build_internal_operator_checklist", "build_proof_pack_template", + # service_scorecard + "build_service_scorecard", "calculate_service_success_score", + "recommend_next_step", "summarize_scorecard_ar", + # whatsapp_ceo_control + "build_ceo_daily_service_brief", "build_end_of_day_service_report", + "build_risk_alert_card", "build_service_approval_card", + # upgrade_paths + "build_upsell_message_ar", "map_service_to_subscription", + "recommend_upgrade", +] diff --git a/dealix/auto_client_acquisition/service_tower/deliverables.py b/dealix/auto_client_acquisition/service_tower/deliverables.py new file mode 100644 index 00000000..afe6cdb4 --- /dev/null +++ b/dealix/auto_client_acquisition/service_tower/deliverables.py @@ -0,0 +1,91 @@ +"""Deliverables + Proof Pack templates per service.""" + +from __future__ import annotations + +from typing import Any + +from .service_catalog import get_service + + +def build_deliverables(service_id: str) -> dict[str, Any]: + """Return the deliverables list for a service.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "deliverables_ar": list(s.deliverables_ar), + "approval_required": True, + } + + +def build_proof_pack_template(service_id: str) -> dict[str, Any]: + """Build a proof-pack template for a service.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "metrics_to_track": list(s.proof_metrics), + "report_sections_ar": [ + "ملخص الفترة", + "ما تم إنجازه (ledger entries)", + "النتائج بالأرقام (الـ proof_metrics)", + "المخاطر التي تم منعها", + "تجربة الأسبوع/الشهر القادم", + "التوصية بالخطوة التالية", + ], + "delivery_format": ["pdf", "json", "whatsapp_summary"], + "approval_required": True, + } + + +def build_client_report_outline(service_id: str) -> dict[str, Any]: + """Outline of the client-facing report for a service.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "title_ar": f"تقرير {s.name_ar}", + "sections_ar": [ + "ملخص تنفيذي (10 أسطر)", + "السياق والأهداف", + "ما عمله Dealix", + "النتائج (الأرقام مقابل الأهداف)", + "أبرز الاعتراضات والـsignals", + "المخاطر التي تم منعها", + "Proof — ledger events", + "التوصية بالخطوة التالية", + ], + "approval_required": True, + } + + +def build_internal_operator_checklist(service_id: str) -> dict[str, Any]: + """Internal operator checklist (for the team running the service).""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "checklist_ar": [ + "مراجعة الـ intake واكتمال الحقول.", + "تشغيل targeting + contactability.", + "صياغة الـ drafts الأولى.", + "إرسال للـ approval center.", + "تنفيذ بعد الاعتماد فقط.", + "تتبع النتائج في الـ Action Ledger.", + "بناء Proof Pack.", + "اقتراح الترقية للعميل.", + ], + "do_not_do_ar": [ + "لا live send بدون env flag + اعتماد.", + "لا إرسال على cold list.", + "لا charge بدون تأكيد.", + "لا تخزين أسرار في الـ payload.", + ], + } diff --git a/dealix/auto_client_acquisition/service_tower/mission_templates.py b/dealix/auto_client_acquisition/service_tower/mission_templates.py new file mode 100644 index 00000000..e03d26eb --- /dev/null +++ b/dealix/auto_client_acquisition/service_tower/mission_templates.py @@ -0,0 +1,94 @@ +"""Mission templates — يحوّل الخدمة إلى workflow قابل للتشغيل.""" + +from __future__ import annotations + +from typing import Any + +from .service_catalog import get_service + +# Map service → growth mission ID (in intelligence_layer.mission_engine). +_SERVICE_TO_MISSION: dict[str, str] = { + "free_growth_diagnostic": "first_10_opportunities", + "list_intelligence": "first_10_opportunities", + "first_10_opportunities_sprint": "first_10_opportunities", + "self_growth_operator": "first_10_opportunities", + "growth_os_monthly": "first_10_opportunities", + "email_revenue_rescue": "revenue_leak_rescue", + "meeting_booking_sprint": "meeting_booking_sprint", + "partner_sprint": "partnership_sprint", + "agency_partner_program": "partnership_sprint", + "whatsapp_compliance_setup": "first_10_opportunities", + "linkedin_lead_gen_setup": "first_10_opportunities", + "executive_growth_brief": "first_10_opportunities", +} + + +def get_default_mission_steps(service_id: str) -> list[dict[str, Any]]: + """Return default workflow steps for a service.""" + s = get_service(service_id) + if s is None: + return [] + steps: list[dict[str, Any]] = [] + for i, name in enumerate(s.workflow_steps): + steps.append({ + "order": i + 1, + "step_id": name, + "label_ar": _STEP_LABELS_AR.get(name, name), + "approval_required": name in { + "approval", "execution_or_export", "drafting", + }, + "live_action": False, + }) + return steps + + +_STEP_LABELS_AR: dict[str, str] = { + "intake": "جمع المدخلات", + "data_check": "فحص جودة البيانات", + "targeting": "تحديد الأهداف", + "contactability": "تقييم إمكانية التواصل", + "strategy": "استراتيجية القناة", + "drafting": "صياغة المسودات", + "approval": "اعتماد بشري", + "execution_or_export": "تنفيذ/تصدير", + "tracking": "متابعة النتائج", + "proof": "Proof Pack", + "upsell": "ترقية الخدمة", + "agency_onboarding": "إعداد الوكالة", + "client_diagnostic": "تشخيص عميل الوكالة", + "proposal": "عرض", + "pilot": "Pilot", + "proof_pack": "Proof Pack", + "revenue_share": "Revenue Share", + "aggregate": "تجميع الإشارات", + "prioritize": "ترتيب الأولويات", + "deliver": "تسليم الموجز", +} + + +def build_service_workflow(service_id: str) -> dict[str, Any]: + """Build the full Arabic workflow for a service.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + + steps = get_default_mission_steps(service_id) + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "workflow_steps": steps, + "deliverables_ar": list(s.deliverables_ar), + "approval_policy": s.approval_policy, + "live_send_allowed": False, + "estimated_completion_days": ( + 7 if s.pricing_model == "sprint" + else 30 if s.pricing_model == "monthly" + else 1 + ), + "linked_growth_mission": _SERVICE_TO_MISSION.get(service_id), + } + + +def map_service_to_growth_mission(service_id: str) -> str | None: + """Return the growth-mission ID linked to a service (or None).""" + return _SERVICE_TO_MISSION.get(service_id) diff --git a/dealix/auto_client_acquisition/service_tower/pricing_engine.py b/dealix/auto_client_acquisition/service_tower/pricing_engine.py new file mode 100644 index 00000000..50772a00 --- /dev/null +++ b/dealix/auto_client_acquisition/service_tower/pricing_engine.py @@ -0,0 +1,118 @@ +"""Pricing engine — quotes + setup + monthly + post-service plan.""" + +from __future__ import annotations + +from typing import Any + +from .service_catalog import get_service + + +def quote_service( + service_id: str, + *, + company_size: str = "small", + urgency: str = "normal", + channels_count: int = 1, +) -> dict[str, Any]: + """Quote a service with company-size + urgency + channels multipliers.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + + p_min = float(s.pricing_min_sar) + p_max = float(s.pricing_max_sar) + if p_min == 0 and p_max == 0: + return { + "service_id": service_id, + "is_free": True, + "estimated_min_sar": 0, + "estimated_max_sar": 0, + "currency": "SAR", + "notes_ar": "خدمة مجانية. تتطلب اعتماد قبل التسليم.", + } + + size_mult = {"micro": 0.8, "small": 1.0, "medium": 1.3, "large": 1.7}.get( + company_size, 1.0, + ) + urgency_mult = {"normal": 1.0, "rush": 1.3, "asap": 1.5}.get(urgency, 1.0) + ch_mult = 1.0 + max(0, channels_count - 1) * 0.15 + + return { + "service_id": service_id, + "estimated_min_sar": round(p_min * size_mult * urgency_mult * ch_mult), + "estimated_max_sar": round(p_max * size_mult * urgency_mult * ch_mult), + "currency": "SAR", + "factors": { + "company_size": company_size, + "urgency": urgency, + "channels_count": channels_count, + }, + "pricing_model": s.pricing_model, + } + + +def calculate_setup_fee(service_id: str) -> dict[str, Any]: + """Suggest a setup fee for monthly services.""" + s = get_service(service_id) + if s is None or s.pricing_model != "monthly": + return {"setup_fee_sar": 0, "currency": "SAR"} + base = s.pricing_min_sar + return { + "setup_fee_sar": int(base * 1.0), # ~one month equivalent + "includes_ar": [ + "ربط القنوات (واتساب/إيميل/تقويم)", + "استيراد القوائم وتصنيف المصادر", + "تدريب الفريق على Approval Center", + "بناء أول Proof Pack", + ], + "currency": "SAR", + } + + +def calculate_monthly_offer(service_id: str) -> dict[str, Any]: + """Return monthly-pricing detail (for monthly services only).""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + if s.pricing_model != "monthly": + return { + "service_id": service_id, + "is_monthly": False, + "notes_ar": "هذه الخدمة ليست شهرية.", + } + return { + "service_id": service_id, + "is_monthly": True, + "monthly_sar": s.pricing_min_sar, + "annual_discount_pct": 15, + "annual_total_sar": int(s.pricing_min_sar * 12 * 0.85), + "currency": "SAR", + } + + +def recommend_plan_after_service( + service_id: str, + *, + outcome: dict[str, Any] | None = None, +) -> dict[str, Any]: + """After a service runs, recommend an upgrade plan.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + outcome = outcome or {} + + upgrade_targets = list(s.upgrade_path) or ["growth_os_monthly"] + next_id = upgrade_targets[0] + next_s = get_service(next_id) + + return { + "from_service": service_id, + "recommended_upgrade": next_id, + "name_ar": next_s.name_ar if next_s else next_id, + "monthly_sar": next_s.pricing_min_sar if next_s else 0, + "reason_ar": ( + f"بعد إثبات قيمة {s.name_ar}، الخطوة الطبيعية هي " + f"الاستمرار مع {next_s.name_ar if next_s else next_id} " + "للحصول على نتائج شهرية مستمرة." + ), + } diff --git a/dealix/auto_client_acquisition/service_tower/service_catalog.py b/dealix/auto_client_acquisition/service_tower/service_catalog.py new file mode 100644 index 00000000..a5013bfb --- /dev/null +++ b/dealix/auto_client_acquisition/service_tower/service_catalog.py @@ -0,0 +1,347 @@ +"""The full Dealix service catalog — 12 productized services.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass(frozen=True) +class Service: + """A single sellable, productized service.""" + id: str + name_ar: str + target_customer_ar: str + outcome_ar: str + inputs_required: tuple[str, ...] + workflow_steps: tuple[str, ...] + deliverables_ar: tuple[str, ...] + pricing_min_sar: int + pricing_max_sar: int + pricing_model: str # "one_time" | "monthly" | "sprint" + risk_level: str # "low" | "medium" | "high" + required_integrations: tuple[str, ...] + approval_policy: str # short label + proof_metrics: tuple[str, ...] + upgrade_path: tuple[str, ...] = field(default_factory=tuple) + + def to_dict(self) -> dict[str, object]: + return { + "id": self.id, "name_ar": self.name_ar, + "target_customer_ar": self.target_customer_ar, + "outcome_ar": self.outcome_ar, + "inputs_required": list(self.inputs_required), + "workflow_steps": list(self.workflow_steps), + "deliverables_ar": list(self.deliverables_ar), + "pricing_min_sar": self.pricing_min_sar, + "pricing_max_sar": self.pricing_max_sar, + "pricing_model": self.pricing_model, + "risk_level": self.risk_level, + "required_integrations": list(self.required_integrations), + "approval_policy": self.approval_policy, + "proof_metrics": list(self.proof_metrics), + "upgrade_path": list(self.upgrade_path), + } + + +_DEFAULT_WORKFLOW: tuple[str, ...] = ( + "intake", "data_check", "targeting", "contactability", + "strategy", "drafting", "approval", + "execution_or_export", "tracking", "proof", "upsell", +) + + +ALL_SERVICES: tuple[Service, ...] = ( + Service( + id="free_growth_diagnostic", + name_ar="تشخيص نمو مجاني", + target_customer_ar="أي شركة B2B تريد عينة قبل Pilot", + outcome_ar="3 فرص + رسالة + تقرير مخاطر + خطة Pilot — خلال 24 ساعة عمل", + inputs_required=("sector", "city", "offer", "goal"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "3 فرص B2B مع why-now", + "رسالة عربية مخصصة", + "تقرير مخاطر", + "خطة Pilot مقترحة", + ), + pricing_min_sar=0, pricing_max_sar=0, + pricing_model="one_time", + risk_level="low", + required_integrations=(), + approval_policy="approval_required_for_share", + proof_metrics=("diagnostic_to_paid_conversion",), + upgrade_path=("first_10_opportunities_sprint", "growth_os_monthly"), + ), + Service( + id="list_intelligence", + name_ar="تحليل القوائم (List Intelligence)", + target_customer_ar="شركات لديها قوائم أرقام/إيميلات/عملاء قدامى", + outcome_ar="تنظيف + تصنيف + أفضل 50 target + رسائل + خطة 7 أيام", + inputs_required=("uploaded_csv", "channels_available"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "قائمة منظفة + dedupe", + "تصنيف safe / needs_review / blocked", + "أفضل 50 target", + "رسائل عربية", + "تقرير مخاطر", + ), + pricing_min_sar=499, pricing_max_sar=1500, + pricing_model="one_time", + risk_level="medium", + required_integrations=("google_sheets",), + approval_policy="draft_only", + proof_metrics=("contacts_classified", "safe_targets_found", "risks_blocked"), + upgrade_path=("growth_os_monthly",), + ), + Service( + id="first_10_opportunities_sprint", + name_ar="10 فرص في 10 دقائق (Sprint)", + target_customer_ar="شركة B2B تحتاج فرصاً مؤهلة بسرعة", + outcome_ar="10 فرص + رسائل + خطة متابعة + Proof Pack — خلال 7 أيام", + inputs_required=("sector", "city", "offer", "goal"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "10 فرص B2B مع why-now", + "10 رسائل عربية", + "خطة متابعة 7 أيام", + "Proof Pack تفصيلي", + ), + pricing_min_sar=499, pricing_max_sar=1500, + pricing_model="sprint", + risk_level="low", + required_integrations=(), + approval_policy="draft_only", + proof_metrics=("opportunities_count", "approval_rate", + "positive_replies", "meetings_drafted"), + upgrade_path=("growth_os_monthly", "self_growth_operator"), + ), + Service( + id="self_growth_operator", + name_ar="مدير نمو شخصي (Self-Growth Operator)", + target_customer_ar="مؤسسون / مستشارون / وكالات صغيرة", + outcome_ar="Daily brief + drafts + متابعة + تقارير أسبوعية", + inputs_required=("company_profile", "goals"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "Daily brief عربي", + "5 cards/day للقرارات", + "Drafts + approvals", + "Weekly learning report", + ), + pricing_min_sar=999, pricing_max_sar=999, + pricing_model="monthly", + risk_level="low", + required_integrations=("gmail", "google_calendar"), + approval_policy="approval_required", + proof_metrics=("decisions_per_day", "drafts_approved", + "meetings_drafted", "pipeline_sar"), + upgrade_path=("growth_os_monthly",), + ), + Service( + id="growth_os_monthly", + name_ar="Growth OS — اشتراك شهري", + target_customer_ar="شركات B2B صغيرة-متوسطة", + outcome_ar="منصة كاملة: قنوات، command feed، proof pack، فريق", + inputs_required=("company_profile", "channels", "team_size"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "ربط القنوات", + "Daily autopilot", + "Approvals مركزية", + "Proof Pack شهري", + "Revenue leak detector", + ), + pricing_min_sar=2999, pricing_max_sar=2999, + pricing_model="monthly", + risk_level="medium", + required_integrations=("gmail", "google_calendar", "moyasar", + "google_sheets"), + approval_policy="approval_required", + proof_metrics=("monthly_pipeline_sar", "monthly_meetings", + "monthly_revenue_influenced", "monthly_risks_blocked"), + upgrade_path=("agency_partner_program",), + ), + Service( + id="email_revenue_rescue", + name_ar="استعادة الإيرادات من الإيميل", + target_customer_ar="شركات إيميل الشركة فيه فرص ضائعة", + outcome_ar="استخراج فرص ضائعة + drafts + meetings + missed revenue report", + inputs_required=("gmail_label", "ICP"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "Scan الـ inbox/labels", + "Drafts للردود المتأخرة", + "Meeting drafts", + "Missed revenue report", + ), + pricing_min_sar=1500, pricing_max_sar=5000, + pricing_model="one_time", + risk_level="high", + required_integrations=("gmail",), + approval_policy="approval_required", + proof_metrics=("opportunities_found", "drafts_created", + "meetings_drafted", "missed_revenue_sar"), + upgrade_path=("growth_os_monthly",), + ), + Service( + id="meeting_booking_sprint", + name_ar="سبرنت حجز الاجتماعات", + target_customer_ar="شركات لديها prospects ولا تحوّلهم لاجتماعات", + outcome_ar="invitations + meeting drafts + briefs + follow-ups", + inputs_required=("prospect_list", "calendar_link"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "دعوات اجتماع", + "Pre-meeting brief", + "Calendar drafts", + "Post-meeting follow-up", + ), + pricing_min_sar=1500, pricing_max_sar=5000, + pricing_model="sprint", + risk_level="medium", + required_integrations=("google_calendar", "gmail"), + approval_policy="approval_required", + proof_metrics=("meetings_drafted", "meetings_confirmed", + "meetings_completed"), + upgrade_path=("growth_os_monthly",), + ), + Service( + id="partner_sprint", + name_ar="سبرنت شراكات", + target_customer_ar="شركات تحتاج نمو عبر الشركاء والوكالات", + outcome_ar="20 شريك محتمل + 10 رسائل + 5 اجتماعات + scorecard", + inputs_required=("sector", "partner_goal"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "قائمة شركاء محتملين", + "Scorecard لكل شريك", + "Outreach drafts", + "Meeting plan", + "Referral agreement draft", + ), + pricing_min_sar=3000, pricing_max_sar=7500, + pricing_model="sprint", + risk_level="medium", + required_integrations=("gmail",), + approval_policy="approval_required", + proof_metrics=("partners_identified", "partner_meetings", + "referral_revenue_sar"), + upgrade_path=("agency_partner_program",), + ), + Service( + id="agency_partner_program", + name_ar="برنامج وكالة شريكة", + target_customer_ar="وكالات تسويق/مبيعات/CRM", + outcome_ar="بيع Dealix لعملاء الوكالة مع co-branding + revenue share", + inputs_required=("agency_profile", "client_count"), + workflow_steps=("agency_onboarding", "client_diagnostic", + "proposal", "pilot", "proof_pack", "revenue_share"), + deliverables_ar=( + "Agency onboarding", + "Client diagnostics", + "Co-branded proof packs", + "Revenue share dashboard", + ), + pricing_min_sar=10000, pricing_max_sar=50000, + pricing_model="one_time", + risk_level="medium", + required_integrations=("gmail", "google_calendar", "moyasar"), + approval_policy="approval_required", + proof_metrics=("clients_added", "agency_revenue_sar", + "co_branded_proofs"), + ), + Service( + id="whatsapp_compliance_setup", + name_ar="إعداد امتثال واتساب", + target_customer_ar="شركات تستخدم واتساب بشكل عشوائي", + outcome_ar="audit + opt-in templates + approval workflow + ledger", + inputs_required=("contact_list", "current_practice"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "تصنيف القوائم", + "Opt-in templates", + "Approval cards", + "Opt-out ledger", + "Safety report", + ), + pricing_min_sar=1500, pricing_max_sar=4000, + pricing_model="one_time", + risk_level="high", + required_integrations=("whatsapp_cloud",), + approval_policy="draft_only", + proof_metrics=("contacts_classified", "opt_ins_collected", + "risks_blocked"), + upgrade_path=("growth_os_monthly",), + ), + Service( + id="linkedin_lead_gen_setup", + name_ar="إعداد LinkedIn Lead Gen", + target_customer_ar="شركات B2B تحتاج decision makers", + outcome_ar="حملة Lead Gen Form + audiences + ربط CRM + content angle", + inputs_required=("ICP", "offer", "ad_budget"), + workflow_steps=_DEFAULT_WORKFLOW, + deliverables_ar=( + "Audience plan", + "Lead magnet", + "Lead Gen Form", + "Hidden fields setup", + "Dealix intake", + "Follow-up drafts", + ), + pricing_min_sar=2000, pricing_max_sar=7500, + pricing_model="one_time", + risk_level="medium", + required_integrations=("linkedin_lead_forms",), + approval_policy="approval_required", + proof_metrics=("leads_captured", "qualified_leads", + "meetings_booked"), + upgrade_path=("growth_os_monthly",), + ), + Service( + id="executive_growth_brief", + name_ar="موجز نمو تنفيذي (Executive Brief)", + target_customer_ar="CEO / Growth Manager / Sales Manager", + outcome_ar="3 قرارات + 3 فرص + 3 مخاطر + Pipeline + اجتماعات اليوم", + inputs_required=("company_profile",), + workflow_steps=("intake", "aggregate", "prioritize", "deliver"), + deliverables_ar=( + "Daily brief عبر واتساب/Email", + "Approval cards (≤3 buttons)", + "Risk alerts", + "Weekly Founder Shadow Board", + ), + pricing_min_sar=499, pricing_max_sar=999, + pricing_model="monthly", + risk_level="low", + required_integrations=(), + approval_policy="approval_required", + proof_metrics=("decisions_made", "alerts_actioned"), + upgrade_path=("growth_os_monthly",), + ), +) + + +def get_service(service_id: str) -> Service | None: + return next((s for s in ALL_SERVICES if s.id == service_id), None) + + +def list_all_services() -> dict[str, object]: + return { + "total": len(ALL_SERVICES), + "services": [s.to_dict() for s in ALL_SERVICES], + } + + +def catalog_summary() -> dict[str, object]: + by_pricing: dict[str, int] = {} + by_risk: dict[str, int] = {} + for s in ALL_SERVICES: + by_pricing[s.pricing_model] = by_pricing.get(s.pricing_model, 0) + 1 + by_risk[s.risk_level] = by_risk.get(s.risk_level, 0) + 1 + return { + "total": len(ALL_SERVICES), + "by_pricing_model": by_pricing, + "by_risk_level": by_risk, + "free_offers": [s.id for s in ALL_SERVICES if s.pricing_max_sar == 0], + } diff --git a/dealix/auto_client_acquisition/service_tower/service_scorecard.py b/dealix/auto_client_acquisition/service_tower/service_scorecard.py new file mode 100644 index 00000000..e6a94c2b --- /dev/null +++ b/dealix/auto_client_acquisition/service_tower/service_scorecard.py @@ -0,0 +1,105 @@ +"""Service scorecard — يقيس نجاح كل خدمة بعد تشغيلها.""" + +from __future__ import annotations + +from typing import Any + +from .service_catalog import get_service + + +def calculate_service_success_score( + service_id: str, metrics: dict[str, Any], +) -> dict[str, Any]: + """Score a service run 0..100 + verdict.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + + score = 0 + + # Generic outcomes that map to most services. + drafts_approved = int(metrics.get("drafts_approved", 0)) + positive_replies = int(metrics.get("positive_replies", 0)) + meetings = int(metrics.get("meetings", 0)) + pipeline_sar = float(metrics.get("pipeline_sar", 0)) + risks_blocked = int(metrics.get("risks_blocked", 0)) + customer_satisfaction = int(metrics.get("customer_satisfaction", 0)) # 0..10 + + score += min(15, drafts_approved * 3) + score += min(20, positive_replies * 5) + score += min(20, meetings * 8) + score += min(20, int(pipeline_sar / 5_000)) + score += min(10, risks_blocked * 2) + score += min(15, customer_satisfaction * 1) + + score = max(0, min(100, score)) + + if score >= 70: + verdict = "strong_outcome" + elif score >= 40: + verdict = "decent_outcome" + else: + verdict = "needs_iteration" + + return { + "service_id": service_id, + "score": score, + "verdict": verdict, + "captured_metrics": metrics, + } + + +def recommend_next_step(metrics: dict[str, Any]) -> dict[str, Any]: + """Recommend the next step for a customer based on outcome metrics.""" + pipeline_sar = float(metrics.get("pipeline_sar", 0)) + meetings = int(metrics.get("meetings", 0)) + csat = int(metrics.get("customer_satisfaction", 0)) + + if csat >= 8 and (pipeline_sar >= 25_000 or meetings >= 2): + return { + "action": "upsell_to_growth_os", + "label_ar": "اعرض Growth OS الشهري — العميل راضٍ والنتائج قوية.", + } + if pipeline_sar < 5_000 and meetings == 0: + return { + "action": "iterate_offer_or_segment", + "label_ar": "غيّر زاوية العرض أو القطاع — النتائج ضعيفة.", + } + return { + "action": "extend_pilot", + "label_ar": "مدّد الـ Pilot لأسبوعين أو جرّب قناة إضافية.", + } + + +def build_service_scorecard( + service_id: str, metrics: dict[str, Any], +) -> dict[str, Any]: + """Build a full Arabic scorecard for a service run.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + score_obj = calculate_service_success_score(service_id, metrics) + next_step = recommend_next_step(metrics) + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "score": score_obj.get("score"), + "verdict": score_obj.get("verdict"), + "metrics": metrics, + "next_step": next_step, + "summary_ar": summarize_scorecard_ar({ + "service_id": service_id, + **score_obj, "next_step": next_step, + }), + } + + +def summarize_scorecard_ar(scorecard: dict[str, Any]) -> str: + s = get_service(scorecard.get("service_id", "")) + name = s.name_ar if s else scorecard.get("service_id", "?") + score = scorecard.get("score", 0) + verdict = scorecard.get("verdict", "?") + next_step = (scorecard.get("next_step") or {}).get("label_ar", "") + return ( + f"{name}: درجة {score} ({verdict}). الخطوة التالية: {next_step}" + ) diff --git a/dealix/auto_client_acquisition/service_tower/service_wizard.py b/dealix/auto_client_acquisition/service_tower/service_wizard.py new file mode 100644 index 00000000..88d4fadd --- /dev/null +++ b/dealix/auto_client_acquisition/service_tower/service_wizard.py @@ -0,0 +1,137 @@ +"""Service wizard — يوصي بالخدمة المناسبة من إجابات بسيطة.""" + +from __future__ import annotations + +from typing import Any + +from .service_catalog import ALL_SERVICES, get_service + + +def recommend_service( + *, + company_type: str = "", + goal: str = "fill_pipeline", + has_contact_list: bool = False, + channels: list[str] | None = None, + budget_sar: int = 1000, +) -> dict[str, Any]: + """ + Recommend the best-fit service based on inputs. Deterministic. + """ + channels = channels or [] + company_type_lc = (company_type or "").lower() + + chosen_id: str + reason: str + + # Highest priority first. + if "agency" in company_type_lc or "وكالة" in company_type: + chosen_id = "agency_partner_program" if budget_sar >= 10_000 else "partner_sprint" + reason = "وكالة → برنامج شريك أو سبرنت شراكات." + elif has_contact_list: + chosen_id = "list_intelligence" + reason = "العميل لديه قائمة → ابدأ بـ List Intelligence." + elif "founder" in company_type_lc or "مؤسس" in company_type: + chosen_id = "self_growth_operator" + reason = "مؤسس بدون فريق نمو → Self-Growth Operator." + elif "executive" in company_type_lc or "ceo" in company_type_lc: + chosen_id = "executive_growth_brief" + reason = "CEO/تنفيذي → موجز نمو يومي." + elif "whatsapp" in company_type_lc or "واتساب" in company_type: + chosen_id = "whatsapp_compliance_setup" + reason = "حالة واتساب عشوائية → امتثال أولاً." + elif goal == "rescue_lost_revenue": + chosen_id = "email_revenue_rescue" + reason = "الهدف استعادة إيراد ضائع → Email Revenue Rescue." + elif goal == "book_meetings": + chosen_id = "meeting_booking_sprint" + reason = "الهدف اجتماعات → Meeting Booking Sprint." + elif goal == "expand_partners": + chosen_id = "partner_sprint" + reason = "الهدف شراكات → Partner Sprint." + elif budget_sar >= 2999: + chosen_id = "growth_os_monthly" + reason = "الميزانية شهرية → Growth OS." + else: + chosen_id = "first_10_opportunities_sprint" + reason = "الافتراضي: ابدأ بـ 10 فرص في 10 دقائق." + + service = get_service(chosen_id) + return { + "recommended_service_id": chosen_id, + "service": service.to_dict() if service else None, + "reason_ar": reason, + "next_step_ar": ( + "املأ نموذج الـ intake، وسنبدأ خلال 24 ساعة عمل." + ), + } + + +def build_intake_questions(service_id: str) -> dict[str, Any]: + """Return intake questions for a service. Empty if service unknown.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}", "questions": []} + + base_q = [ + {"key": "company_name", "label_ar": "اسم الشركة", "required": True}, + {"key": "sector", "label_ar": "القطاع", "required": True}, + {"key": "city", "label_ar": "المدينة", "required": True}, + {"key": "decision_maker_name", "label_ar": "اسم صانع القرار", "required": True}, + {"key": "decision_maker_role", "label_ar": "المسمى الوظيفي", "required": True}, + ] + extra = [] + if "uploaded_csv" in s.inputs_required: + extra.append({"key": "uploaded_csv", "label_ar": "ملف CSV", "required": True}) + if "offer" in s.inputs_required: + extra.append({"key": "offer", "label_ar": "وصف العرض", "required": True}) + if "goal" in s.inputs_required: + extra.append({"key": "goal", "label_ar": "الهدف الأساسي", "required": True}) + if "channels_available" in s.inputs_required: + extra.append({"key": "channels", "label_ar": "القنوات المتاحة", "required": False}) + + return { + "service_id": service_id, + "service_name_ar": s.name_ar, + "questions": base_q + extra, + "approval_required": True, + } + + +def validate_service_inputs( + service_id: str, payload: dict[str, Any], +) -> dict[str, Any]: + """Validate intake payload against service requirements.""" + s = get_service(service_id) + if s is None: + return {"valid": False, "errors_ar": [f"خدمة غير معروفة: {service_id}"]} + + errors: list[str] = [] + for required in s.inputs_required: + if required in ("uploaded_csv", "offer", "goal", "channels_available", + "ICP", "calendar_link", "company_profile", + "current_practice", "ad_budget", "client_count", + "partner_goal", "team_size", "channels", "agency_profile", + "prospect_list", "gmail_label", "contact_list", + "goals", "sector", "city"): + if not payload.get(required): + errors.append(f"الحقل ناقص: {required}") + + return { + "valid": not errors, + "errors_ar": errors, + "service_id": service_id, + } + + +def summarize_recommendation_ar(result: dict[str, Any]) -> str: + """Build a one-paragraph Arabic recommendation summary.""" + sid = result.get("recommended_service_id", "?") + reason = result.get("reason_ar", "") + svc = result.get("service") or {} + name = svc.get("name_ar", sid) + outcome = svc.get("outcome_ar", "") + return ( + f"الخدمة المقترحة: {name}. السبب: {reason} " + f"المخرجات: {outcome}" + ) diff --git a/dealix/auto_client_acquisition/service_tower/upgrade_paths.py b/dealix/auto_client_acquisition/service_tower/upgrade_paths.py new file mode 100644 index 00000000..14db9253 --- /dev/null +++ b/dealix/auto_client_acquisition/service_tower/upgrade_paths.py @@ -0,0 +1,59 @@ +"""Upgrade paths — يوصي بالخدمة التالية بعد كل خدمة.""" + +from __future__ import annotations + +from typing import Any + +from .service_catalog import get_service + + +def recommend_upgrade( + service_id: str, + *, + results: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Recommend the next service for a customer to buy.""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + + upgrade_targets = list(s.upgrade_path) or ["growth_os_monthly"] + next_id = upgrade_targets[0] + next_s = get_service(next_id) + + return { + "from_service": service_id, + "from_service_name_ar": s.name_ar, + "recommended_service_id": next_id, + "recommended_service_name_ar": next_s.name_ar if next_s else next_id, + "monthly_sar": next_s.pricing_min_sar if next_s else 0, + "reason_ar": ( + f"بعد {s.name_ar}، الترقية الطبيعية هي " + f"{next_s.name_ar if next_s else next_id} للحفاظ على الاستمرارية." + ), + } + + +def build_upsell_message_ar( + service_id: str, + next_offer: str, +) -> str: + """Build a one-paragraph Arabic upsell message.""" + s = get_service(service_id) + next_s = get_service(next_offer) + if not s or not next_s: + return "بعد إثبات النتائج، نوصي بالترقية للخدمة التالية." + return ( + f"شاكر لك على تجربة {s.name_ar}. " + f"بناءً على النتائج، الترقية المنطقية هي {next_s.name_ar} " + "للاستمرار في النمو شهرياً مع نفس مستوى الـ Proof Pack. " + "أرسل لي تأكيد ونبدأ الأسبوع القادم." + ) + + +def map_service_to_subscription(service_id: str) -> str: + """Map any service to its eventual subscription.""" + s = get_service(service_id) + if s is None: + return "growth_os_monthly" + return s.upgrade_path[0] if s.upgrade_path else "growth_os_monthly" diff --git a/dealix/auto_client_acquisition/service_tower/whatsapp_ceo_control.py b/dealix/auto_client_acquisition/service_tower/whatsapp_ceo_control.py new file mode 100644 index 00000000..c62a4221 --- /dev/null +++ b/dealix/auto_client_acquisition/service_tower/whatsapp_ceo_control.py @@ -0,0 +1,88 @@ +"""WhatsApp CEO Control — كل القرارات بكروت عربية ≤3 أزرار.""" + +from __future__ import annotations + +from typing import Any + +from .service_catalog import get_service + + +def build_ceo_daily_service_brief() -> dict[str, Any]: + """The daily service brief sent to the CEO via WhatsApp/Email.""" + return { + "type": "ceo_daily_service_brief", + "title_ar": "موجز الخدمات اليومي", + "summary_ar": [ + "3 خدمات نشطة اليوم.", + "5 رسائل drafts تنتظر اعتمادك.", + "2 Free Diagnostic مكتمل وينتظر التسليم.", + "1 شريك وكالة جاهز للعرض.", + "0 مخاطر سمعة (الحالة صحية).", + ], + "buttons_ar": ["اعرض المسودات", "موافقة جماعية", "لاحقاً"], + "approval_required": True, + } + + +def build_service_approval_card( + service_id: str, action: str, +) -> dict[str, Any]: + """Approval card for a single service action (draft send / publish / charge).""" + s = get_service(service_id) + if s is None: + return {"error": f"unknown service: {service_id}"} + label_ar_by_action = { + "send_email": "إرسال إيميل", + "send_whatsapp": "إرسال واتساب", + "insert_calendar": "إدراج موعد", + "create_payment_link": "إنشاء رابط دفع", + "publish_review_reply": "نشر رد تقييم", + "share_diagnostic": "مشاركة Free Diagnostic", + } + return { + "type": "service_approval", + "service_id": service_id, + "service_name_ar": s.name_ar, + "action": action, + "title_ar": f"اعتماد: {label_ar_by_action.get(action, action)}", + "summary_ar": f"يتم تنفيذ هذا الفعل ضمن خدمة {s.name_ar}.", + "risk_level": s.risk_level, + "buttons_ar": ["اعتمد", "عدّل", "ارفض"], + "approval_required": True, + "live_send_allowed": False, + } + + +def build_risk_alert_card() -> dict[str, Any]: + """A risk alert card surfaced to the CEO.""" + return { + "type": "risk_alert", + "title_ar": "تنبيه مخاطر", + "summary_ar": ( + "ارتفاع نسبة الـ bounce على الإيميل تجاوز الحد الآمن. " + "اقتراح: إيقاف الحملات الجديدة 14 يوماً + تنظيف القائمة." + ), + "risk_level": "high", + "buttons_ar": ["أوقف القناة", "خفّض الحجم", "تجاهل"], + "approval_required": True, + } + + +def build_end_of_day_service_report() -> dict[str, Any]: + """End-of-day report on services run today.""" + return { + "type": "end_of_day_service_report", + "title_ar": "تقرير نهاية اليوم — الخدمات", + "summary_ar": [ + "خدمات منفذة اليوم: 3.", + "Drafts معتمدة: 6.", + "ردود إيجابية: 2.", + "اجتماعات مجدولة: 1.", + "Pipeline متأثر: 24,000 ريال.", + "مخاطر تم منعها: 8.", + ], + "next_day_focus_ar": ( + "غداً: تابع الردود الإيجابية، اعتمد رسائل Partner Sprint، " + "سلّم 2 Free Diagnostic للعملاء الجدد." + ), + } diff --git a/dealix/auto_client_acquisition/targeting_os/__init__.py b/dealix/auto_client_acquisition/targeting_os/__init__.py new file mode 100644 index 00000000..9bac23d3 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/__init__.py @@ -0,0 +1,177 @@ +"""Targeting & Acquisition OS — يستهدف بذكاء، يقيّم المخاطر، يقترح القنوات. + +Account-first targeting (شركات قبل أشخاص) + buying-committee mapping + +contactability gate + multi-channel strategy + reputation guard + +daily autopilot + self-growth mode + free diagnostic + contract drafts. + +كل شيء deterministic، عربي، draft/approval-first، لا scraping ولا cold WA. +""" + +from __future__ import annotations + +from .account_finder import ( + AccountSignal, + explain_why_now, + rank_accounts, + recommend_account_source_strategy, + recommend_accounts, + score_account_fit, +) +from .acquisition_scorecard import ( + build_acquisition_scorecard, + calculate_meetings_booked, + calculate_pipeline_created, + calculate_productivity_score, + calculate_risks_blocked, +) +from .buyer_role_mapper import ( + ALL_BUYER_ROLES, + draft_role_based_angle, + map_buying_committee, + recommend_decision_maker_roles, + recommend_influencer_roles, +) +from .contact_source_policy import ( + ALL_SOURCES, + allowed_channels_for_source, + classify_source, + required_review_level, + retention_recommendation, + source_risk_score, +) +from .contactability_matrix import ( + ACTION_MODES, + BLOCK_REASONS, + allowed_action_modes, + block_reason_codes, + evaluate_contactability, + explain_contactability_ar, +) +from .contract_drafts import ( + draft_agency_partner_outline, + draft_dpa_outline, + draft_pilot_agreement_outline, + draft_referral_agreement_outline, + draft_scope_of_work, +) +from .daily_autopilot import ( + build_daily_targeting_brief, + build_end_of_day_report, + prioritize_cards, + recommend_today_actions, +) +from .email_strategy import ( + build_followup_sequence, + draft_b2b_email, + include_unsubscribe_footer, + recommend_pacing, + score_email_risk, +) +from .free_diagnostic import ( + analyze_uploaded_list_preview, + build_free_growth_diagnostic, + build_mini_proof_plan, + recommend_paid_pilot_offer, +) +from .linkedin_strategy import ( + build_lead_gen_form_plan, + build_manual_research_task, + build_safe_connection_message, + linkedin_do_not_do, + recommend_linkedin_strategy, +) +from .outreach_scheduler import ( + build_outreach_plan, + enforce_daily_limits, + schedule_followups, + stop_on_opt_out, + summarize_plan_ar, +) +from .reputation_guard import ( + calculate_channel_reputation, + recommend_recovery_action, + risk_thresholds, + should_pause_channel, + summarize_reputation_ar, +) +from .self_growth_mode import ( + build_dealix_self_growth_plan, + build_free_service_offer, + build_self_growth_daily_brief, + build_weekly_learning_report, + recommend_dealix_targets, +) +from .service_offers import ( + build_offer_card, + estimate_service_price, + list_targeting_services, + recommend_service_offer, +) +from .social_strategy import ( + build_social_listening_plan, + draft_public_reply, + recommend_social_sources, + social_do_not_do, +) +from .whatsapp_strategy import ( + build_opt_in_request_template, + draft_whatsapp_message, + requires_opt_in, + score_whatsapp_risk, + whatsapp_do_not_do, +) + +__all__ = [ + # account_finder + "AccountSignal", "explain_why_now", "rank_accounts", + "recommend_account_source_strategy", "recommend_accounts", "score_account_fit", + # acquisition_scorecard + "build_acquisition_scorecard", "calculate_meetings_booked", + "calculate_pipeline_created", "calculate_productivity_score", + "calculate_risks_blocked", + # buyer_role_mapper + "ALL_BUYER_ROLES", "draft_role_based_angle", "map_buying_committee", + "recommend_decision_maker_roles", "recommend_influencer_roles", + # contact_source_policy + "ALL_SOURCES", "allowed_channels_for_source", "classify_source", + "required_review_level", "retention_recommendation", "source_risk_score", + # contactability_matrix + "ACTION_MODES", "BLOCK_REASONS", "allowed_action_modes", + "block_reason_codes", "evaluate_contactability", "explain_contactability_ar", + # contract_drafts + "draft_agency_partner_outline", "draft_dpa_outline", + "draft_pilot_agreement_outline", "draft_referral_agreement_outline", + "draft_scope_of_work", + # daily_autopilot + "build_daily_targeting_brief", "build_end_of_day_report", + "prioritize_cards", "recommend_today_actions", + # email_strategy + "build_followup_sequence", "draft_b2b_email", + "include_unsubscribe_footer", "recommend_pacing", "score_email_risk", + # free_diagnostic + "analyze_uploaded_list_preview", "build_free_growth_diagnostic", + "build_mini_proof_plan", "recommend_paid_pilot_offer", + # linkedin_strategy + "build_lead_gen_form_plan", "build_manual_research_task", + "build_safe_connection_message", "linkedin_do_not_do", + "recommend_linkedin_strategy", + # outreach_scheduler + "build_outreach_plan", "enforce_daily_limits", + "schedule_followups", "stop_on_opt_out", "summarize_plan_ar", + # reputation_guard + "calculate_channel_reputation", "recommend_recovery_action", + "risk_thresholds", "should_pause_channel", "summarize_reputation_ar", + # self_growth_mode + "build_dealix_self_growth_plan", "build_free_service_offer", + "build_self_growth_daily_brief", "build_weekly_learning_report", + "recommend_dealix_targets", + # service_offers + "build_offer_card", "estimate_service_price", + "list_targeting_services", "recommend_service_offer", + # social_strategy + "build_social_listening_plan", "draft_public_reply", + "recommend_social_sources", "social_do_not_do", + # whatsapp_strategy + "build_opt_in_request_template", "draft_whatsapp_message", + "requires_opt_in", "score_whatsapp_risk", "whatsapp_do_not_do", +] diff --git a/dealix/auto_client_acquisition/targeting_os/account_finder.py b/dealix/auto_client_acquisition/targeting_os/account_finder.py new file mode 100644 index 00000000..761c6e67 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/account_finder.py @@ -0,0 +1,215 @@ +"""Account-first targeting — يبحث عن الشركات المناسبة قبل الأشخاص.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +# Signals that indicate a company is "in market" right now. +ACCOUNT_SIGNALS_AR: dict[str, str] = { + "hiring_sales": "توظيف مبيعات", + "new_branch": "فرع جديد", + "website_updated": "تحديث الموقع", + "active_ads": "إعلانات نشطة", + "event_participation": "مشاركة في فعاليات", + "google_reviews": "تقييمات Google نشطة", + "booking_link": "صفحة حجز/طلب", + "crm_visible": "بيانات CRM متوفرة", + "growing_team": "نمو الفريق", + "partner_potential": "إمكانية شراكة", + "expansion_news": "أخبار توسع", + "leadership_change": "تغيير قيادي", +} + + +@dataclass(frozen=True) +class AccountSignal: + """A single buying-readiness signal on a company.""" + key: str + label_ar: str + weight: int # 1..10 + why_ar: str + + def to_dict(self) -> dict[str, object]: + return { + "key": self.key, "label_ar": self.label_ar, + "weight": self.weight, "why_ar": self.why_ar, + } + + +# Default signal weights — can be overridden per sector. +_DEFAULT_WEIGHTS: dict[str, int] = { + "hiring_sales": 9, + "new_branch": 8, + "expansion_news": 9, + "active_ads": 7, + "growing_team": 7, + "leadership_change": 8, + "booking_link": 5, + "website_updated": 4, + "google_reviews": 5, + "crm_visible": 3, + "event_participation": 6, + "partner_potential": 6, +} + + +def _signal_objs(signals: dict[str, bool] | list[str]) -> list[AccountSignal]: + out: list[AccountSignal] = [] + if isinstance(signals, list): + signals = {s: True for s in signals} + for key, val in signals.items(): + if not val or key not in ACCOUNT_SIGNALS_AR: + continue + out.append(AccountSignal( + key=key, + label_ar=ACCOUNT_SIGNALS_AR[key], + weight=_DEFAULT_WEIGHTS.get(key, 3), + why_ar=f"إشارة: {ACCOUNT_SIGNALS_AR[key]}", + )) + return out + + +def score_account_fit(account: dict[str, Any]) -> dict[str, Any]: + """Score an account 0..100 based on its signals + sector+size match.""" + signals = _signal_objs(account.get("signals", {})) + base = sum(s.weight for s in signals) + score = min(100, base * 4) # ~25 weight points = max 100 + if account.get("sector_match"): + score = min(100, score + 10) + if account.get("city_match"): + score = min(100, score + 5) + + if score >= 70: + tier = "hot" + elif score >= 40: + tier = "warm" + elif score >= 15: + tier = "watching" + else: + tier = "cold" + + return { + "score": score, + "tier": tier, + "signals": [s.to_dict() for s in signals], + "signal_count": len(signals), + } + + +def explain_why_now(account: dict[str, Any]) -> str: + """Build an Arabic 'why now' line from an account's signals.""" + signals = _signal_objs(account.get("signals", {})) + if not signals: + return "لا توجد إشارات شراء واضحة الآن — متابعة دورية مقترحة." + top = sorted(signals, key=lambda s: -s.weight)[:2] + labels = " + ".join(s.label_ar for s in top) + company = account.get("name") or "الشركة" + return f"{company} تظهر إشارات: {labels}. نافذة فرصة مناسبة الآن." + + +def recommend_account_source_strategy(account: dict[str, Any]) -> dict[str, Any]: + """Recommend safe sources for reaching this account's decision-makers.""" + has_crm = bool(account.get("crm_visible")) + has_ads = bool(account.get("active_ads")) + has_events = bool(account.get("event_participation")) + + primary = [] + if has_crm: + primary.append("crm_customer") + primary.append("website_form") + primary.append("linkedin_lead_form") + + if has_ads: + primary.append("ads_retargeting") + if has_events: + primary.append("event_lead") + + return { + "primary_sources": primary, + "blocked_sources": ["scraped_email", "scraped_phone", "purchased_list"], + "notes_ar": ( + "ابدأ بمصادر مصرّح بها: قوائم العميل، Lead Gen Forms، " + "نماذج الموقع، شركاء، أحداث. لا scraping ولا قوائم مشتراة." + ), + } + + +def recommend_accounts( + sector: str, + city: str, + *, + offer: str = "", + goal: str = "fill_pipeline", + limit: int = 10, + seed_signals: list[str] | None = None, +) -> dict[str, Any]: + """ + Generate a deterministic list of recommended target accounts. + + This is a structural template — production reads from real data sources + (Google Maps, CRM, web forms, etc). The output shape stays identical. + """ + seed_signals = seed_signals or [ + "hiring_sales", "new_branch", "active_ads", + "growing_team", "booking_link", "google_reviews", + ] + sector_label_ar = { + "training": "التدريب", "saas": "البرمجيات", "real_estate": "العقار", + "retail": "التجزئة", "healthcare": "الرعاية الصحية", + "logistics": "اللوجستيات", "fintech": "الفنتك", + "agency": "الوكالات", "education": "التعليم", + }.get(sector.lower(), sector) + + accounts: list[dict[str, Any]] = [] + n = max(1, min(limit, 25)) + for i in range(n): + # Spread signals across accounts deterministically. + my_signals = {seed_signals[(i + j) % len(seed_signals)]: True + for j in range(2 + (i % 3))} + acct = { + "name": f"شركة {sector_label_ar} #{i + 1} في {city}", + "sector": sector, + "city": city, + "signals": my_signals, + "sector_match": True, + "city_match": True, + } + scored = score_account_fit(acct) + sources = recommend_account_source_strategy(acct) + acct.update({ + "fit_score": scored["score"], + "tier": scored["tier"], + "why_now_ar": explain_why_now(acct), + "primary_sources": sources["primary_sources"], + "best_angle_ar": ( + f"عرض Pilot 7 أيام لاستخراج 10 فرص في قطاع {sector_label_ar}." + if not offer else + f"العرض المقترح: {offer}." + ), + "recommended_channel": ( + "email_first" + if "crm_visible" in my_signals + else "linkedin_lead_form_first" + ), + "risk_level": "low" if scored["score"] >= 50 else "medium", + }) + accounts.append(acct) + + accounts = rank_accounts(accounts) + return { + "sector": sector, "city": city, "goal": goal, "offer": offer, + "total": len(accounts), + "accounts": accounts, + "do_not_do_ar": [ + "لا scraping للبيانات.", + "لا cold WhatsApp.", + "لا auto-DM على LinkedIn.", + "لا charge بدون موافقة.", + ], + } + + +def rank_accounts(accounts: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Sort accounts by fit_score desc; stable for ties.""" + return sorted(accounts, key=lambda a: -int(a.get("fit_score", 0))) diff --git a/dealix/auto_client_acquisition/targeting_os/acquisition_scorecard.py b/dealix/auto_client_acquisition/targeting_os/acquisition_scorecard.py new file mode 100644 index 00000000..a0a3df7f --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/acquisition_scorecard.py @@ -0,0 +1,86 @@ +"""Acquisition scorecard — يقيس النتائج بشكل deterministic.""" + +from __future__ import annotations + +from typing import Any + + +def calculate_pipeline_created(opportunities: list[dict[str, Any]]) -> dict[str, Any]: + """Sum expected_value_sar across opportunities.""" + total = sum(float(o.get("expected_value_sar", 0)) for o in opportunities or []) + return { + "opportunities_count": len(opportunities or []), + "pipeline_sar": round(total, 2), + } + + +def calculate_meetings_booked(events: list[dict[str, Any]]) -> dict[str, Any]: + """Count meetings by status.""" + drafted = sum(1 for e in events or [] if e.get("status") == "drafted") + confirmed = sum(1 for e in events or [] if e.get("status") == "confirmed") + completed = sum(1 for e in events or [] if e.get("status") == "completed") + return { + "drafted": drafted, "confirmed": confirmed, "completed": completed, + "total": drafted + confirmed + completed, + } + + +def calculate_risks_blocked(actions: list[dict[str, Any]]) -> dict[str, Any]: + """Count actions that were blocked by policy/contactability.""" + blocked = [a for a in actions or [] if a.get("status") == "blocked"] + by_reason: dict[str, int] = {} + for a in blocked: + reason = a.get("block_reason", "unknown") + by_reason[reason] = by_reason.get(reason, 0) + 1 + return {"total": len(blocked), "by_reason": by_reason} + + +def calculate_productivity_score(metrics: dict[str, Any]) -> dict[str, Any]: + """Compute a productivity score 0..100 from key acquisition metrics.""" + accounts = int(metrics.get("accounts_researched", 0)) + drafts = int(metrics.get("drafts_created", 0)) + approvals = int(metrics.get("approvals_received", 0)) + replies = int(metrics.get("positive_replies", 0)) + meetings = int(metrics.get("meetings_booked", 0)) + + score = 0 + score += min(20, accounts // 3) + score += min(20, drafts * 2) + score += min(20, approvals * 4) + score += min(20, replies * 5) + score += min(20, meetings * 8) + score = max(0, min(100, score)) + + if score >= 70: + verdict = "strong" + elif score >= 40: + verdict = "decent" + else: + verdict = "needs_focus" + + return {"score": score, "verdict": verdict} + + +def build_acquisition_scorecard(metrics: dict[str, Any]) -> dict[str, Any]: + """Build a comprehensive Arabic acquisition scorecard.""" + pipeline = calculate_pipeline_created(metrics.get("opportunities", [])) + meetings = calculate_meetings_booked(metrics.get("events", [])) + risks = calculate_risks_blocked(metrics.get("actions", [])) + productivity = calculate_productivity_score(metrics) + + return { + "summary_ar": [ + f"الحسابات المُحلّلة: {metrics.get('accounts_researched', 0)}", + f"أصحاب القرار المُعرَّفين: {metrics.get('decision_makers_mapped', 0)}", + f"رسائل drafts: {metrics.get('drafts_created', 0)}", + f"اعتمادات: {metrics.get('approvals_received', 0)}", + f"ردود إيجابية: {metrics.get('positive_replies', 0)}", + f"اجتماعات: {meetings['total']}", + f"Pipeline متأثر: {pipeline['pipeline_sar']:.0f} ريال", + f"مخاطر تم منعها: {risks['total']}", + ], + "pipeline": pipeline, + "meetings": meetings, + "risks_blocked": risks, + "productivity_score": productivity, + } diff --git a/dealix/auto_client_acquisition/targeting_os/buyer_role_mapper.py b/dealix/auto_client_acquisition/targeting_os/buyer_role_mapper.py new file mode 100644 index 00000000..858a0b93 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/buyer_role_mapper.py @@ -0,0 +1,151 @@ +"""Map buying committees — من غالباً يقرر داخل الشركة.""" + +from __future__ import annotations + +from typing import Any + +# All buyer roles Dealix knows about, with Arabic labels. +ALL_BUYER_ROLES: dict[str, str] = { + "founder_ceo": "المؤسس / الرئيس التنفيذي", + "coo": "مدير العمليات", + "head_of_sales": "مدير المبيعات", + "marketing_manager": "مدير التسويق", + "business_development": "تطوير الأعمال", + "operations_manager": "مدير العمليات التشغيلية", + "clinic_manager": "مدير العيادة", + "branch_manager": "مدير الفرع", + "hr_manager": "مدير الموارد البشرية", + "procurement_manager": "مدير المشتريات", + "agency_owner": "صاحب الوكالة", + "store_manager": "مدير المتجر", + "growth_manager": "مدير النمو", + "cto": "المدير التقني", +} + +# Sector-specific decision-maker priors (descending priority). +_DM_BY_SECTOR: dict[str, list[str]] = { + "training": ["founder_ceo", "head_of_sales", "hr_manager"], + "saas": ["founder_ceo", "head_of_sales", "growth_manager"], + "real_estate": ["founder_ceo", "head_of_sales", "branch_manager"], + "retail": ["founder_ceo", "store_manager", "marketing_manager"], + "healthcare": ["clinic_manager", "founder_ceo", "operations_manager"], + "logistics": ["coo", "operations_manager", "founder_ceo"], + "fintech": ["founder_ceo", "growth_manager", "cto"], + "agency": ["agency_owner", "head_of_sales", "growth_manager"], + "education": ["founder_ceo", "operations_manager", "marketing_manager"], + "consulting": ["founder_ceo", "business_development", "head_of_sales"], +} + +_INFLUENCERS_BY_SECTOR: dict[str, list[str]] = { + "training": ["marketing_manager", "operations_manager"], + "saas": ["marketing_manager", "cto"], + "real_estate": ["marketing_manager"], + "retail": ["operations_manager"], + "healthcare": ["marketing_manager", "operations_manager"], + "logistics": ["procurement_manager"], + "fintech": ["marketing_manager", "head_of_sales"], + "agency": ["marketing_manager", "business_development"], + "education": ["hr_manager"], + "consulting": ["marketing_manager"], +} + +# Goal-based message angles per role. +_ROLE_ANGLES_AR: dict[str, str] = { + "founder_ceo": "نمو إيرادات ملموس بدون توظيف فريق كبير.", + "coo": "تنظيم العمليات وقياس الأثر يومياً.", + "head_of_sales": "ملء الـ pipeline بفرص مؤهلة + متابعة منظمة.", + "marketing_manager": "تحويل الـ traffic والإعلانات إلى اجتماعات.", + "business_development": "فتح قنوات شراكة وتوزيع جديدة.", + "operations_manager": "أتمتة المتابعات + تقليل الوقت الضائع.", + "clinic_manager": "تذكير المرضى + ردود التقييمات + قنوات حجز.", + "branch_manager": "إدارة عملاء الفرع + reactivation.", + "hr_manager": "برامج تدريب وتوظيف بدون فوضى inbox.", + "procurement_manager": "تقييم مزودين + التزامات SLA واضحة.", + "agency_owner": "خدمة عملاء الوكالة + Proof Pack + revenue share.", + "store_manager": "استرجاع العملاء + payment links + reviews.", + "growth_manager": "تجارب نمو منظمة + قياس Proof.", + "cto": "أمان البيانات + PDPL + تكاملات مصرّحة.", +} + + +def _norm_sector(sector: str) -> str: + s = (sector or "").lower().strip() + return s if s in _DM_BY_SECTOR else "saas" + + +def map_buying_committee( + sector: str, + *, + company_size: str = "small", + goal: str = "fill_pipeline", +) -> dict[str, Any]: + """Build a buying-committee map for a sector + company-size.""" + s = _norm_sector(sector) + dm_keys = _DM_BY_SECTOR[s] + inf_keys = _INFLUENCERS_BY_SECTOR[s] + + # For small companies, the founder is almost always the primary DM. + if company_size in ("micro", "small") and "founder_ceo" not in dm_keys[:2]: + dm_keys = ["founder_ceo"] + [k for k in dm_keys if k != "founder_ceo"] + + return { + "sector": s, + "company_size": company_size, + "goal": goal, + "primary_decision_maker": { + "role_key": dm_keys[0], + "label_ar": ALL_BUYER_ROLES[dm_keys[0]], + "angle_ar": _ROLE_ANGLES_AR[dm_keys[0]], + }, + "secondary_decision_makers": [ + {"role_key": k, "label_ar": ALL_BUYER_ROLES[k], + "angle_ar": _ROLE_ANGLES_AR[k]} + for k in dm_keys[1:] + ], + "influencers": [ + {"role_key": k, "label_ar": ALL_BUYER_ROLES[k], + "angle_ar": _ROLE_ANGLES_AR[k]} + for k in inf_keys + ], + "approach_notes_ar": ( + "ابدأ بمحاور أعلى — المؤسس أو مدير المبيعات. " + "اشمل الـ influencers في الرسالة الثانية لبناء التوافق الداخلي." + ), + } + + +def recommend_decision_maker_roles( + sector: str, *, goal: str = "fill_pipeline", +) -> list[dict[str, str]]: + s = _norm_sector(sector) + return [ + {"role_key": k, "label_ar": ALL_BUYER_ROLES[k], + "angle_ar": _ROLE_ANGLES_AR[k]} + for k in _DM_BY_SECTOR[s] + ] + + +def recommend_influencer_roles( + sector: str, *, goal: str = "fill_pipeline", +) -> list[dict[str, str]]: + s = _norm_sector(sector) + return [ + {"role_key": k, "label_ar": ALL_BUYER_ROLES[k], + "angle_ar": _ROLE_ANGLES_AR[k]} + for k in _INFLUENCERS_BY_SECTOR[s] + ] + + +def draft_role_based_angle( + role_key: str, *, sector: str = "saas", offer: str = "", +) -> dict[str, str]: + """Build a one-sentence Arabic angle suited to a role.""" + role_key = role_key if role_key in ALL_BUYER_ROLES else "founder_ceo" + role_ar = ALL_BUYER_ROLES[role_key] + base_angle = _ROLE_ANGLES_AR[role_key] + offer_part = f" — {offer}" if offer else "" + return { + "role_key": role_key, + "role_ar": role_ar, + "angle_ar": f"رسالة لـ{role_ar}: {base_angle}{offer_part}", + } diff --git a/dealix/auto_client_acquisition/targeting_os/contact_source_policy.py b/dealix/auto_client_acquisition/targeting_os/contact_source_policy.py new file mode 100644 index 00000000..2bfc463b --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/contact_source_policy.py @@ -0,0 +1,148 @@ +"""Contact source policy — كل contact له مصدر، غرض، ومستوى مخاطرة.""" + +from __future__ import annotations + +# All recognized contact sources, ordered roughly safest → riskiest. +ALL_SOURCES: tuple[str, ...] = ( + "crm_customer", + "inbound_lead", + "website_form", + "linkedin_lead_form", + "event_lead", + "referral", + "partner_intro", + "manual_research", + "uploaded_list", + "unknown_source", + "cold_list", + "opt_out", +) + +# Risk score per source (0..100; higher = riskier). +_SOURCE_RISK: dict[str, int] = { + "crm_customer": 5, + "inbound_lead": 5, + "website_form": 10, + "linkedin_lead_form": 10, + "event_lead": 20, + "referral": 25, + "partner_intro": 25, + "manual_research": 50, + "uploaded_list": 60, + "unknown_source": 80, + "cold_list": 95, + "opt_out": 100, +} + + +def classify_source(source: str) -> dict[str, object]: + """Classify a single source string. Unknown maps to `unknown_source`.""" + s = (source or "").lower().strip() + if s not in ALL_SOURCES: + s = "unknown_source" + return {"source": s, "risk_score": _SOURCE_RISK[s]} + + +def allowed_channels_for_source( + source: str, *, opt_in_status: str = "unknown", +) -> dict[str, object]: + """ + Return which channels Dealix may attempt for this source/opt-in combo. + + Each channel is "safe" / "needs_review" / "blocked". + """ + s = classify_source(source)["source"] + opt = (opt_in_status or "unknown").lower() + + if s == "opt_out": + return { + "source": s, + "channels": {ch: "blocked" for ch in + ("whatsapp", "email", "linkedin", "phone", "social_dm")}, + "notes_ar": "العميل سحب موافقته — كل القنوات محظورة.", + } + + safe_inbound = s in ("crm_customer", "inbound_lead", "website_form", + "linkedin_lead_form", "referral", "partner_intro") + is_unknown = s in ("unknown_source", "manual_research", "uploaded_list", + "cold_list") + + out: dict[str, str] = {} + # WhatsApp — strict + if opt == "yes" and not s == "cold_list": + out["whatsapp"] = "safe" + elif s == "inbound_lead" or s == "crm_customer": + out["whatsapp"] = "needs_review" + else: + out["whatsapp"] = "blocked" + + # Email — looser when business context exists + if safe_inbound: + out["email"] = "safe" + elif is_unknown: + out["email"] = "needs_review" + else: + out["email"] = "needs_review" + + # LinkedIn — only via lead forms / manual approved + if s == "linkedin_lead_form": + out["linkedin"] = "safe" + else: + out["linkedin"] = "needs_review" + + # Phone — heavy review + out["phone"] = "blocked" if s in ("cold_list", "unknown_source") else "needs_review" + + # Social DM — only with explicit context + out["social_dm"] = "blocked" if s in ("cold_list", "unknown_source") else "needs_review" + + return { + "source": s, + "opt_in_status": opt, + "channels": out, + "notes_ar": ( + "البريد افضل قناة في الغالب لمصادر العمل المعروفة. " + "واتساب يحتاج opt-in واضح. لينكدإن عبر Lead Forms فقط." + ), + } + + +def required_review_level(source: str) -> str: + """Returns: 'auto_safe' | 'human_review' | 'block'.""" + s = classify_source(source)["source"] + if s == "opt_out": + return "block" + if s in ("crm_customer", "inbound_lead", "website_form", + "linkedin_lead_form"): + return "auto_safe" + if s in ("event_lead", "referral", "partner_intro"): + return "human_review" + return "human_review" + + +def retention_recommendation(source: str) -> dict[str, object]: + """Return PDPL-shaped retention guidance per source.""" + s = classify_source(source)["source"] + if s == "crm_customer": + days = 365 * 3 # 3 years + elif s in ("inbound_lead", "website_form", "linkedin_lead_form", + "event_lead", "referral", "partner_intro"): + days = 365 * 2 + else: + days = 180 + return { + "source": s, + "retention_days": days, + "lawful_basis_ar": ( + "علاقة قائمة" if s == "crm_customer" + else "موافقة" if s in ("website_form", "linkedin_lead_form", + "inbound_lead", "event_lead") + else "مصلحة مشروعة محدودة" + ), + "notes_ar": "حذف تلقائي عند تجاوز المدة أو طلب opt-out.", + } + + +def source_risk_score(source: str) -> int: + """Return the integer risk score for the source.""" + return int(classify_source(source)["risk_score"]) diff --git a/dealix/auto_client_acquisition/targeting_os/contactability_matrix.py b/dealix/auto_client_acquisition/targeting_os/contactability_matrix.py new file mode 100644 index 00000000..1560dc2c --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/contactability_matrix.py @@ -0,0 +1,134 @@ +"""Contactability matrix — هل التواصل مع هذا الـcontact مسموح؟""" + +from __future__ import annotations + +from typing import Any + +from .contact_source_policy import ( + allowed_channels_for_source, + classify_source, + source_risk_score, +) + +ACTION_MODES: tuple[str, ...] = ( + "suggest_only", + "draft_only", + "approval_required", + "approved_execute", + "blocked", +) + +BLOCK_REASONS: dict[str, str] = { + "opt_out": "العميل سحب موافقته.", + "cold_whatsapp": "واتساب بارد محظور (PDPL).", + "no_lawful_basis": "لا يوجد أساس نظامي للتواصل.", + "missing_consent": "موافقة opt-in مفقودة.", + "secret_in_payload": "الـ payload يحوي قيمة حساسة.", + "high_value_no_approval": "صفقة عالية القيمة بدون اعتماد.", + "channel_paused": "القناة موقوفة لتدهور السمعة.", + "frequency_cap_hit": "تجاوز سقف التواصل الأسبوعي.", + "unknown_source": "مصدر الـ contact غير معروف — تحتاج مراجعة.", +} + + +def block_reason_codes() -> dict[str, str]: + """Expose all block reason codes (Arabic).""" + return dict(BLOCK_REASONS) + + +def evaluate_contactability( + contact: dict[str, Any], + *, + desired_channel: str | None = None, +) -> dict[str, Any]: + """ + Evaluate whether contacting `contact` via `desired_channel` is permitted. + + Returns a structured verdict with status and Arabic reasons. + """ + source = contact.get("source", "unknown_source") + opt_in = contact.get("opt_in_status", "unknown") + opt_out = bool(contact.get("opt_out", False)) + has_relationship = bool(contact.get("has_relationship", False)) + + risk = source_risk_score(source) + classified = classify_source(source)["source"] + + if opt_out or classified == "opt_out": + return { + "status": "blocked", + "reason_codes": ["opt_out"], + "reasons_ar": [BLOCK_REASONS["opt_out"]], + "allowed_action_mode": "blocked", + "allowed_channels": [], + } + + channel_map = allowed_channels_for_source(source, opt_in_status=str(opt_in))["channels"] + + if desired_channel: + ch = desired_channel.lower() + ch_status = channel_map.get(ch, "blocked") + if ch_status == "blocked": + reason = "cold_whatsapp" if ch == "whatsapp" else "no_lawful_basis" + return { + "status": "blocked", + "reason_codes": [reason], + "reasons_ar": [BLOCK_REASONS[reason]], + "allowed_action_mode": "blocked", + "allowed_channels": [k for k, v in channel_map.items() if v != "blocked"], + } + if ch_status == "needs_review": + return { + "status": "needs_review", + "reason_codes": ["unknown_source"] if classified == "unknown_source" else [], + "reasons_ar": ( + [BLOCK_REASONS["unknown_source"]] if classified == "unknown_source" + else ["تحتاج مراجعة بشرية قبل الإرسال."] + ), + "allowed_action_mode": "approval_required", + "allowed_channels": [k for k, v in channel_map.items() if v != "blocked"], + } + # safe + return { + "status": "safe", + "reason_codes": [], + "reasons_ar": [], + "allowed_action_mode": "draft_only" if not has_relationship else "approval_required", + "allowed_channels": [k for k, v in channel_map.items() if v != "blocked"], + } + + # No desired_channel → return per-channel verdict + return { + "status": "safe" if any(v == "safe" for v in channel_map.values()) else "needs_review", + "reason_codes": [], + "reasons_ar": [], + "allowed_action_mode": "draft_only", + "allowed_channels": [k for k, v in channel_map.items() if v != "blocked"], + "channel_status": channel_map, + "risk_score": risk, + } + + +def explain_contactability_ar(result: dict[str, Any]) -> str: + """Build a human Arabic explanation from a contactability result.""" + status = result.get("status", "unknown") + reasons = result.get("reasons_ar", []) + channels = result.get("allowed_channels", []) + if status == "blocked": + return f"محظور: {' / '.join(reasons) or 'سياسة عامة'}." + if status == "needs_review": + return ( + f"يحتاج مراجعة: {' / '.join(reasons) or 'بدون مصدر واضح'}. " + f"القنوات المتاحة بعد المراجعة: {', '.join(channels) or 'لا شيء'}." + ) + return f"آمن. القنوات المسموحة: {', '.join(channels)}." + + +def allowed_action_modes(result: dict[str, Any]) -> list[str]: + """Return the action modes available given a contactability verdict.""" + status = result.get("status", "blocked") + if status == "blocked": + return ["blocked"] + if status == "needs_review": + return ["suggest_only", "draft_only", "approval_required"] + return ["draft_only", "approval_required", "approved_execute"] diff --git a/dealix/auto_client_acquisition/targeting_os/contract_drafts.py b/dealix/auto_client_acquisition/targeting_os/contract_drafts.py new file mode 100644 index 00000000..2666de0f --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/contract_drafts.py @@ -0,0 +1,121 @@ +"""Contract draft outlines — Arabic skeletons; legal review required.""" + +from __future__ import annotations + +from typing import Any + + +_DISCLAIMER_AR = ( + "هذه مسودة هيكلية فقط، ليست استشارة قانونية. " + "لا تُوقَّع قبل مراجعة محامٍ مرخّص في المملكة العربية السعودية." +) + + +def draft_pilot_agreement_outline() -> dict[str, Any]: + """Pilot Agreement outline (Arabic skeleton).""" + return { + "title_ar": "اتفاقية تجربة Pilot لخدمة Dealix", + "sections_ar": [ + "الأطراف والتعريفات.", + "نطاق الـ Pilot ومدته (7 أيام).", + "المدخلات المطلوبة من العميل.", + "المخرجات المُتفق عليها (10 فرص + رسائل + Proof Pack).", + "السرية وعدم استخدام بيانات العميل لأغراض أخرى.", + "PDPL وحقوق الموضوعات (الأشخاص).", + "السعر وطريقة الدفع (Pilot أو case study).", + "إنهاء الاتفاقية والاستمرارية.", + "حدود المسؤولية.", + "القانون الواجب التطبيق والاختصاص.", + ], + "approval_required": True, + "legal_review_required": True, + "not_legal_advice": True, + "disclaimer_ar": _DISCLAIMER_AR, + } + + +def draft_dpa_outline() -> dict[str, Any]: + """Data Processing Addendum outline (Arabic skeleton, PDPL-aware).""" + return { + "title_ar": "ملحق معالجة البيانات (DPA)", + "sections_ar": [ + "التعريفات حسب نظام حماية البيانات الشخصية السعودي (PDPL).", + "أدوار الأطراف (Controller / Processor).", + "أنواع البيانات والـ subjects.", + "أغراض المعالجة.", + "الإجراءات الأمنية المطبّقة.", + "نقل البيانات خارج المملكة (إن وُجد).", + "الاحتفاظ والإتلاف.", + "حقوق الموضوعات (طلبات الوصول/التصحيح/الحذف).", + "خرق البيانات والإبلاغ.", + "الـ subprocessors المعتمدون.", + "التدقيق والامتثال.", + ], + "approval_required": True, + "legal_review_required": True, + "not_legal_advice": True, + "disclaimer_ar": _DISCLAIMER_AR, + } + + +def draft_referral_agreement_outline() -> dict[str, Any]: + """Referral Agreement outline.""" + return { + "title_ar": "اتفاقية إحالة (Referral)", + "sections_ar": [ + "تعريف الـ Referrer والإحالة المؤهلة.", + "نموذج الـ revenue share (نسبة + مدة).", + "شروط الدفع وتاريخ الاستحقاق.", + "السرية.", + "عدم الإغراء (no-poach اختيارية).", + "سياسات PDPL لمشاركة بيانات الـ leads.", + "إنهاء الاتفاقية.", + ], + "approval_required": True, + "legal_review_required": True, + "not_legal_advice": True, + "disclaimer_ar": _DISCLAIMER_AR, + } + + +def draft_agency_partner_outline() -> dict[str, Any]: + """Agency Partner Agreement outline (white-label/co-branded).""" + return { + "title_ar": "اتفاقية شريك وكالة لـ Dealix", + "sections_ar": [ + "هيكل الشراكة (revenue share / setup fee / co-branding).", + "نطاق الخدمات المقدّمة من الوكالة لعملائها.", + "Proof Packs مشتركة العلامة.", + "حقوق الملكية الفكرية.", + "السرية والـ NDAs.", + "PDPL ونقل البيانات بين Dealix والوكالة.", + "حدود المسؤولية والـ SLA.", + "إنهاء الاتفاقية وتسليم العملاء.", + ], + "approval_required": True, + "legal_review_required": True, + "not_legal_advice": True, + "disclaimer_ar": _DISCLAIMER_AR, + } + + +def draft_scope_of_work() -> dict[str, Any]: + """Generic Scope-of-Work outline.""" + return { + "title_ar": "نطاق العمل (SOW)", + "sections_ar": [ + "ملخص الخدمة.", + "المدخلات المطلوبة من العميل.", + "المخرجات والـ deliverables.", + "الجدول الزمني والـ milestones.", + "المسؤوليات والـ approvals.", + "السعر وطريقة الدفع.", + "حدود نطاق العمل وما خارجه.", + "تغييرات النطاق (Change Requests).", + "معايير القبول (Acceptance Criteria).", + ], + "approval_required": True, + "legal_review_required": True, + "not_legal_advice": True, + "disclaimer_ar": _DISCLAIMER_AR, + } diff --git a/dealix/auto_client_acquisition/targeting_os/daily_autopilot.py b/dealix/auto_client_acquisition/targeting_os/daily_autopilot.py new file mode 100644 index 00000000..db9fc7b6 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/daily_autopilot.py @@ -0,0 +1,106 @@ +"""Daily autopilot — يومياً يبني brief + يقترح أفعال + ينظمها بالأولوية.""" + +from __future__ import annotations + +from typing import Any + + +def build_daily_targeting_brief( + company_profile: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Build today's Arabic targeting brief for the founder/growth manager.""" + company_profile = company_profile or {} + sector = company_profile.get("sector", "saas") + city = company_profile.get("city", "Riyadh") + + return { + "greeting_ar": "صباح الخير 👋", + "summary_ar": [ + f"عندك اليوم: 10 شركات جديدة مناسبة في قطاع {sector} ({city}).", + "5 رسائل drafts تنتظر اعتمادك.", + "3 leads متأخرة في المتابعة (>72 ساعة).", + "1 فرصة شريك في جدة جاهزة للتواصل.", + "1 قناة (واتساب) تحتاج مراجعة سمعة.", + ], + "priority_decisions_ar": [ + "اعتمد 5 رسائل إيميل (10 دقائق).", + "راجع 12 رقم بدون مصدر واضح قبل أي واتساب.", + "احجز ديمو مع شريك الوكالة هذا الأسبوع.", + ], + "do_not_do_today_ar": [ + "لا تفعّل live WhatsApp send.", + "لا ترفع قائمة باردة بدون تصنيف مصدر.", + "لا تعد بنتائج مضمونة في الرسائل.", + ], + } + + +def recommend_today_actions( + company_profile: dict[str, Any] | None = None, +) -> list[dict[str, Any]]: + """Return ordered actions for today (deterministic 7-action set).""" + company_profile = company_profile or {} + return [ + {"id": "approve_5_email_drafts", "label_ar": "اعتمد 5 مسودات إيميل", + "minutes": 10, "approval_required": True, "priority": 1}, + {"id": "review_unknown_source_contacts", "label_ar": "راجع 12 رقم بدون مصدر", + "minutes": 8, "approval_required": True, "priority": 2}, + {"id": "schedule_partner_demo", "label_ar": "احجز ديمو شريك", + "minutes": 5, "approval_required": True, "priority": 3}, + {"id": "respond_to_overdue_leads", "label_ar": "رد على 3 leads متأخرة", + "minutes": 12, "approval_required": True, "priority": 4}, + {"id": "review_whatsapp_quality", "label_ar": "راجع مؤشرات سمعة واتساب", + "minutes": 5, "approval_required": False, "priority": 5}, + {"id": "draft_one_partner_message", "label_ar": "اكتب رسالة شريك وكالة", + "minutes": 8, "approval_required": True, "priority": 6}, + {"id": "log_proof_events", "label_ar": "حدّث Proof Ledger", + "minutes": 3, "approval_required": False, "priority": 7}, + ] + + +def prioritize_cards(cards: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Sort cards by `priority` (asc), then by `risk_level` (high first).""" + risk_rank = {"high": 0, "medium": 1, "low": 2, None: 3} + return sorted( + cards, + key=lambda c: ( + int(c.get("priority", 99)), + risk_rank.get(c.get("risk_level"), 9), + ), + ) + + +def build_end_of_day_report( + day_metrics: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Build today's Arabic end-of-day report from metrics.""" + m = day_metrics or {} + accounts = int(m.get("accounts_analyzed", 32)) + opps = int(m.get("opportunities_generated", 10)) + drafts = int(m.get("drafts_approved", 6)) + replies = int(m.get("positive_replies", 2)) + meetings = int(m.get("meetings_drafted", 1)) + risks = int(m.get("risks_blocked", 8)) + + return { + "today_metrics": { + "accounts_analyzed": accounts, + "opportunities_generated": opps, + "drafts_approved": drafts, + "positive_replies": replies, + "meetings_drafted": meetings, + "risks_blocked": risks, + }, + "summary_ar": [ + f"تم تحليل {accounts} حساب اليوم.", + f"تم توليد {opps} فرصة جديدة.", + f"تم اعتماد {drafts} مسودة.", + f"تم تسجيل {replies} رد إيجابي.", + f"تم تجهيز {meetings} اجتماع.", + f"تم منع {risks} مخاطر تلقائياً.", + ], + "tomorrow_recommendation_ar": ( + "غداً: ركّز على متابعة الردود الإيجابية أولاً، ثم اعتماد رسائل جديدة، " + "ثم جدولة 1-2 ديمو إن أمكن." + ), + } diff --git a/dealix/auto_client_acquisition/targeting_os/email_strategy.py b/dealix/auto_client_acquisition/targeting_os/email_strategy.py new file mode 100644 index 00000000..37f4f42d --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/email_strategy.py @@ -0,0 +1,160 @@ +"""Email strategy — drafts only, unsubscribe always, pacing-aware.""" + +from __future__ import annotations + +from typing import Any + + +def draft_b2b_email( + contact: dict[str, Any], + *, + offer: str = "", + why_now: str = "", + tone: str = "professional_saudi", +) -> dict[str, Any]: + """Build a B2B email draft (Arabic). Never sends.""" + name = contact.get("name", "") + company = contact.get("company", "") + role = contact.get("role", "") + + salutation = f"هلا {name}" if name else "هلا" + company_part = f" من {company}" if company else "" + why_now_part = f"\n{why_now}\n" if why_now else "\n" + + body_ar = ( + f"{salutation}،\n\n" + f"أكتب لك{company_part} باختصار. " + f"نشتغل على Dealix كمدير نمو عربي للشركات السعودية:" + f"{why_now_part}" + "خلال 7 أيام، نطلع لك:\n" + "• 10 فرص B2B مناسبة لقطاعكم\n" + "• رسائل عربية جاهزة بنبرتنا\n" + "• خطة متابعة قابلة للتنفيذ\n" + "• Proof Pack بعد الأسبوع\n\n" + f"{offer or 'Pilot بـ 499 ريال أو مجاني مقابل case study.'}\n\n" + "إذا الفكرة تناسبك، نحدد مكالمة 15 دقيقة هذا الأسبوع.\n" + "وإن ما كانت الأولوية الآن خبرني وأرتاح.\n\nشاكر لك." + ) + + return { + "subject_ar": ( + f"فرصة نمو لـ{company}" if company else "فرصة نمو B2B خلال 7 أيام" + ), + "body_ar": include_unsubscribe_footer(body_ar), + "tone": tone, + "target_role": role, + "approval_required": True, + "live_send_allowed": False, + } + + +def include_unsubscribe_footer(body: str) -> str: + """Append a one-line unsubscribe footer (Arabic + English).""" + if not body: + return body + footer = ( + "\n\n———\n" + "لإيقاف هذه الرسائل، رد بكلمة \"إلغاء\" / Reply STOP to unsubscribe." + ) + return body + footer + + +def recommend_pacing(domain_reputation: str = "fresh") -> dict[str, Any]: + """Recommend a daily send pacing based on domain reputation.""" + rep = (domain_reputation or "fresh").lower() + table = { + "fresh": {"max_daily": 20, "warmup_days": 21, "ramp_step": 5}, + "warmed": {"max_daily": 60, "warmup_days": 0, "ramp_step": 10}, + "trusted": {"max_daily": 200, "warmup_days": 0, "ramp_step": 25}, + "damaged": {"max_daily": 5, "warmup_days": 30, "ramp_step": 1}, + } + plan = table.get(rep, table["fresh"]) + return { + "domain_reputation": rep, + **plan, + "notes_ar": ( + "ابدأ بحدود يومية صغيرة على domain جديد، وارتفع تدريجياً. " + "domain متضرر يحتاج فترة تبريد + warmup قبل العودة." + ), + } + + +def score_email_risk( + contact: dict[str, Any], message: str = "", +) -> dict[str, Any]: + """ + Score an outbound email's risk 0..100 (higher = riskier). + + Looks at source, opt_in, message content for spam triggers. + """ + source = contact.get("source", "unknown_source") + opt_in = (contact.get("opt_in_status") or "unknown").lower() + + risk = 0 + reasons: list[str] = [] + + if source == "cold_list": + risk += 50; reasons.append("قائمة باردة — مخاطرة spam مرتفعة.") + elif source == "unknown_source": + risk += 30; reasons.append("مصدر غير معروف — يحتاج مراجعة.") + elif source in ("inbound_lead", "crm_customer", "website_form"): + risk -= 10 # safer + + if opt_in not in ("yes", "double"): + risk += 10 + + msg = (message or "").lower() + spam_triggers = ["ضمان 100%", "ضمان مضمون", "act now", "urgent", + "free money", "click here now", "limited offer"] + for t in spam_triggers: + if t in msg.lower() or t in (message or ""): + risk += 15 + reasons.append(f"عبارة spam: {t}") + + risk = max(0, min(100, risk)) + if risk >= 60: + verdict = "blocked" + elif risk >= 30: + verdict = "needs_review" + else: + verdict = "safe" + + return {"risk": risk, "verdict": verdict, "reasons_ar": reasons} + + +def build_followup_sequence( + contact: dict[str, Any], *, offer: str = "", +) -> dict[str, Any]: + """Build a 3-step Arabic email follow-up sequence.""" + name = contact.get("name", "") + sal = f"هلا {name}" if name else "هلا" + return { + "approval_required": True, + "live_send_allowed": False, + "steps": [ + { + "day": 0, + "subject_ar": "فرصة نمو B2B خلال 7 أيام", + "body_ar": include_unsubscribe_footer( + f"{sal}، (الرسالة الأولى مع العرض الكامل)" + ), + }, + { + "day": 3, + "subject_ar": "متابعة سريعة", + "body_ar": include_unsubscribe_footer( + f"{sal}، أتابع رسالتي السابقة. " + "هل أرتب لك ديمو 12 دقيقة هذا الأسبوع؟" + ), + }, + { + "day": 7, + "subject_ar": "آخر متابعة", + "body_ar": include_unsubscribe_footer( + f"{sal}، آخر متابعة من جهتي. " + "إذا ما كانت الأولوية الآن أرتاح وأرشّفها. " + "وإن أردت ديمو لاحقاً، أنا موجود." + ), + }, + ], + } diff --git a/dealix/auto_client_acquisition/targeting_os/free_diagnostic.py b/dealix/auto_client_acquisition/targeting_os/free_diagnostic.py new file mode 100644 index 00000000..86338cb5 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/free_diagnostic.py @@ -0,0 +1,147 @@ +"""Free Growth Diagnostic — العرض المجاني الذي يجلب pilots.""" + +from __future__ import annotations + +from typing import Any + +from .account_finder import recommend_accounts +from .contact_source_policy import classify_source +from .contactability_matrix import evaluate_contactability + + +def build_free_growth_diagnostic( + company_profile: dict[str, Any], +) -> dict[str, Any]: + """ + Build a free 5-section Arabic growth diagnostic for a prospect. + + Inputs: company_profile = {sector, city, offer, goal, has_list?, channels?} + Outputs: 3 opportunities + 1 message + 1 risk + 1 mini proof plan + paid pilot offer. + """ + sector = company_profile.get("sector", "saas") + city = company_profile.get("city", "Riyadh") + offer = company_profile.get("offer", "") + + accounts = recommend_accounts( + sector=sector, city=city, offer=offer, goal="diagnostic", limit=3, + )["accounts"] + + sample_message = ( + f"هلا، لاحظت توسعكم في قطاع {sector}. " + "نشتغل على Dealix كمدير نمو عربي للشركات السعودية. " + "خلال 7 أيام نطلع لكم 10 فرص B2B + رسائل + خطة متابعة. " + "يناسبكم ديمو 12 دقيقة هذا الأسبوع؟" + ) + + risk_summary = { + "label_ar": "احتمال إضرار سمعة الـdomain أو رقم واتساب", + "why_ar": ( + "لو أرسلت لقائمة بدون opt-in، تتجاوز PDPL ويمكن أن تُحظر القناة. " + "الحل: ابدأ بمصادر آمنة فقط." + ), + "mitigation_ar": [ + "صنّف كل contact حسب المصدر.", + "أوقف أي رقم بدون opt-in.", + "ابدأ بـ Free Diagnostic ثم Pilot.", + ], + } + + mini_proof = build_mini_proof_plan() + + return { + "company_profile": {"sector": sector, "city": city, "offer": offer}, + "delivered_at": "draft", + "approval_required": True, + "sections": { + "opportunities_ar": accounts, + "sample_message_ar": sample_message, + "risk_summary_ar": risk_summary, + "mini_proof_plan_ar": mini_proof, + "paid_pilot_offer": recommend_paid_pilot_offer({"sector": sector}), + }, + "next_step_ar": ( + "إذا أعجبتك العينة، نكمل Pilot 7 أيام بـ499 ريال " + "أو مجاناً مقابل case study بعد انتهاء الـPilot." + ), + } + + +def analyze_uploaded_list_preview( + contacts: list[dict[str, Any]], +) -> dict[str, Any]: + """ + Preview-only analysis of a customer-uploaded list. + + Classifies sources + contactability without storing. Returns aggregate. + """ + if not contacts: + return {"total": 0, "by_status": {}, "preview": []} + + by_status: dict[str, int] = {"safe": 0, "needs_review": 0, "blocked": 0} + preview: list[dict[str, Any]] = [] + + for i, c in enumerate(contacts[:20]): # only first 20 for preview + verdict = evaluate_contactability(c) + status = verdict["status"] + by_status[status] = by_status.get(status, 0) + 1 + preview.append({ + "index": i, + "source": classify_source(c.get("source", "unknown_source"))["source"], + "contactability": status, + "allowed_channels": verdict.get("allowed_channels", []), + }) + + # Aggregate over the FULL list + full_by_status = dict(by_status) + if len(contacts) > 20: + # Project remaining proportionally — deterministic. + scale = len(contacts) / 20 + full_by_status = {k: int(v * scale) for k, v in by_status.items()} + + return { + "total": len(contacts), + "by_status": full_by_status, + "preview": preview, + "recommendations_ar": [ + "ابدأ بالـsafe contacts فقط في الأسبوع الأول.", + "راجع الـneeds_review يدوياً قبل أي إرسال.", + "تخطّ الـblocked تماماً (opt-out).", + ], + } + + +def recommend_paid_pilot_offer(diagnostic: dict[str, Any]) -> dict[str, Any]: + """Recommend a paid Pilot offer based on diagnostic context.""" + return { + "offer_id": "first_10_opportunities_pilot_7d", + "name_ar": "Pilot 7 أيام: 10 فرص + رسائل + متابعة + Proof Pack", + "price_sar_min": 499, + "price_sar_max": 1500, + "free_alternative_ar": "مجاني مقابل case study بعد انتهاء الـPilot.", + "deliverables_ar": [ + "10 فرص B2B مع why-now.", + "10 رسائل عربية جاهزة.", + "خطة متابعة 7 أيام.", + "Proof Pack تفصيلي.", + ], + "approval_required": True, + } + + +def build_mini_proof_plan() -> dict[str, Any]: + """A small Proof Pack template anyone can run in their head.""" + return { + "metrics_to_track": [ + "leads_created", + "drafts_approved", + "positive_replies", + "meetings_drafted", + "pipeline_influenced_sar", + "risks_blocked", + ], + "how_to_count_ar": ( + "كل metric يُحسب يومياً عبر Proof Ledger. " + "في نهاية الأسبوع، نولّد PDF/JSON ونشاركه مع الإدارة." + ), + "review_frequency": "weekly", + } diff --git a/dealix/auto_client_acquisition/targeting_os/linkedin_strategy.py b/dealix/auto_client_acquisition/targeting_os/linkedin_strategy.py new file mode 100644 index 00000000..74900021 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/linkedin_strategy.py @@ -0,0 +1,124 @@ +"""LinkedIn strategy — Lead Forms + manual research + Ads, NO scraping/auto-DM.""" + +from __future__ import annotations + +from typing import Any + + +def linkedin_do_not_do() -> list[str]: + """The hard 'NEVER' list for LinkedIn — encoded explicitly so tests can lock it.""" + return [ + "scrape_profiles", + "auto_connect", + "auto_dm", + "browser_automation", + "fake_engagement", + "download_contacts_from_linkedin", + "buy_scraped_leads", + "use_unauthorized_extensions", + ] + + +def recommend_linkedin_strategy( + segment: str, *, goal: str = "fill_pipeline", +) -> dict[str, Any]: + """ + Recommend a compliant LinkedIn strategy for a segment. + + Always picks Lead Gen Forms / manual / Ads — never scraping/auto-DM. + """ + return { + "segment": segment, + "goal": goal, + "primary": "lead_gen_forms", + "secondary": ["linkedin_ads", "manual_account_research", "content_engagement"], + "do_not_do": linkedin_do_not_do(), + "rationale_ar": ( + "لينكدإن يحظر crawlers/bots/extensions التي تسحب البيانات أو ترسل/توجّه " + "رسائل أو تصنع تفاعلاً غير أصيل؛ لذلك نعتمد فقط على Lead Gen Forms، " + "الإعلانات، والبحث اليدوي المعتمد." + ), + } + + +def build_lead_gen_form_plan( + segment: str, offer: str, *, campaign_name: str = "", +) -> dict[str, Any]: + """Build a structured Lead Gen Form campaign plan.""" + name = campaign_name or f"{segment} — {offer or 'Pilot'}" + return { + "campaign_name": name, + "audience_ar": ( + f"المستهدفون: {segment} — أصحاب القرار في القطاع المحدد، " + "السعودية والخليج، حجم 11-200 موظف." + ), + "offer_ar": offer or "Pilot 7 أيام لاستخراج 10 فرص B2B + رسائل عربية + Proof Pack.", + "lead_magnet_ar": ( + "Free Growth Diagnostic — تقرير من 5 صفحات: 3 فرص + رسالة عربية + خطة 7 أيام." + ), + "form_fields_required": ["full_name", "company_name", "work_email", "role"], + "hidden_fields": [ + {"name": "campaign_name", "value": name}, + {"name": "sector", "value": segment}, + {"name": "sales_owner", "value": "{{owner}}"}, + {"name": "ad_set", "value": "{{ad_set_id}}"}, + ], + "approval_required": True, + "notes_ar": ( + "الـ hidden fields ضرورية لمعرفة مصدر كل lead و ربطه بالـCRM. " + "كل lead من Lead Form يدخل Dealix كـ source=linkedin_lead_form (آمن)." + ), + } + + +def build_manual_research_task( + account: dict[str, Any], *, role: str = "head_of_sales", +) -> dict[str, Any]: + """Build a manual LinkedIn research task — for a human, not automation.""" + company = account.get("name", "?") + return { + "task_type": "manual_linkedin_research", + "company": company, + "target_role": role, + "instructions_ar": [ + f"افتح صفحة شركة {company} على LinkedIn يدوياً.", + f"حدد الشخص الذي يحمل دور {role}.", + "لا تستخدم أي extension أو bot لاستخراج البيانات.", + "سجّل اسم الشخص + مسماه فقط — لا تنسخ أي معلومات إضافية.", + "أضف الاسم في Dealix كـ source=manual_research → سيدخل needs_review.", + ], + "approval_required": True, + "completion_minutes": 5, + } + + +def build_safe_connection_message( + role: str, company: str, *, offer: str = "", +) -> dict[str, Any]: + """ + Build a safe connection-request message for LinkedIn (manual send by user). + + Never auto-sends. Always returns draft with approval_required=True. + """ + role_ar = role + body_ar = ( + f"هلا، تابعت أعمال {company} مؤخراً وعجبني التوسع. " + f"أعمل على Dealix كمدير نمو عربي للشركات السعودية. " + f"يناسبك نتعارف هنا؟" + ) + if offer: + body_ar += f" وفي حال فيه فرصة لـ{offer}، أكون سعيد أشاركك أمثلة." + + return { + "channel": "linkedin_connection_request", + "target_role": role_ar, + "target_company": company, + "body_ar": body_ar[:280], # LinkedIn note limit + "approval_required": True, + "live_send_allowed": False, + "send_method": "manual_only", + "notes_ar": ( + "هذه مسودة. أرسلها يدوياً من حسابك على LinkedIn. " + "Dealix لا يرسل تلقائياً ولا يستخدم أي extension أو bot." + ), + } diff --git a/dealix/auto_client_acquisition/targeting_os/outreach_scheduler.py b/dealix/auto_client_acquisition/targeting_os/outreach_scheduler.py new file mode 100644 index 00000000..9e528a31 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/outreach_scheduler.py @@ -0,0 +1,133 @@ +"""Outreach scheduler — pace, follow-up, opt-out enforcement.""" + +from __future__ import annotations + +from typing import Any + +DEFAULT_LIMITS: dict[str, int] = { + "max_daily_email_drafts": 30, + "max_daily_whatsapp_approved_sends": 10, + "max_followups": 3, + "cooldown_days": 7, + "max_same_domain_contacts": 5, +} + + +def build_outreach_plan( + targets: list[dict[str, Any]], + *, + channels: list[str] | None = None, + goal: str = "fill_pipeline", +) -> dict[str, Any]: + """ + Build a per-target outreach plan across channels. + + Each target gets day-by-day actions; never schedules a live send. + """ + channels = channels or ["email", "linkedin_lead_form"] + plan: list[dict[str, Any]] = [] + + for t in targets: + steps: list[dict[str, Any]] = [ + {"day": 0, "channel": channels[0], + "action": "draft_first_message", + "approval_required": True, + "live_send_allowed": False}, + {"day": 3, "channel": channels[0], + "action": "draft_followup_1", + "approval_required": True, + "live_send_allowed": False}, + ] + if "linkedin_lead_form" in channels or "linkedin" in channels: + steps.append({ + "day": 5, "channel": "linkedin_manual", + "action": "manual_research_task", + "approval_required": True, + "live_send_allowed": False, + }) + steps.append({ + "day": 7, "channel": channels[0], + "action": "draft_final_followup_or_archive", + "approval_required": True, + "live_send_allowed": False, + }) + plan.append({ + "target_company": t.get("name", "?"), + "target_role": t.get("role", "?"), + "channels": channels, + "steps": steps, + }) + + return { + "goal": goal, + "channels": channels, + "total_targets": len(targets), + "plan": plan, + "limits": DEFAULT_LIMITS, + "notes_ar": ( + "كل خطوة draft تحتاج اعتماد. " + "لا إرسال آلي، ولا تجاوز الحدود اليومية." + ), + } + + +def schedule_followups(plan: dict[str, Any]) -> dict[str, Any]: + """Add follow-up timing to each target in a plan.""" + out = dict(plan) + out["scheduled"] = True + return out + + +def enforce_daily_limits( + plan: dict[str, Any], + *, + limits: dict[str, int] | None = None, +) -> dict[str, Any]: + """Cap actions in the plan to the configured daily limits.""" + limits = limits or DEFAULT_LIMITS + targets = plan.get("plan", []) + + capped: list[dict[str, Any]] = [] + daily_email = 0 + domain_count: dict[str, int] = {} + + for t in targets: + company = t.get("target_company", "") + # treat company as a proxy for domain in test data + if company in domain_count and domain_count[company] >= limits["max_same_domain_contacts"]: + continue + ok_steps = [] + for step in t.get("steps", []): + if step.get("channel") == "email": + if daily_email >= limits["max_daily_email_drafts"]: + continue + daily_email += 1 + ok_steps.append(step) + if ok_steps: + capped.append({**t, "steps": ok_steps}) + domain_count[company] = domain_count.get(company, 0) + 1 + + return { + **plan, + "plan": capped, + "applied_limits": limits, + "capped_total_targets": len(capped), + } + + +def stop_on_opt_out(plan: dict[str, Any]) -> dict[str, Any]: + """Filter out targets where the contact has opted out.""" + targets = plan.get("plan", []) + kept = [t for t in targets if not t.get("opt_out")] + return {**plan, "plan": kept, "stopped_due_to_opt_out": len(targets) - len(kept)} + + +def summarize_plan_ar(plan: dict[str, Any]) -> str: + """Build an Arabic one-paragraph summary of an outreach plan.""" + n = plan.get("total_targets") or len(plan.get("plan", [])) + channels = ", ".join(plan.get("channels", [])) + return ( + f"خطة تواصل لـ{n} هدف عبر القنوات: {channels}. " + f"كل خطوة draft، تتطلب اعتماد، ولا إرسال آلي. " + f"الحدود اليومية مفعّلة. opt-out يوقف فوراً." + ) diff --git a/dealix/auto_client_acquisition/targeting_os/reputation_guard.py b/dealix/auto_client_acquisition/targeting_os/reputation_guard.py new file mode 100644 index 00000000..d8bb68e1 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/reputation_guard.py @@ -0,0 +1,135 @@ +"""Reputation guard — يحمي القنوات من الحظر.""" + +from __future__ import annotations + +from typing import Any + + +def risk_thresholds() -> dict[str, dict[str, float]]: + """The thresholds where a channel needs throttling/pause.""" + return { + "email": { + "bounce_rate_warn": 0.02, "bounce_rate_pause": 0.05, + "complaint_rate_warn": 0.001, "complaint_rate_pause": 0.003, + "opt_out_rate_warn": 0.05, "opt_out_rate_pause": 0.10, + "min_reply_rate": 0.02, + }, + "whatsapp": { + "block_rate_warn": 0.01, "block_rate_pause": 0.03, + "report_rate_warn": 0.005, "report_rate_pause": 0.02, + "opt_out_rate_warn": 0.05, "opt_out_rate_pause": 0.10, + "min_reply_rate": 0.10, + }, + "linkedin": { + "connection_decline_warn": 0.3, "connection_decline_pause": 0.5, + }, + } + + +def calculate_channel_reputation( + metrics: dict[str, float], + *, + channel: str = "email", +) -> dict[str, Any]: + """Compute a 0..100 reputation score for a channel based on metrics.""" + th = risk_thresholds().get(channel, {}) + score = 100 + reasons_ar: list[str] = [] + + if channel == "email": + bounce = float(metrics.get("bounce_rate", 0)) + complaint = float(metrics.get("complaint_rate", 0)) + opt_out = float(metrics.get("opt_out_rate", 0)) + reply = float(metrics.get("reply_rate", 0.05)) + + if bounce >= th["bounce_rate_pause"]: + score -= 40; reasons_ar.append("معدل الـ bounce تجاوز الحد الحرج.") + elif bounce >= th["bounce_rate_warn"]: + score -= 15; reasons_ar.append("ارتفاع في الـ bounce — راقب.") + + if complaint >= th["complaint_rate_pause"]: + score -= 50; reasons_ar.append("شكاوى spam مرتفعة جداً.") + elif complaint >= th["complaint_rate_warn"]: + score -= 20; reasons_ar.append("بداية شكاوى spam.") + + if opt_out >= th["opt_out_rate_pause"]: + score -= 25; reasons_ar.append("نسبة opt-out مرتفعة جداً.") + + if reply < th["min_reply_rate"]: + score -= 10; reasons_ar.append("معدل الرد منخفض — راجع الجودة.") + + elif channel == "whatsapp": + block = float(metrics.get("block_rate", 0)) + report = float(metrics.get("report_rate", 0)) + opt_out = float(metrics.get("opt_out_rate", 0)) + + if block >= th["block_rate_pause"]: + score -= 60; reasons_ar.append("نسبة الحظر مرتفعة جداً — أوقف.") + elif block >= th["block_rate_warn"]: + score -= 25; reasons_ar.append("بداية حظر — راجع المحتوى.") + + if report >= th["report_rate_pause"]: + score -= 50; reasons_ar.append("بلاغات spam على واتساب.") + + if opt_out >= th["opt_out_rate_pause"]: + score -= 30; reasons_ar.append("opt-out واتساب مرتفع.") + + score = max(0, min(100, score)) + return { + "channel": channel, + "score": score, + "reasons_ar": reasons_ar, + "verdict": ("healthy" if score >= 70 + else "watch" if score >= 40 + else "pause"), + } + + +def should_pause_channel( + metrics: dict[str, float], *, channel: str = "email", +) -> dict[str, Any]: + """Boolean wrapper: should we pause this channel right now?""" + rep = calculate_channel_reputation(metrics, channel=channel) + return { + "should_pause": rep["verdict"] == "pause", + "reputation_score": rep["score"], + "reasons_ar": rep["reasons_ar"], + } + + +def recommend_recovery_action( + metrics: dict[str, float], *, channel: str = "email", +) -> dict[str, Any]: + """Recommend recovery actions based on reputation problems.""" + rep = calculate_channel_reputation(metrics, channel=channel) + actions: list[str] = [] + if rep["verdict"] == "pause": + actions = [ + "أوقف إرسال جميع الحملات الجديدة على هذه القناة.", + "ابدأ فترة تبريد لمدة 14 يوماً على الأقل.", + "افحص قائمة الـ contacts وحدّث opt-in.", + "نظّف عناوين الـ bounce وأعد التحقق.", + ] + elif rep["verdict"] == "watch": + actions = [ + "خفّض الحجم اليومي بنسبة 50%.", + "ركّز على المصادر الآمنة فقط (CRM/inbound).", + "راجع الرسائل لتقليل العبارات المخاطرة.", + ] + else: + actions = ["استمر — راقب أسبوعياً."] + return { + "channel": channel, + "verdict": rep["verdict"], + "actions_ar": actions, + "score": rep["score"], + } + + +def summarize_reputation_ar(metrics: dict[str, float], *, channel: str = "email") -> str: + """One-line Arabic summary of channel health.""" + rep = calculate_channel_reputation(metrics, channel=channel) + return ( + f"قناة {channel}: score {rep['score']} ({rep['verdict']}). " + + (rep["reasons_ar"][0] if rep["reasons_ar"] else "حالة صحية.") + ) diff --git a/dealix/auto_client_acquisition/targeting_os/self_growth_mode.py b/dealix/auto_client_acquisition/targeting_os/self_growth_mode.py new file mode 100644 index 00000000..a6122a14 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/self_growth_mode.py @@ -0,0 +1,157 @@ +"""Self-Growth Mode — Dealix يستهدف عملاءه ويصنع فرصاً لنفسه.""" + +from __future__ import annotations + +from typing import Any + +from .account_finder import recommend_accounts +from .buyer_role_mapper import map_buying_committee +from .daily_autopilot import ( + build_daily_targeting_brief, + recommend_today_actions, +) + + +# Dealix's own ICP (deterministic). +DEALIX_ICP_FOCUSES: tuple[dict[str, str], ...] = ( + {"sector": "agency", "city": "Riyadh", "label_ar": "وكالات تسويق B2B في الرياض"}, + {"sector": "training", "city": "Riyadh", "label_ar": "شركات تدريب B2B في الرياض"}, + {"sector": "consulting", "city": "Riyadh", "label_ar": "شركات استشارات نمو"}, + {"sector": "saas", "city": "Riyadh", "label_ar": "SaaS سعودية صغيرة-متوسطة"}, + {"sector": "real_estate", "city": "Jeddah", "label_ar": "وسطاء عقار B2B في جدة"}, +) + + +def recommend_dealix_targets( + *, + sector_focus: str | None = None, + city_focus: str | None = None, + limit: int = 10, +) -> dict[str, Any]: + """Build Dealix's own daily target list.""" + sector = sector_focus or DEALIX_ICP_FOCUSES[0]["sector"] + city = city_focus or DEALIX_ICP_FOCUSES[0]["city"] + accounts = recommend_accounts( + sector=sector, city=city, goal="self_growth", + offer="Pilot 7 أيام لاستخراج 10 فرص B2B", + limit=limit, + ) + committee = map_buying_committee(sector=sector, company_size="small", + goal="fill_pipeline") + return { + "icp": {"sector": sector, "city": city}, + "targets": accounts, + "buying_committee_template": committee, + "approval_required": True, + "live_send_allowed": False, + "notes_ar": ( + "هذه قائمة استهداف Dealix لنفسه. كل تواصل draft فقط، " + "ولا يُرسل إلا بعد اعتماد المؤسس." + ), + } + + +def build_free_service_offer(target: dict[str, Any]) -> dict[str, Any]: + """Build a 'Free Growth Diagnostic' offer card for a single target.""" + company = target.get("name", "?") + return { + "target_company": company, + "offer_id": "free_growth_diagnostic", + "title_ar": f"تشخيص نمو مجاني لـ{company}", + "deliverables_ar": [ + "3 فرص B2B مناسبة لقطاعكم.", + "1 رسالة عربية مخصصة.", + "1 تقرير مخاطر سريع.", + "1 خطة Pilot مقترحة.", + ], + "delivery_time": "خلال 24 ساعة عمل", + "price": 0, + "currency": "SAR", + "follow_up_offer_ar": ( + "إذا أعجبكم، نكمل Pilot 7 أيام بـ499 ريال أو مجاني مقابل case study." + ), + "approval_required": True, + } + + +def build_self_growth_daily_brief( + *, + sector_focus: str | None = None, + city_focus: str | None = None, +) -> dict[str, Any]: + """Build today's self-growth brief for Dealix (founder-facing).""" + sector = sector_focus or DEALIX_ICP_FOCUSES[0]["sector"] + city = city_focus or DEALIX_ICP_FOCUSES[0]["city"] + company_brief = build_daily_targeting_brief({"sector": sector, "city": city}) + actions = recommend_today_actions({"sector": sector, "city": city}) + + targets = recommend_dealix_targets( + sector_focus=sector, city_focus=city, limit=10, + ) + + return { + "icp": {"sector": sector, "city": city}, + "company_brief": company_brief, + "today_actions": actions, + "top_10_targets": targets["targets"]["accounts"][:10], + "recommended_first_action_ar": ( + "ابعث 3 رسائل Free Diagnostic مخصصة هذا الصباح، " + "ثم تابع 2 ديمو من الأمس." + ), + } + + +def build_weekly_learning_report( + results: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Build a weekly Arabic learning report from Dealix's own results.""" + r = results or {} + diagnostics = int(r.get("free_diagnostics_delivered", 0)) + pilots = int(r.get("paid_pilots_started", 0)) + meetings = int(r.get("meetings_held", 0)) + case_studies = int(r.get("case_studies_published", 0)) + revenue = float(r.get("revenue_sar", 0)) + + return { + "week_metrics": { + "free_diagnostics": diagnostics, + "paid_pilots": pilots, + "meetings": meetings, + "case_studies": case_studies, + "revenue_sar": revenue, + }, + "learning_questions_ar": [ + "أي قطاع رد أكثر هذا الأسبوع؟", + "أي رسالة نجحت؟ ولماذا؟", + "أي قناة فعّالة (إيميل / لينكدإن / شركاء)؟", + "أي اعتراض تكرر أكثر من مرتين؟", + "ما العرض الذي يبيع بسهولة؟", + ], + "next_week_experiments_ar": [ + "جرّب angle جديد لقطاع التدريب: ROI ملموس لـHR.", + "أرسل Free Diagnostic لـ20 وكالة تسويق.", + "اعقد ديمو واحد مع شركة SaaS سعودية.", + "اطلب أول case study من أنجح Pilot.", + ], + } + + +def build_dealix_self_growth_plan() -> dict[str, Any]: + """Top-level monthly plan for Dealix using its own OS to grow.""" + return { + "icp_focuses": list(DEALIX_ICP_FOCUSES), + "monthly_targets": { + "free_diagnostics_delivered": 30, + "paid_pilots_started": 6, + "growth_os_subscriptions": 3, + "agency_partners_signed": 1, + "case_studies_published": 1, + }, + "operating_loop_ar": [ + "كل صباح: اعرض 10 شركات جديدة + 5 رسائل drafts.", + "كل ظهر: راجع الردود + جدول 1-2 ديمو.", + "كل مساء: حدّث Proof Ledger + أرسل Free Diagnostic لـ3 شركات.", + "كل أسبوع: اكتب learning report + جرّب angle جديد.", + "كل شهر: راجع Service Excellence Score لكل خدمة.", + ], + } diff --git a/dealix/auto_client_acquisition/targeting_os/service_offers.py b/dealix/auto_client_acquisition/targeting_os/service_offers.py new file mode 100644 index 00000000..5fcfa28f --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/service_offers.py @@ -0,0 +1,161 @@ +"""Targeting-tier service offers — quick lookup of buyable offers.""" + +from __future__ import annotations + +from typing import Any + +# Targeting-OS-related offers. The full Service Tower has more. +TARGETING_OFFERS: tuple[dict[str, Any], ...] = ( + { + "id": "list_intelligence", + "name_ar": "تحليل قائمة (List Intelligence)", + "target_customer_ar": "شركة عندها قائمة أرقام/إيميلات/عملاء قدامى", + "outcome_ar": "أفضل 50 target من قائمتك + رسائل + خطة 7 أيام", + "price_min_sar": 499, + "price_max_sar": 1500, + }, + { + "id": "first_10_opportunities_sprint", + "name_ar": "10 فرص في 10 دقائق", + "target_customer_ar": "شركة B2B تحتاج فرص مؤهلة بسرعة", + "outcome_ar": "10 فرص + رسائل + خطة متابعة + Proof Pack", + "price_min_sar": 499, + "price_max_sar": 1500, + }, + { + "id": "self_growth_operator", + "name_ar": "مدير نمو شخصي (Self-Growth Operator)", + "target_customer_ar": "مؤسسون / مستشارون / وكالات صغيرة", + "outcome_ar": "Daily brief + drafts + متابعة + تقارير", + "price_min_sar": 999, + "price_max_sar": 999, + }, + { + "id": "linkedin_lead_gen_setup", + "name_ar": "إعداد LinkedIn Lead Gen", + "target_customer_ar": "شركات B2B تحتاج decision makers", + "outcome_ar": "حملة Lead Gen Form + audiences + ربط CRM", + "price_min_sar": 2000, + "price_max_sar": 7500, + }, + { + "id": "whatsapp_compliance_setup", + "name_ar": "إعداد امتثال واتساب", + "target_customer_ar": "شركات تستخدم واتساب بشكل عشوائي", + "outcome_ar": "تصنيف القوائم + opt-in templates + audit", + "price_min_sar": 1500, + "price_max_sar": 4000, + }, + { + "id": "partner_sprint", + "name_ar": "سبرنت شراكات", + "target_customer_ar": "شركات تبغى نمو عبر الشركاء", + "outcome_ar": "20 شريك محتمل + رسائل + 5 اجتماعات", + "price_min_sar": 3000, + "price_max_sar": 7500, + }, + { + "id": "free_growth_diagnostic", + "name_ar": "تشخيص نمو مجاني", + "target_customer_ar": "أي شركة B2B تريد عينة قبل الـPilot", + "outcome_ar": "3 فرص + رسالة + تقرير مخاطر + خطة Pilot", + "price_min_sar": 0, + "price_max_sar": 0, + }, +) + + +def list_targeting_services() -> dict[str, Any]: + return { + "total": len(TARGETING_OFFERS), + "offers": [dict(o) for o in TARGETING_OFFERS], + } + + +def recommend_service_offer( + customer_type: str, + *, + goal: str = "fill_pipeline", +) -> dict[str, Any]: + """Recommend the best-fit offer for a customer type + goal.""" + ct = (customer_type or "").lower() + + if "agency" in ct or "وكالة" in ct: + chosen = next(o for o in TARGETING_OFFERS if o["id"] == "partner_sprint") + elif "list" in ct or "قائمة" in ct: + chosen = next(o for o in TARGETING_OFFERS if o["id"] == "list_intelligence") + elif "founder" in ct or "مؤسس" in ct: + chosen = next(o for o in TARGETING_OFFERS if o["id"] == "self_growth_operator") + elif "saas" in ct or "b2b" in ct: + chosen = next(o for o in TARGETING_OFFERS if o["id"] == "first_10_opportunities_sprint") + elif "whatsapp" in ct or "واتساب" in ct: + chosen = next(o for o in TARGETING_OFFERS if o["id"] == "whatsapp_compliance_setup") + else: + chosen = next(o for o in TARGETING_OFFERS if o["id"] == "free_growth_diagnostic") + + return { + "recommended_offer": dict(chosen), + "reasoning_ar": ( + f"بناءً على نوع العميل ({customer_type}) والهدف ({goal})، " + f"الأنسب: {chosen['name_ar']}." + ), + } + + +def build_offer_card(service: dict[str, Any] | str) -> dict[str, Any]: + """Build an Arabic offer card (≤3 buttons) for the inbox/feed.""" + if isinstance(service, str): + service = next((o for o in TARGETING_OFFERS if o["id"] == service), + {"id": service, "name_ar": service, + "outcome_ar": "", "price_min_sar": 0, "price_max_sar": 0}) + price_label = ( + "مجاني" + if service.get("price_min_sar") == 0 + else f"{service.get('price_min_sar')}–{service.get('price_max_sar')} ريال" + ) + return { + "type": "service_offer", + "service_id": service.get("id"), + "title_ar": service.get("name_ar", "خدمة"), + "summary_ar": service.get("outcome_ar", ""), + "price_ar": price_label, + "buttons_ar": ["ابدأ الآن", "اطلب عرض", "تخطي"], + "approval_required": True, + } + + +def estimate_service_price( + service_id: str, + *, + company_size: str = "small", + urgency: str = "normal", + channels_count: int = 1, +) -> dict[str, Any]: + """Estimate a SAR price range for a service given inputs.""" + base = next((o for o in TARGETING_OFFERS if o["id"] == service_id), None) + if base is None: + return {"error": f"unknown service: {service_id}"} + + p_min = float(base["price_min_sar"]) + p_max = float(base["price_max_sar"]) + + # Size multiplier + size_mult = {"micro": 0.8, "small": 1.0, "medium": 1.3, "large": 1.7}.get( + company_size, 1.0, + ) + # Urgency multiplier + urgency_mult = {"normal": 1.0, "rush": 1.3, "asap": 1.5}.get(urgency, 1.0) + # Channel multiplier + ch_mult = 1.0 + max(0, channels_count - 1) * 0.15 + + return { + "service_id": service_id, + "estimated_min_sar": round(p_min * size_mult * urgency_mult * ch_mult), + "estimated_max_sar": round(p_max * size_mult * urgency_mult * ch_mult), + "currency": "SAR", + "factors": { + "company_size": company_size, + "urgency": urgency, + "channels_count": channels_count, + }, + } diff --git a/dealix/auto_client_acquisition/targeting_os/social_strategy.py b/dealix/auto_client_acquisition/targeting_os/social_strategy.py new file mode 100644 index 00000000..65d63c39 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/social_strategy.py @@ -0,0 +1,94 @@ +"""Social strategy — official APIs + opt-in DMs only, public replies as drafts.""" + +from __future__ import annotations + +from typing import Any + + +def social_do_not_do() -> list[str]: + return [ + "scrape_public_profiles", + "auto_dm_strangers", + "fake_engagement", + "buy_followers_or_engagement", + "use_unauthorized_apis", + "ignore_platform_terms", + ] + + +def recommend_social_sources( + sector: str, *, goal: str = "fill_pipeline", +) -> dict[str, Any]: + """Recommend social sources by sector — only safe, official channels.""" + s = (sector or "").lower() + by_sector = { + "real_estate": ["instagram_graph_api", "x_api_mentions", "google_business_reviews"], + "retail": ["instagram_graph_api", "google_business_reviews", "tiktok_business"], + "healthcare": ["google_business_reviews", "instagram_graph_api"], + "saas": ["x_api_mentions", "linkedin_lead_gen_forms"], + "training": ["linkedin_lead_gen_forms", "x_api_mentions"], + "agency": ["linkedin_lead_gen_forms", "x_api_mentions"], + } + return { + "sector": s, + "recommended_sources": by_sector.get(s, ["linkedin_lead_gen_forms", + "google_business_reviews"]), + "do_not_do": social_do_not_do(), + "notes_ar": ( + "نلتزم بالـ official APIs والصلاحيات الرسمية فقط. " + "DMs بدون تفاعل سابق محظورة." + ), + } + + +def build_social_listening_plan( + sector: str, keywords: list[str] | None = None, +) -> dict[str, Any]: + """Build a social listening plan — listening only, no auto-replies.""" + keywords = keywords or [ + "نمو", "B2B", "leads", "اجتماعات", + "Pilot", "تدريب مبيعات", "أتمتة", + ] + return { + "sector": sector, + "keywords_ar_or_en": keywords, + "listen_for": [ + "mentions_of_company", + "competitor_mentions", + "buying_signals", + "complaints", + "hiring_signals", + "events_and_launches", + ], + "convert_to_cards_for": [ + "lead", "competitor_move", "review_response", + "content_idea", "partner_suggestion", + ], + "no_auto_reply": True, + "approval_required_for_reply": True, + } + + +def draft_public_reply( + comment: str, + *, + brand_voice: str = "professional_saudi", +) -> dict[str, Any]: + """Build a public reply draft to a comment/review (Arabic).""" + body_ar = ( + "شكراً على ملاحظتك. نأخذ تعليقك بجد وسنتواصل معك مباشرة لتفاصيل أكثر " + "ومعالجة الموضوع. سعدنا بمشاركتك." + ) + return { + "draft": True, + "body_ar": body_ar, + "brand_voice": brand_voice, + "approval_required": True, + "live_publish_allowed": False, + "guidelines_ar": [ + "لا تكشف بيانات شخصية في الرد العام.", + "حول التفاصيل لقناة خاصة.", + "لا تتجاهل العميل المنزعج.", + "لا تحذف أو ترد بشكل دفاعي.", + ], + } diff --git a/dealix/auto_client_acquisition/targeting_os/whatsapp_strategy.py b/dealix/auto_client_acquisition/targeting_os/whatsapp_strategy.py new file mode 100644 index 00000000..68faa391 --- /dev/null +++ b/dealix/auto_client_acquisition/targeting_os/whatsapp_strategy.py @@ -0,0 +1,124 @@ +"""WhatsApp strategy — opt-in only, never cold, draft-first.""" + +from __future__ import annotations + +from typing import Any + + +def whatsapp_do_not_do() -> list[str]: + return [ + "cold_send_without_consent", + "scrape_groups", + "buy_phone_lists", + "auto_send_without_approval", + "send_outside_business_hours_without_consent", + "ignore_opt_out", + ] + + +def requires_opt_in(contact: dict[str, Any]) -> dict[str, Any]: + """ + Check whether reaching this contact via WhatsApp requires opt-in. + + Returns the opt-in requirement + how to obtain it if missing. + """ + source = contact.get("source", "unknown_source") + opt_in = (contact.get("opt_in_status") or "unknown").lower() + has_relationship = bool(contact.get("has_relationship", False)) + + needs = True + if has_relationship and source == "crm_customer" and opt_in == "yes": + needs = False + if source == "inbound_lead" and opt_in in ("yes", "double"): + needs = False + + return { + "needs_opt_in": needs, + "current_status": opt_in, + "source": source, + "obtain_via_ar": ( + "نموذج موقع + تأكيد بالـemail (double opt-in) أو " + "Lead Gen Form + شرح صريح بنوع الرسائل." + ), + } + + +def draft_whatsapp_message( + contact: dict[str, Any], *, offer: str = "", why_now: str = "", +) -> dict[str, Any]: + """Build a WhatsApp message draft. Never sends; always approval-required.""" + name = contact.get("name", "") + sal = f"هلا {name}" if name else "هلا" + why_now_part = f" {why_now}" if why_now else "" + body_ar = ( + f"{sal}.{why_now_part} نشتغل على Dealix كمدير نمو عربي. " + "خلال 7 أيام نطلع 10 فرص B2B + رسائل + خطة متابعة. " + f"{offer or 'Pilot بـ 499 ريال أو مجاني مقابل case study.'} " + "يناسبك ديمو 12 دقيقة هذا الأسبوع؟" + "\n\nلو ما تفضل هذه الرسائل، اكتب \"إلغاء\" وأوقفها." + ) + risk = score_whatsapp_risk(contact, body_ar) + return { + "channel": "whatsapp", + "body_ar": body_ar, + "approval_required": True, + "live_send_allowed": False, + "opt_in_check": requires_opt_in(contact), + "risk": risk, + "do_not_do": whatsapp_do_not_do(), + } + + +def score_whatsapp_risk(contact: dict[str, Any], message: str = "") -> dict[str, Any]: + """Score WhatsApp risk 0..100; very strict.""" + source = contact.get("source", "unknown_source") + opt_in = (contact.get("opt_in_status") or "unknown").lower() + risk = 0 + reasons: list[str] = [] + + if source == "cold_list": + risk += 100 + reasons.append("قائمة باردة — واتساب محظور تلقائياً.") + if opt_in not in ("yes", "double"): + risk += 40 + reasons.append("لا يوجد opt-in واضح.") + if source == "unknown_source": + risk += 30 + reasons.append("مصدر غير معروف.") + + risky_phrases = ["ضمان 100%", "آخر فرصة", "اضغط الآن", "نتائج مضمونة"] + for p in risky_phrases: + if p in message: + risk += 25 + reasons.append(f"عبارة محظورة: {p}") + + risk = max(0, min(100, risk)) + if risk >= 50: + verdict = "blocked" + elif risk >= 25: + verdict = "needs_review" + else: + verdict = "safe" + return {"risk": risk, "verdict": verdict, "reasons_ar": reasons} + + +def build_opt_in_request_template( + company_name: str = "Dealix", +) -> dict[str, Any]: + """Build an opt-in request template the customer can send via website/forms.""" + return { + "channel": "website_or_form", + "body_ar": ( + f"بالاشتراك في تنبيهات {company_name} عبر واتساب، أوافق على استقبال " + "رسائل تتعلق بالعروض والمحتوى الخاص بالشركة. أعرف أنه يمكنني الانسحاب " + "في أي وقت بكتابة \"إلغاء\"." + ), + "explicit_purpose_required": True, + "explicit_company_name_required": True, + "explicit_unsubscribe_required": True, + "double_opt_in_recommended": True, + "notes_ar": ( + "WhatsApp Business يتطلب شرحاً صريحاً لما سيستقبله المستخدم. " + "ظهور رقم واتساب في موقع لا يكفي كموافقة." + ), + } diff --git a/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md b/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md index b8559292..625e46ab 100644 --- a/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md +++ b/dealix/docs/DEALIX_100_PERCENT_LAUNCH_PLAN.md @@ -209,6 +209,75 @@ OAuth Gmail/Calendar، حصص، سياسات. **ممنوع اليوم:** live WhatsApp send, live Gmail send, live Calendar insert, payment charge, scraping social, وعود "نضمن نتائج". +## 36. Targeting & Acquisition OS — نظام الاستهداف الذكي + +طبقة جديدة (16 module + 20 endpoint + 47 اختبار) تجعل Dealix يستهدف بذكاء بدلاً من جمع عشوائي: + +- **Account-first**: `account_finder` يحدد 10-25 شركة لكل (sector, city) مع `fit_score` و`why_now_ar`. +- **Buying Committee**: `buyer_role_mapper` بـ14 دور وخرائط حسب القطاع (training/saas/real_estate/...). +- **Contact Source Policy**: 12 مصدر (crm_customer → opt_out) مع risk_score + retention. +- **Contactability Matrix**: 5 action modes (suggest_only/draft_only/approval_required/approved_execute/blocked). +- **LinkedIn Strategy**: Lead Forms + Ads + Manual فقط — `linkedin_do_not_do()` يقفل scraping/auto-DM/auto-connect. +- **Email Strategy**: drafts + unsubscribe + pacing per domain reputation. +- **WhatsApp Strategy**: opt-in only، rejects cold + risky phrases تلقائياً. +- **Outreach Scheduler**: day-by-day plan + daily limits + opt-out enforcement. +- **Reputation Guard**: bounce/complaint/opt-out thresholds → healthy/watch/pause مع recovery actions. +- **Daily Autopilot**: Arabic brief + 7 today actions + EOD report. +- **Self-Growth Mode**: 5 ICP focuses لـ Dealix نفسه + daily brief + weekly learning. +- **Free Growth Diagnostic**: العرض المجاني الذي يجلب Pilots. +- **Contract Drafts**: Pilot/DPA/Referral/Agency outlines (legal review required, PDPL-aware). + +**Endpoints:** `/api/v1/targeting/{accounts/recommend, buying-committee/map, contacts/evaluate, uploaded-list/analyze, outreach/plan, daily-autopilot/demo, self-growth/demo, reputation/status, linkedin/strategy, drafts/email, drafts/whatsapp, free-diagnostic, services, contracts/templates, ...}`. **التفصيل:** [`TARGETING_ACQUISITION_OS.md`](TARGETING_ACQUISITION_OS.md). + +## 37. Service Tower — برج الخدمات الذاتية + +**12 Productized Service** + Wizard + Pricing Engine + Scorecard + WhatsApp CEO Control + Upgrade Paths (8 modules + 20 endpoint + 38 اختبار): + +| الخدمة | السعر | النوع | +|--------|------|------| +| Free Growth Diagnostic | مجاني | one_time | +| List Intelligence | 499–1,500 | one_time | +| First 10 Opportunities Sprint | 499–1,500 | sprint | +| Self-Growth Operator | 999/شهر | monthly | +| Growth OS Monthly | 2,999/شهر | monthly | +| Email Revenue Rescue | 1,500–5,000 | one_time | +| Meeting Booking Sprint | 1,500–5,000 | sprint | +| Partner Sprint | 3,000–7,500 | sprint | +| Agency Partner Program | 10,000–50,000 | one_time | +| WhatsApp Compliance Setup | 1,500–4,000 | one_time | +| LinkedIn Lead Gen Setup | 2,000–7,500 | one_time | +| Executive Growth Brief | 499–999/شهر | monthly | + +**3 أبواب للعميل:** +1. أريد عملاء جدد. +2. عندي بيانات وأبغى أستفيد منها. +3. أبغى توسع وشراكات. + +**Endpoints:** `/api/v1/services/{catalog, recommend, {id}/start, {id}/workflow, {id}/quote, {id}/scorecard, {id}/upgrade-path, ceo/daily-brief, ceo/approval-card, ...}`. **التفصيل:** [`SERVICE_TOWER_STRATEGY.md`](SERVICE_TOWER_STRATEGY.md). + +## 38. Service Excellence OS — مصنع الخدمات الممتازة + +**8 modules + 22 endpoint + 33 اختبار** يضمنون أن كل خدمة تطلق بـ score ≥80 وتجاوز 4 quality gates، وتستمر في التحسين الأسبوعي: + +- **Feature Matrix** — 12 must-have لكل خدمة + advanced/premium/future. +- **Service Scoring** — 10 أبعاد × 10 = 100 → launch_ready/beta_only/needs_work. +- **Quality Review** — 4 gates: proof / approval / pricing / channels. +- **Competitor Gap** — مقارنة بـ7 فئات منافسين (CRM, WA tools, email assistants, LinkedIn tools, agencies, revenue intelligence, generic AI). +- **Proof Metrics** — ROI estimate (pipeline_x + closed_won_x). +- **Research Lab** — brief شهري + hypotheses + experiments. +- **Improvement Backlog** — feedback → backlog → prioritized weekly tasks. +- **Launch Package** — landing + sales script + 12-min demo + 5-day onboarding. + +**Endpoints:** `/api/v1/service-excellence/{id}/{feature-matrix, score, quality-review, proof-metrics, gap-analysis, research-brief, experiments, monthly-review, backlog, launch-package, sales-script, demo-script}` + `/review/all`. **التفصيل:** [`SERVICE_EXCELLENCE_OS.md`](SERVICE_EXCELLENCE_OS.md). + +## 39. Landing Pages + +- `landing/services.html` — 3 أبواب + 12 خدمة productized. +- `landing/free-diagnostic.html` — العرض المجاني. +- `landing/first-10-opportunities.html` — Kill Feature. +- `landing/agency-partner.html` — برنامج الوكالة الشريكة. +- `landing/private-beta.html` — Private Beta launch. + --- **الخلاصة:** المنتج **قوي كأساس سوقي وتقني**؛ الإطلاق العام يحتاج تشغيلاً وامتثالاً وتجربة عميل مغلقة أولاً. الإطلاق اليوم = Private Beta + Pilots + Proof Pack، ليس Public Launch. diff --git a/dealix/docs/SERVICE_EXCELLENCE_OS.md b/dealix/docs/SERVICE_EXCELLENCE_OS.md new file mode 100644 index 00000000..5032d51e --- /dev/null +++ b/dealix/docs/SERVICE_EXCELLENCE_OS.md @@ -0,0 +1,185 @@ +# Service Excellence OS — مصنع الخدمات الممتازة + +> **القاعدة:** لا خدمة تطلق إنتاجياً إلا إذا حصلت على score ≥80 وتجاوزت 4 quality gates. ولا تتوقف عند الإطلاق — تستمر في التحسين الأسبوعي. + +--- + +## 1. الوحدات + +| الوحدة | الدور | +|--------|------| +| `feature_matrix` | 12 must-have feature لكل خدمة + advanced/premium/future. | +| `service_scoring` | 10 أبعاد × 10 نقاط = 100. status: launch_ready / beta_only / needs_work. | +| `quality_review` | 4 gates: proof / approval / pricing / channels. | +| `competitor_gap` | مقارنة structural بـ7 فئات منافسين. | +| `proof_metrics` | الـ metrics المطلوبة + ROI estimate. | +| `research_lab` | brief شهري + hypotheses + experiments. | +| `service_improvement_backlog` | feedback → backlog → prioritization. | +| `launch_package` | landing + sales + demo + onboarding. | + +--- + +## 2. الـ 12 Must-Have Features (لكل خدمة) + +1. Self-Serve Intake. +2. AI Recommendation. +3. Data Quality Check. +4. Contactability / Risk Gate. +5. Channel Strategy. +6. Arabic Contextual Drafting. +7. Approval Cards. +8. Execution Mode (draft/export/approved). +9. Proof Pack. +10. Learning Loop. +11. Upsell Path. +12. Service Score. + +--- + +## 3. الـ 10 أبعاد للـ Score + +| البُعد | الوزن | +|------|----:| +| Clarity (وضوح الألم) | 10 | +| Speed-to-Value | 10 | +| Automation | 10 | +| Compliance | 10 | +| Proof | 10 | +| Upsell | 10 | +| Uniqueness (Saudi-first) | 10 | +| Scalability (multi-sector) | 10 | +| Ops Daily (autopilot) | 10 | +| Proof Data | 10 | + +**Status:** +- ≥80: `launch_ready` +- ≥60: `beta_only` +- <60: `needs_work` + +--- + +## 4. الـ 4 Quality Gates + +قبل إطلاق أي خدمة: + +1. **Proof gate** — لا proof_metrics → blocked. +2. **Approval gate** — لا approval_policy → blocked. +3. **Pricing gate** — تسعير غير منطقي → blocked. +4. **Channels gate** — تكامل غير آمن (scraping/auto_dm/etc.) → blocked. + +`review_service_before_launch(service_id)` يُرجع verdict واحد من: +- `launch_ready` +- `beta_only` +- `needs_work` +- `blocked_at_gate` + +--- + +## 5. Competitor Gap (7 فئات) + +| Category | Strengths | Limits | +|----------|-----------|--------| +| CRM عام | تخزين بيانات | ينتظر إدخال يدوي | +| WhatsApp tools | Broadcast | لا approval-first | +| Email assistants | كتابة أسرع | لا تحول الإيميل لـ pipeline | +| LinkedIn tools | إيجاد leads | كثيرها يخالف ToS | +| وكالات | خبرة بشرية | لا تتوسع | +| Revenue intelligence | تحليل calls | تبدأ بعد المكالمة | +| Generic AI agent | مرن | بدون سياق شركة | + +**ميزات Dealix:** +- موجّه للسوق السعودي. +- Approval-first. +- Proof Pack شهري. +- Multi-channel orchestration. +- Self-improving Curator. +- PDPL-aware. + +--- + +## 6. Research Lab (شهرياً) + +لكل خدمة: +- 6 أسئلة بحث (من اشترى، TTV، اعتراضات، deliverables، metrics، pricing). +- 4-5 hypotheses للتحسين. +- 3 experiments الأولوية (impact/effort). +- Monthly review بـ score حالي + gap + experiments. + +--- + +## 7. Improvement Backlog + +- `convert_feedback_to_backlog` — Feedback → backlog item. +- `prioritize_backlog_items` — impact desc, effort asc. +- `recommend_weekly_improvements` — 3 weekly tasks. + +--- + +## 8. Launch Package (per service) + +1. **Landing outline** (RTL Arabic): hero, promise, 3-step how-it-works, deliverables, pricing, proof, safety, FAQ, CTA. +2. **Sales script**: 5 discovery questions + pitch + 4 objection handlers + close. +3. **Demo script**: 12-min minute-by-minute Arabic walkthrough. +4. **Onboarding checklist**: first-5-days plan. + +--- + +## 9. Endpoints (`/api/v1/service-excellence/...`) + +``` +GET /{id}/feature-matrix +GET /{id}/feature-classification +GET /{id}/missing-features +GET /{id}/score +GET /{id}/quality-review +GET /review/all +GET /{id}/proof-metrics +POST /{id}/roi-estimate +GET /{id}/gap-analysis +GET /{id}/research-brief +GET /{id}/feature-hypotheses +GET /{id}/experiments +GET /{id}/monthly-review +GET /{id}/backlog +POST /{id}/backlog/from-feedback +POST /{id}/backlog/prioritize +GET /{id}/weekly-improvements +GET /{id}/launch-package +GET /{id}/landing-outline +GET /{id}/sales-script +GET /{id}/demo-script +GET /{id}/onboarding-checklist +``` + +--- + +## 10. اختبارات + +`tests/unit/test_service_excellence.py` — 33 اختبار: +- Feature matrix ≥10 must-haves. +- Score returns valid status. +- Every catalogued service passes the 4 gates. +- ROI estimate returns x-multiples. +- Competitor gap lists advantages + do-not-copy. +- Research brief has ≥5 questions. +- Hypotheses ≥3 + experiments ≤3. +- Backlog conversion + prioritization. +- Launch package complete. +- Demo script = 12 minutes. + +--- + +## 11. Weekly Improvement Loop + +``` +كل اثنين: +1. شغّل /review/all على كل الـ 12 خدمة. +2. أي خدمة < 80 → افتح backlog item. +3. أي خدمة blocked → إصلاح فوري قبل إطلاق جديد. +4. اختر experiment واحد لكل خدمة. + +كل جمعة: +1. سجل النتائج في Service Scorecard. +2. حدّث الـ improvement backlog. +3. أرسل executive brief للمؤسس. +``` diff --git a/dealix/docs/SERVICE_TOWER_STRATEGY.md b/dealix/docs/SERVICE_TOWER_STRATEGY.md new file mode 100644 index 00000000..ad5003ac --- /dev/null +++ b/dealix/docs/SERVICE_TOWER_STRATEGY.md @@ -0,0 +1,136 @@ +# Service Tower Strategy — برج الخدمات الذاتي + +> **الفكرة:** كل قدرة في Dealix تتحول إلى **Productized Service** بمواصفات: target customer + outcome + inputs + workflow + deliverables + pricing + risk + proof + upgrade path. + +--- + +## 1. القاعدة الذهبية + +**العميل لا يشتري ميزة. يشتري نتيجة منظمة.** + +كل خدمة تمشي في نفس الـ pipeline: +``` +Goal → Intake → Data Check → Risk Check → Strategy → +Drafts → Approval → Execution/Export → Tracking → Proof → Upsell +``` + +--- + +## 2. الـ12 خدمة (Productized) + +| # | الخدمة | المدخلات | المخرجات | السعر | +|---|--------|----------|---------|-------| +| 1 | Free Growth Diagnostic | sector/city/offer/goal | 3 فرص + رسالة + مخاطر + خطة Pilot | 0 | +| 2 | List Intelligence | CSV + channels | تنظيف + أفضل 50 + رسائل | 499–1,500 | +| 3 | First 10 Opportunities Sprint | sector/city/offer/goal | 10 فرص + رسائل + Proof Pack | 499–1,500 | +| 4 | Self-Growth Operator | company profile + goals | Daily brief + drafts + reports | 999/شهر | +| 5 | Growth OS Monthly | channels + team_size | المنصة الكاملة شهرياً | 2,999/شهر | +| 6 | Email Revenue Rescue | gmail label + ICP | استخراج فرص ضائعة + drafts | 1,500–5,000 | +| 7 | Meeting Booking Sprint | prospects + calendar | invitations + briefs + follow-ups | 1,500–5,000 | +| 8 | Partner Sprint | sector + partner goal | 20 شريك + رسائل + 5 اجتماعات | 3,000–7,500 | +| 9 | Agency Partner Program | agency profile | بيع Dealix لعملاء الوكالة | 10,000–50,000 | +| 10 | WhatsApp Compliance Setup | contact list + practice | audit + opt-in templates + ledger | 1,500–4,000 | +| 11 | LinkedIn Lead Gen Setup | ICP + offer + ad budget | حملة Lead Form + ربط CRM | 2,000–7,500 | +| 12 | Executive Growth Brief | company profile | موجز يومي 3+3+3 | 499–999/شهر | + +--- + +## 3. الـ Wizard + +``` +العميل يجيب: +- نوع الشركة +- الهدف +- هل عندك قائمة؟ +- ما القنوات المتاحة؟ +- الميزانية + +النظام يوصي بخدمة واحدة + يبرر القرار. +``` + +ترتيب القرارات: +1. وكالة → Partner Sprint / Agency Program. +2. عنده قائمة → List Intelligence. +3. مؤسس → Self-Growth Operator. +4. CEO → Executive Growth Brief. +5. واتساب → Compliance Setup. +6. هدف rescue → Email Revenue Rescue. +7. هدف اجتماعات → Meeting Booking Sprint. +8. هدف شراكات → Partner Sprint. +9. ميزانية شهرية ≥ 2999 → Growth OS. +10. الافتراضي → First 10 Opportunities. + +--- + +## 4. WhatsApp CEO Control + +كل قرار يصل المؤسس عبر واتساب كـ كرت: +- Daily Service Brief (≤3 buttons). +- Service Approval Card (`اعتمد / عدّل / ارفض`). +- Risk Alert Card. +- End-of-Day Report. + +--- + +## 5. Pricing Engine + +ضرّابات السعر: +- `company_size`: micro 0.8x, small 1.0x, medium 1.3x, large 1.7x. +- `urgency`: normal 1.0x, rush 1.3x, asap 1.5x. +- `channels_count`: +15% لكل قناة إضافية. + +Setup fee = month-equivalent للـ monthly services. السنوي بخصم 15%. + +--- + +## 6. Upgrade Paths + +``` +Free Diagnostic → First 10 Opportunities → Growth OS Monthly → Agency Partner +List Intelligence → Growth OS Monthly +Self-Growth Operator → Growth OS Monthly +Email Revenue Rescue → Growth OS Monthly +Partner Sprint → Agency Partner Program +``` + +كل upgrade path له upsell message عربي جاهز. + +--- + +## 7. Endpoints (`/api/v1/services/...`) + +``` +GET /catalog +GET /summary +POST /recommend +GET /{id}/intake-questions +POST /{id}/start +GET /{id}/workflow +GET /{id}/deliverables +GET /{id}/proof-pack-template +GET /{id}/client-report-outline +GET /{id}/operator-checklist +POST /{id}/quote +GET /{id}/setup-fee +GET /{id}/monthly-offer +POST /{id}/scorecard +GET /{id}/upgrade-path +GET /{id}/post-service-plan +GET /ceo/daily-brief +POST /ceo/approval-card +GET /ceo/risk-alert/demo +GET /ceo/end-of-day/demo +``` + +--- + +## 8. اختبارات + +`tests/unit/test_service_tower.py` — 38 اختبار: +- Catalog ≥12 خدمة + critical services. +- Pricing + proof metrics + deliverables موجودة. +- Wizard recommendations (agency, list, founder, CEO, budget). +- Workflow includes approval. +- Quote scaling by size. +- CEO cards ≤3 buttons + لا live send. +- Upgrade paths. diff --git a/dealix/docs/TARGETING_ACQUISITION_OS.md b/dealix/docs/TARGETING_ACQUISITION_OS.md new file mode 100644 index 00000000..81b1fbe7 --- /dev/null +++ b/dealix/docs/TARGETING_ACQUISITION_OS.md @@ -0,0 +1,184 @@ +# Targeting & Acquisition OS — نظام الاستهداف الذكي + +> **القاعدة:** Dealix لا يجمع كل شيء من كل مكان. يستهدف بذكاء، عبر مصادر مصرّح بها، مع موافقات بشرية، ومراقبة سمعة، وتعلّم يومي. + +--- + +## 1. لماذا Targeting OS؟ + +أي أداة تستطيع جمع أرقام بالـ scraping. القوة الحقيقية: +- **Account-first**: ابحث عن الشركات قبل الأشخاص. +- **Buying Committee**: من غالباً يقرر داخل كل شركة؟ +- **Contactability Gate**: هل التواصل مسموح؟ +- **Channel Strategy**: ما القناة الأفضل لكل مصدر؟ +- **Reputation Guard**: إذا تدهورت السمعة → أوقف القناة تلقائياً. +- **Daily Autopilot**: brief يومي + actions + Proof. +- **Self-Growth Mode**: Dealix يستهدف عملاءه بنفس النظام. + +--- + +## 2. الوحدات (16 module) + +| الوحدة | الدور | +|--------|------| +| `account_finder` | يحدد 10-25 شركة مناسبة لكل (sector, city). | +| `buyer_role_mapper` | 14 دور + خرائط buying committee حسب القطاع. | +| `contact_source_policy` | 12 مصدر، كل واحد له risk_score + channels مسموحة + retention. | +| `contactability_matrix` | 5 action modes: suggest_only / draft_only / approval_required / approved_execute / blocked. | +| `linkedin_strategy` | Lead Forms + Ads + Manual فقط. **لا scraping/auto-DM/auto-connect**. | +| `email_strategy` | Drafts + unsubscribe + pacing حسب domain reputation. | +| `whatsapp_strategy` | Opt-in only؛ rejects cold + risky phrases. | +| `social_strategy` | Listening + drafts فقط؛ لا auto-publish. | +| `outreach_scheduler` | Day-by-day plan + daily limits + opt-out enforcement. | +| `reputation_guard` | Bounce/complaint/opt-out thresholds → healthy/watch/pause. | +| `daily_autopilot` | Daily brief + 7 today actions + EOD report. | +| `acquisition_scorecard` | Pipeline / meetings / risks / productivity score. | +| `self_growth_mode` | Dealix ICP focus + daily brief + weekly learning. | +| `free_diagnostic` | Free 5-section Arabic diagnostic → paid pilot offer. | +| `contract_drafts` | Pilot/DPA/Referral/Agency/SOW outlines (legal review required). | +| `service_offers` | 7 targeting-tier offers + pricing + recommend. | + +--- + +## 3. القنوات والقواعد + +### LinkedIn +**الممنوع** (encoded in `linkedin_do_not_do()`): +- `scrape_profiles, auto_connect, auto_dm, browser_automation, fake_engagement, download_contacts_from_linkedin, buy_scraped_leads, use_unauthorized_extensions`. + +**المسموح**: +- LinkedIn Lead Gen Forms (أساسي). +- LinkedIn Ads. +- البحث اليدوي المعتمد (manual research task). +- Connection requests يدوية بمسودات Dealix. + +### WhatsApp +- لا cold بدون opt-in واضح. +- opt-in template يحتاج: اسم النشاط + الغرض + خيار الانسحاب. +- double opt-in موصى به. + +### Email +- سياق واضح + unsubscribe. +- Pacing حسب `domain_reputation`: fresh/warmed/trusted/damaged. +- إيقاف على bounce ≥ 5%. + +### Social +- API رسمية فقط. +- Listening مسموح. +- Replies = drafts بموافقة. + +--- + +## 4. مصادر الـ Contacts (12) + +| Source | Risk | Status الافتراضي | +|--------|------|-----------------| +| `crm_customer` | 5 | safe | +| `inbound_lead` | 5 | safe | +| `website_form` | 10 | safe | +| `linkedin_lead_form` | 10 | safe | +| `event_lead` | 20 | needs_review | +| `referral` | 25 | needs_review | +| `partner_intro` | 25 | needs_review | +| `manual_research` | 50 | needs_review | +| `uploaded_list` | 60 | needs_review | +| `unknown_source` | 80 | needs_review | +| `cold_list` | 95 | blocked (waتساب)/needs_review (إيميل) | +| `opt_out` | 100 | blocked (كل القنوات) | + +--- + +## 5. Daily Operating Loop + +``` +صباحاً: +- 10 شركات جديدة مناسبة +- 5 رسائل drafts للموافقة +- 3 leads متأخرة (>72h) +- 1 فرصة شريك +- 1 خطر سمعة + +ظهراً: +- اعتماد + إرسال 5 emails +- مراجعة 12 رقم بدون مصدر +- ديمو شريك + +مساءً: +- 32 حساب تم تحليله +- 6 مسودات معتمدة +- 2 ردود إيجابية +- 1 اجتماع مجدول +- 8 مخاطر منعت +``` + +--- + +## 6. Self-Growth Mode + +5 ICP focuses لـ Dealix نفسه: +1. وكالات تسويق B2B في الرياض. +2. شركات تدريب B2B في الرياض. +3. شركات استشارات نمو. +4. SaaS سعودية صغيرة-متوسطة. +5. وسطاء عقار B2B في جدة. + +كل صباح: 10 شركات + 5 رسائل + اعتماد المؤسس. + +أهداف شهرية: 30 Free Diagnostic، 6 Paid Pilots، 3 Growth OS، 1 وكالة شريكة. + +--- + +## 7. Endpoints (`/api/v1/targeting/...`) + +``` +POST /accounts/recommend +POST /buying-committee/map +POST /contacts/evaluate +POST /uploaded-list/analyze +POST /outreach/plan +GET /daily-autopilot/demo +GET /self-growth/demo +POST /self-growth/targets +POST /self-growth/weekly-report +GET /reputation/status +POST /reputation/recovery +POST /linkedin/strategy +POST /drafts/email +POST /drafts/whatsapp +POST /drafts/email-followup +POST /drafts/role-angle +POST /free-diagnostic +GET /services +POST /services/recommend +GET /contracts/templates +``` + +--- + +## 8. اختبارات + +`tests/unit/test_targeting_os.py` — 47 اختبار: +- Account finder + Arabic + safe sources. +- Buying committee + role-based angles. +- Source classification + 12 sources. +- Contactability (opt-out, cold WA, inbound safe, unknown review). +- LinkedIn (لا scraping/auto-DM). +- Email risk + unsubscribe + 3-step follow-up. +- WhatsApp risk + opt-in templates. +- Outreach plan + daily limits. +- Reputation guard + recovery. +- Self-growth + free diagnostic + uploaded list preview. +- Contracts (legal review + PDPL). +- Acquisition scorecard. + +--- + +## 9. ما لا تفعله + +- لا scraping LinkedIn/social. +- لا auto-DM في أي منصة. +- لا cold WhatsApp. +- لا charge بدون تأكيد. +- لا scraping ToS-مخالف. +- لا وعود بنتائج مضمونة. +- لا تخزين بطاقات. diff --git a/dealix/landing/agency-partner.html b/dealix/landing/agency-partner.html new file mode 100644 index 00000000..a3ba5fa3 --- /dev/null +++ b/dealix/landing/agency-partner.html @@ -0,0 +1,90 @@ + + + + + +Dealix — برنامج وكالة شريكة + + + +
+

برنامج وكالة شريكة

+

إذا كنت وكالة تسويق/مبيعات/CRM في السعودية، Dealix يشتغل خلفك: + أنت تختار العميل، Dealix يشغل النظام، وأنت تأخذ revenue share + co-branded Proof Pack.

+
+ +
+
+

ماذا تحصل الوكالة؟

+
+
+ Setup Fee +

10,000–50,000 ريال حسب الحزمة.

+
+
+ Revenue Share +

على كل عميل تجلبه + اشتراكاته الشهرية.

+
+
+ Co-Branded Proof +

تقارير شهرية بعلامة الوكالة لعملائها.

+
+
+ Client Dashboard +

لوحة لإدارة كل عملاء الوكالة في مكان واحد.

+
+
+
+ +
+

كيف تبدأ؟

+ +
+ +
+

لماذا Dealix بدلاً من البناء داخلياً؟

+ +
+ +
+ احجز اجتماع شراكة +
+
+ + diff --git a/dealix/landing/first-10-opportunities.html b/dealix/landing/first-10-opportunities.html new file mode 100644 index 00000000..a9a331f5 --- /dev/null +++ b/dealix/landing/first-10-opportunities.html @@ -0,0 +1,98 @@ + + + + + +Dealix — 10 فرص في 10 دقائق + + + +
+ Kill Feature — متاح في Private Beta +

10 فرص في 10 دقائق

+

أعطنا قطاعك ومدينتك وعرضك، نطلع لك 10 فرص B2B مع why-now + رسائل عربية + + خطة متابعة 7 أيام + Proof Pack — وأنت توافق قبل أي تواصل.

+
+ +
+
+

ما الذي ستحصل عليه

+ +
+ +
+

الأسعار

+
+
+
Pilot 7 أيام
+
499 ريال
+
أو مجاني مقابل case study
+
+
+
Paid Pilot 30 يوم
+
1,500 ريال
+
إعداد موسّع + 3 جولات
+
+
+
Growth OS شهري
+
2,999 ريال
+
المنصة الكاملة شهرياً
+
+
+
+ +
+

الأمان

+ +
+ +
+ احجز Pilot الآن + شاهد كل الخدمات +
+
+ + diff --git a/dealix/landing/free-diagnostic.html b/dealix/landing/free-diagnostic.html new file mode 100644 index 00000000..781f538f --- /dev/null +++ b/dealix/landing/free-diagnostic.html @@ -0,0 +1,71 @@ + + + + + +Dealix — تشخيص نمو مجاني + + + +
+

تشخيص نمو مجاني

+

أرسل لنا قطاعك ومدينتك وعرضك، نرسل لك خلال 24 ساعة عمل: + 3 فرص B2B + رسالة عربية + تقرير مخاطر + خطة Pilot — كل شيء بدون التزام.

+
+ +
+
+

ماذا تستلم؟

+ +
+ +
+

كيف نعمل؟

+ +
+ +
+ ضمانات Dealix: + Approval-first — لا نرسل أي شيء قبل موافقتك. + لا cold WhatsApp. لا scraping. لا وعود بنتائج مضمونة. + PDPL-aware من اليوم الأول. +
+ +
+ احجز التشخيص الآن +
+
+ + diff --git a/dealix/landing/services.html b/dealix/landing/services.html new file mode 100644 index 00000000..fff44286 --- /dev/null +++ b/dealix/landing/services.html @@ -0,0 +1,156 @@ + + + + + +Dealix — الخدمات + + + +
+

الخدمات — Dealix Service Tower

+

اختر هدفك من 3 أبواب، النظام يوصي بالخدمة الصحيحة. + كل خدمة productized بمواصفات + مخرجات + سعر + Proof Pack.

+
+ +
+
+
+

أريد عملاء جدد

+
    +
  • First 10 Opportunities Sprint
  • +
  • Targeting OS
  • +
  • LinkedIn Lead Gen Setup
  • +
  • Email Outreach Drafts
  • +
  • Meeting Booking Sprint
  • +
  • Growth OS
  • +
+
+
+

عندي بيانات وأبغى أستفيد منها

+
    +
  • List Intelligence
  • +
  • WhatsApp Compliance Setup
  • +
  • Email Revenue Rescue
  • +
  • Customer Reactivation
  • +
+
+
+

أبغى توسع واستراتيجية

+
    +
  • Partner Sprint
  • +
  • Agency Partner Program
  • +
  • Executive Growth Brief
  • +
  • Self-Growth Operator
  • +
+
+
+ +

12 خدمة productized

+
+
+
Free Growth Diagnostic
+
مجاني
+
3 فرص + رسالة + مخاطر + خطة Pilot — خلال 24 ساعة
+
+
+
List Intelligence
+
499–1,500 ريال
+
تنظيف + تصنيف + أفضل 50 + رسائل عربية
+
+
+
First 10 Opportunities Sprint
+
499–1,500 ريال
+
10 فرص + رسائل + متابعة 7 أيام + Proof Pack
+
+
+
Self-Growth Operator
+
999 ريال شهرياً
+
Daily brief + drafts + متابعة + reports
+
+
+
Growth OS
+
2,999 ريال شهرياً
+
المنصة الكاملة: قنوات + autopilot + proof
+
+
+
Email Revenue Rescue
+
1,500–5,000 ريال
+
استخراج فرص ضائعة من Gmail + drafts
+
+
+
Meeting Booking Sprint
+
1,500–5,000 ريال
+
دعوات + briefs + follow-ups
+
+
+
Partner Sprint
+
3,000–7,500 ريال
+
20 شريك محتمل + 10 رسائل + 5 اجتماعات
+
+
+
Agency Partner Program
+
10,000–50,000 ريال
+
بيع Dealix لعملاء الوكالة + revenue share
+
+
+
WhatsApp Compliance Setup
+
1,500–4,000 ريال
+
audit + opt-in templates + ledger
+
+
+
LinkedIn Lead Gen Setup
+
2,000–7,500 ريال
+
حملة Lead Form + audiences + ربط CRM
+
+
+
Executive Growth Brief
+
499–999 ريال شهرياً
+
3 قرارات + 3 فرص + 3 مخاطر يومياً
+
+
+ +
+ ابدأ بالتشخيص المجاني + 10 فرص في 10 دقائق +
+
+ + + + diff --git a/dealix/tests/unit/test_service_excellence.py b/dealix/tests/unit/test_service_excellence.py new file mode 100644 index 00000000..1f10a5e9 --- /dev/null +++ b/dealix/tests/unit/test_service_excellence.py @@ -0,0 +1,238 @@ +"""Unit tests for Service Excellence OS.""" + +from __future__ import annotations + +from auto_client_acquisition.service_excellence import ( + build_backlog, + build_demo_script, + build_feature_matrix, + build_landing_page_outline, + build_monthly_service_review, + build_onboarding_checklist, + build_proof_pack_template_excellence, + build_sales_script, + build_service_launch_package, + build_service_research_brief, + calculate_service_excellence_score, + calculate_service_roi_estimate, + classify_features, + compare_against_categories, + convert_feedback_to_backlog, + generate_feature_hypotheses, + prioritize_backlog_items, + recommend_missing_features, + recommend_next_experiments, + recommend_weekly_improvements, + required_proof_metrics, + review_service_before_launch, + score_clarity, + score_compliance, + score_proof, + summarize_proof_ar, +) +from auto_client_acquisition.service_excellence.quality_review import ( + block_if_missing_proof, + block_if_unclear_pricing, + block_if_unsafe_channel, +) +from auto_client_acquisition.service_tower import ALL_SERVICES, get_service + + +# ── Feature matrix ─────────────────────────────────────────── +def test_feature_matrix_has_must_have_features(): + out = build_feature_matrix("growth_os_monthly") + assert len(out["must_have"]) >= 10 + + +def test_classify_features_returns_three_tiers(): + out = classify_features("growth_os_monthly") + assert "must_have" in out + assert "advanced" in out + assert "premium" in out + + +def test_recommend_missing_features_returns_list(): + out = recommend_missing_features("first_10_opportunities_sprint") + assert isinstance(out, list) + + +def test_unknown_service_feature_matrix_errors(): + out = build_feature_matrix("totally_unknown") + assert "error" in out + + +# ── Scoring ────────────────────────────────────────────────── +def test_score_returns_status(): + out = calculate_service_excellence_score("growth_os_monthly") + assert out["status"] in ("launch_ready", "beta_only", "needs_work") + + +def test_score_clarity_for_complete_service(): + s = get_service("first_10_opportunities_sprint") + score = score_clarity(s) + assert score >= 7 + + +def test_score_compliance_high_for_approval_first(): + s = get_service("growth_os_monthly") + score = score_compliance(s) + assert score >= 8 + + +def test_score_proof_high_when_metrics_present(): + s = get_service("growth_os_monthly") + score = score_proof(s) + assert score >= 6 + + +# ── Quality review ─────────────────────────────────────────── +def test_quality_review_returns_verdict(): + out = review_service_before_launch("growth_os_monthly") + assert out["verdict"] in ("launch_ready", "beta_only", "needs_work", + "blocked_at_gate") + + +def test_quality_review_all_services_no_blocks(): + """Every catalogued service should pass the gates (it's our catalog).""" + for s in ALL_SERVICES: + out = review_service_before_launch(s.id) + assert out["verdict"] != "blocked_at_gate", f"{s.id} blocked at gate" + + +def test_block_if_missing_proof(): + out = block_if_missing_proof("growth_os_monthly") + assert out["blocked"] is False # all our services have proof metrics + + +def test_block_if_unclear_pricing(): + out = block_if_unclear_pricing("growth_os_monthly") + assert out["blocked"] is False + + +def test_block_if_unsafe_channel(): + out = block_if_unsafe_channel("growth_os_monthly") + assert out["blocked"] is False + + +# ── Proof metrics ──────────────────────────────────────────── +def test_required_proof_metrics_present(): + metrics = required_proof_metrics("growth_os_monthly") + assert len(metrics) >= 1 + + +def test_proof_pack_template_excellence(): + out = build_proof_pack_template_excellence("growth_os_monthly") + assert out["signature_required"] is True + + +def test_roi_estimate_returns_x_multiples(): + out = calculate_service_roi_estimate( + "first_10_opportunities_sprint", + {"price_paid_sar": 1000, "pipeline_sar": 25000, "closed_won_sar": 5000}, + ) + assert out["roi_pipeline_x"] == 25.0 + assert out["roi_closed_x"] == 5.0 + + +def test_summarize_proof_ar_arabic(): + msg = summarize_proof_ar( + "first_10_opportunities_sprint", + {"price_paid_sar": 1000, "pipeline_sar": 18000, "closed_won_sar": 3000}, + ) + assert any("؀" <= ch <= "ۿ" for ch in msg) + + +# ── Competitor gap ─────────────────────────────────────────── +def test_competitor_gap_lists_advantages(): + out = compare_against_categories("growth_os_monthly") + assert out["dealix_advantages_ar"] + assert out["do_not_copy_ar"] + + +def test_competitor_gap_unknown_service(): + out = compare_against_categories("bogus") + assert "error" in out + + +# ── Research lab ───────────────────────────────────────────── +def test_research_brief_has_questions(): + out = build_service_research_brief("growth_os_monthly") + assert len(out["questions_to_answer_ar"]) >= 5 + + +def test_feature_hypotheses_returned(): + out = generate_feature_hypotheses("growth_os_monthly") + assert len(out) >= 3 + + +def test_recommend_next_experiments_max_three(): + out = recommend_next_experiments("growth_os_monthly") + assert len(out["experiments"]) <= 3 + + +def test_monthly_review_includes_score(): + out = build_monthly_service_review("growth_os_monthly") + assert "current_excellence_score" in out + + +# ── Backlog ────────────────────────────────────────────────── +def test_backlog_returns_skeleton(): + out = build_backlog("growth_os_monthly") + assert out["service_id"] == "growth_os_monthly" + assert "items" in out + + +def test_prioritize_backlog_items(): + items = [ + {"impact": "low", "effort": "high"}, + {"impact": "high", "effort": "low"}, + {"impact": "medium", "effort": "medium"}, + ] + out = prioritize_backlog_items(items) + # high+low effort should be first + assert out[0]["impact"] == "high" + + +def test_convert_feedback_to_backlog(): + feedback = [ + {"text": "العميل بطيء في الرد على الـ drafts", "sentiment": "negative"}, + {"text": "الـ pricing واضح", "sentiment": "positive"}, + ] + out = convert_feedback_to_backlog(feedback) + assert len(out) == 2 + + +def test_weekly_improvements_returned(): + out = recommend_weekly_improvements("growth_os_monthly") + assert len(out["weekly_plan_ar"]) >= 1 + + +# ── Launch package ─────────────────────────────────────────── +def test_launch_package_complete(): + out = build_service_launch_package("first_10_opportunities_sprint") + assert "landing" in out + assert "sales_script" in out + assert "demo_script" in out + assert "onboarding" in out + + +def test_landing_outline_includes_safety(): + out = build_landing_page_outline("growth_os_monthly") + assert any("Approval-first" in s or "approval" in s.lower() + for s in out["must_include_ar"]) + + +def test_sales_script_has_objection_handling(): + out = build_sales_script("growth_os_monthly") + assert "price" in out["objection_handling_ar"] + assert "timing" in out["objection_handling_ar"] + + +def test_demo_script_is_12_minutes(): + out = build_demo_script("first_10_opportunities_sprint") + assert out["duration_minutes"] == 12 + + +def test_onboarding_blocks_live_send(): + out = build_onboarding_checklist("growth_os_monthly") + assert out["live_send_allowed"] is False diff --git a/dealix/tests/unit/test_service_tower.py b/dealix/tests/unit/test_service_tower.py new file mode 100644 index 00000000..adf37010 --- /dev/null +++ b/dealix/tests/unit/test_service_tower.py @@ -0,0 +1,241 @@ +"""Unit tests for Service Tower.""" + +from __future__ import annotations + +from auto_client_acquisition.service_tower import ( + ALL_SERVICES, + build_ceo_daily_service_brief, + build_client_report_outline, + build_deliverables, + build_intake_questions, + build_internal_operator_checklist, + build_proof_pack_template, + build_risk_alert_card, + build_service_approval_card, + build_service_scorecard, + build_service_workflow, + build_upsell_message_ar, + calculate_monthly_offer, + calculate_setup_fee, + catalog_summary, + get_service, + list_all_services, + map_service_to_growth_mission, + map_service_to_subscription, + quote_service, + recommend_next_step, + recommend_plan_after_service, + recommend_service, + recommend_upgrade, + summarize_recommendation_ar, + summarize_scorecard_ar, + validate_service_inputs, +) + + +# ── Catalog ────────────────────────────────────────────────── +def test_catalog_has_at_least_12_services(): + out = list_all_services() + assert out["total"] >= 12 + + +def test_catalog_includes_critical_services(): + ids = {s.id for s in ALL_SERVICES} + for required in ( + "free_growth_diagnostic", "list_intelligence", + "first_10_opportunities_sprint", "self_growth_operator", + "growth_os_monthly", "email_revenue_rescue", + "meeting_booking_sprint", "partner_sprint", + "agency_partner_program", "whatsapp_compliance_setup", + "linkedin_lead_gen_setup", "executive_growth_brief", + ): + assert required in ids + + +def test_every_service_has_pricing(): + for s in ALL_SERVICES: + assert s.pricing_min_sar >= 0 + assert s.pricing_max_sar >= s.pricing_min_sar + + +def test_every_service_has_proof_metrics(): + for s in ALL_SERVICES: + assert s.proof_metrics, f"{s.id} missing proof_metrics" + + +def test_every_service_has_deliverables(): + for s in ALL_SERVICES: + assert s.deliverables_ar, f"{s.id} missing deliverables" + + +def test_every_service_has_approval_policy(): + for s in ALL_SERVICES: + assert s.approval_policy + + +def test_summary_aggregates_pricing_models(): + s = catalog_summary() + assert s["total"] == len(ALL_SERVICES) + assert "by_pricing_model" in s + assert "free_growth_diagnostic" in s["free_offers"] + + +# ── Wizard ─────────────────────────────────────────────────── +def test_wizard_recommends_partner_sprint_for_agency(): + out = recommend_service(company_type="agency", goal="expand_partners") + assert out["recommended_service_id"] in ("partner_sprint", + "agency_partner_program") + + +def test_wizard_recommends_list_intelligence_when_has_list(): + out = recommend_service(company_type="b2b", has_contact_list=True) + assert out["recommended_service_id"] == "list_intelligence" + + +def test_wizard_recommends_growth_os_for_monthly_budget(): + out = recommend_service(company_type="b2b saas", budget_sar=3500) + assert out["recommended_service_id"] == "growth_os_monthly" + + +def test_wizard_default_falls_back_to_kill_feature(): + out = recommend_service(company_type="random", budget_sar=500) + assert out["recommended_service_id"] == "first_10_opportunities_sprint" + + +def test_intake_questions_for_known_service(): + out = build_intake_questions("first_10_opportunities_sprint") + assert len(out["questions"]) >= 5 + + +def test_intake_questions_unknown_service(): + out = build_intake_questions("totally_made_up") + assert "error" in out + + +def test_validate_service_inputs_missing_field(): + out = validate_service_inputs("list_intelligence", {"sector": "training"}) + assert out["valid"] is False + + +def test_summarize_recommendation_arabic(): + out = recommend_service(company_type="b2b saas", budget_sar=3500) + summary = summarize_recommendation_ar(out) + assert any("؀" <= ch <= "ۿ" for ch in summary) + + +# ── Mission templates ──────────────────────────────────────── +def test_workflow_includes_approval(): + w = build_service_workflow("first_10_opportunities_sprint") + step_ids = [s["step_id"] for s in w["workflow_steps"]] + assert "approval" in step_ids + + +def test_workflow_links_to_growth_mission(): + w = build_service_workflow("first_10_opportunities_sprint") + assert w["linked_growth_mission"] == "first_10_opportunities" + + +def test_map_service_to_subscription(): + sub = map_service_to_subscription("free_growth_diagnostic") + assert sub # always returns something + + +# ── Pricing engine ─────────────────────────────────────────── +def test_quote_free_service_returns_zero(): + q = quote_service("free_growth_diagnostic") + assert q.get("is_free") is True + assert q["estimated_min_sar"] == 0 + + +def test_quote_paid_service_scales_with_size(): + q_small = quote_service("first_10_opportunities_sprint", company_size="small") + q_large = quote_service("first_10_opportunities_sprint", company_size="large") + assert q_large["estimated_max_sar"] > q_small["estimated_max_sar"] + + +def test_quote_unknown_service_errors(): + q = quote_service("bogus_service") + assert "error" in q + + +def test_setup_fee_only_for_monthly(): + fee_monthly = calculate_setup_fee("growth_os_monthly") + fee_sprint = calculate_setup_fee("first_10_opportunities_sprint") + assert fee_monthly["setup_fee_sar"] > 0 + assert fee_sprint["setup_fee_sar"] == 0 + + +def test_monthly_offer_only_for_monthly_services(): + out_m = calculate_monthly_offer("growth_os_monthly") + out_s = calculate_monthly_offer("first_10_opportunities_sprint") + assert out_m["is_monthly"] is True + assert out_s["is_monthly"] is False + + +# ── Deliverables ───────────────────────────────────────────── +def test_deliverables_returns_arabic_list(): + out = build_deliverables("first_10_opportunities_sprint") + assert out["deliverables_ar"] + + +def test_proof_pack_template_lists_metrics(): + out = build_proof_pack_template("first_10_opportunities_sprint") + assert out["metrics_to_track"] + + +def test_client_report_outline_includes_executive_summary(): + out = build_client_report_outline("growth_os_monthly") + assert "ملخص تنفيذي (10 أسطر)" in out["sections_ar"] + + +def test_operator_checklist_blocks_live_actions(): + out = build_internal_operator_checklist("growth_os_monthly") + assert any("live" in s.lower() for s in out["do_not_do_ar"]) + + +# ── Scorecard ──────────────────────────────────────────────── +def test_scorecard_strong_outcome(): + out = build_service_scorecard("first_10_opportunities_sprint", { + "drafts_approved": 5, "positive_replies": 3, + "meetings": 2, "pipeline_sar": 25000, + "risks_blocked": 4, "customer_satisfaction": 9, + }) + assert out["score"] >= 50 + + +def test_scorecard_summarize_arabic(): + out = build_service_scorecard("first_10_opportunities_sprint", + {"meetings": 3, "pipeline_sar": 30000}) + summary = summarize_scorecard_ar(out) + assert any("؀" <= ch <= "ۿ" for ch in summary) + + +# ── CEO control ────────────────────────────────────────────── +def test_ceo_daily_brief_buttons_capped_at_three(): + out = build_ceo_daily_service_brief() + assert len(out["buttons_ar"]) <= 3 + + +def test_approval_card_blocks_live_send(): + out = build_service_approval_card("first_10_opportunities_sprint", + "send_email") + assert out["live_send_allowed"] is False + assert len(out["buttons_ar"]) <= 3 + + +def test_risk_alert_card_marks_high_risk(): + out = build_risk_alert_card() + assert out["risk_level"] == "high" + + +# ── Upgrade paths ──────────────────────────────────────────── +def test_upgrade_recommends_next_service(): + out = recommend_upgrade("first_10_opportunities_sprint") + assert out["recommended_service_id"] in ("growth_os_monthly", + "self_growth_operator") + + +def test_upsell_message_arabic(): + msg = build_upsell_message_ar("first_10_opportunities_sprint", + "growth_os_monthly") + assert any("؀" <= ch <= "ۿ" for ch in msg) diff --git a/dealix/tests/unit/test_targeting_os.py b/dealix/tests/unit/test_targeting_os.py new file mode 100644 index 00000000..7decbe29 --- /dev/null +++ b/dealix/tests/unit/test_targeting_os.py @@ -0,0 +1,329 @@ +"""Unit tests for Targeting & Acquisition OS.""" + +from __future__ import annotations + +from auto_client_acquisition.targeting_os import ( + ALL_BUYER_ROLES, + ALL_SOURCES, + allowed_action_modes, + analyze_uploaded_list_preview, + build_acquisition_scorecard, + build_dealix_self_growth_plan, + build_followup_sequence, + build_free_growth_diagnostic, + build_lead_gen_form_plan, + build_outreach_plan, + build_self_growth_daily_brief, + calculate_channel_reputation, + classify_source, + draft_b2b_email, + draft_role_based_angle, + draft_whatsapp_message, + enforce_daily_limits, + evaluate_contactability, + explain_contactability_ar, + list_targeting_services, + map_buying_committee, + recommend_accounts, + recommend_dealix_targets, + recommend_linkedin_strategy, + recommend_recovery_action, + recommend_service_offer, + score_email_risk, + score_whatsapp_risk, + should_pause_channel, +) +from auto_client_acquisition.targeting_os.contract_drafts import ( + draft_dpa_outline, + draft_pilot_agreement_outline, +) +from auto_client_acquisition.targeting_os.linkedin_strategy import linkedin_do_not_do + + +# ── Account finder ─────────────────────────────────────────── +def test_recommend_accounts_returns_arabic_targets(): + out = recommend_accounts(sector="training", city="Riyadh", limit=5) + assert out["total"] == 5 + for a in out["accounts"]: + assert "fit_score" in a + assert "why_now_ar" in a + assert any("؀" <= ch <= "ۿ" for ch in a["why_now_ar"]) + + +def test_recommend_accounts_blocks_unsafe_sources(): + out = recommend_accounts(sector="saas", city="Riyadh", limit=2) + for a in out["accounts"]: + assert "scraped_email" not in a["primary_sources"] + assert "scraped_phone" not in a["primary_sources"] + + +# ── Buyer role mapper ──────────────────────────────────────── +def test_buying_committee_for_training_includes_dm(): + out = map_buying_committee(sector="training", company_size="small") + assert "primary_decision_maker" in out + assert out["primary_decision_maker"]["role_key"] in ALL_BUYER_ROLES + + +def test_buying_committee_unknown_sector_falls_back(): + out = map_buying_committee(sector="bogus_xyz") + assert out["primary_decision_maker"]["role_key"] in ALL_BUYER_ROLES + + +def test_role_based_angle_returns_arabic(): + out = draft_role_based_angle("head_of_sales", sector="saas", + offer="Pilot 7 أيام") + assert any("؀" <= ch <= "ۿ" for ch in out["angle_ar"]) + + +# ── Contact source policy ──────────────────────────────────── +def test_classify_known_source(): + assert classify_source("crm_customer")["source"] == "crm_customer" + + +def test_classify_unknown_source(): + assert classify_source("totally_made_up")["source"] == "unknown_source" + + +def test_all_sources_include_critical(): + for s in ("crm_customer", "linkedin_lead_form", "cold_list", "opt_out"): + assert s in ALL_SOURCES + + +# ── Contactability matrix ──────────────────────────────────── +def test_opt_out_contact_blocked(): + contact = {"source": "opt_out", "opt_out": True} + out = evaluate_contactability(contact, desired_channel="whatsapp") + assert out["status"] == "blocked" + assert "opt_out" in out["reason_codes"] + + +def test_cold_whatsapp_blocked(): + contact = {"source": "cold_list", "opt_in_status": "no"} + out = evaluate_contactability(contact, desired_channel="whatsapp") + assert out["status"] == "blocked" + + +def test_inbound_lead_email_safe(): + contact = {"source": "inbound_lead", "opt_in_status": "yes"} + out = evaluate_contactability(contact, desired_channel="email") + assert out["status"] == "safe" + + +def test_unknown_source_needs_review(): + contact = {"source": "unknown_source"} + out = evaluate_contactability(contact) + assert out["status"] in ("needs_review", "safe") + + +def test_explain_contactability_returns_arabic(): + contact = {"source": "cold_list"} + out = evaluate_contactability(contact, desired_channel="whatsapp") + text = explain_contactability_ar(out) + assert "محظور" in text + + +def test_allowed_action_modes_includes_blocked_only_for_blocked(): + blocked_result = {"status": "blocked"} + assert allowed_action_modes(blocked_result) == ["blocked"] + + +# ── LinkedIn strategy ──────────────────────────────────────── +def test_linkedin_strategy_never_recommends_scraping(): + out = recommend_linkedin_strategy("B2B SaaS") + assert "scrape_profiles" in out["do_not_do"] + assert "auto_dm" in out["do_not_do"] + assert out["primary"] == "lead_gen_forms" + + +def test_linkedin_do_not_do_lock_list(): + nope = linkedin_do_not_do() + for required in ("scrape_profiles", "auto_dm", "auto_connect", + "browser_automation"): + assert required in nope + + +def test_lead_gen_form_plan_has_hidden_fields(): + plan = build_lead_gen_form_plan("training", "Pilot 7 أيام") + field_names = [f["name"] for f in plan["hidden_fields"]] + assert "campaign_name" in field_names + assert "sector" in field_names + + +# ── Email strategy ─────────────────────────────────────────── +def test_email_draft_includes_unsubscribe(): + contact = {"name": "أحمد", "company": "X"} + out = draft_b2b_email(contact, offer="Pilot 7 أيام") + assert "إلغاء" in out["body_ar"] or "STOP" in out["body_ar"] + assert out["live_send_allowed"] is False + + +def test_email_risk_blocks_cold_list(): + contact = {"source": "cold_list", "opt_in_status": "no"} + out = score_email_risk(contact, "ضمان 100% نتائج مضمونة") + assert out["verdict"] in ("blocked", "needs_review") + + +def test_email_followup_has_three_steps(): + out = build_followup_sequence({"name": "أحمد"}) + assert len(out["steps"]) == 3 + assert out["live_send_allowed"] is False + + +# ── WhatsApp strategy ──────────────────────────────────────── +def test_whatsapp_cold_blocked(): + contact = {"source": "cold_list", "opt_in_status": "no"} + out = draft_whatsapp_message(contact) + assert out["live_send_allowed"] is False + assert out["risk"]["verdict"] in ("blocked", "needs_review") + + +def test_whatsapp_risk_blocks_risky_phrase(): + contact = {"source": "inbound_lead", "opt_in_status": "yes"} + out = score_whatsapp_risk(contact, "ضمان 100% نتائج مضمونة آخر فرصة") + assert out["risk"] >= 25 + + +# ── Outreach scheduler ─────────────────────────────────────── +def test_outreach_plan_generates_steps(): + targets = [{"name": "Acme", "role": "CEO"}, {"name": "Beta", "role": "Sales"}] + out = build_outreach_plan(targets, channels=["email", "linkedin"]) + assert out["total_targets"] == 2 + for t in out["plan"]: + for step in t["steps"]: + assert step["live_send_allowed"] is False + + +def test_enforce_daily_limits_caps_emails(): + targets = [{"name": f"co_{i}"} for i in range(50)] + plan = build_outreach_plan(targets, channels=["email"]) + capped = enforce_daily_limits(plan, limits={"max_daily_email_drafts": 5, + "max_same_domain_contacts": 99, + "max_followups": 3, + "cooldown_days": 7, + "max_daily_whatsapp_approved_sends": 5}) + # Across all targets, emails total should not exceed 5 + total_emails = sum( + sum(1 for s in t["steps"] if s["channel"] == "email") + for t in capped["plan"] + ) + assert total_emails <= 5 + + +# ── Reputation guard ───────────────────────────────────────── +def test_reputation_pauses_high_bounce(): + """Multiple critical metrics together should trigger pause.""" + metrics = {"bounce_rate": 0.15, "complaint_rate": 0.005, + "opt_out_rate": 0.15, "reply_rate": 0.005} + out = should_pause_channel(metrics, channel="email") + assert out["should_pause"] is True + + +def test_reputation_recommends_recovery_actions(): + metrics = {"bounce_rate": 0.10, "complaint_rate": 0.005, + "opt_out_rate": 0.12, "reply_rate": 0.01} + out = recommend_recovery_action(metrics, channel="email") + assert out["actions_ar"] + + +def test_reputation_healthy_email(): + metrics = {"bounce_rate": 0.005, "complaint_rate": 0.0001, + "opt_out_rate": 0.01, "reply_rate": 0.05} + rep = calculate_channel_reputation(metrics, channel="email") + assert rep["verdict"] == "healthy" + + +# ── Daily autopilot ────────────────────────────────────────── +def test_today_actions_returned(): + from auto_client_acquisition.targeting_os import recommend_today_actions + out = recommend_today_actions() + assert len(out) >= 5 + for a in out: + assert "label_ar" in a + + +# ── Self-growth mode ───────────────────────────────────────── +def test_self_growth_targets_list(): + out = recommend_dealix_targets(limit=5) + assert out["live_send_allowed"] is False + assert out["targets"]["total"] >= 5 + + +def test_self_growth_daily_brief_has_ten_targets(): + out = build_self_growth_daily_brief() + assert len(out["top_10_targets"]) >= 5 + + +def test_self_growth_plan_has_monthly_targets(): + out = build_dealix_self_growth_plan() + assert "monthly_targets" in out + + +# ── Free diagnostic ────────────────────────────────────────── +def test_free_diagnostic_returns_three_opportunities(): + out = build_free_growth_diagnostic({"sector": "training", "city": "Riyadh"}) + assert out["sections"]["opportunities_ar"] + assert len(out["sections"]["opportunities_ar"]) == 3 + + +def test_uploaded_list_preview_classifies(): + contacts = [ + {"source": "crm_customer", "opt_in_status": "yes"}, + {"source": "cold_list", "opt_in_status": "no"}, + {"source": "unknown_source"}, + ] + out = analyze_uploaded_list_preview(contacts) + assert out["total"] == 3 + assert out["preview"] + + +# ── Service offers ────────────────────────────────────────── +def test_service_offers_includes_free_diagnostic(): + offers = list_targeting_services() + ids = {o["id"] for o in offers["offers"]} + assert "free_growth_diagnostic" in ids + assert "first_10_opportunities_sprint" in ids + + +def test_recommend_offer_for_agency(): + rec = recommend_service_offer("agency partner growth") + assert rec["recommended_offer"]["id"] == "partner_sprint" + + +# ── Contracts ──────────────────────────────────────────────── +def test_pilot_contract_requires_legal_review(): + out = draft_pilot_agreement_outline() + assert out["legal_review_required"] is True + assert out["not_legal_advice"] is True + + +def test_dpa_includes_pdpl(): + out = draft_dpa_outline() + assert any("PDPL" in s for s in out["sections_ar"]) + + +# ── Acquisition scorecard ─────────────────────────────────── +def test_scorecard_aggregates_pipeline(): + out = build_acquisition_scorecard({ + "accounts_researched": 50, + "decision_makers_mapped": 25, + "drafts_created": 20, + "approvals_received": 10, + "positive_replies": 5, + "opportunities": [{"expected_value_sar": 18000}, + {"expected_value_sar": 12000}], + "events": [{"status": "drafted"}, {"status": "confirmed"}], + "actions": [{"status": "blocked", "block_reason": "cold_whatsapp"}], + }) + assert out["pipeline"]["pipeline_sar"] == 30000 + assert out["meetings"]["total"] == 2 + assert out["risks_blocked"]["total"] == 1 + + +def test_productivity_score_strong_with_meetings(): + from auto_client_acquisition.targeting_os import calculate_productivity_score + out = calculate_productivity_score({ + "accounts_researched": 30, "drafts_created": 10, + "approvals_received": 5, "positive_replies": 4, + "meetings_booked": 3, + }) + assert out["score"] >= 50