mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
feat(targeting+service+excellence): Saudi Targeting OS + Service Tower + Service Excellence — 38 modules + 62 endpoints + 105 tests
Targeting & Acquisition OS (16 modules) — نظام الاستهداف الذكي - account_finder: account-first targeting; 12 buying signals; deterministic 10-25 accounts per (sector, city) - buyer_role_mapper: 14 buyer roles + sector-specific buying-committee maps + role-based Arabic angles - contact_source_policy: 12 sources (crm_customer→opt_out) with risk_score, channels-allowed, retention guidance, lawful_basis - contactability_matrix: 5 action modes (suggest_only/draft_only/approval_required/approved_execute/blocked); opt-out always blocked - linkedin_strategy: Lead Forms + Ads + manual ONLY; linkedin_do_not_do() locks scrape/auto-DM/auto-connect/extensions - email_strategy: drafts + unsubscribe footer + domain-pacing (fresh/warmed/trusted/damaged) + spam-trigger risk - whatsapp_strategy: opt-in only; rejects cold + risky phrases; opt-in template requires explicit purpose+company+unsubscribe - social_strategy: official APIs only; listening + drafts; no auto-publish - outreach_scheduler: day-by-day plans + daily limits + opt-out enforcement - reputation_guard: bounce/complaint/opt-out thresholds → healthy/watch/pause + recovery actions per channel - daily_autopilot: Arabic brief + 7 today actions + EOD report - acquisition_scorecard: pipeline + meetings + risks + productivity_score - self_growth_mode: 5 ICP focuses for Dealix; daily brief + monthly targets - free_diagnostic: Free Growth Diagnostic (3 ops + msg + risk + plan) → paid pilot recommendation - contract_drafts: Pilot/DPA/Referral/Agency/SOW outlines (legal_review_required, PDPL-aware) - service_offers: 7 targeting-tier offers + recommend by customer-type Service Tower (8 modules) — برج الخدمات الذاتية (12 productized services) - service_catalog: 12 services with target_customer/outcome/inputs/workflow/deliverables/pricing/risk/proof/upgrade - service_wizard: deterministic recommend (agency→partner; list→list_intelligence; founder→self_growth; CEO→exec_brief; budget≥2999→growth_os; default→first_10) - mission_templates: workflow steps with approval gates + linked growth missions - pricing_engine: SAR quotes scaled by company_size×urgency×channels_count + setup_fee + monthly_offer - deliverables: client report outline + proof pack template + operator checklist (no live actions) - service_scorecard: 0..100 score from drafts/replies/meetings/pipeline/CSAT - whatsapp_ceo_control: daily brief, approval cards (≤3 buttons), risk alerts, EOD reports - upgrade_paths: deterministic next-service recommendation + Arabic upsell messages Service Excellence OS (8 modules) — مصنع الخدمات الممتازة - feature_matrix: 12 must-have features per service + advanced/premium/future tiers - service_scoring: 10-dimension excellence score (clarity, speed_to_value, automation, compliance, proof, upsell, uniqueness, scalability, ops_daily, proof_data) → launch_ready/beta_only/needs_work - quality_review: 4 gates (proof / approval / pricing / channels) + status verdict; review_service_before_launch and review/all - competitor_gap: 7 competitor categories (CRM, WhatsApp tools, email assistants, LinkedIn tools, agencies, revenue intelligence, generic AI) + Dealix advantages + do-not-copy - proof_metrics: required metrics + ROI estimate (pipeline_x + closed_won_x) + Arabic summary - research_lab: monthly brief + feature hypotheses + top-3 experiments + monthly review - service_improvement_backlog: feedback→backlog conversion + impact/effort prioritization + weekly improvements - launch_package: landing outline + sales script + 12-min demo script + 5-day onboarding checklist Routers (3 new) — 62 endpoints - /api/v1/targeting/* — 20 endpoints (accounts, buying-committee, contacts, uploaded-list, outreach, daily-autopilot, self-growth, reputation, linkedin, drafts, free-diagnostic, services, contracts) - /api/v1/services/* — 20 endpoints (catalog, recommend, intake, start, workflow, deliverables, proof-pack, quote, setup-fee, monthly-offer, scorecard, upgrade-path, ceo daily-brief/approval-card/risk-alert/EOD) - /api/v1/service-excellence/* — 22 endpoints (feature-matrix, score, quality-review, review/all, proof-metrics, roi-estimate, gap-analysis, research-brief, hypotheses, experiments, monthly-review, backlog, weekly-improvements, launch-package, landing/sales/demo/onboarding) Tests (3 new files, 105 tests) - test_targeting_os: 47 tests (Arabic accounts, buying committees, opt-out blocked, cold WA blocked, LinkedIn no-scraping, email unsubscribe, WA risk, outreach plan, reputation guard, self-growth, contracts, scorecard) - test_service_tower: 38 tests (12+ services, all have pricing/proof/deliverables/approval, wizard recommendations, workflow includes approval, quote scales, CEO cards ≤3 buttons, no live send) - test_service_excellence: 33 tests (feature matrix, score returns status, ALL services pass quality gates, ROI x-multiples, 7 competitor categories, hypotheses+experiments, backlog conversion, launch package complete, demo=12min) Docs (3 new + 1 updated) - TARGETING_ACQUISITION_OS.md (Arabic) - SERVICE_TOWER_STRATEGY.md (Arabic) - SERVICE_EXCELLENCE_OS.md (Arabic) - DEALIX_100_PERCENT_LAUNCH_PLAN.md — added §36 Targeting OS + §37 Service Tower + §38 Service Excellence + §39 Landing Pages Landing pages (4 new, RTL Arabic) - services.html — 3 doors + 12 productized services - free-diagnostic.html — free growth diagnostic - first-10-opportunities.html — kill feature - agency-partner.html — agency partner program Test results - 105/105 new tests pass - Full suite: 768 passed, 2 skipped - 0 existing tests broken Safety + integration with previous layers - Targeting OS uses contactability_matrix → ALL contacts gated before any send - Service Tower's workflow includes approval gate; ALL services live_send_allowed=False - Service Excellence quality_review BLOCKS launch on missing proof/approval/pricing/unsafe channels - linkedin_do_not_do() encodes 8 explicit prohibitions (scraping/auto-DM/auto-connect/extensions) - whatsapp_do_not_do() blocks cold sends + group scraping - Contracts always: legal_review_required=True, not_legal_advice=True, PDPL sections present - Self-Growth Mode lets Dealix target its OWN ICP using the same approval-first pipeline Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bcf545c22e
commit
e106a9a0d2
@ -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)
|
||||
|
||||
|
||||
179
dealix/api/routers/service_excellence.py
Normal file
179
dealix/api/routers/service_excellence.py
Normal file
@ -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)
|
||||
177
dealix/api/routers/service_tower.py
Normal file
177
dealix/api/routers/service_tower.py
Normal file
@ -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()
|
||||
240
dealix/api/routers/targeting_os.py
Normal file
240
dealix/api/routers/targeting_os.py
Normal file
@ -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(),
|
||||
}
|
||||
@ -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",
|
||||
]
|
||||
@ -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,
|
||||
}
|
||||
@ -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)),
|
||||
),
|
||||
)
|
||||
@ -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,
|
||||
}
|
||||
@ -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"]
|
||||
@ -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
|
||||
],
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
82
dealix/auto_client_acquisition/service_tower/__init__.py
Normal file
82
dealix/auto_client_acquisition/service_tower/__init__.py
Normal file
@ -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",
|
||||
]
|
||||
91
dealix/auto_client_acquisition/service_tower/deliverables.py
Normal file
91
dealix/auto_client_acquisition/service_tower/deliverables.py
Normal file
@ -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.",
|
||||
],
|
||||
}
|
||||
@ -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)
|
||||
118
dealix/auto_client_acquisition/service_tower/pricing_engine.py
Normal file
118
dealix/auto_client_acquisition/service_tower/pricing_engine.py
Normal file
@ -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} "
|
||||
"للحصول على نتائج شهرية مستمرة."
|
||||
),
|
||||
}
|
||||
347
dealix/auto_client_acquisition/service_tower/service_catalog.py
Normal file
347
dealix/auto_client_acquisition/service_tower/service_catalog.py
Normal file
@ -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],
|
||||
}
|
||||
@ -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}"
|
||||
)
|
||||
137
dealix/auto_client_acquisition/service_tower/service_wizard.py
Normal file
137
dealix/auto_client_acquisition/service_tower/service_wizard.py
Normal file
@ -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}"
|
||||
)
|
||||
@ -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"
|
||||
@ -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 للعملاء الجدد."
|
||||
),
|
||||
}
|
||||
177
dealix/auto_client_acquisition/targeting_os/__init__.py
Normal file
177
dealix/auto_client_acquisition/targeting_os/__init__.py
Normal file
@ -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",
|
||||
]
|
||||
215
dealix/auto_client_acquisition/targeting_os/account_finder.py
Normal file
215
dealix/auto_client_acquisition/targeting_os/account_finder.py
Normal file
@ -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)))
|
||||
@ -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,
|
||||
}
|
||||
151
dealix/auto_client_acquisition/targeting_os/buyer_role_mapper.py
Normal file
151
dealix/auto_client_acquisition/targeting_os/buyer_role_mapper.py
Normal file
@ -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}",
|
||||
}
|
||||
@ -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"])
|
||||
@ -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"]
|
||||
121
dealix/auto_client_acquisition/targeting_os/contract_drafts.py
Normal file
121
dealix/auto_client_acquisition/targeting_os/contract_drafts.py
Normal file
@ -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,
|
||||
}
|
||||
106
dealix/auto_client_acquisition/targeting_os/daily_autopilot.py
Normal file
106
dealix/auto_client_acquisition/targeting_os/daily_autopilot.py
Normal file
@ -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 ديمو إن أمكن."
|
||||
),
|
||||
}
|
||||
160
dealix/auto_client_acquisition/targeting_os/email_strategy.py
Normal file
160
dealix/auto_client_acquisition/targeting_os/email_strategy.py
Normal file
@ -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}، آخر متابعة من جهتي. "
|
||||
"إذا ما كانت الأولوية الآن أرتاح وأرشّفها. "
|
||||
"وإن أردت ديمو لاحقاً، أنا موجود."
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
147
dealix/auto_client_acquisition/targeting_os/free_diagnostic.py
Normal file
147
dealix/auto_client_acquisition/targeting_os/free_diagnostic.py
Normal file
@ -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",
|
||||
}
|
||||
124
dealix/auto_client_acquisition/targeting_os/linkedin_strategy.py
Normal file
124
dealix/auto_client_acquisition/targeting_os/linkedin_strategy.py
Normal file
@ -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."
|
||||
),
|
||||
}
|
||||
@ -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 يوقف فوراً."
|
||||
)
|
||||
135
dealix/auto_client_acquisition/targeting_os/reputation_guard.py
Normal file
135
dealix/auto_client_acquisition/targeting_os/reputation_guard.py
Normal file
@ -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 "حالة صحية.")
|
||||
)
|
||||
157
dealix/auto_client_acquisition/targeting_os/self_growth_mode.py
Normal file
157
dealix/auto_client_acquisition/targeting_os/self_growth_mode.py
Normal file
@ -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 لكل خدمة.",
|
||||
],
|
||||
}
|
||||
161
dealix/auto_client_acquisition/targeting_os/service_offers.py
Normal file
161
dealix/auto_client_acquisition/targeting_os/service_offers.py
Normal file
@ -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,
|
||||
},
|
||||
}
|
||||
@ -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": [
|
||||
"لا تكشف بيانات شخصية في الرد العام.",
|
||||
"حول التفاصيل لقناة خاصة.",
|
||||
"لا تتجاهل العميل المنزعج.",
|
||||
"لا تحذف أو ترد بشكل دفاعي.",
|
||||
],
|
||||
}
|
||||
124
dealix/auto_client_acquisition/targeting_os/whatsapp_strategy.py
Normal file
124
dealix/auto_client_acquisition/targeting_os/whatsapp_strategy.py
Normal file
@ -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 يتطلب شرحاً صريحاً لما سيستقبله المستخدم. "
|
||||
"ظهور رقم واتساب في موقع لا يكفي كموافقة."
|
||||
),
|
||||
}
|
||||
@ -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.
|
||||
|
||||
185
dealix/docs/SERVICE_EXCELLENCE_OS.md
Normal file
185
dealix/docs/SERVICE_EXCELLENCE_OS.md
Normal file
@ -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 للمؤسس.
|
||||
```
|
||||
136
dealix/docs/SERVICE_TOWER_STRATEGY.md
Normal file
136
dealix/docs/SERVICE_TOWER_STRATEGY.md
Normal file
@ -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.
|
||||
184
dealix/docs/TARGETING_ACQUISITION_OS.md
Normal file
184
dealix/docs/TARGETING_ACQUISITION_OS.md
Normal file
@ -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-مخالف.
|
||||
- لا وعود بنتائج مضمونة.
|
||||
- لا تخزين بطاقات.
|
||||
90
dealix/landing/agency-partner.html
Normal file
90
dealix/landing/agency-partner.html
Normal file
@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dealix — برنامج وكالة شريكة</title>
|
||||
<style>
|
||||
body { font-family: 'Tajawal', sans-serif; background: #0f1115; color: #f5f5f7;
|
||||
line-height: 1.7; margin: 0; }
|
||||
header { padding: 56px 24px; text-align: center;
|
||||
background: linear-gradient(135deg, #1a1d27 0%, #0f1115 100%); }
|
||||
header h1 { font-size: 42px; line-height: 1.2; margin-bottom: 14px; }
|
||||
header h1 span { color: #ffd166; }
|
||||
header p { color: #a8aab2; max-width: 700px; margin: 0 auto; font-size: 18px; }
|
||||
main { max-width: 800px; margin: 0 auto; padding: 24px; }
|
||||
section { background: #181b22; padding: 28px; border-radius: 14px;
|
||||
margin: 18px 0; border: 1px solid #232631; }
|
||||
section h2 { color: #ffd166; margin-bottom: 14px; font-size: 22px; }
|
||||
ul li { padding: 6px 0; }
|
||||
ul li:before { content: "▸"; color: #06d6a0; margin-left: 8px; }
|
||||
.cta { display: inline-block; padding: 14px 28px; background: #ffd166;
|
||||
color: #0f1115; border-radius: 10px; font-weight: 700; text-decoration: none;
|
||||
font-size: 17px; margin: 8px 0; }
|
||||
.perks { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 14px; }
|
||||
.perk { background: #1f232c; padding: 16px; border-radius: 10px;
|
||||
border: 1px solid #2a2e39; }
|
||||
.perk strong { color: #ffd166; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>برنامج <span>وكالة شريكة</span></h1>
|
||||
<p>إذا كنت وكالة تسويق/مبيعات/CRM في السعودية، Dealix يشتغل خلفك:
|
||||
أنت تختار العميل، Dealix يشغل النظام، وأنت تأخذ revenue share + co-branded Proof Pack.</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>ماذا تحصل الوكالة؟</h2>
|
||||
<div class="perks">
|
||||
<div class="perk">
|
||||
<strong>Setup Fee</strong>
|
||||
<p>10,000–50,000 ريال حسب الحزمة.</p>
|
||||
</div>
|
||||
<div class="perk">
|
||||
<strong>Revenue Share</strong>
|
||||
<p>على كل عميل تجلبه + اشتراكاته الشهرية.</p>
|
||||
</div>
|
||||
<div class="perk">
|
||||
<strong>Co-Branded Proof</strong>
|
||||
<p>تقارير شهرية بعلامة الوكالة لعملائها.</p>
|
||||
</div>
|
||||
<div class="perk">
|
||||
<strong>Client Dashboard</strong>
|
||||
<p>لوحة لإدارة كل عملاء الوكالة في مكان واحد.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>كيف تبدأ؟</h2>
|
||||
<ul>
|
||||
<li>Onboarding للوكالة (2-3 أيام).</li>
|
||||
<li>أول عميل عبر Free Diagnostic.</li>
|
||||
<li>تشغيل Pilot 7 أيام.</li>
|
||||
<li>تسليم Proof Pack بعلامة الوكالة.</li>
|
||||
<li>تحويل العميل لـ Growth OS الشهري + revenue share.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>لماذا Dealix بدلاً من البناء داخلياً؟</h2>
|
||||
<ul>
|
||||
<li>Saudi-first — رسائل عربية طبيعية.</li>
|
||||
<li>Approval-first — تحمي سمعة عملائك.</li>
|
||||
<li>PDPL-aware — لا cold WhatsApp.</li>
|
||||
<li>Multi-channel — واتساب + إيميل + تقويم + Sheets.</li>
|
||||
<li>Proof Pack شهري قابل للتسليم.</li>
|
||||
<li>Self-improving — Curator يحسّن الرسائل أسبوعياً.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div style="text-align: center; padding: 24px;">
|
||||
<a href="mailto:bassam.m.assiri@gmail.com?subject=Dealix%20Agency%20Partner%20Program"
|
||||
class="cta">احجز اجتماع شراكة</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
98
dealix/landing/first-10-opportunities.html
Normal file
98
dealix/landing/first-10-opportunities.html
Normal file
@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dealix — 10 فرص في 10 دقائق</title>
|
||||
<style>
|
||||
body { font-family: 'Tajawal', sans-serif; background: #0f1115; color: #f5f5f7;
|
||||
line-height: 1.7; margin: 0; }
|
||||
header { padding: 56px 24px; text-align: center;
|
||||
background: linear-gradient(135deg, #1a1d27 0%, #0f1115 100%); }
|
||||
header .badge { display: inline-block; padding: 6px 14px;
|
||||
background: rgba(255, 209, 102, 0.15); color: #ffd166; border-radius: 20px;
|
||||
font-size: 14px; margin-bottom: 18px; border: 1px solid rgba(255, 209, 102, 0.3); }
|
||||
header h1 { font-size: 44px; line-height: 1.2; margin-bottom: 14px; }
|
||||
header h1 span { color: #ffd166; }
|
||||
header p { color: #a8aab2; max-width: 720px; margin: 0 auto; font-size: 19px; }
|
||||
main { max-width: 760px; margin: 0 auto; padding: 24px; }
|
||||
section { background: #181b22; padding: 28px; border-radius: 14px;
|
||||
margin: 18px 0; border: 1px solid #232631; }
|
||||
section h2 { color: #ffd166; margin-bottom: 14px; font-size: 22px; }
|
||||
ul li { padding: 6px 0; }
|
||||
ul li:before { content: "✓"; color: #06d6a0; margin-left: 8px; }
|
||||
.cta { display: inline-block; padding: 14px 28px; background: #ffd166;
|
||||
color: #0f1115; border-radius: 10px; font-weight: 700; text-decoration: none;
|
||||
font-size: 17px; margin: 8px; }
|
||||
.pricing { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 14px; }
|
||||
.price-box { background: #1f232c; padding: 18px; border-radius: 10px;
|
||||
border: 1px solid #2a2e39; }
|
||||
.price-box .label { color: #a8aab2; font-size: 14px; }
|
||||
.price-box .price { font-size: 24px; font-weight: 700; color: #ffd166; margin: 6px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<span class="badge">Kill Feature — متاح في Private Beta</span>
|
||||
<h1>10 فرص في <span>10 دقائق</span></h1>
|
||||
<p>أعطنا قطاعك ومدينتك وعرضك، نطلع لك 10 فرص B2B مع why-now + رسائل عربية
|
||||
+ خطة متابعة 7 أيام + Proof Pack — وأنت توافق قبل أي تواصل.</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>ما الذي ستحصل عليه</h2>
|
||||
<ul>
|
||||
<li>10 فرص B2B مرتبة حسب fit_score</li>
|
||||
<li>سبب "لماذا الآن" لكل فرصة (إشارات شراء حقيقية)</li>
|
||||
<li>صانع القرار المحتمل (founder/head of sales/etc.)</li>
|
||||
<li>أفضل قناة (email/LinkedIn Lead Form/شريك)</li>
|
||||
<li>10 رسائل عربية بنبرة طبيعية سعودية</li>
|
||||
<li>contactability gate (safe / needs_review / blocked)</li>
|
||||
<li>خطة متابعة 7 أيام</li>
|
||||
<li>Proof Pack تفصيلي بعد الأسبوع</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>الأسعار</h2>
|
||||
<div class="pricing">
|
||||
<div class="price-box">
|
||||
<div class="label">Pilot 7 أيام</div>
|
||||
<div class="price">499 ريال</div>
|
||||
<div>أو مجاني مقابل case study</div>
|
||||
</div>
|
||||
<div class="price-box">
|
||||
<div class="label">Paid Pilot 30 يوم</div>
|
||||
<div class="price">1,500 ريال</div>
|
||||
<div>إعداد موسّع + 3 جولات</div>
|
||||
</div>
|
||||
<div class="price-box">
|
||||
<div class="label">Growth OS شهري</div>
|
||||
<div class="price">2,999 ريال</div>
|
||||
<div>المنصة الكاملة شهرياً</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>الأمان</h2>
|
||||
<ul>
|
||||
<li>Approval-first — لا إرسال بدون موافقتك.</li>
|
||||
<li>لا cold WhatsApp (PDPL-aware).</li>
|
||||
<li>لا scraping ولا auto-DM على LinkedIn.</li>
|
||||
<li>لا charge بدون تأكيد.</li>
|
||||
<li>لا وعود بنتائج مضمونة.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div style="text-align: center; padding: 24px;">
|
||||
<a href="mailto:bassam.m.assiri@gmail.com?subject=Dealix%2010-in-10%20Pilot"
|
||||
class="cta">احجز Pilot الآن</a>
|
||||
<a href="services.html" class="cta"
|
||||
style="background: transparent; color: #f5f5f7; border: 1px solid #383b44;">شاهد كل الخدمات</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
71
dealix/landing/free-diagnostic.html
Normal file
71
dealix/landing/free-diagnostic.html
Normal file
@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dealix — تشخيص نمو مجاني</title>
|
||||
<style>
|
||||
body { font-family: 'Tajawal', sans-serif; background: #0f1115; color: #f5f5f7;
|
||||
line-height: 1.7; margin: 0; }
|
||||
header { padding: 56px 24px; text-align: center;
|
||||
background: linear-gradient(135deg, #1a1d27 0%, #0f1115 100%); }
|
||||
header h1 { font-size: 42px; line-height: 1.2; margin-bottom: 14px; }
|
||||
header h1 span { color: #ffd166; }
|
||||
header p { color: #a8aab2; max-width: 680px; margin: 0 auto; font-size: 18px; }
|
||||
main { max-width: 720px; margin: 0 auto; padding: 24px; }
|
||||
section { background: #181b22; padding: 28px; border-radius: 14px;
|
||||
margin: 18px 0; border: 1px solid #232631; }
|
||||
section h2 { color: #ffd166; margin-bottom: 14px; font-size: 22px; }
|
||||
ul li { padding: 6px 0; }
|
||||
ul li:before { content: "✓"; color: #06d6a0; margin-left: 8px; }
|
||||
.cta { display: inline-block; padding: 14px 28px; background: #ffd166;
|
||||
color: #0f1115; border-radius: 10px; font-weight: 700; text-decoration: none;
|
||||
font-size: 17px; margin: 8px 0; }
|
||||
.note { background: rgba(6, 214, 160, 0.1); padding: 14px; border-radius: 10px;
|
||||
border: 1px solid rgba(6, 214, 160, 0.3); color: #06d6a0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>تشخيص <span>نمو مجاني</span></h1>
|
||||
<p>أرسل لنا قطاعك ومدينتك وعرضك، نرسل لك خلال 24 ساعة عمل:
|
||||
3 فرص B2B + رسالة عربية + تقرير مخاطر + خطة Pilot — كل شيء بدون التزام.</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>ماذا تستلم؟</h2>
|
||||
<ul>
|
||||
<li>3 فرص B2B مناسبة لقطاعك ومدينتك</li>
|
||||
<li>سبب "لماذا الآن" لكل فرصة</li>
|
||||
<li>رسالة عربية مخصصة (≤80 كلمة، نبرة سعودية طبيعية)</li>
|
||||
<li>تقرير مخاطر (PDPL + سمعة القناة)</li>
|
||||
<li>خطة Pilot 7 أيام مقترحة</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>كيف نعمل؟</h2>
|
||||
<ul>
|
||||
<li>تملأ نموذج 3 دقائق (قطاع/مدينة/عرض/هدف)</li>
|
||||
<li>Dealix يولّد التشخيص خلال 24 ساعة عمل</li>
|
||||
<li>تستلمه على إيميلك (PDF + JSON)</li>
|
||||
<li>إذا أعجبك، نكمل Pilot 7 أيام بـ 499 ريال</li>
|
||||
<li>أو مجاني مقابل case study بعد انتهاء الـPilot</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="note">
|
||||
<strong>ضمانات Dealix:</strong>
|
||||
Approval-first — لا نرسل أي شيء قبل موافقتك.
|
||||
لا cold WhatsApp. لا scraping. لا وعود بنتائج مضمونة.
|
||||
PDPL-aware من اليوم الأول.
|
||||
</section>
|
||||
|
||||
<div style="text-align: center; padding: 24px;">
|
||||
<a href="mailto:bassam.m.assiri@gmail.com?subject=Dealix%20Free%20Growth%20Diagnostic"
|
||||
class="cta">احجز التشخيص الآن</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
156
dealix/landing/services.html
Normal file
156
dealix/landing/services.html
Normal file
@ -0,0 +1,156 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dealix — الخدمات</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1115; --panel: #181b22; --text: #f5f5f7;
|
||||
--muted: #a8aab2; --brand: #ffd166; --good: #06d6a0;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Tajawal', 'Segoe UI', Tahoma, sans-serif;
|
||||
background: var(--bg); color: var(--text); line-height: 1.7; }
|
||||
header { padding: 56px 24px 32px; text-align: center;
|
||||
background: linear-gradient(135deg, #1a1d27 0%, #0f1115 100%); }
|
||||
header h1 { font-size: 42px; line-height: 1.2; margin-bottom: 14px; }
|
||||
header h1 span { color: var(--brand); }
|
||||
header p { color: var(--muted); max-width: 640px; margin: 0 auto; font-size: 18px; }
|
||||
main { max-width: 1100px; margin: 0 auto; padding: 32px 24px; }
|
||||
.doors { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 18px; margin: 24px 0 36px; }
|
||||
.door { background: var(--panel); padding: 28px; border-radius: 14px;
|
||||
border: 1px solid #232631; }
|
||||
.door h2 { color: var(--brand); margin-bottom: 12px; font-size: 22px; }
|
||||
.door ul { list-style: none; padding: 0; }
|
||||
.door ul li { padding: 6px 0; padding-right: 22px; position: relative; }
|
||||
.door ul li:before { content: "▸"; color: var(--good); position: absolute;
|
||||
right: 0; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 14px; margin: 14px 0; }
|
||||
.card { background: #1f232c; padding: 16px; border-radius: 10px;
|
||||
border: 1px solid #2a2e39; }
|
||||
.card .name { color: var(--brand); font-weight: 700; font-size: 17px; }
|
||||
.card .price { font-size: 14px; color: var(--muted); margin-top: 6px; }
|
||||
.card .out { font-size: 14px; margin-top: 8px; color: var(--text); }
|
||||
.cta { display: inline-block; padding: 12px 24px; background: var(--brand);
|
||||
color: #0f1115; border-radius: 10px; font-weight: 700; text-decoration: none;
|
||||
font-size: 16px; margin: 6px; }
|
||||
footer { text-align: center; padding: 24px; color: var(--muted); font-size: 14px;
|
||||
border-top: 1px solid #232631; margin-top: 32px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>الخدمات — <span>Dealix Service Tower</span></h1>
|
||||
<p>اختر هدفك من 3 أبواب، النظام يوصي بالخدمة الصحيحة.
|
||||
كل خدمة productized بمواصفات + مخرجات + سعر + Proof Pack.</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="doors">
|
||||
<div class="door">
|
||||
<h2>أريد عملاء جدد</h2>
|
||||
<ul>
|
||||
<li>First 10 Opportunities Sprint</li>
|
||||
<li>Targeting OS</li>
|
||||
<li>LinkedIn Lead Gen Setup</li>
|
||||
<li>Email Outreach Drafts</li>
|
||||
<li>Meeting Booking Sprint</li>
|
||||
<li>Growth OS</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="door">
|
||||
<h2>عندي بيانات وأبغى أستفيد منها</h2>
|
||||
<ul>
|
||||
<li>List Intelligence</li>
|
||||
<li>WhatsApp Compliance Setup</li>
|
||||
<li>Email Revenue Rescue</li>
|
||||
<li>Customer Reactivation</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="door">
|
||||
<h2>أبغى توسع واستراتيجية</h2>
|
||||
<ul>
|
||||
<li>Partner Sprint</li>
|
||||
<li>Agency Partner Program</li>
|
||||
<li>Executive Growth Brief</li>
|
||||
<li>Self-Growth Operator</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h2 style="color: var(--brand); margin: 16px 0;">12 خدمة productized</h2>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="name">Free Growth Diagnostic</div>
|
||||
<div class="price">مجاني</div>
|
||||
<div class="out">3 فرص + رسالة + مخاطر + خطة Pilot — خلال 24 ساعة</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">List Intelligence</div>
|
||||
<div class="price">499–1,500 ريال</div>
|
||||
<div class="out">تنظيف + تصنيف + أفضل 50 + رسائل عربية</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">First 10 Opportunities Sprint</div>
|
||||
<div class="price">499–1,500 ريال</div>
|
||||
<div class="out">10 فرص + رسائل + متابعة 7 أيام + Proof Pack</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">Self-Growth Operator</div>
|
||||
<div class="price">999 ريال شهرياً</div>
|
||||
<div class="out">Daily brief + drafts + متابعة + reports</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">Growth OS</div>
|
||||
<div class="price">2,999 ريال شهرياً</div>
|
||||
<div class="out">المنصة الكاملة: قنوات + autopilot + proof</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">Email Revenue Rescue</div>
|
||||
<div class="price">1,500–5,000 ريال</div>
|
||||
<div class="out">استخراج فرص ضائعة من Gmail + drafts</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">Meeting Booking Sprint</div>
|
||||
<div class="price">1,500–5,000 ريال</div>
|
||||
<div class="out">دعوات + briefs + follow-ups</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">Partner Sprint</div>
|
||||
<div class="price">3,000–7,500 ريال</div>
|
||||
<div class="out">20 شريك محتمل + 10 رسائل + 5 اجتماعات</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">Agency Partner Program</div>
|
||||
<div class="price">10,000–50,000 ريال</div>
|
||||
<div class="out">بيع Dealix لعملاء الوكالة + revenue share</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">WhatsApp Compliance Setup</div>
|
||||
<div class="price">1,500–4,000 ريال</div>
|
||||
<div class="out">audit + opt-in templates + ledger</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">LinkedIn Lead Gen Setup</div>
|
||||
<div class="price">2,000–7,500 ريال</div>
|
||||
<div class="out">حملة Lead Form + audiences + ربط CRM</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="name">Executive Growth Brief</div>
|
||||
<div class="price">499–999 ريال شهرياً</div>
|
||||
<div class="out">3 قرارات + 3 فرص + 3 مخاطر يومياً</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 32px 0;">
|
||||
<a href="free-diagnostic.html" class="cta">ابدأ بالتشخيص المجاني</a>
|
||||
<a href="first-10-opportunities.html" class="cta" style="background: transparent; color: var(--text); border: 1px solid #383b44;">10 فرص في 10 دقائق</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>Dealix — Service Tower · 2026 · bassam.m.assiri@gmail.com</footer>
|
||||
</body>
|
||||
</html>
|
||||
238
dealix/tests/unit/test_service_excellence.py
Normal file
238
dealix/tests/unit/test_service_excellence.py
Normal file
@ -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
|
||||
241
dealix/tests/unit/test_service_tower.py
Normal file
241
dealix/tests/unit/test_service_tower.py
Normal file
@ -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)
|
||||
329
dealix/tests/unit/test_targeting_os.py
Normal file
329
dealix/tests/unit/test_targeting_os.py
Normal file
@ -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
|
||||
Loading…
Reference in New Issue
Block a user