merge: origin/ai-company into sync/dealix-full-complete

Resolve add/add conflicts by keeping local Dealix tree; include upstream-only files from ai-company.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sami Assiri 2026-05-01 21:03:17 +03:00
commit b888abd2b2
84 changed files with 8987 additions and 44 deletions

View File

@ -2,7 +2,8 @@
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta
from core._py_compat import UTC
from typing import Any
from fastapi import APIRouter, HTTPException, Query

View File

@ -0,0 +1,46 @@
"""Connector Catalog router — every external integration with risk + launch phase."""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter
from auto_client_acquisition.connector_catalog import (
all_risks,
catalog_summary,
connector_risks,
connector_status,
get_connector,
list_connectors,
)
router = APIRouter(prefix="/api/v1/connector-catalog", tags=["connector-catalog"])
@router.get("/catalog")
async def catalog() -> dict[str, Any]:
return list_connectors()
@router.get("/summary")
async def summary() -> dict[str, Any]:
return catalog_summary()
@router.get("/status")
async def status() -> dict[str, Any]:
return connector_status()
@router.get("/risks")
async def risks() -> dict[str, Any]:
return all_risks()
@router.get("/{connector_key}")
async def detail(connector_key: str) -> dict[str, Any]:
c = get_connector(connector_key)
if c is None:
return {"error": f"unknown connector: {connector_key}"}
return {**c.to_dict(), "risks_ar": connector_risks(connector_key)}

View File

@ -0,0 +1,172 @@
"""Revenue Company OS router — command feed + work units + proof + memory."""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Body
from auto_client_acquisition.revenue_company_os import (
REVENUE_EDGE_TYPES,
REVENUE_WORK_UNIT_TYPES,
aggregate_work_units,
build_card_from_event,
build_channel_health_snapshot,
build_command_feed_for_customer,
build_growth_memory_demo,
build_opportunity_factory_demo,
build_revenue_action_graph_demo,
build_revenue_proof_ledger_demo,
build_revenue_work_unit,
build_service_factory_demo,
build_weekly_self_improvement_report,
instantiate_service,
revenue_os_command_feed_demo,
)
router = APIRouter(prefix="/api/v1/revenue-os", tags=["revenue-company-os"])
# ── Command Feed ─────────────────────────────────────────────
@router.get("/command-feed/demo")
async def command_feed_demo() -> dict[str, Any]:
return revenue_os_command_feed_demo()
@router.post("/events/ingest")
async def events_ingest(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
"""Convert one event → Arabic decision card. Never executes anything."""
return build_card_from_event(payload)
@router.post("/command-feed/build")
async def command_feed_build(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return build_command_feed_for_customer(
customer_id=payload.get("customer_id", "demo"),
events=payload.get("events", []),
)
# ── Work Units ───────────────────────────────────────────────
@router.get("/work-units/types")
async def work_unit_types() -> dict[str, Any]:
return {"types": list(REVENUE_WORK_UNIT_TYPES)}
@router.post("/work-units/build")
async def work_units_build(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return build_revenue_work_unit(
unit_type=payload.get("unit_type", ""),
service_id=payload.get("service_id", ""),
customer_id=payload.get("customer_id", ""),
risk_level=payload.get("risk_level", "low"),
revenue_influenced_sar=float(payload.get("revenue_influenced_sar", 0)),
proof_event=payload.get("proof_event", ""),
notes=payload.get("notes", ""),
)
except ValueError as exc:
return {"error": str(exc)}
@router.post("/work-units/aggregate")
async def work_units_aggregate(
units: list[dict[str, Any]] = Body(default_factory=list, embed=True),
) -> dict[str, Any]:
return aggregate_work_units(units)
@router.get("/work-units/demo")
async def work_units_demo() -> dict[str, Any]:
"""Demo aggregation across 12 sample units."""
return build_revenue_proof_ledger_demo()
# ── Proof Ledger ─────────────────────────────────────────────
@router.get("/proof-ledger/demo")
async def proof_ledger_demo() -> dict[str, Any]:
return build_revenue_proof_ledger_demo()
# ── Action Graph ─────────────────────────────────────────────
@router.get("/action-graph/edge-types")
async def action_graph_edge_types() -> dict[str, Any]:
return {"edge_types": list(REVENUE_EDGE_TYPES)}
@router.get("/action-graph/demo")
async def action_graph_demo() -> dict[str, Any]:
return build_revenue_action_graph_demo()
# ── Channel Health ───────────────────────────────────────────
@router.post("/channel-health/snapshot")
async def channel_health_snapshot(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_channel_health_snapshot(
metrics_per_channel=payload.get("metrics_per_channel"),
)
@router.get("/channel-health/demo")
async def channel_health_demo() -> dict[str, Any]:
return build_channel_health_snapshot()
# ── Opportunity Factory ──────────────────────────────────────
@router.post("/opportunity-factory")
async def opportunity_factory(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_opportunity_factory_demo(
sector=payload.get("sector", "training"),
city=payload.get("city", "Riyadh"),
limit=int(payload.get("limit", 5)),
)
@router.get("/opportunity-factory/demo")
async def opportunity_factory_demo() -> dict[str, Any]:
return build_opportunity_factory_demo()
# ── Service Factory ──────────────────────────────────────────
@router.post("/service-factory")
async def service_factory(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return instantiate_service(
service_id=payload.get("service_id", ""),
customer_id=payload.get("customer_id", ""),
company_size=payload.get("company_size", "small"),
urgency=payload.get("urgency", "normal"),
)
@router.get("/service-factory/demo")
async def service_factory_demo() -> dict[str, Any]:
return build_service_factory_demo()
# ── Growth Memory ────────────────────────────────────────────
@router.get("/growth-memory/demo")
async def growth_memory_demo() -> dict[str, Any]:
return build_growth_memory_demo()
# ── Self-Improvement Loop ────────────────────────────────────
@router.post("/self-improvement/weekly-report")
async def self_improvement_weekly(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
return build_weekly_self_improvement_report(weekly_metrics=payload)
@router.get("/self-improvement/demo")
async def self_improvement_demo() -> dict[str, Any]:
return build_weekly_self_improvement_report(weekly_metrics={
"approval_rate": 0.42,
"reply_rate": 0.05,
"meeting_rate": 0.018,
"blocked_actions": 12,
"service_revenue_sar": {
"first_10_opportunities_sprint": 1500,
"list_intelligence": 999,
"growth_os_monthly": 2999,
},
"top_objections": ["price", "timing"],
"channel_outcomes": {"email": "healthy", "whatsapp": "watch"},
})

View File

@ -0,0 +1,39 @@
"""Lightweight in-memory cost tracker (per process; persistence is the ledger's job)."""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
@dataclass
class CostTracker:
"""Track agent run costs in memory for the current process."""
by_workflow: dict[str, float] = field(default_factory=lambda: defaultdict(float))
by_provider: dict[str, float] = field(default_factory=lambda: defaultdict(float))
by_task_type: dict[str, float] = field(default_factory=lambda: defaultdict(float))
total: float = 0.0
runs: int = 0
def record(
self,
*,
workflow_name: str,
provider_key: str,
task_type: str,
cost_estimate: float,
) -> None:
self.by_workflow[workflow_name] += cost_estimate
self.by_provider[provider_key] += cost_estimate
self.by_task_type[task_type] += cost_estimate
self.total += cost_estimate
self.runs += 1
def summary(self) -> dict[str, object]:
return {
"runs": self.runs,
"total": round(self.total, 4),
"by_workflow": {k: round(v, 4) for k, v in self.by_workflow.items()},
"by_provider": {k: round(v, 4) for k, v in self.by_provider.items()},
"by_task_type": {k: round(v, 4) for k, v in self.by_task_type.items()},
}

View File

@ -6,7 +6,7 @@ ICP Matcher Agent — scores how well a lead fits our Ideal Customer Profile.
from __future__ import annotations
from dataclasses import dataclass, field
from enum import StrEnum
from core._py_compat import StrEnum
from typing import Any
from auto_client_acquisition.agents.intake import Lead

View File

@ -7,7 +7,7 @@ from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from core._py_compat import StrEnum
from typing import Any
from core.agents.base import BaseAgent

View File

@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from core._py_compat import StrEnum
from typing import Literal
CostClass = Literal["low", "medium", "high"]

View File

@ -0,0 +1,28 @@
"""Connector Catalog — every external integration with launch phase + risk level.
Higher-level than channel_registry: this catalogues every *integration* Dealix
can offer, including read-only and beta-status connectors, with launch phase.
"""
from __future__ import annotations
from .catalog import (
ALL_CONNECTORS,
Connector,
catalog_summary,
get_connector,
list_connectors,
)
from .risks import all_risks, connector_risks
from .status import connector_status
__all__ = [
"ALL_CONNECTORS",
"Connector",
"all_risks",
"catalog_summary",
"connector_risks",
"connector_status",
"get_connector",
"list_connectors",
]

View File

@ -0,0 +1,263 @@
"""The connector catalog — 12+ integrations Dealix exposes."""
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Connector:
"""One external integration."""
key: str
label_ar: str
label_en: str
capability: str # short verb phrase
required_scopes: tuple[str, ...]
beta_status: str # "live" | "beta" | "coming_soon"
allowed_actions: tuple[str, ...]
blocked_actions: tuple[str, ...]
risk_level: str # "low" | "medium" | "high"
launch_phase: str # "phase_1" | "phase_2" | "phase_3" | "phase_4"
notes_ar: str = ""
docs_url: str = ""
def to_dict(self) -> dict[str, object]:
return {
"key": self.key, "label_ar": self.label_ar, "label_en": self.label_en,
"capability": self.capability,
"required_scopes": list(self.required_scopes),
"beta_status": self.beta_status,
"allowed_actions": list(self.allowed_actions),
"blocked_actions": list(self.blocked_actions),
"risk_level": self.risk_level,
"launch_phase": self.launch_phase,
"notes_ar": self.notes_ar,
"docs_url": self.docs_url,
}
ALL_CONNECTORS: tuple[Connector, ...] = (
Connector(
key="whatsapp_cloud",
label_ar="واتساب",
label_en="WhatsApp Business Cloud",
capability="send/receive WA business messages",
required_scopes=("messages_send", "messages_receive_webhook"),
beta_status="beta",
allowed_actions=("draft_message", "respond_to_inbound", "send_with_approval"),
blocked_actions=("cold_send_without_consent", "scrape_groups"),
risk_level="high",
launch_phase="phase_1",
notes_ar="ممنوع الإرسال البارد بدون opt-in واضح. PDPL.",
docs_url="https://developers.facebook.com/docs/whatsapp",
),
Connector(
key="gmail",
label_ar="Gmail",
label_en="Gmail",
capability="read/draft/send email",
required_scopes=("gmail.compose", "gmail.modify"),
beta_status="beta",
allowed_actions=("create_draft", "read_label_inbox"),
blocked_actions=("auto_send_without_approval", "delete_thread"),
risk_level="high",
launch_phase="phase_1",
notes_ar="ابدأ بإنشاء drafts فقط — لا إرسال حي افتراضياً.",
docs_url="https://developers.google.com/gmail/api",
),
Connector(
key="google_calendar",
label_ar="تقويم Google",
label_en="Google Calendar",
capability="draft/insert calendar events",
required_scopes=("calendar.events",),
beta_status="beta",
allowed_actions=("draft_event", "list_busy"),
blocked_actions=("auto_insert_without_approval", "delete_event"),
risk_level="medium",
launch_phase="phase_1",
notes_ar="إدراج الموعد يحتاج موافقة المستخدم.",
docs_url="https://developers.google.com/workspace/calendar/api",
),
Connector(
key="google_meet",
label_ar="Google Meet",
label_en="Google Meet",
capability="read transcripts",
required_scopes=("meetings.space.readonly", "conferenceRecords.readonly"),
beta_status="beta",
allowed_actions=("read_transcript_with_consent",),
blocked_actions=("realtime_listen_without_consent",),
risk_level="high",
launch_phase="phase_2",
notes_ar="قراءة transcripts فقط بعد موافقة كل المشاركين.",
docs_url="https://developers.google.com/meet/api",
),
Connector(
key="moyasar",
label_ar="مدفوعات Moyasar",
label_en="Moyasar",
capability="payment links + invoices",
required_scopes=("payments.create", "invoices.create", "webhook.subscribe"),
beta_status="beta",
allowed_actions=("create_payment_link_draft", "create_invoice_draft"),
blocked_actions=("auto_charge_card", "store_card_number"),
risk_level="high",
launch_phase="phase_1",
notes_ar="لا يخزّن بطاقات. payment link أو invoice فقط.",
docs_url="https://docs.moyasar.com",
),
Connector(
key="linkedin_lead_forms",
label_ar="LinkedIn Lead Forms",
label_en="LinkedIn Lead Gen Forms",
capability="ingest qualified leads from ads/events",
required_scopes=("r_ads_leadgen_automation",),
beta_status="coming_soon",
allowed_actions=("ingest_form_lead",),
blocked_actions=("auto_dm_without_opt_in", "scrape_profiles"),
risk_level="medium",
launch_phase="phase_2",
notes_ar="leads مصرّح بها — مدخل آمن.",
docs_url="https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/leads",
),
Connector(
key="google_business_profile",
label_ar="Google Business Profile",
label_en="Google Business Profile",
capability="manage reviews + posts",
required_scopes=("business.manage", "reviews.read"),
beta_status="coming_soon",
allowed_actions=("read_reviews", "draft_review_reply"),
blocked_actions=("auto_publish_review_reply",),
risk_level="medium",
launch_phase="phase_2",
notes_ar="أساسي للمتاجر/العيادات والسمعة المحلية.",
docs_url="https://developers.google.com/my-business",
),
Connector(
key="x_api",
label_ar="X (Twitter)",
label_en="X API",
capability="ingest mentions + DMs (with permission)",
required_scopes=("tweet.read", "users.read", "dm.read"),
beta_status="coming_soon",
allowed_actions=("read_mentions", "ingest_dm_with_consent"),
blocked_actions=("scrape_firehose", "auto_dm_strangers"),
risk_level="high",
launch_phase="phase_3",
notes_ar="حسب خطة الـ API — لا scraping.",
docs_url="https://docs.x.com/x-api/overview",
),
Connector(
key="instagram_graph",
label_ar="Instagram",
label_en="Instagram Graph API",
capability="ingest comments + DMs",
required_scopes=("instagram_manage_comments", "instagram_manage_messages"),
beta_status="coming_soon",
allowed_actions=("read_comments", "draft_reply"),
blocked_actions=("auto_publish_reply",),
risk_level="high",
launch_phase="phase_3",
notes_ar="الموافقة على الرد قبل النشر.",
docs_url="https://developers.facebook.com/docs/instagram-api",
),
Connector(
key="google_sheets",
label_ar="Google Sheets",
label_en="Google Sheets",
capability="read/write structured lists",
required_scopes=("sheets.read", "sheets.write_with_approval"),
beta_status="beta",
allowed_actions=("read_sheet", "append_with_approval"),
blocked_actions=("auto_overwrite_without_approval",),
risk_level="low",
launch_phase="phase_1",
notes_ar="مصدر leads ووجهة لتقارير ProofPack.",
docs_url="https://developers.google.com/sheets/api",
),
Connector(
key="crm_generic",
label_ar="CRM",
label_en="CRM (HubSpot/Salesforce/Zoho/etc)",
capability="sync contacts + opportunities",
required_scopes=("crm.contacts", "crm.opportunities"),
beta_status="beta",
allowed_actions=("read_contacts", "draft_opportunity"),
blocked_actions=("delete_contact", "auto_overwrite_owner"),
risk_level="medium",
launch_phase="phase_2",
notes_ar="مصدر pipeline — متوافق مع CRM متعددة.",
docs_url="",
),
Connector(
key="website_forms",
label_ar="نماذج الموقع",
label_en="Website Forms",
capability="ingest form submissions",
required_scopes=("webhook.receive",),
beta_status="live",
allowed_actions=("ingest_form_submission",),
blocked_actions=(),
risk_level="low",
launch_phase="phase_1",
notes_ar="مصدر leads مملوك للعميل — أكثر أماناً.",
docs_url="",
),
Connector(
key="composio",
label_ar="Composio (اختياري)",
label_en="Composio Integration Backend",
capability="managed auth + 500+ toolkits",
required_scopes=("composio.toolkit",),
beta_status="coming_soon",
allowed_actions=("delegated_tool_call_with_approval",),
blocked_actions=("bypass_dealix_policy",),
risk_level="medium",
launch_phase="phase_4",
notes_ar="يُستخدم خلف Dealix Tool Gateway فقط — لا يُفتح مباشرة.",
docs_url="https://docs.composio.dev",
),
Connector(
key="mcp_gateway",
label_ar="MCP Gateway (اختياري)",
label_en="Model Context Protocol Gateway",
capability="standardized tool/data access",
required_scopes=("mcp.tools",),
beta_status="coming_soon",
allowed_actions=("delegated_tool_call_with_approval",),
blocked_actions=("execute_arbitrary_command", "open_unrestricted_tools"),
risk_level="high",
launch_phase="phase_4",
notes_ar="MCP مفتوحة خطرة — تُستخدم بـ allowlist صارم فقط.",
docs_url="https://modelcontextprotocol.io",
),
)
def get_connector(key: str) -> Connector | None:
return next((c for c in ALL_CONNECTORS if c.key == key), None)
def list_connectors() -> dict[str, object]:
return {
"total": len(ALL_CONNECTORS),
"connectors": [c.to_dict() for c in ALL_CONNECTORS],
}
def catalog_summary() -> dict[str, object]:
by_phase: dict[str, int] = {}
by_status: dict[str, int] = {}
by_risk: dict[str, int] = {}
for c in ALL_CONNECTORS:
by_phase[c.launch_phase] = by_phase.get(c.launch_phase, 0) + 1
by_status[c.beta_status] = by_status.get(c.beta_status, 0) + 1
by_risk[c.risk_level] = by_risk.get(c.risk_level, 0) + 1
return {
"total": len(ALL_CONNECTORS),
"by_launch_phase": by_phase,
"by_beta_status": by_status,
"by_risk_level": by_risk,
}

View File

@ -0,0 +1,76 @@
"""Per-connector risk dossier — Arabic, deterministic."""
from __future__ import annotations
from .catalog import ALL_CONNECTORS, get_connector
CONNECTOR_RISKS_AR: dict[str, list[str]] = {
"whatsapp_cloud": [
"PDPL: لا تواصل بدون opt-in واضح.",
"نسبة بلاغ مرتفعة قد توقف الرقم.",
"Pricing per-conversation — راقب التكلفة.",
],
"gmail": [
"إرسال خاطئ يضر سمعة الـ domain.",
"scopes واسعة قد تكشف بيانات حساسة.",
"ابدأ بإنشاء drafts فقط.",
],
"google_calendar": [
"إدراج موعد بدون موافقة يخرّب جدول العميل.",
"احذر تسريب بيانات الحضور.",
],
"google_meet": [
"قراءة transcripts بدون موافقة الجميع تنتهك الخصوصية.",
"PDPL + توافق دولي للضيوف.",
],
"moyasar": [
"لا يخزّن بيانات بطاقة داخل Dealix.",
"أي charge بدون user_confirmed يجب أن يُحظر.",
],
"linkedin_lead_forms": [
"Compliance with LinkedIn lead automation T&Cs.",
"اعرف source كل lead قبل التواصل.",
],
"google_business_profile": [
"ردود تلقائية على reviews تخلق مشاكل قانونية.",
"احتفظ بـ review/reply ledger.",
],
"x_api": [
"خطة الـ API تحدد ما هو متاح فعلاً.",
"scraping مخالف للـ ToS.",
],
"instagram_graph": [
"DMs الباردة محظورة.",
"Comments العامة آمنة، DMs تحتاج صلاحيات.",
],
"google_sheets": [
"كتابة عشوائية تتلف بيانات العميل.",
"اطلب موافقة قبل overwrite.",
],
"crm_generic": [
"مزامنة مفتوحة قد تكتب owner خاطئ.",
"اقرأ أولاً، اكتب draft فقط.",
],
"website_forms": [
"بيانات تأتي من جهة العميل — أقل خطر.",
],
"composio": [
"أي tool خلف Composio يجب أن يمر من Dealix policy أولاً.",
],
"mcp_gateway": [
"MCP مفتوحة + tools بدون allowlist = تنفيذ أوامر خطر.",
"حافظ على allowlist + audit + approval.",
],
}
def connector_risks(key: str) -> list[str]:
"""Risks for a single connector. Empty if connector unknown."""
if get_connector(key) is None:
return []
return list(CONNECTOR_RISKS_AR.get(key, []))
def all_risks() -> dict[str, list[str]]:
"""Risks for every catalogued connector."""
return {c.key: list(CONNECTOR_RISKS_AR.get(c.key, [])) for c in ALL_CONNECTORS}

View File

@ -0,0 +1,32 @@
"""Demo connector-status snapshot (deterministic; production reads env state)."""
from __future__ import annotations
from .catalog import ALL_CONNECTORS
def connector_status() -> dict[str, object]:
"""
Return current status for each catalogued connector.
During private beta everything is `not_connected` connecting flips to
`connected_draft_only` first, then `connected_live_with_approval` after a
full safety review.
"""
statuses: list[dict[str, object]] = []
for c in ALL_CONNECTORS:
if c.beta_status == "live":
mode = "connected_draft_only"
elif c.beta_status == "beta":
mode = "connected_draft_only"
else:
mode = "not_connected"
statuses.append({
"key": c.key,
"label_ar": c.label_ar,
"beta_status": c.beta_status,
"launch_phase": c.launch_phase,
"mode": mode,
"risk_level": c.risk_level,
})
return {"total": len(ALL_CONNECTORS), "statuses": statuses}

View File

@ -0,0 +1,96 @@
"""
Arabic Growth Operator Dealix's customer-facing growth-execution layer.
This package bundles the building blocks for the operator experience:
- client_profile : Saudi B2B Growth Profile per customer
- contact_importer : safe upload + normalize + classify uploaded numbers
- contactability : per-contact "can we contact?" decision
- targeting : segmenting + ranking + Top-10 with Why-Now stub
- message_planner : Arabic drafts + follow-ups + objection responses
- partnership_planner : partner suggestions + outreach drafts + scorecard
- meeting_planner : agenda + calendar draft + post-meeting follow-up
- payment_offer : Moyasar payment-link draft (no charge)
- proof_pack : weekly evidence pack with revenue + risk metrics
- mission_planner : Growth Missions (10-in-10, recover-stalled, etc.)
DESIGN INVARIANTS
- draft-only by default; nothing is sent / charged / scheduled live
- every outbound has approval_required=True
- PDPL: no cold WhatsApp without lawful basis; uploads classified safely
- deterministic: same input same output (testable without external APIs)
"""
from auto_client_acquisition.growth_operator.client_profile import (
ClientGrowthProfile,
build_demo_profile,
profile_from_dict,
)
from auto_client_acquisition.growth_operator.contact_importer import (
classify_contact_source,
dedupe_contacts,
detect_opt_out,
normalize_phone,
summarize_import,
)
from auto_client_acquisition.growth_operator.contactability import (
CONTACTABILITY_LABELS,
contactability_summary,
score_contactability,
)
from auto_client_acquisition.growth_operator.message_planner import (
draft_arabic_message,
draft_followup,
draft_objection_response,
)
from auto_client_acquisition.growth_operator.meeting_planner import (
build_calendar_draft,
build_meeting_agenda,
build_post_meeting_followup,
)
from auto_client_acquisition.growth_operator.mission_planner import (
GROWTH_MISSIONS,
list_missions,
run_mission,
)
from auto_client_acquisition.growth_operator.partnership_planner import (
draft_partner_outreach,
partner_scorecard,
suggest_partner_types,
)
from auto_client_acquisition.growth_operator.payment_offer import (
build_moyasar_payment_link_draft,
sar_to_halalas,
)
from auto_client_acquisition.growth_operator.proof_pack import (
build_weekly_proof_pack,
)
from auto_client_acquisition.growth_operator.targeting import (
rank_targets,
recommend_top_10,
segment_contacts,
why_now_stub,
)
__all__ = [
# client_profile
"ClientGrowthProfile", "build_demo_profile", "profile_from_dict",
# contact_importer
"normalize_phone", "dedupe_contacts", "classify_contact_source",
"detect_opt_out", "summarize_import",
# contactability
"CONTACTABILITY_LABELS", "score_contactability", "contactability_summary",
# targeting
"segment_contacts", "rank_targets", "recommend_top_10", "why_now_stub",
# message_planner
"draft_arabic_message", "draft_followup", "draft_objection_response",
# partnership_planner
"suggest_partner_types", "draft_partner_outreach", "partner_scorecard",
# meeting_planner
"build_meeting_agenda", "build_calendar_draft", "build_post_meeting_followup",
# payment_offer
"build_moyasar_payment_link_draft", "sar_to_halalas",
# proof_pack
"build_weekly_proof_pack",
# mission_planner
"GROWTH_MISSIONS", "list_missions", "run_mission",
]

View File

@ -0,0 +1,116 @@
"""
Client Growth Profile the per-customer config that turns Dealix from a
generic operator into a specialized one.
Without this profile every agent works on a generic prompt; with it,
every draft, every Why-Now, and every recommendation is grounded in:
the customer's offer, ICP, sales cycle, channels, objection history,
approval rules, and compliance constraints.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass
class ClientGrowthProfile:
"""Per-customer growth context fed to every agent decision."""
customer_id: str
company_name: str
sector: str
city: str
offer_one_liner: str
ideal_customer: str
average_deal_size_sar: float = 0.0
current_channels: tuple[str, ...] = () # e.g. ("whatsapp", "email")
sales_cycle_days: int = 30
common_objections: tuple[str, ...] = ()
approval_rules: dict[str, Any] = field(default_factory=dict)
compliance_rules: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"customer_id": self.customer_id,
"company_name": self.company_name,
"sector": self.sector,
"city": self.city,
"offer_one_liner": self.offer_one_liner,
"ideal_customer": self.ideal_customer,
"average_deal_size_sar": self.average_deal_size_sar,
"current_channels": list(self.current_channels),
"sales_cycle_days": self.sales_cycle_days,
"common_objections": list(self.common_objections),
"approval_rules": self.approval_rules,
"compliance_rules": self.compliance_rules,
}
def is_specialized(self) -> bool:
"""A profile becomes 'specialized' once the minimum context is set."""
return all([
self.sector,
self.city,
self.offer_one_liner,
self.ideal_customer,
])
# Sane defaults reflecting Saudi B2B norms — used until customer overrides.
_DEFAULT_APPROVAL_RULES: dict[str, Any] = {
"require_human_for_first_send": True,
"require_human_for_high_value_deals_above_sar": 100_000,
"max_consecutive_followups": 3,
"quiet_hours_riyadh": [21, 8], # no outbound 9pm-8am Riyadh
"blocked_dates": [],
}
_DEFAULT_COMPLIANCE_RULES: dict[str, Any] = {
"no_cold_whatsapp_without_lawful_basis": True,
"require_unsubscribe_in_email": True,
"blocked_keywords": ["ضمان 100", "نتائج مضمونة", "رقم الهوية", "iban"],
"weekly_message_cap_per_contact": 2,
"min_cohort_for_benchmarks": 5,
}
def profile_from_dict(data: dict[str, Any]) -> ClientGrowthProfile:
"""Build a profile from a dict; missing optional fields fall back to defaults."""
return ClientGrowthProfile(
customer_id=str(data.get("customer_id") or ""),
company_name=str(data.get("company_name") or ""),
sector=str(data.get("sector") or "").lower().strip(),
city=str(data.get("city") or "").strip(),
offer_one_liner=str(data.get("offer_one_liner") or "").strip(),
ideal_customer=str(data.get("ideal_customer") or "").strip(),
average_deal_size_sar=float(data.get("average_deal_size_sar") or 0),
current_channels=tuple(data.get("current_channels") or ()),
sales_cycle_days=int(data.get("sales_cycle_days") or 30),
common_objections=tuple(data.get("common_objections") or ()),
approval_rules=data.get("approval_rules") or dict(_DEFAULT_APPROVAL_RULES),
compliance_rules=data.get("compliance_rules") or dict(_DEFAULT_COMPLIANCE_RULES),
)
def build_demo_profile(*, customer_id: str = "demo") -> ClientGrowthProfile:
"""Deterministic demo profile — used in /docs and test fixtures."""
return ClientGrowthProfile(
customer_id=customer_id,
company_name="Demo Saudi B2B Co.",
sector="real_estate",
city="الرياض",
offer_one_liner="منصة سعودية لتشغيل الإيرادات + اكتشاف فرص B2B",
ideal_customer="شركات تطوير عقاري متوسطة، 50-200 موظف، مهتمة بـ pre-sales pipeline",
average_deal_size_sar=85_000,
current_channels=("whatsapp", "email"),
sales_cycle_days=45,
common_objections=(
"السعر عالي",
"كلم الشريك",
"بعد العيد",
"وش يضمن النتائج؟",
"أرسل العرض واتساب",
),
approval_rules=dict(_DEFAULT_APPROVAL_RULES),
compliance_rules=dict(_DEFAULT_COMPLIANCE_RULES),
)

View File

@ -0,0 +1,162 @@
"""
Contact Importer safely intake uploaded customer phone/email lists.
Steps:
1. normalize_phone Saudi-friendly E.164 normalizer
2. dedupe_contacts drop exact phone duplicates (keep richest record)
3. classify_contact_source existing / lead / inbound / event / cold / unknown
4. detect_opt_out flags contacts marked as opted-out / blocked
5. summarize_import top-level report ready for the dashboard
"""
from __future__ import annotations
import re
from typing import Any
# ── Phone normalization ──────────────────────────────────────────
_DIGITS_RE = re.compile(r"\D+")
def normalize_phone(raw: str | None) -> str:
"""
Normalize Saudi phone numbers to E.164-like form starting with 966.
Accepts: +966500000001, 0500000001, 500000001, 00966500000001,
+966 (50) 000-0001, etc.
Returns: bare digits (e.g. "966500000001") or "" if invalid.
"""
if not raw:
return ""
s = _DIGITS_RE.sub("", str(raw))
if not s:
return ""
# Strip leading 00 (international prefix)
if s.startswith("00"):
s = s[2:]
# Already starts with 966
if s.startswith("966") and len(s) == 12:
return s
# Local 0-prefixed (e.g. 0512345678)
if s.startswith("0") and len(s) == 10:
return "966" + s[1:]
# Bare 9-digit local mobile (e.g. 512345678)
if len(s) == 9 and s.startswith("5"):
return "966" + s
# Already bare with country code but no leading +
if len(s) == 12 and s.startswith("966"):
return s
return s if 10 <= len(s) <= 15 else ""
# ── Dedup ────────────────────────────────────────────────────────
def dedupe_contacts(contacts: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
Drop exact phone duplicates. When two records share a phone, keep the one
with more non-empty fields (richer record).
"""
seen: dict[str, dict[str, Any]] = {}
for c in contacts:
phone = normalize_phone(c.get("phone", ""))
if not phone:
# Records with no phone are kept as-is, keyed by name+email
key = f"name:{c.get('name','').strip().lower()}|email:{c.get('email','').strip().lower()}"
if key not in seen:
seen[key] = c
continue
c_norm = dict(c)
c_norm["phone"] = phone
existing = seen.get(phone)
if existing is None:
seen[phone] = c_norm
else:
existing_filled = sum(1 for v in existing.values() if v)
new_filled = sum(1 for v in c_norm.values() if v)
if new_filled > existing_filled:
seen[phone] = c_norm
return list(seen.values())
# ── Source classification ────────────────────────────────────────
SOURCE_LABELS: tuple[str, ...] = (
"existing_customer",
"old_lead",
"inbound_lead",
"event_lead",
"cold_list",
"referral",
"unknown",
)
def classify_contact_source(contact: dict[str, Any]) -> str:
"""Classify a contact's source. Conservative: unknown by default."""
src = str(contact.get("source", "")).lower().strip()
rel = str(contact.get("relationship_status", "")).lower().strip()
last = contact.get("last_contacted_at")
if rel in ("existing", "customer", "client", "active") or src in (
"existing_customer", "customer", "active_customer",
):
return "existing_customer"
if rel in ("inbound", "form_submission") or src in (
"inbound", "website_form", "form_submission",
):
return "inbound_lead"
if src in ("event", "exhibition", "conference", "trade_show"):
return "event_lead"
if src in ("referral", "introduction"):
return "referral"
if rel in ("lead", "prospect") or last:
return "old_lead"
if src in ("cold", "scraped", "purchased_list"):
return "cold_list"
return "unknown"
# ── Opt-out detection ────────────────────────────────────────────
_OPT_OUT_TOKENS = {
"opt_out", "opted_out", "unsubscribed", "blocked", "do_not_contact",
"stop", "remove", "إلغاء", "اشتراك", "ايقاف", "إيقاف",
}
def detect_opt_out(contact: dict[str, Any]) -> bool:
"""Return True if the record is flagged as opted-out / blocked."""
flag = str(contact.get("opt_in_status", "")).lower().strip()
if flag in _OPT_OUT_TOKENS:
return True
if str(contact.get("status", "")).lower() in _OPT_OUT_TOKENS:
return True
notes = str(contact.get("notes", "")).lower()
if any(tok in notes for tok in _OPT_OUT_TOKENS):
return True
return False
# ── Summary ──────────────────────────────────────────────────────
def summarize_import(contacts: list[dict[str, Any]]) -> dict[str, Any]:
"""Top-level report for the upload dashboard. Pure function."""
total = len(contacts)
deduped = dedupe_contacts(contacts)
by_source: dict[str, int] = {label: 0 for label in SOURCE_LABELS}
opt_out_count = 0
invalid_phone = 0
for c in deduped:
if detect_opt_out(c):
opt_out_count += 1
if not c.get("phone") or len(str(c.get("phone"))) < 9:
invalid_phone += 1
src = classify_contact_source(c)
by_source[src] = by_source.get(src, 0) + 1
return {
"raw_total": total,
"after_dedupe": len(deduped),
"duplicates_removed": total - len(deduped),
"invalid_phone": invalid_phone,
"opt_out_count": opt_out_count,
"by_source": by_source,
"ready_to_review": len(deduped) - opt_out_count - invalid_phone,
}

View File

@ -0,0 +1,186 @@
"""
Contactability per-contact "can we contact?" decision with PDPL reasons.
Default policy: **no cold WhatsApp** without lawful basis.
PDPL Art.5 emphasizes lawful basis, consent, and purpose limitation.
"""
from __future__ import annotations
from typing import Any
from auto_client_acquisition.growth_operator.contact_importer import (
classify_contact_source,
detect_opt_out,
normalize_phone,
)
# ── Decision labels ──────────────────────────────────────────────
CONTACTABILITY_LABELS: tuple[str, ...] = (
"safe", # consent + lawful basis verified
"needs_review", # source unclear; pending operator confirmation
"blocked", # opt-out / banned / invalid / breaches policy
)
def score_contactability(
contact: dict[str, Any],
*,
channel: str = "whatsapp",
require_consent_for_cold_whatsapp: bool = True,
) -> dict[str, Any]:
"""
Decide whether this contact can be approached on this channel today.
Returns:
{
"label": "safe"|"needs_review"|"blocked",
"channel": "...",
"reasons": [...], # human-readable Arabic reasons
"next_action": "...", # what the operator should do
}
"""
reasons: list[str] = []
label: str = "safe"
# 1) Opt-out / banned wins everything
if detect_opt_out(contact):
return {
"label": "blocked",
"channel": channel,
"reasons": ["العميل سجل opt-out أو محظور — لا تواصل بأي شكل."],
"next_action": "remove_from_lists",
}
# 2) Phone validity
phone = normalize_phone(contact.get("phone"))
if channel == "whatsapp" and not phone:
return {
"label": "blocked",
"channel": channel,
"reasons": ["لا يوجد رقم صالح — WhatsApp مستحيل."],
"next_action": "remove_or_collect_phone",
}
# 3) Source classification
src = classify_contact_source(contact)
# Cold WhatsApp without consent → blocked
if channel == "whatsapp" and require_consent_for_cold_whatsapp:
if src == "cold_list":
return {
"label": "blocked",
"channel": channel,
"reasons": [
"WhatsApp البارد ممنوع بدون lawful basis (PDPL م.5).",
"السياسة: لا cold WhatsApp افتراضياً.",
],
"next_action": "switch_to_email_or_get_consent",
}
if src == "unknown":
return {
"label": "needs_review",
"channel": channel,
"reasons": [
"مصدر الرقم غير محدد — يحتاج توثيق lawful basis.",
"ارجع للمشغّل لإقرار العلاقة قبل الإرسال.",
],
"next_action": "operator_confirms_source",
}
# 4) Healthy paths
if src in ("existing_customer", "inbound_lead", "referral"):
return {
"label": "safe",
"channel": channel,
"reasons": [
f"علاقة قائمة ({src}) — أساس قانوني قائم لـ business contact.",
],
"next_action": "draft_message_with_approval",
}
if src == "old_lead":
last = contact.get("last_contacted_at")
if last:
reasons.append("lead سابق — تواصل ضمن نافذة شهور قابلة للتبرير.")
label = "safe"
else:
reasons.append("lead سابق بدون تاريخ تواصل — يحتاج warm-up قصير.")
label = "needs_review"
return {
"label": label,
"channel": channel,
"reasons": reasons,
"next_action": (
"draft_short_followup_with_approval" if label == "safe"
else "operator_confirms_continuity"
),
}
if src == "event_lead":
return {
"label": "safe",
"channel": channel,
"reasons": ["lead من فعالية مع موافقة ضمنية على المتابعة بـ 30 يوم."],
"next_action": "draft_event_followup_with_approval",
}
# 5) Email channel — more permissive (List-Unsubscribe header makes it safer)
if channel == "email":
if src == "unknown":
return {
"label": "needs_review",
"channel": "email",
"reasons": ["مصدر غير محدد — أرسل عبر إيميل مع List-Unsubscribe إن قبلت."],
"next_action": "operator_confirms_source",
}
return {
"label": "safe",
"channel": "email",
"reasons": [f"مصدر مقبول للإيميل B2B ({src})."],
"next_action": "draft_email_with_approval",
}
# Fallback (defensive)
return {
"label": "needs_review",
"channel": channel,
"reasons": ["لا تطابق سياسة معروفة — يحتاج مراجعة المشغّل."],
"next_action": "operator_review_required",
}
def contactability_summary(
contacts: list[dict[str, Any]],
*,
channel: str = "whatsapp",
) -> dict[str, Any]:
"""Bulk classification report for the upload dashboard."""
counts: dict[str, int] = {label: 0 for label in CONTACTABILITY_LABELS}
next_actions: dict[str, int] = {}
sample_blocked: list[dict[str, Any]] = []
sample_review: list[dict[str, Any]] = []
sample_safe: list[dict[str, Any]] = []
for c in contacts:
decision = score_contactability(c, channel=channel)
counts[decision["label"]] += 1
next_actions[decision["next_action"]] = next_actions.get(decision["next_action"], 0) + 1
if decision["label"] == "blocked" and len(sample_blocked) < 5:
sample_blocked.append({**c, **decision})
elif decision["label"] == "needs_review" and len(sample_review) < 5:
sample_review.append({**c, **decision})
elif decision["label"] == "safe" and len(sample_safe) < 5:
sample_safe.append({**c, **decision})
return {
"channel": channel,
"total": len(contacts),
"by_label": counts,
"by_next_action": next_actions,
"sample_safe": sample_safe,
"sample_review": sample_review,
"sample_blocked": sample_blocked,
"policy_note": (
"لا cold WhatsApp بدون lawful basis — السياسة الافتراضية. "
"العميل يقدر يعدل القاعدة لكل قائمة بعد توثيق المصدر."
),
}

View File

@ -0,0 +1,149 @@
"""
Meeting Operator agenda + calendar draft + post-meeting follow-up.
Pure drafting only. No live Google Calendar event creation here
the actual `events.insert` happens elsewhere (and only after explicit
user authorization via OAuth).
"""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Any
def build_meeting_agenda(
*,
contact_name: str,
company: str,
purpose_ar: str = "اكتشاف وتأهيل أولي",
duration_minutes: int = 20,
) -> dict[str, Any]:
"""Generate a deterministic Saudi-friendly agenda."""
if duration_minutes <= 15:
slots_ar = [
"تعارف سريع (٢ دقائق)",
"فهم وضع الشركة الحالي (٥ دقائق)",
"عرض موجز لـ Dealix (٥ دقائق)",
"تحديد الخطوة التالية (٣ دقائق)",
]
elif duration_minutes <= 30:
slots_ar = [
"تعارف وأهداف الاجتماع (٣ دقائق)",
f"الوضع الحالي لدى {company} (٧ دقائق)",
"كيف يدعم Dealix هدفكم (١٠ دقائق)",
"أسئلة مفتوحة (٥ دقائق)",
"الخطوات التالية + توقيت المتابعة (٥ دقائق)",
]
else:
slots_ar = [
"تعارف وأهداف الاجتماع (٥ دقائق)",
f"التشخيص العميق لـ {company} (١٥ دقيقة)",
"عرض demo حي مع سيناريو فعلي (١٥ دقيقة)",
"ROI breakdown (٥ دقائق)",
"أسئلة + تحديات تنفيذية (١٠ دقائق)",
"الخطة المقترحة + الموافقات المطلوبة (١٠ دقائق)",
]
return {
"title_ar": f"اجتماع Dealix × {company}",
"purpose_ar": purpose_ar,
"duration_minutes": duration_minutes,
"agenda_ar": slots_ar,
"attendees_suggested_ar": [contact_name, "مؤسس / مدير مبيعات Dealix"],
"approval_required": True,
"approval_status": "pending_approval",
}
def build_calendar_draft(
*,
contact_email: str | None,
contact_name: str,
company: str,
proposed_start_iso: str | None = None,
duration_minutes: int = 20,
) -> dict[str, Any]:
"""
Build a Google-Calendar-shaped draft (NOT inserted live).
Suggests the next business hour slot if no start is provided.
Real `events.insert` happens only after the operator approves AND
has authorized Calendar OAuth.
"""
if proposed_start_iso:
try:
start_dt = datetime.fromisoformat(proposed_start_iso.replace("Z", "+00:00")).replace(tzinfo=None)
except ValueError:
start_dt = _next_business_hour()
else:
start_dt = _next_business_hour()
end_dt = start_dt + timedelta(minutes=duration_minutes)
summary_ar = f"اجتماع Dealix × {company}"
description_ar = (
f"اجتماع مع {contact_name} من {company} لاستكشاف فرصة استخدام "
f"Dealix لتشغيل النمو. مدة الاجتماع: {duration_minutes} دقيقة."
)
return {
"summary": summary_ar,
"description": description_ar,
"start": {
"dateTime": start_dt.isoformat(),
"timeZone": "Asia/Riyadh",
},
"end": {
"dateTime": end_dt.isoformat(),
"timeZone": "Asia/Riyadh",
},
"attendees": [
{"email": contact_email} for contact_email in [contact_email] if contact_email
],
"conference_data_request": {
"createRequest": {
"requestId": f"dealix-meet-{int(start_dt.timestamp())}",
"conferenceSolutionKey": {"type": "hangoutsMeet"},
}
},
"live_inserted": False,
"approval_required": True,
"approval_status": "pending_approval",
"compliance_note_ar": (
"draft فقط — لا يُنشأ event حي في Google Calendar حتى موافقة "
"OAuth صريحة + ضغطة المستخدم 'أنشئ الاجتماع'."
),
}
def build_post_meeting_followup(
*,
contact_name: str,
company: str,
summary_ar: str,
next_step_ar: str = "أرسل recap + pilot offer",
) -> dict[str, Any]:
"""Generate the post-meeting follow-up draft."""
body_ar = (
f"شكراً أستاذ {contact_name} على وقتكم الصباحي.\n\n"
f"خلاصة الاجتماع:\n{summary_ar}\n\n"
f"الخطوة التالية: {next_step_ar}\n\n"
f"نسعد بمتابعة الموضوع متى ناسبكم."
)
return {
"channel_recommendation": "email",
"subject_ar": f"شكراً {contact_name} — متابعة اجتماع {company}",
"body_ar": body_ar,
"approval_required": True,
"approval_status": "pending_approval",
}
# ── Internal helpers ────────────────────────────────────────────
def _next_business_hour(*, now: datetime | None = None) -> datetime:
"""Next 09:00-17:00 Riyadh slot (demo helper; not timezone-perfect)."""
n = now or datetime.now(timezone.utc).replace(tzinfo=None)
# Push to next day 10am UTC ~ 1pm Riyadh — safe demo slot
candidate = (n + timedelta(days=1)).replace(hour=10, minute=0, second=0, microsecond=0)
# Skip Friday (Saudi weekend = Fri-Sat)
while candidate.weekday() in (4, 5):
candidate += timedelta(days=1)
return candidate

View File

@ -0,0 +1,265 @@
"""
Saudi Message Engine Arabic drafts that don't sound like spam.
Style rules (encoded in templates):
- short (4 sentences for first message)
- non-exaggerated (no "ضمان 100%", no "نتائج مضمونة")
- explicit reason for outreach (not generic)
- simple ask (one CTA, low-commitment)
- sector-aware tone
- approval_required = True ALWAYS
"""
from __future__ import annotations
import hashlib
from typing import Any
# ── Saudi B2B opening line bank — sector-aware ──────────────────
_OPENERS_BY_SECTOR_AR: dict[str, list[str]] = {
"real_estate": [
"السلام عليكم أستاذ {name}،\nلاحظت أنكم تتوسعون في {city}.",
"مرحباً أستاذ {name}،\nمتابع نشاطكم في تطوير العقار في {city}.",
],
"clinics": [
"السلام عليكم دكتور {name}،\nشاهدت تطور خدمات العيادة في {city}.",
"مرحباً دكتور {name}،\nأقدر اهتمامكم بتجربة المرضى في {city}.",
],
"logistics": [
"السلام عليكم أستاذ {name}،\nلاحظت توسعكم في خدمات الشحن في {city}.",
"مرحباً أستاذ {name}،\nقطاع اللوجستيات في {city} يتحرك بسرعة.",
],
"training": [
"السلام عليكم أستاذ {name}،\nمتابع أثر برامجكم التدريبية في {city}.",
"مرحباً أستاذ {name}،\nالطلب على التدريب الـ B2B يتزايد في {city}.",
],
"default": [
"السلام عليكم أستاذ {name}،\nمتابع نشاطكم في {city}.",
"مرحباً أستاذ {name}،\nلاحظت تطوركم في {city}.",
],
}
# A single short reason + ask combo. Keep under 4 sentences total.
_REASON_TEMPLATES_AR: dict[str, str] = {
"existing_customer": "باعتبار العلاقة القائمة معكم، عندي اقتراح سريع يخدم {goal}.",
"inbound_lead": "بناءً على اهتمامكم الأخير، عندي خطوة واضحة لتسريع {goal}.",
"referral": "وصلتني توصية مهنية للتواصل معكم بخصوص {goal}.",
"event_lead": "بعد لقائنا الأخير، حضّرت اقتراح صغير يخدم {goal}.",
"old_lead": "بمناسبة الموسم الجديد، عندي تحديث يهم {goal}.",
"unknown": "بعد البحث في خدماتكم، عندي فرضية صغيرة تخدم {goal}.",
"cold_list": "بعد البحث في خدماتكم، عندي فرضية صغيرة تخدم {goal}.",
}
_ASK_TEMPLATES_AR: list[str] = [
"يناسبك أرسل لك مثال سريع؟",
"هل ١٥ دقيقة الأسبوع الجاي مناسبة لمشاركة الفكرة؟",
"تفضّل أرسل ملخص بصفحة واحدة أو نتفق على مكالمة قصيرة؟",
]
def _pick(seq: list[str], seed: str) -> str:
"""Deterministic choice — same seed → same pick."""
if not seq:
return ""
h = hashlib.md5(seed.encode("utf-8")).digest()
return seq[h[0] % len(seq)]
def _resolve_name(contact: dict[str, Any]) -> str:
n = (contact.get("name") or "").strip()
if not n:
return "الفاضل"
parts = n.split()
return parts[0] if parts else n
def _resolve_city(contact: dict[str, Any], default: str = "السعودية") -> str:
return (contact.get("city") or default).strip()
def _resolve_sector(contact: dict[str, Any], default: str = "default") -> str:
s = (contact.get("sector") or default).lower().strip()
return s if s in _OPENERS_BY_SECTOR_AR else "default"
# ── Public API ──────────────────────────────────────────────────
def draft_arabic_message(
contact: dict[str, Any],
*,
profile: dict[str, Any] | None = None,
source: str | None = None,
goal_ar: str = "تشغيل نمو B2B بلا إرسال عشوائي",
) -> dict[str, Any]:
"""
Build a Saudi-tone Arabic outreach draft.
- profile: optional ClientGrowthProfile.to_dict() for offer context
- source: classify_contact_source override; auto-derived if None
"""
from auto_client_acquisition.growth_operator.contact_importer import (
classify_contact_source,
)
src = source or classify_contact_source(contact)
name = _resolve_name(contact)
city = _resolve_city(contact)
sector = _resolve_sector(contact)
seed = f"{contact.get('phone','')}{contact.get('name','')}{src}"
opener = _pick(_OPENERS_BY_SECTOR_AR[sector], seed).format(name=name, city=city)
reason = _REASON_TEMPLATES_AR.get(src, _REASON_TEMPLATES_AR["unknown"]).format(goal=goal_ar)
ask = _pick(_ASK_TEMPLATES_AR, seed + "ask")
offer_line = ""
if profile and profile.get("offer_one_liner"):
offer_line = f"\n\nنحن: {profile['offer_one_liner']}."
body_ar = f"{opener}\n\n{reason}{offer_line}\n\n{ask}"
return {
"channel_recommendation": "whatsapp" if contact.get("phone") else "email",
"subject_ar": None,
"body_ar": body_ar,
"source_classification": src,
"approval_required": True,
"approval_status": "pending_approval",
"guardrails_ar": [
"لا تُرسل قبل موافقة المشغّل.",
"لا تستخدم في WhatsApp البارد بدون lawful basis.",
"احذف أي مبالغة قبل الإرسال.",
],
"estimated_length_chars": len(body_ar),
}
def draft_followup(
contact: dict[str, Any],
*,
days_since_last: int,
last_outcome: str = "no_reply",
) -> dict[str, Any]:
"""Short follow-up draft based on last outcome."""
name = _resolve_name(contact)
seed = f"f{contact.get('phone','')}{last_outcome}{days_since_last}"
if last_outcome == "no_reply" and days_since_last <= 3:
body = (
f"السلام عليكم أستاذ {name}،\n\n"
"أعرف أن جدولكم مزدحم. لو الفكرة لا تناسب الآن، أقدر أرسل ملخص "
"بصفحة واحدة تراجعونه على راحتكم. هل أرسل؟"
)
elif last_outcome == "no_reply":
body = (
f"السلام عليكم أستاذ {name}،\n\n"
f"مر {days_since_last} يوم على رسالتي السابقة. لو لا يناسب الآن، "
"أقدر أعود في التوقيت الأنسب لكم — متى يناسب؟"
)
elif last_outcome == "objection":
body = (
f"شكراً أستاذ {name} على وضوحكم. "
"بناءً على ما ذكرتم، حضّرت توضيح مختصر يجاوب على نقطتكم تحديداً. "
"هل أرسل؟"
)
elif last_outcome == "positive":
body = (
f"شكراً أستاذ {name}. "
"أحجز ١٥ دقيقة هذا الأسبوع لمناقشة الخطوة التالية — متى يناسبك؟"
)
else:
body = (
f"السلام عليكم أستاذ {name}،\n\n"
f"تابعت معكم سابقاً. لو فيه تحديث، يسعدني أعرف."
)
return {
"body_ar": body,
"purpose": f"followup_{last_outcome}_d{days_since_last}",
"approval_required": True,
"approval_status": "pending_approval",
}
# ── Objection-to-Action library ─────────────────────────────────
_OBJECTION_RESPONSES_AR: dict[str, dict[str, Any]] = {
"send_offer_whatsapp": {
"interpretation_ar": "اهتمام متوسط — ليس إغلاق، لكن مفتوح للمعلومات.",
"response_ar": (
"تمام، أرسل خلال دقائق ملف صفحتين بالعربي + voice note قصير "
"يشرح أهم ٣ نقاط. ثم نتفق على متابعة بعد يومين."
),
"next_action": "send_pdf_then_followup_in_2d",
"score_delta": +5,
},
"after_eid": {
"interpretation_ar": "تأجيل ثقافي مفهوم — احترم التوقيت السعودي.",
"response_ar": (
"إن شاء الله. أسجل تذكير لـ بعد العيد بأسبوع، وأرسل لكم Pulse "
"الشهري حتى ذلك الحين. كل عام وأنتم بخير."
),
"next_action": "schedule_post_eid_followup",
"score_delta": +1,
},
"talk_to_partner": {
"interpretation_ar": "stakeholder جديد — يحتاج intro + ملف موجز.",
"response_ar": (
"محترم — أحضّر لكم ملف من صفحتين بالعربي مهيأ للعرض على الشريك. "
"هل أرسله مباشرة لكم أو نعمل اجتماع ثلاثي قصير؟"
),
"next_action": "arm_champion_with_2page_brief",
"score_delta": +3,
},
"price_high": {
"interpretation_ar": "اعتراض قيمة — يحتاج ROI breakdown، ليس خصم.",
"response_ar": (
"حقكم تركّزون على القيمة. أرسل ROI breakdown يوضح تكلفة الـ lead "
"المؤهل لدينا مقارنة بالبدائل. توافقون؟"
),
"next_action": "send_roi_breakdown",
"score_delta": +5,
},
"have_vendor": {
"interpretation_ar": "منافس قائم — اسأل عن الفجوة الفعلية.",
"response_ar": (
"ممتاز — مع مَن؟ والسؤال المهم: هل الـ leads مؤهلة فعلاً أم form fills؟ "
"إن فيه فجوة، نقدر نكمل وليس نستبدل. مجاناً نعمل audit."
),
"next_action": "offer_free_audit_position_as_complement",
"score_delta": +2,
},
"no_need": {
"interpretation_ar": "رفض/توقيت — الأنسب nurture بدون ضغط.",
"response_ar": (
"متفهم تماماً. نسجلكم في Pulse الشهري المجاني، ونعود حين تتغير "
"الأولويات. شاكرين وقتكم."
),
"next_action": "nurture_via_monthly_pulse",
"score_delta": -2,
},
}
def draft_objection_response(
objection_id: str,
*,
contact: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Look up an objection and return a Saudi-toned response draft."""
obj = _OBJECTION_RESPONSES_AR.get(objection_id)
if obj is None:
return {
"objection_id": objection_id,
"interpretation_ar": "اعتراض غير مصنّف — يحتاج تشخيص يدوي.",
"response_ar": (
"شكراً على وضوحكم. ممكن تشاركوني السبب الرئيسي حتى أعطيكم "
"إجابة مناسبة؟"
),
"next_action": "diagnostic_question",
"score_delta": 0,
"approval_required": True,
"approval_status": "pending_approval",
}
return {
"objection_id": objection_id,
**obj,
"approval_required": True,
"approval_status": "pending_approval",
}

View File

@ -0,0 +1,154 @@
"""
Growth Missions outcome-shaped tasks instead of features.
Each mission has: id, title_ar, goal_ar, steps (ordered, approval-gated),
expected_duration_days, kill_metric (the ONE number that proves success).
Pure deterministic. Production wires each step to the relevant agent.
"""
from __future__ import annotations
from typing import Any
GROWTH_MISSIONS: tuple[dict[str, Any], ...] = (
{
"id": "first_10_opportunities",
"title_ar": "اطلع لي 10 فرص",
"goal_ar": "اكتشاف 10 شركات سعودية مناسبة + رسائل عربية + موافقة + متابعة أسبوع.",
"expected_duration_days": 7,
"kill_metric": "ten_drafts_approved",
"steps_ar": [
"تحديد القطاع والمدينة + المعايير الأساسية.",
"اكتشاف 30 شركة مرشحة من المصادر المسموحة.",
"فلترة لـ 10 بأعلى Why-Now score.",
"كتابة 10 رسائل عربية بحالة pending_approval.",
"موافقة المشغّل على عينة → إرسال آمن.",
"تصنيف الردود + اقتراح متابعة لكل واحدة.",
"Proof Pack أسبوعي عند الإغلاق.",
],
"primary_endpoint": "/api/v1/innovation/opportunities/ten-in-ten",
"approval_required": True,
},
{
"id": "recover_stalled_deals",
"title_ar": "أنقذ الصفقات المتوقفة",
"goal_ar": "اكتشف الصفقات بدون نشاط 14+ يوم + اقترح متابعات multi-thread.",
"expected_duration_days": 5,
"kill_metric": "stalled_deals_revived",
"steps_ar": [
"قراءة pipeline الحالي + revenue_graph.leak_detector.",
"تصنيف الصفقات: stalled / single-threaded / no-proposal.",
"اقتراح multi-thread (DM إضافي داخل الحساب).",
"كتابة drafts متابعة pending_approval لكل صفقة.",
"موافقة المشغّل → إرسال + جدولة re-check بعد 7 أيام.",
],
"primary_endpoint": "/api/v1/revenue-os/leaks",
"approval_required": True,
},
{
"id": "partnership_sprint",
"title_ar": "ابدأ شراكات",
"goal_ar": "تحديد + التواصل مع 5 شركاء محتملين خلال أسبوعين.",
"expected_duration_days": 14,
"kill_metric": "partner_intros_replied",
"steps_ar": [
"تحديد قطاع العميل + حجمه → اقتراح أنواع شركاء مناسبة.",
"ترشيح 5 شركاء محتملين بأعلى strategic_value.",
"كتابة outreach warm لكل واحد.",
"موافقة المشغّل → إرسال على email.",
"متابعة الردود + جدولة 20 دقيقة لكل ردّ إيجابي.",
"Partner scorecard أولي بعد المكالمات.",
],
"primary_endpoint": "/api/v1/growth-operator/partners/suggest",
"approval_required": True,
},
{
"id": "safe_whatsapp_campaign",
"title_ar": "جهز حملة واتساب آمنة",
"goal_ar": "تحويل قائمة العميل إلى حملة WhatsApp يحترم PDPL + opt-in.",
"expected_duration_days": 3,
"kill_metric": "safe_messages_drafted",
"steps_ar": [
"رفع قائمة الأرقام عبر contact_importer.",
"تنظيف + dedupe + classify_source.",
"فحص contactability — إخراج blocked/needs_review.",
"كتابة رسائل عربية لكل segment آمن.",
"موافقة المشغّل لكل segment على حدة.",
"إرسال آمن مع opt-out في كل رسالة.",
],
"primary_endpoint": "/api/v1/growth-operator/contacts/import-preview",
"approval_required": True,
},
{
"id": "meeting_booking_sprint",
"title_ar": "احجز لي 3 اجتماعات",
"goal_ar": "حجز 3 اجتماعات مع leads أعلى Why-Now خلال 5 أيام عمل.",
"expected_duration_days": 5,
"kill_metric": "meetings_confirmed",
"steps_ar": [
"اختيار أعلى 10 leads من Top-10 السابق.",
"إعداد agenda + calendar draft لكل واحد.",
"كتابة intro + ask مكالمة 15 دقيقة.",
"موافقة + إرسال WhatsApp/email.",
"تأكيد الحضور قبل الاجتماع بـ 24 ساعة.",
"post-meeting follow-up draft.",
],
"primary_endpoint": "/api/v1/growth-operator/meetings/draft",
"approval_required": True,
},
{
"id": "list_cleanup",
"title_ar": "ارفع قائمتي ونظفها",
"goal_ar": "تحويل ملف غير منظم إلى قائمة contactability-classified جاهزة.",
"expected_duration_days": 1,
"kill_metric": "safe_contacts_extracted",
"steps_ar": [
"رفع CSV/Excel.",
"normalize_phone + dedupe.",
"classify_contact_source لكل سطر.",
"score_contactability لكل سطر.",
"تقرير: safe / needs_review / blocked + عدد + عينة.",
"اقتراح: ابدأ بالـ safe فقط، مع plan لـ needs_review.",
],
"primary_endpoint": "/api/v1/growth-operator/contacts/import-preview",
"approval_required": True,
},
)
def list_missions() -> dict[str, Any]:
"""Return the canonical mission catalog."""
return {
"count": len(GROWTH_MISSIONS),
"missions": list(GROWTH_MISSIONS),
"kill_feature_id": "first_10_opportunities",
}
def run_mission(mission_id: str, *, payload: dict[str, Any] | None = None) -> dict[str, Any]:
"""
Plan a mission run returns the execution outline + first-step
prompt for the operator. Does NOT actually execute steps; that's
done by routing each step to its primary endpoint.
"""
mission = next((m for m in GROWTH_MISSIONS if m["id"] == mission_id), None)
if mission is None:
return {
"error": f"unknown_mission: {mission_id}",
"available_ids": [m["id"] for m in GROWTH_MISSIONS],
}
return {
"mission_id": mission_id,
"title_ar": mission["title_ar"],
"goal_ar": mission["goal_ar"],
"kill_metric": mission["kill_metric"],
"expected_duration_days": mission["expected_duration_days"],
"current_step_index": 0,
"next_step_ar": mission["steps_ar"][0],
"primary_endpoint": mission["primary_endpoint"],
"payload_received": payload or {},
"approval_required": True,
"approval_status": "pending_approval",
}

View File

@ -0,0 +1,163 @@
"""
Partnership Operator propose partner types + outreach drafts + scorecard.
Keep deterministic; partner suggestions come from a curated catalog
tuned for Saudi B2B (agencies, consultants, integrators, CRM vendors,
founder communities, sector influencers).
"""
from __future__ import annotations
import hashlib
from typing import Any
PARTNER_TYPES: tuple[dict[str, Any], ...] = (
{
"key": "marketing_agency",
"label_ar": "وكالة تسويق B2B",
"rationale_ar": "لديها عملاء يحتاجون lead-gen — Dealix يكمل خدماتها (لا يستبدلها).",
"model_ar": "Reseller / Revenue share 20-30%",
"ideal_size": "10-50 موظف",
},
{
"key": "sales_consultant",
"label_ar": "مستشار مبيعات / مدرب",
"rationale_ar": "يحتاج أداة عملية تثبت توصياته للعملاء.",
"model_ar": "Affiliate fixed fee + ongoing commission",
"ideal_size": "1-5 موظف",
},
{
"key": "tech_integrator",
"label_ar": "تكامل تقني / شريك Supabase أو Make.com",
"rationale_ar": "ينفّذ التكاملات للعملاء الكبار.",
"model_ar": "Implementation revenue share",
"ideal_size": "5-20 موظف",
},
{
"key": "crm_vendor",
"label_ar": "مزود CRM (Zoho/Salla/Odoo سعودي)",
"rationale_ar": "Dealix طبقة نمو فوق الـ CRM، لا منافس مباشر.",
"model_ar": "Co-sell + technical alliance",
"ideal_size": "30+ موظف",
},
{
"key": "founder_community",
"label_ar": "مجتمع مؤسسين سعوديين",
"rationale_ar": "الوصول لـ early adopters السعوديين عبر referrals.",
"model_ar": "Community partnership + free seats",
"ideal_size": "50+ عضو",
},
{
"key": "sector_influencer",
"label_ar": "خبير قطاعي (عقار / صحة / لوجستيات)",
"rationale_ar": "ثقة جاهزة في القطاع تختصر دورة البيع.",
"model_ar": "Equity / advisory + revenue referral",
"ideal_size": "1-3 موظف",
},
)
def suggest_partner_types(
*,
sector: str = "",
customer_size: str = "smb",
) -> dict[str, Any]:
"""Recommend ranked partner types for the given customer profile."""
suggestions = []
for p in PARTNER_TYPES:
priority = 50
if customer_size == "smb" and p["key"] in ("marketing_agency", "sales_consultant", "founder_community"):
priority += 25
if customer_size == "enterprise" and p["key"] in ("crm_vendor", "tech_integrator"):
priority += 25
if sector and sector.lower() in ("real_estate", "clinics", "logistics"):
if p["key"] == "sector_influencer":
priority += 20
suggestions.append({**p, "priority": priority})
suggestions.sort(key=lambda x: x["priority"], reverse=True)
return {
"sector": sector,
"customer_size": customer_size,
"suggestions": suggestions[:5],
"next_action": "draft_outreach_for_top_3",
}
def draft_partner_outreach(
*,
partner_type_key: str,
partner_name: str = "",
customer_name: str = "Dealix",
) -> dict[str, Any]:
"""Generate a warm partnership outreach draft."""
pt = next((p for p in PARTNER_TYPES if p["key"] == partner_type_key), None)
if pt is None:
return {
"error": "unknown_partner_type",
"approval_required": True,
"approval_status": "pending_approval",
}
target = partner_name or pt["label_ar"]
seed = hashlib.md5(f"{partner_type_key}{partner_name}".encode()).digest()
angle_idx = seed[0] % 2
angles_ar = [
"تكامل خدماتنا يخدم نفس عملائكم بأقل احتكاك.",
"نموذج revenue share واضح + pilot على عميل واحد قبل الالتزام.",
]
body_ar = (
f"السلام عليكم،\n\n"
f"أنا من فريق {customer_name}. تابعنا عملكم ووجدناه قريب جداً من جمهورنا.\n\n"
f"الفكرة باختصار: {angles_ar[angle_idx]}\n\n"
f"هل ١٥-٢٠ دقيقة الأسبوع الجاي مناسبة لاستكشاف فرصة شراكة؟"
)
return {
"partner_type": pt,
"channel_recommendation": "email",
"body_ar": body_ar,
"approval_required": True,
"approval_status": "pending_approval",
"suggested_next_steps": [
"1. رسالة warm",
"2. مكالمة 20 دقيقة",
"3. عرض partner revenue share",
"4. pilot على عميل واحد",
],
}
def partner_scorecard(
*,
partner_id: str,
intros_made: int = 0,
deals_influenced: int = 0,
revenue_share_paid_sar: float = 0.0,
relationship_age_months: int = 0,
) -> dict[str, Any]:
"""Compute a simple partner-health scorecard."""
activity_score = min(100, intros_made * 8 + deals_influenced * 15)
longevity_bonus = min(20, relationship_age_months * 2)
overall = min(100, activity_score + longevity_bonus)
if overall >= 75:
tier = "platinum"
elif overall >= 50:
tier = "gold"
elif overall >= 25:
tier = "silver"
else:
tier = "bronze"
return {
"partner_id": partner_id,
"overall_score": overall,
"tier": tier,
"intros_made": intros_made,
"deals_influenced": deals_influenced,
"revenue_share_paid_sar": round(revenue_share_paid_sar, 2),
"relationship_age_months": relationship_age_months,
"next_action_ar": (
"احتفظ بالعلاقة بنشاط ثابت — ربع سنوي." if tier in ("platinum", "gold")
else "حفّز التفاعل — اقتراح pilot جديد أو إحالة محتملة."
),
}

View File

@ -0,0 +1,97 @@
"""
Payment-in-Chat Moyasar payment-link drafts (NO live charge).
In production: link goes to a Moyasar hosted checkout. The user enters
their card on Moyasar's domain (PCI-safe), not inside WhatsApp.
This module produces a STRUCTURED draft only the actual
`POST /v1/payments` call to Moyasar happens elsewhere with the
customer's secret key.
"""
from __future__ import annotations
import uuid
from typing import Any
# ── Pricing (mirrors landing/pricing.html + business/pricing_strategy.py) ──
PLAN_CATALOG_SAR: dict[str, dict[str, Any]] = {
"founder_operator": {"label_ar": "مشغّل المؤسس", "amount_sar": 499.0},
"growth_os": {"label_ar": "نظام النمو (Growth OS)", "amount_sar": 2999.0},
"scale_os": {"label_ar": "نظام التوسّع (Scale OS)", "amount_sar": 7999.0},
"performance_pilot": {"label_ar": "Pay-per-Result pilot 30 يوم", "amount_sar": 1.0}, # placeholder
}
def sar_to_halalas(amount_sar: float) -> int:
"""Convert SAR to halalas (Moyasar's smallest unit). 1 SAR = 100 halalas."""
if amount_sar < 0:
raise ValueError("amount_sar must be non-negative")
return int(round(amount_sar * 100))
def build_moyasar_payment_link_draft(
*,
plan_key: str,
customer_id: str,
contact_email: str | None = None,
locale: str = "ar",
callback_url: str = "https://dealix.sa/payment-success.html",
cancel_url: str = "https://dealix.sa/payment-cancelled.html",
custom_amount_sar: float | None = None,
) -> dict[str, Any]:
"""
Build a Moyasar payment payload (NOT yet sent to Moyasar API).
Returns a dict the operator can review + approve. The actual
`POST /v1/payments` is fired elsewhere by the billing service.
"""
plan = PLAN_CATALOG_SAR.get(plan_key)
if plan is None and custom_amount_sar is None:
return {
"error": f"unknown_plan: {plan_key}",
"approval_required": True,
"approval_status": "pending_approval",
"live_charged": False,
}
amount_sar = custom_amount_sar if custom_amount_sar is not None else plan["amount_sar"]
label_ar = (plan["label_ar"] if plan else "خطة مخصصة")
description_ar = (
f"اشتراك Dealix — {label_ar}. "
f"المبلغ {amount_sar:,.2f} ريال شامل ضريبة القيمة المضافة 15%."
)
return {
"moyasar_request_draft": {
"amount": sar_to_halalas(amount_sar),
"currency": "SAR",
"description": description_ar,
"callback_url": callback_url,
"cancel_url": cancel_url,
"metadata": {
"customer_id": customer_id,
"plan_key": plan_key,
"locale": locale,
"draft_id": f"draft_pay_{uuid.uuid4().hex[:16]}",
},
},
"amount_sar": amount_sar,
"amount_halalas": sar_to_halalas(amount_sar),
"label_ar": label_ar,
"channel_recommendation": "whatsapp_with_link",
"in_chat_message_ar": (
f"الباقة المقترحة:\n{label_ar}{amount_sar:,.0f} ريال\n\n"
"[ادفع الآن] [أرسل فاتورة] [كلم المبيعات]\n\n"
"ملاحظة: الدفع آمن عبر Moyasar (سعودي مرخّص). فاتورة ZATCA "
"تصلكم تلقائياً بعد التأكيد."
),
"approval_required": True,
"approval_status": "pending_approval",
"live_charged": False,
"compliance_note_ar": (
"draft فقط — لا يتم خصم أي مبلغ حتى يضغط العميل 'ادفع' "
"على Moyasar وتصلنا webhook 'paid'."
),
}

View File

@ -0,0 +1,162 @@
"""
Weekly Proof Pack evidence the customer can show their CEO/board.
Tracks: opportunities discovered, messages approved, replies, meetings,
deals, pipeline, blocked risks (PDPL gates that fired), revenue leaks
recovered, next week plan.
Pure function. No I/O.
"""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
def _grade_week(*, pipeline_sar: float, plan_cost_sar: float, deals_won: int) -> str:
"""Quick A+/A/B/C/D grade for the week."""
if plan_cost_sar <= 0:
return "B"
multiple = pipeline_sar / plan_cost_sar
if multiple >= 5 and deals_won >= 1:
return "A+"
if multiple >= 3:
return "A"
if multiple >= 1.5:
return "B"
if multiple >= 0.5:
return "C"
return "D"
def build_weekly_proof_pack(
*,
customer_id: str,
customer_name: str,
week_label: str,
plan_cost_weekly_sar: float = 750,
# Activity
opportunities_discovered: int = 0,
messages_drafted: int = 0,
messages_approved: int = 0,
messages_sent: int = 0,
replies_received: int = 0,
positive_replies: int = 0,
meetings_booked: int = 0,
meetings_held: int = 0,
proposals_sent: int = 0,
deals_won: int = 0,
# Money
pipeline_added_sar: float = 0.0,
revenue_won_sar: float = 0.0,
# Risk / quality
risky_drafts_blocked: int = 0,
revenue_leaks_recovered: int = 0,
avg_response_minutes: int = 0,
# Best of
best_message_subject: str | None = None,
best_message_reply_rate: float | None = None,
) -> dict[str, Any]:
"""Build the weekly proof pack (Markdown + structured)."""
approve_rate = (
round(messages_approved / messages_drafted, 4) if messages_drafted else 0.0
)
reply_rate = (
round(replies_received / messages_sent, 4) if messages_sent else 0.0
)
grade = _grade_week(
pipeline_sar=pipeline_added_sar,
plan_cost_sar=plan_cost_weekly_sar,
deals_won=deals_won,
)
multiple = round(pipeline_added_sar / plan_cost_weekly_sar, 2) if plan_cost_weekly_sar else 0.0
headline_ar = (
f"{pipeline_added_sar:,.0f} ريال pipeline + "
f"{meetings_booked} اجتماع + {risky_drafts_blocked} مخاطرة محبوطة "
f"خلال {week_label}"
)
activity = {
"فرص مكتشفة": opportunities_discovered,
"مسودات": messages_drafted,
f"موافقات ({approve_rate*100:.0f}%)": messages_approved,
"مُرسلة": messages_sent,
f"ردود ({reply_rate*100:.1f}%)": replies_received,
"ردود إيجابية": positive_replies,
"اجتماعات محجوزة": meetings_booked,
"اجتماعات منعقدة": meetings_held,
"عروض مرسلة": proposals_sent,
"صفقات مكسوبة": deals_won,
}
money = {
"Pipeline مضاف": f"{pipeline_added_sar:,.0f} ريال",
"إيراد محسوم": f"{revenue_won_sar:,.0f} ريال",
"Multiple على تكلفة الأسبوع": f"{multiple}×",
}
quality = {
"drafts خطرة محبوطة (PDPL gates)": risky_drafts_blocked,
"تسريبات إيراد منقذة": revenue_leaks_recovered,
"متوسط زمن الرد (دقيقة)": avg_response_minutes,
}
next_week_plan_ar = []
if reply_rate < 0.05:
next_week_plan_ar.append("اختبر صياغة مختلفة للسطر الأول — معدل الرد منخفض.")
if avg_response_minutes > 60:
next_week_plan_ar.append(f"قلل زمن الرد من {avg_response_minutes} إلى أقل من 60 دقيقة.")
if meetings_booked == 0 and replies_received >= 5:
next_week_plan_ar.append("ركّز على qualifying — ردود كثيرة بدون اجتماع.")
if deals_won == 0 and proposals_sent >= 2:
next_week_plan_ar.append("مراجعة العروض المرسلة + جلسة Deal Coach.")
if not next_week_plan_ar:
next_week_plan_ar.append("ركّز على الـ scale — زد عدد الـ leads بنسبة 30%.")
md_lines = [
f"# Dealix Proof Pack — {customer_name}",
f"**الفترة:** {week_label}",
f"**التقييم:** {grade}",
"",
"## TL;DR",
headline_ar,
"",
"## النشاط",
*(f"- {k}: {v}" for k, v in activity.items()),
"",
"## المال",
*(f"- {k}: {v}" for k, v in money.items()),
"",
"## الجودة + الأمان",
*(f"- {k}: {v}" for k, v in quality.items()),
"",
"## أفضل أداء",
f"- subject الأنجح: {best_message_subject or ''}",
f"- معدل ردها: {(best_message_reply_rate or 0)*100:.1f}%",
"",
"## خطة الأسبوع القادم",
*(f"- {x}" for x in next_week_plan_ar),
"",
f"_Generated by Dealix at {datetime.now(timezone.utc).isoformat()}_",
]
return {
"customer_id": customer_id,
"customer_name": customer_name,
"week_label": week_label,
"grade": grade,
"headline_ar": headline_ar,
"activity": activity,
"money": money,
"quality": quality,
"best_message": {
"subject": best_message_subject,
"reply_rate": best_message_reply_rate,
},
"next_week_plan_ar": next_week_plan_ar,
"markdown_export": "\n".join(md_lines),
"approval_required": False,
"compliance_note_ar": (
"هذا تقرير قراءة فقط — يُولَّد من سجلات حقيقية ولا يحوي أي PII "
"خارج هوية الشركة المشتركة."
),
}

View File

@ -0,0 +1,146 @@
"""
Targeting turn a list of safe contacts into a ranked Top-N with Why-Now.
Pure functions; no LLM calls. Heuristic ranking:
- existing customer / inbound lead: highest base score
- event lead: strong recency boost
- old lead with last_contacted_at: medium
- referral: high trust
- unknown / cold: filtered out unless explicitly allowed
"""
from __future__ import annotations
import hashlib
from typing import Any
from auto_client_acquisition.growth_operator.contact_importer import (
classify_contact_source,
detect_opt_out,
normalize_phone,
)
from auto_client_acquisition.growth_operator.contactability import (
score_contactability,
)
# ── Segments ─────────────────────────────────────────────────────
_SEGMENT_BASE_SCORE: dict[str, float] = {
"existing_customer": 90.0,
"inbound_lead": 85.0,
"referral": 80.0,
"event_lead": 75.0,
"old_lead": 60.0,
"unknown": 35.0,
"cold_list": 20.0,
}
def segment_contacts(contacts: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
"""Group contacts into segments using classify_contact_source."""
segs: dict[str, list[dict[str, Any]]] = {
"existing_customer": [],
"inbound_lead": [],
"referral": [],
"event_lead": [],
"old_lead": [],
"unknown": [],
"cold_list": [],
"blocked_or_invalid": [],
}
for c in contacts:
if detect_opt_out(c) or not normalize_phone(c.get("phone")):
segs["blocked_or_invalid"].append(c)
continue
src = classify_contact_source(c)
segs.setdefault(src, []).append(c)
return segs
# ── Why-Now stub (deterministic; placeholder until live signals) ──
_WHY_NOW_TEMPLATES_AR: dict[str, str] = {
"existing_customer": "علاقة قائمة — توقيت ممتاز لعرض expansion / upsell.",
"inbound_lead": "أبدى اهتماماً مؤخراً — السرعة (≤24 ساعة) ترفع التحويل.",
"referral": "قادم بإحالة موثوقة — احترام السياق المهني.",
"event_lead": "تواصل من فعالية مؤخراً — نافذة 30 يوم ذهبية.",
"old_lead": "lead سابق — انتهز موسم/حدث جديد للعودة.",
"unknown": "مصدر غير محدد — يحتاج warm-up + توثيق lawful basis.",
"cold_list": "قائمة باردة — لا تواصل قبل توثيق العلاقة.",
}
def why_now_stub(contact: dict[str, Any], *, sector_hint: str = "") -> dict[str, Any]:
"""
Deterministic Why-Now stub.
In production, this is replaced by a live signal-driven explainer
that reads market_intelligence + company website diff + jobs.
"""
src = classify_contact_source(contact)
company = contact.get("company") or contact.get("name") or ""
rationale = _WHY_NOW_TEMPLATES_AR.get(src, "تواصل قياسي — راجع المصدر قبل الإرسال.")
if sector_hint and src in ("event_lead", "inbound_lead", "old_lead"):
rationale += f" · مرتبط بقطاع {sector_hint}."
# Synthetic stable score (testable, no entropy)
seed = hashlib.md5(f"{company}|{src}|{sector_hint}".encode()).digest()
bonus = (seed[0] % 21) - 10 # -10..+10
return {
"rationale_ar": rationale,
"score_modifier": bonus,
"source": src,
}
# ── Ranking ──────────────────────────────────────────────────────
def rank_targets(
contacts: list[dict[str, Any]],
*,
sector_hint: str = "",
channel: str = "whatsapp",
require_safe: bool = True,
) -> list[dict[str, Any]]:
"""
Score every contact, optionally filter to safe-only, return sorted desc.
Each item in the result is the original contact + score + why_now + decision.
"""
out: list[dict[str, Any]] = []
for c in contacts:
decision = score_contactability(c, channel=channel)
if require_safe and decision["label"] != "safe":
continue
why = why_now_stub(c, sector_hint=sector_hint)
base = _SEGMENT_BASE_SCORE.get(why["source"], 30.0)
score = max(0.0, min(100.0, base + why["score_modifier"]))
out.append({
**c,
"fit_score": round(score, 1),
"why_now": why,
"contactability": decision,
})
out.sort(key=lambda x: x["fit_score"], reverse=True)
return out
def recommend_top_10(
contacts: list[dict[str, Any]],
*,
sector_hint: str = "",
channel: str = "whatsapp",
) -> dict[str, Any]:
"""The Top-10 view consumed by the dashboard's Growth Radar tile."""
ranked = rank_targets(
contacts, sector_hint=sector_hint, channel=channel, require_safe=True,
)
top = ranked[:10]
return {
"channel": channel,
"sector_hint": sector_hint,
"candidates_evaluated": len(contacts),
"candidates_safe": len(ranked),
"top": top,
"recommendation_ar": (
f"اخترنا أعلى {len(top)} فرصة آمنة من قائمة {len(contacts)} "
f"بعد فلترة المخاطرة. كل واحدة بحالة pending_approval."
),
}

View File

@ -2,7 +2,8 @@
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta
from core._py_compat import UTC
from typing import Any
from sqlalchemy import select

View File

@ -3,7 +3,8 @@
from __future__ import annotations
import uuid
from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta
from core._py_compat import UTC
from typing import Any
from sqlalchemy import func, select

View File

@ -0,0 +1,92 @@
"""Intelligence Command Feed — Arabic decision cards with ≤3 buttons."""
from __future__ import annotations
from typing import Any
INTEL_CARD_TYPES: tuple[str, ...] = (
"opportunity",
"revenue_leak",
"approval_needed",
"meeting_prep",
"payment_followup",
"partner_suggestion",
"social_signal",
"review_response",
"competitive_move",
)
def build_command_feed_demo() -> dict[str, Any]:
"""Deterministic Arabic command feed for demo + tests."""
cards = [
{
"type": "opportunity",
"title_ar": "فرصة نمو — شركة تدريب في الرياض",
"summary_ar": "نشروا 3 وظائف مبيعات جديدة → توسع واضح في فريق المبيعات.",
"why_it_matters_ar": "التوسع = ميزانية = نافذة شراء ≤30 يوم.",
"recommended_action_ar": "رسالة قصيرة تعرض تجربة 7 أيام.",
"expected_impact_sar": 18_000,
"risk_level": "low",
"buttons_ar": ("قبول", "تخطّي", "اكتب رسالة"),
},
{
"type": "revenue_leak",
"title_ar": "تسريب إيراد — 7 leads بلا متابعة",
"summary_ar": "آخر تواصل قبل 72+ ساعة. الردود تتراجع 14%/ساعة.",
"why_it_matters_ar": "الإهمال خسارة pipeline متراكمة.",
"recommended_action_ar": "اعتمد 7 follow-ups جاهزة.",
"expected_impact_sar": 42_000,
"risk_level": "medium",
"buttons_ar": ("اعتمد", "عدّل", "تخطّي"),
},
{
"type": "partner_suggestion",
"title_ar": "فرصة شراكة — وكالة B2B في جدة",
"summary_ar": "عملاؤها يحتاجون lead-gen → Dealix يكمل خدماتها.",
"why_it_matters_ar": "الشراكة الواحدة تفتح 3-5 leads warmer.",
"recommended_action_ar": "رسالة partnership warm + اقتراح pilot.",
"expected_impact_sar": 60_000,
"risk_level": "low",
"buttons_ar": ("اكتب رسالة", "احجز اجتماع", "تخطّي"),
},
{
"type": "meeting_prep",
"title_ar": "اجتماع غداً مع شركة العقار الذهبي",
"summary_ar": "جاهز: ملف الشركة + 5 أسئلة + 3 اعتراضات + عرض مناسب.",
"why_it_matters_ar": "الاجتماع المُحضَّر يرفع الإغلاق 40%+.",
"recommended_action_ar": "افتح التحضير + راجع الأجندة.",
"expected_impact_sar": 250_000,
"risk_level": "low",
"buttons_ar": ("افتح التحضير", "اكتب أجندة", "أرسل تأكيد"),
},
{
"type": "review_response",
"title_ar": "تقييم Google جديد — 2 نجوم",
"summary_ar": "العميل اشتكى من التأخر في الرد.",
"why_it_matters_ar": "تقييم سلبي بدون رد ≤24 ساعة يضرّ السمعة المحلية.",
"recommended_action_ar": "اعتذار قصير + طلب تواصل + حل.",
"expected_impact_sar": 1_000,
"risk_level": "high",
"buttons_ar": ("اعتمد الرد", "صعّد للمدير", "تخطّي"),
},
{
"type": "competitive_move",
"title_ar": "منافس أطلق pricing جديد",
"summary_ar": "خفّضوا 15% على باقة Growth — يستهدفون نفس عملاءك.",
"why_it_matters_ar": "الردود السريعة تحفظ الـ pipeline.",
"recommended_action_ar": "حملة مضادة + ROI breakdown مقارن.",
"expected_impact_sar": 80_000,
"risk_level": "medium",
"buttons_ar": ("جهّز رد", "نبّه المبيعات", "تخطّي"),
},
]
# Validate constraints
for c in cards:
assert c["type"] in INTEL_CARD_TYPES
assert len(c["buttons_ar"]) <= 3
return {
"feed_size": len(cards),
"cards": cards,
"policy_note_ar": "كل كرت عربي + ≤3 buttons + approval-aware.",
}

View File

@ -0,0 +1,36 @@
"""Cost policy — classify a task's cost class without locking to specific tokens prices."""
from __future__ import annotations
from typing import Literal
CostClass = Literal["low", "mid", "high"]
def classify_cost(
*,
task_type: str,
expected_input_tokens: int = 0,
expected_output_tokens: int = 0,
bulk: bool = False,
) -> CostClass:
"""
Heuristic cost class.
- bulk volume low
- large output (>1500 tokens) high
- strategic / vision / arabic_copywriting mid
- everything else low
"""
if bulk:
return "low"
if expected_output_tokens > 1500 or expected_input_tokens > 8000:
return "high"
if task_type in {
"strategic_reasoning", "vision_analysis",
"compliance_guardrail", "meeting_analysis",
}:
return "mid"
if task_type in {"arabic_copywriting"}:
return "mid"
return "low"

View File

@ -0,0 +1,60 @@
"""Build a deterministic fallback chain for any task type."""
from __future__ import annotations
from .provider_registry import ALL_PROVIDERS, Provider
def _supports(p: Provider, task_type: str, *, requires_arabic: bool, requires_vision: bool) -> bool:
if task_type not in p.capabilities:
return False
if requires_arabic and not p.supports_arabic:
return False
if requires_vision and not p.supports_vision:
return False
return True
def build_fallback_chain(
task_type: str,
*,
requires_arabic: bool = False,
requires_vision: bool = False,
sensitivity: str = "low",
primary_key: str | None = None,
) -> list[str]:
"""
Return an ordered list of provider keys to try for a task.
Rules:
- if `primary_key` is supplied and supports the task, it goes first.
- high-sensitivity workloads prefer KSA-region or self-hosted.
- among the rest, lower cost_class is preferred.
"""
candidates = [
p for p in ALL_PROVIDERS
if _supports(p, task_type,
requires_arabic=requires_arabic,
requires_vision=requires_vision)
]
cost_order = {"low": 0, "mid": 1, "high": 2}
privacy_order = {"self_hosted": 0, "ksa_region": 1, "vendor_cloud": 2}
if sensitivity == "high":
candidates.sort(key=lambda p: (
privacy_order.get(p.privacy_tier, 9),
cost_order.get(p.cost_class, 9),
))
else:
candidates.sort(key=lambda p: (
cost_order.get(p.cost_class, 9),
privacy_order.get(p.privacy_tier, 9),
))
chain = [p.key for p in candidates]
if primary_key:
if primary_key in chain:
chain.remove(primary_key)
chain.insert(0, primary_key)
return chain

View File

@ -0,0 +1,32 @@
"""Demo usage dashboard for the model router (deterministic)."""
from __future__ import annotations
from .provider_registry import ALL_PROVIDERS, ALL_TASK_TYPES
from .task_router import route_task
def build_usage_demo() -> dict[str, object]:
"""
Demo: route every task type once and surface aggregate stats.
Used by /api/v1/model-router/usage/demo to show the router behavior.
"""
routes: list[dict[str, object]] = []
for tt in ALL_TASK_TYPES:
d = route_task(tt, requires_arabic=(tt == "arabic_copywriting"))
routes.append(d.to_dict())
cost_counts: dict[str, int] = {}
primary_counts: dict[str, int] = {}
for r in routes:
cost_counts[str(r.get("cost_class"))] = cost_counts.get(str(r.get("cost_class")), 0) + 1
primary_counts[str(r.get("primary_provider"))] = primary_counts.get(str(r.get("primary_provider")), 0) + 1
return {
"providers_total": len(ALL_PROVIDERS),
"task_types_total": len(ALL_TASK_TYPES),
"routes": routes,
"cost_counts": cost_counts,
"primary_counts": primary_counts,
}

View File

@ -3,8 +3,9 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from datetime import datetime
from core._py_compat import UTC
from core._py_compat import StrEnum
from typing import Any

View File

@ -4,8 +4,9 @@ from __future__ import annotations
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from datetime import datetime
from core._py_compat import UTC
from core._py_compat import StrEnum
from typing import Any
from uuid import uuid4

View File

@ -11,8 +11,9 @@ This module powers a Boardy-style operator for Sami, but specialized for Dealix:
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from enum import StrEnum
from datetime import datetime, timedelta
from core._py_compat import UTC
from core._py_compat import StrEnum
from typing import Any
from uuid import uuid4

View File

@ -0,0 +1,80 @@
"""
Platform Proof Ledger value rolled up across the entire platform.
Tracks: leads, meetings, drafts, sends, payments, revenue influenced,
risks blocked, time saved, partner ops. Pure functions.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass
class PlatformProofLedger:
"""Aggregated platform value over a period."""
customer_id: str
period_label: str
leads_created: int = 0
meetings_booked: int = 0
drafts_approved: int = 0
messages_sent: int = 0
payments_initiated: int = 0
payments_paid: int = 0
revenue_influenced_sar: float = 0.0
risks_blocked: int = 0
time_saved_hours: float = 0.0
partner_opportunities: int = 0
by_channel: dict[str, dict[str, float]] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"customer_id": self.customer_id,
"period_label": self.period_label,
"totals": {
"leads_created": self.leads_created,
"meetings_booked": self.meetings_booked,
"drafts_approved": self.drafts_approved,
"messages_sent": self.messages_sent,
"payments_initiated": self.payments_initiated,
"payments_paid": self.payments_paid,
"revenue_influenced_sar": self.revenue_influenced_sar,
"risks_blocked": self.risks_blocked,
"time_saved_hours": self.time_saved_hours,
"partner_opportunities": self.partner_opportunities,
},
"by_channel": self.by_channel,
}
def build_demo_platform_proof(
*,
customer_id: str = "demo",
period_label: str = "May 2026",
) -> PlatformProofLedger:
"""Deterministic demo for the dashboard."""
return PlatformProofLedger(
customer_id=customer_id,
period_label=period_label,
leads_created=72,
meetings_booked=14,
drafts_approved=58,
messages_sent=58,
payments_initiated=4,
payments_paid=3,
revenue_influenced_sar=185_000,
risks_blocked=21, # cold whatsapp + secrets in payload + opt-out + ...
time_saved_hours=42,
partner_opportunities=6,
by_channel={
"whatsapp": {"messages_sent": 33, "replies": 12, "meetings": 5},
"gmail": {"drafts": 18, "sent": 18, "replies": 6},
"google_calendar": {"events_drafted": 14, "events_inserted": 0},
"moyasar": {"links_drafted": 4, "paid": 3},
"google_business_profile": {"reviews_replied": 8},
"linkedin_lead_forms": {"leads_ingested": 11},
"website_forms": {"leads_ingested": 22},
},
)

View File

@ -8,7 +8,7 @@ contract.
from __future__ import annotations
from dataclasses import dataclass, field
from enum import StrEnum
from core._py_compat import StrEnum
from typing import Any
from uuid import uuid4

View File

@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from core._py_compat import StrEnum
from typing import Any

View File

@ -3,7 +3,8 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from datetime import datetime
from core._py_compat import UTC
from math import exp
from typing import Any

View File

@ -3,8 +3,9 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from datetime import datetime
from core._py_compat import UTC
from core._py_compat import StrEnum
from hashlib import sha256
from typing import Any
from uuid import uuid4

View File

@ -6,7 +6,7 @@ Sector Intelligence Agent — Saudi sector deep knowledge.
from __future__ import annotations
from dataclasses import dataclass, field
from enum import StrEnum
from core._py_compat import StrEnum
from typing import Any
from core.agents.base import BaseAgent

49
dealix/core/_py_compat.py Normal file
View File

@ -0,0 +1,49 @@
"""
Python compatibility shims makes the codebase work on Python 3.10 + 3.11+.
Two stdlib features used heavily but only available on 3.11+:
- `from datetime import UTC` `core._py_compat.UTC`
- `from enum import StrEnum` `core._py_compat.StrEnum`
This module is import-safe everywhere (no third-party deps) and adds
zero runtime cost on 3.11+ (it just re-exports the stdlib names).
"""
from __future__ import annotations
import sys
# ── UTC ─────────────────────────────────────────────────────────
if sys.version_info >= (3, 11):
from datetime import UTC # type: ignore[attr-defined]
else:
from datetime import timezone
UTC = timezone.utc # type: ignore[assignment]
# ── StrEnum ─────────────────────────────────────────────────────
if sys.version_info >= (3, 11):
from enum import StrEnum # type: ignore[attr-defined]
else:
from enum import Enum
class StrEnum(str, Enum):
"""3.10-compatible StrEnum backport.
Behaves like 3.11's enum.StrEnum: members are strings, str(member)
returns the value (not 'ClassName.MEMBER').
"""
def __new__(cls, value):
if not isinstance(value, str):
raise TypeError(f"values of StrEnum must be str, got {type(value)}")
obj = str.__new__(cls, value)
obj._value_ = value
return obj
def __str__(self):
return str.__str__(self)
__all__ = ["UTC", "StrEnum"]

View File

@ -6,7 +6,7 @@ Model routing configuration — maps tasks to the best LLM provider.
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from core._py_compat import StrEnum
class Provider(StrEnum):

View File

@ -5,9 +5,11 @@ from __future__ import annotations
import hashlib
import re
import uuid
from datetime import UTC, datetime
from datetime import datetime
from typing import Any
from core._py_compat import UTC
import phonenumbers

View File

@ -11,7 +11,8 @@ These drive policy evaluation, approval routing, and audit handling.
from __future__ import annotations
from enum import Enum, StrEnum
from enum import Enum
from core._py_compat import StrEnum
class ApprovalClass(StrEnum):

View File

@ -8,8 +8,9 @@ action is appended as an AuditEntry. Entries are append-only.
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from enum import StrEnum
from datetime import datetime
from core._py_compat import UTC
from core._py_compat import StrEnum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field

View File

@ -11,7 +11,8 @@ Per the blueprint, no critical output leaves the Decision Plane without:
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from datetime import datetime
from core._py_compat import UTC
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator

View File

@ -10,7 +10,8 @@ Every event in the platform carries this envelope for:
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from datetime import datetime
from core._py_compat import UTC
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field

View File

@ -15,7 +15,8 @@ Per the blueprint, every high-stakes decision ships with a pack containing:
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from datetime import datetime
from core._py_compat import UTC
from typing import Any
from pydantic import BaseModel, ConfigDict, Field

View File

@ -28,7 +28,7 @@ import json
import time
import uuid
from dataclasses import asdict, dataclass
from enum import StrEnum
from core._py_compat import StrEnum
from typing import Any
import redis.asyncio as redis

View File

@ -24,7 +24,8 @@ import logging
import uuid
from collections import deque
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from datetime import datetime
from core._py_compat import UTC
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:

View File

@ -11,8 +11,9 @@ from __future__ import annotations
import uuid
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from enum import StrEnum
from datetime import datetime, timedelta
from core._py_compat import UTC
from core._py_compat import StrEnum
from typing import Any
from dealix.classifications import ApprovalClass

View File

@ -13,7 +13,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from enum import StrEnum
from core._py_compat import StrEnum
from dealix.classifications import (
NEVER_AUTO_EXECUTE,

View File

@ -15,7 +15,8 @@ from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime
from datetime import datetime
from core._py_compat import UTC
from typing import Any

View File

@ -0,0 +1,351 @@
# Arabic Growth Operator — Full Spec
> **الرؤية:** Dealix ليس CRM ولا أداة WhatsApp ولا بوت رسائل. هو **Saudi Autonomous Revenue OS**: بوت عربي ذكي داخل WhatsApp/الداشبورد يفهم الشركة، السوق السعودي، الأرقام المرفوعة من العميل، الشراكات، الاجتماعات، المتابعة، الدفع، والامتثال — ويقترح وينفذ بموافقة واضحة.
> **آخر تحديث:** 2026-05-01
> **حالة الكود:** ✅ مبني، 50/50 unit tests خضراء على Python 3.10 venv
---
## 1. الجملة المحورية
> **Dealix هو مدير نمو عربي ذكي للشركات السعودية:
> يعرف من تستهدف، ماذا تقول، متى تتابع، من تشارك، وكيف تثبت أن كل هذا جاب نتيجة.**
---
## 2. تجربة WhatsApp مثل Boardy لكن أقوى
**Boardy** يقترح علاقات.
**Dealix** يقترح علاقات + leads + رسائل + اجتماعات + مدفوعات + proof + revenue.
كل بطاقة في الـ feed:
- **Why now** ولماذا الآن تحديداً
- **Recommended action** بعربي طبيعي
- **3 buttons فقط** (حد WhatsApp Reply Buttons): قبول / تخطي / رسالة
- لو ضغط "رسالة" → يدخل draft mode: اعتماد / تعديل / إلغاء
---
## 3. أنواع العملاء التي تخدمهم
| النوع | كيف يخدمه Dealix |
|---|---|
| **صاحب الشركة** | daily brief 3 قرارات صباحاً، قرار مطلوب feed، Proof Pack أسبوعي |
| **مدير المبيعات** | deals at risk، reps slow follow-up، messages to approve، forecast، coaching |
| **متجر / SMB** | تصنيف العملاء VIP/inactive/repeat/leads، حملات استرجاع، payment links، عروض موسمية |
| **مؤسس فردي** | First 10 Customers Autopilot، Personal Operator، Strategic Board Brief |
| **وكالة تسويق** | reseller / revenue share + سياسة موافقات لكل عميل |
---
## 4. استخدام الأرقام المرفوعة من العميل
> **القاعدة الذهبية:** الأرقام لازم تكون مملوكة/مصرّح بها أو عندها علاقة مناسبة. لا cold WhatsApp بدون lawful basis.
العميل يرفع ملف:
```text
name, phone, company, city, sector, source, relationship_status,
opt_in_status, last_contacted_at, notes
```
ثم Dealix يعمل تلقائياً:
1. **normalize_phone** — تطبيع للأرقام السعودية (E.164)
2. **dedupe_contacts** — إزالة التكرار، الإبقاء على السجل الأغنى
3. **classify_contact_source** — existing / inbound / event / referral / old_lead / cold / unknown
4. **detect_opt_out** — إشارات الـ opt-out بالعربي + الإنجليزي
5. **score_contactability**`safe / needs_review / blocked` مع أسباب عربية واضحة
6. **summarize_import** — ملخص مرئي يطلع للعميل
### المخرج للعميل
```text
رفعت 1,000 رقم.
- 420 آمن للتواصل
- 180 يحتاج مراجعة
- 90 opt-out أو ممنوع
- 310 غير واضح المصدر
أقترح نبدأ فقط بـ 420 رقم الآمنة.
[اعرض العينة] [جهز الرسائل] [احذف غير الآمن]
```
---
## 5. Contactability + opt-in
### القرارات الـ 3
- **safe**: علاقة قائمة (existing/inbound/referral) + رقم صالح + ليس opt-out
- **needs_review**: مصدر غير واضح / lead قديم بدون last_contacted_at
- **blocked**: opt-out / cold WhatsApp بدون lawful basis / رقم غير صالح
### قاعدة WhatsApp الافتراضية
```text
لا cold WhatsApp بدون lawful basis (PDPL م.5).
السياسة: لا cold WhatsApp افتراضياً.
```
العميل يقدر يعدل القاعدة لكل قائمة بعد توثيق المصدر، لكن الأمر الافتراضي هو **الحماية**.
---
## 6. WhatsApp Approvals
WhatsApp Reply Buttons محدودة بـ 3:
```text
[قبول] [تخطي] [رسالة]
```
لو ضغط "رسالة":
```text
[اعتماد] [تعديل] [إلغاء]
```
كل draft يخرج بـ:
- `approval_required: True`
- `approval_status: "pending_approval"`
- `guardrails_ar`: قائمة قواعد عربية واضحة
---
## 7. Gmail Drafts (لا إرسال مباشر)
- Endpoint يستخدم `gmail.compose` scope فقط
- ينشئ مسودة في صندوق المستخدم بـ label `DRAFT`
- المستخدم يضغط "Send" بنفسه من Gmail
---
## 8. Calendar Drafts (لا إنشاء مباشر)
- `build_calendar_draft()` يرجع dict مطابق لـ Google Calendar `events.insert` body
- `live_inserted: False` دائماً
- `conferenceDataVersion: 1` للحصول على Google Meet
- الـ insert الفعلي يحدث في خدمة منفصلة، فقط بعد:
- موافقة OAuth صريحة من المستخدم
- ضغط زر "أنشئ الاجتماع"
---
## 9. Payment Links (Moyasar)
- `build_moyasar_payment_link_draft()` يبني payload بصيغة Moyasar
- المبلغ بالـ halalas (1 SAR = 100)
- `live_charged: False` دائماً
- `POST /v1/payments` الفعلي يحدث في billing service
داخل المحادثة:
```text
الباقة المقترحة:
نظام النمو (Growth OS) — 2,999 ريال
[ادفع الآن] [أرسل فاتورة] [كلم المبيعات]
```
ضغط "ادفع الآن" → ينقل لـ Moyasar Hosted Checkout (PCI-safe)، **ليس** إدخال بطاقة داخل WhatsApp.
---
## 10. الشراكات
`suggest_partner_types()` يرجع 6 أنواع شركاء جاهزة:
- marketing_agency
- sales_consultant
- tech_integrator (Supabase / Make.com)
- crm_vendor (Zoho / Salla / Odoo سعودي)
- founder_community
- sector_influencer (عقار / صحة / لوجستيات)
كل نوع له: rationale_ar، model_ar (Reseller / Revenue share / Affiliate / Equity)، ideal_size.
`partner_scorecard()` يحسب: tier (platinum / gold / silver / bronze) من intros + deals + revenue share + age.
---
## 11. الاجتماعات
`build_meeting_agenda()` يخلق agenda سعودي مناسب:
- 15min → 4 فقرات
- 20-30min → 5 فقرات
- 45min+ → 6 فقرات (يشمل demo حي + ROI breakdown)
`build_post_meeting_followup()` ينتج draft شكر + ملخص + خطوة تالية.
---
## 12. Proof Pack
`build_weekly_proof_pack()` يولد تقرير أسبوعي:
- **Activity:** 10 أرقام (opportunities, drafts, sent, replies, meetings, proposals, deals)
- **Money:** Pipeline + Revenue + Multiple
- **Quality:** drafts خطرة محبوطة (PDPL gates) + leaks recovered + avg response
- **Best of:** أفضل subject + reply rate
- **Next week plan:** قائمة عربية ديناميكية بناءً على الأرقام
التقدير: A+ / A / B / C / D حسب pipeline / cost multiple + deals.
تصدير Markdown جاهز للإرسال للإدارة.
---
## 13. Growth Missions (6 مهمات outcome-shaped)
| ID | Title AR | Kill Metric | Endpoint |
|---|---|---|---|
| **first_10_opportunities** ⭐ | اطلع لي 10 فرص | ten_drafts_approved | `/api/v1/innovation/opportunities/ten-in-ten` |
| recover_stalled_deals | أنقذ الصفقات المتوقفة | stalled_deals_revived | `/api/v1/revenue-os/leaks` |
| partnership_sprint | ابدأ شراكات | partner_intros_replied | `/api/v1/growth-operator/partners/suggest` |
| safe_whatsapp_campaign | جهز حملة واتساب آمنة | safe_messages_drafted | `/api/v1/growth-operator/contacts/import-preview` |
| meeting_booking_sprint | احجز لي 3 اجتماعات | meetings_confirmed | `/api/v1/growth-operator/meetings/draft` |
| list_cleanup | ارفع قائمتي ونظفها | safe_contacts_extracted | `/api/v1/growth-operator/contacts/import-preview` |
**Kill feature:** `first_10_opportunities` — الميزة التي تبيع.
---
## 14. حدود البحث في السوق والسوشال
### المصادر المسموحة
- موقع الشركة + صفحات عامة
- Google Search / Maps (API مع keys)
- LinkedIn (API / يدوي مصرّح)
- X / Instagram / Facebook Graph API (بإذن العميل)
- Job boards / event pages / tender feeds
- CRM العميل + ملفاته + Google Sheets / Gmail / Calendar (بإذن)
- WhatsApp opt-in / inbound
### ما لا يُبنى أبداً
- Scraping مخالف
- تجاوز login
- DM تلقائي بدون موافقة
- جمع أرقام عشوائية
- تخزين PII غير ضرورية
- إرسال جماعي غير مصرح
> **اللغة الصحيحة في المنتج:**
> "Dealix يبحث في المصادر المصرح بها والمتاحة، ويحوّلها إلى فرص قابلة للمراجعة، ولا يرسل بدون موافقة."
---
## 15. القواعد الـ 12 الأساسية (Compliance)
1. **Client Growth Profile** — كل عميل له ملف، بدونه البوت عام
2. **Contactability Engine** — كل رقم/lead له decision
3. **WhatsApp Approval OS** — 3 buttons فقط، draft-first
4. **Lead Intelligence** — fit_score, intent_score, why_now, best_angle, risk
5. **Saudi Message Engine** — قصير، غير مبالغ، سبب واضح، طلب بسيط
6. **Objection-to-Action** — كل رد → action مع interpretation
7. **Meeting Operator** — agenda + draft + followup، بدون live insert
8. **Gmail Draft Operator**`gmail.compose` فقط، draft مع label DRAFT
9. **Payment-in-Chat** — Moyasar payment link، لا بطاقات في WhatsApp
10. **Partnership Operator** — 6 أنواع + outreach + scorecard
11. **Proof Pack** — weekly evidence ضد churn
12. **Growth Missions** — outcome-shaped tasks (لا dashboard معقد)
---
## 16. ما يُنفَّذ الآن (في الكود)
✅ **مبني وعليه 50 unit test ناجحة:**
```
auto_client_acquisition/growth_operator/
├── __init__.py # exports
├── client_profile.py # ClientGrowthProfile + defaults
├── contact_importer.py # normalize/dedupe/classify/opt_out/summarize
├── contactability.py # safe/needs_review/blocked + reasons
├── targeting.py # segment + rank + top-10 + why_now stub
├── message_planner.py # Arabic drafts + followups + objections
├── partnership_planner.py # types + outreach + scorecard
├── meeting_planner.py # agenda + calendar draft + followup
├── payment_offer.py # Moyasar payment link draft
├── proof_pack.py # weekly proof pack
└── mission_planner.py # 6 growth missions
api/routers/growth_operator.py # 16 endpoints under /api/v1/growth-operator/
tests/unit/test_growth_operator.py # 50 passing
```
### Endpoints الـ 16
```
POST /api/v1/growth-operator/contacts/import-preview
POST /api/v1/growth-operator/contactability/score
POST /api/v1/growth-operator/targets/top-10
POST /api/v1/growth-operator/messages/draft
POST /api/v1/growth-operator/messages/followup
POST /api/v1/growth-operator/messages/objection-response
POST /api/v1/growth-operator/partners/suggest
POST /api/v1/growth-operator/partners/outreach
POST /api/v1/growth-operator/partners/scorecard
POST /api/v1/growth-operator/meetings/draft
POST /api/v1/growth-operator/meetings/post-followup
POST /api/v1/growth-operator/payment-offer/draft
GET /api/v1/growth-operator/missions
POST /api/v1/growth-operator/missions/{id}/run
GET /api/v1/growth-operator/proof-pack/demo
POST /api/v1/growth-operator/profile
```
---
## 17. ما يُؤجَّل (بعد أول 10 عملاء)
❌ **لا تنفذ الآن:**
- Live WhatsApp send (نظل draft-first)
- Live Calendar `events.insert` (يحتاج OAuth + UI confirm)
- Live Moyasar charges (يحتاج billing service منفصل)
- Live LinkedIn / X / Instagram scraping
- Multi-tenant SSO
- Mobile app
- MCP gateway مفتوح
- Marketplace خارجي
- Local LLM infra
---
## 18. الجاهزية للتجربة (Beta)
| المعيار | الحالة |
|---|---|
| Unit tests | ✅ 527 passed (50 منها growth_operator + 477 موجودة) |
| AST + import sanity | ✅ كل الـ 13 ملف |
| Approval invariant | ✅ كل draft عنده `approval_required: True` |
| لا live charge / send | ✅ كل drafts فقط |
| PDPL guardrails | ✅ no cold WhatsApp by default |
| Arabic body content | ✅ كل القوالب عربية طبيعية |
| Endpoint coverage | ✅ 16 endpoint + 6 missions + Top-10 + Proof Pack |
**جاهز للـ private beta** بمجرد:
1. ربط Railway env vars
2. ربط Moyasar live keys
3. ربط WhatsApp Cloud / Green API
4. 10 شركات pilot
---
## 19. المقارنة الموجزة
| ضد | قوته | تميزنا |
|---|---|---|
| **Boardy** | اقتراح علاقات | علاقات + leads + رسائل + اجتماعات + payments + proof |
| **HubSpot** | عام، شامل | عربي، سعودي، WhatsApp-first، outcome-first |
| **Gong** | conversation intelligence | نبدأ من market signal → action، لا فقط analytics |
| **أدوات WhatsApp** | إرسال bulk | نقرر هل ترسل، ماذا، لمن، متى، آمن أم لا |
| **الوكالات** | تنفيذ يدوي | system قابل للقياس + شفاف + scalable |
---
## 20. الخطوة التالية المباشرة
1. **Beta-recruit 10 شركات سعودية** (real_estate / clinics / training / agencies — لكل قطاع 2-3)
2. **شغّل `first_10_opportunities` mission** لكل شركة في أول 24 ساعة
3. **اقصد 50% approval rate + 5+ replies** في أول أسبوع
4. **أرسل Proof Pack الأسبوع الأول** لكل عميل
5. **اجمع feedback** وحدّث القوالب
> هذا النهج (kill feature → 10 عملاء → proof) أقوى من إطلاق عام بدون validation.
> Boardy للعلاقات + Dealix للنمو والإيرادات.
— Dealix · Saudi Autonomous Revenue Platform · 🇸🇦

View File

@ -0,0 +1,238 @@
# Dealix — Launch Master Runbook (AR)
> **الهدف:** إخراج Dealix من «جاهزية الكود» إلى «إطلاق تجاري حقيقي» بـ 10 خطوات واضحة.
> **الجمهور:** سامي (المؤسس) + أي عضو فريق onboarding مستقبلي.
> **آخر تحديث:** 2026-05-01
---
## ✅ ما تم بالفعل (مرفوع على GitHub `ai-company`)
- [x] **Backend:** 28 router · 266 endpoint · 24 DB table
- [x] **Modules:** revenue_memory · orchestrator · market_intelligence · copilot · revenue_science · compliance_os · vertical_os · revenue_graph · customer_success · ecosystem · personal_operator · v3 · business · innovation · ai
- [x] **Frontend:** 33 صفحة landing (privacy, terms, signup, welcome, payment success/cancel, 404, 500 — كلها أُضيفت اليوم)
- [x] **Tests:** 477 passed, 2 skipped على Python 3.10 venv
- [x] **CI:** Dealix API CI خضراء على GitHub
- [x] **Compat:** Python 3.10 + 3.11+ shim
- [x] **Legal:** privacy.html + terms.html (PDPL-aligned)
- [x] **Security:** SECURITY.md + LICENSE
- [x] **Sitemap + robots** محدّث
---
## 🚦 ما يحتاج خطوات يدوية للإطلاق التجاري
### 1⃣ النطاق و DNS
- [ ] شراء/تأكيد ملكية `dealix.sa` (الأولوية) أو `dealix.me` (مؤقت)
- [ ] DNS records:
- `A` → IP الخادم Railway/Cloudflare
- `CNAME api` → Railway public domain
- `CNAME www` → root
- `MX` → Google Workspace / Zoho Mail
- `TXT` → SPF + DMARC + DKIM (لإيميلات outbound)
- [ ] SSL certificate (Railway/Cloudflare auto)
- [ ] Cloudflare proxy + WAF rules
### 2⃣ Railway / Hosting
- [ ] إنشاء مشروع Railway باسم `dealix-api`
- [ ] ربط GitHub repo: `VoXc2/system-prompts-and-models-of-ai-tools`
- [ ] **Root directory:** `dealix/`
- [ ] **Branch:** `ai-company` (ثم نحول إلى `main` بعد الاستقرار)
- [ ] **Build command:** auto (Railway يلتقط Dockerfile)
- [ ] **Start command:** `uvicorn api.main:app --host 0.0.0.0 --port $PORT`
- [ ] إضافة Postgres add-on (Saudi-region لو متاح، أو EU/Bahrain)
- [ ] إضافة Redis add-on (للجلسات + rate limiting)
### 3⃣ Environment Variables (Railway → Variables)
من `dealix/.env.example`، الحرجة للإطلاق:
```bash
# Core
APP_ENV=production
APP_NAME=Dealix
APP_HOST=0.0.0.0
APP_PORT=$PORT
DATABASE_URL=$RAILWAY_POSTGRES_URL
REDIS_URL=$RAILWAY_REDIS_URL
# Security
API_KEY_PRIMARY=<generate via openssl rand -hex 32>
JWT_SECRET=<generate via openssl rand -hex 32>
CORS_ORIGINS=https://dealix.sa,https://www.dealix.sa
# LLM (one provider minimum)
ANTHROPIC_API_KEY=sk-ant-...
GROQ_API_KEY=gsk_...
# WhatsApp (one provider minimum)
GREEN_API_INSTANCE_ID=...
GREEN_API_TOKEN=...
# OR Meta WhatsApp Cloud:
META_WHATSAPP_PHONE_ID=...
META_WHATSAPP_TOKEN=...
META_WHATSAPP_VERIFY_TOKEN=<random>
# Gmail OAuth (per-customer flow)
GOOGLE_OAUTH_CLIENT_ID=...
GOOGLE_OAUTH_CLIENT_SECRET=...
GOOGLE_OAUTH_REDIRECT_URI=https://api.dealix.sa/auth/google/callback
# Moyasar (Saudi billing)
MOYASAR_PUBLIC_KEY=pk_live_...
MOYASAR_SECRET_KEY=sk_live_...
MOYASAR_WEBHOOK_SECRET=<set in Moyasar dashboard>
# Observability
SENTRY_DSN=https://...@sentry.io/...
LANGFUSE_PUBLIC_KEY=...
LANGFUSE_SECRET_KEY=...
POSTHOG_API_KEY=phc_...
# Supabase (project memory + pgvector)
SUPABASE_URL=https://....supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ... (server only — never client)
```
### 4⃣ Database Migrations
- [ ] تنفيذ `alembic upgrade head` على Railway Postgres
- [ ] تنفيذ `supabase/migrations/202605010001_v3_project_memory.sql` على Supabase
- [ ] تأكيد الفهارس على pgvector (HNSW) للمحادثات
### 5⃣ Domain → API → Frontend
- [ ] `api.dealix.sa` → Railway service
- [ ] `dealix.sa` و `www.dealix.sa` → static hosting من `landing/` (Cloudflare Pages أو Netlify)
- [ ] إعادة توجيه `dealix.me``dealix.sa` (لو الاثنين موجودان)
- [ ] تحديث `CORS_ORIGINS` ليشمل النطاق الفعلي
### 6⃣ المدفوعات (Moyasar)
- [ ] حساب Moyasar مفعّل (يحتاج CR + IBAN سعودي)
- [ ] webhook URL: `https://api.dealix.sa/api/v1/webhooks/moyasar`
- [ ] اختبار الدفع بمبلغ رمزي (1 ريال) قبل الإطلاق
- [ ] تأكد من ZATCA invoice template (15% VAT تلقائي)
### 7⃣ WhatsApp Business Account
- [ ] WABA verified عبر Meta أو موزع معتمد (مثل Green API بحساب Saudi)
- [ ] رقم سعودي (+966) موثّق
- [ ] template messages معتمدة بالعربية:
- `welcome_v1` — تأكيد الاشتراك
- `daily_brief_v1` — التقرير اليومي
- `approval_pending_v1` — تنبيه drafts بحاجة موافقة
- [ ] webhook signature verified
### 8⃣ Email Deliverability
- [ ] Google Workspace أو Zoho Mail لـ `@dealix.sa`
- [ ] SPF: `v=spf1 include:_spf.google.com ~all`
- [ ] DKIM: تفعيل من Google Workspace
- [ ] DMARC: `v=DMARC1; p=quarantine; rua=mailto:dmarc@dealix.sa`
- [ ] التسخين (warm-up) لمدة 14 يوم قبل الإرسال الكثيف
### 9⃣ Observability live
- [ ] Sentry — إنشاء project + DSN
- [ ] Langfuse — حساب + public/secret keys
- [ ] PostHog — موقع
- [ ] Status page (statusapi.io أو internal `/status.html` يربط بـ `/health/deep`)
- [ ] Uptime monitor (Better Uptime / UptimeRobot) → `/health`
### 🔟 Beta Launch Day (T-Day)
- [ ] **T-7 days:** smoke test كامل على staging
- [ ] **T-3 days:** invite-only beta (5 شركات أصدقاء)
- [ ] **T-1 day:** dry run — 24h لتشغيل النظام بصمت
- [ ] **T-Day morning:**
- [ ] post على LinkedIn (announcement)
- [ ] WhatsApp blast لقائمة الـ 50 شركة
- [ ] إرسال press release لـ TechCrunch Arabia / Wamda
- [ ] **T+1:** مراقبة active 24/7 لأول 72 ساعة
- [ ] **T+7:** retrospective + fix top-3 bugs
---
## 🧪 Smoke Test Manual (قبل الإطلاق)
```bash
# 1. Health
curl https://api.dealix.sa/health
# expected: {"status": "ok", ...}
# 2. Deep health
curl https://api.dealix.sa/health/deep -H "X-API-Key: $API_KEY_PRIMARY"
# expected: db, redis, llm checks all OK
# 3. Public landing pages
for page in / /pricing.html /privacy.html /terms.html /signup.html /command-center.html; do
echo -n "$page: "
curl -s -o /dev/null -w "%{http_code}\n" https://dealix.sa$page
done
# expected: all 200
# 4. Trigger a workflow (with API key)
curl -X POST https://api.dealix.sa/api/v1/revenue-os/workflows/run \
-H "X-API-Key: $API_KEY_PRIMARY" -H "Content-Type: application/json" \
-d '{"customer_id":"smoke","autonomy_mode":"draft_and_approve"}'
# expected: 8 tasks created, all awaiting_approval (since draft mode)
# 5. Copilot ask
curl -X POST https://api.dealix.sa/api/v1/revenue-os/copilot/ask \
-H "X-API-Key: $API_KEY_PRIMARY" -H "Content-Type: application/json" \
-d '{"question_ar":"وش أسوي اليوم؟","customer_id":"smoke","context":{}}'
# expected: intent=what_to_do_today + answer + 3 actions
# 6. Compliance risk gate
curl -X POST https://api.dealix.sa/api/v1/revenue-os/compliance/campaign-risk \
-H "X-API-Key: $API_KEY_PRIMARY" -H "Content-Type: application/json" \
-d '{"target_count":100,"contacts_with_consent":80,"contacts_opted_out":20,
"contacts_no_lawful_basis":0,"template_body":"ضمان 100% رقم الهوية",
"has_unsubscribe_link":false}'
# expected: risk_band="blocked" + 2 blockers
```
---
## 📊 KPIs لأول 30 يوم
| المقياس | الهدف |
|---|---|
| Uptime | ≥99.5% |
| API p95 latency | <200ms |
| Beta signups | 10-20 شركة |
| First Daily Run completed | ≥80% من الـ signups |
| First WhatsApp draft approved | ≥50% |
| Errors / 1000 requests | <5 |
| Stripe/Moyasar successful payment rate | ≥95% |
| NPS من أول 5 عملاء | ≥30 |
---
## 🆘 خطة الطوارئ (Rollback Plan)
لو حصلت مشكلة كارثية:
1. **Database:** استرجاع من Railway snapshot (آخر 24 ساعة)
2. **API:** إرجاع لآخر commit مستقر عبر Railway → Deployments → rollback
3. **Frontend:** rollback CDN إلى آخر deploy stable
4. **WhatsApp:** تعطيل الـ outbound حتى توضّح المشكلة (PDPL gate)
5. **التواصل:** post status update فوراً + إيميل لكل المتأثرين خلال ساعة
**جهات الاتصال الطارئة:**
- Railway support: support@railway.app
- Moyasar: support@moyasar.com (24/7)
- Sentry: support.sentry.io
- WhatsApp Cloud / Green API: حسب الموزع
---
## 🎯 الجملة الأخيرة قبل الإطلاق
> **"البرنامج جاهز. النظام جاهز. الباكد إند والفرونت إند جاهزون.
> الآن: ربط الحسابات + اختبار يدوي + إطلاق صامت 7 أيام، ثم إعلان كبير."**
— Dealix · Saudi Autonomous Revenue Platform · 🇸🇦

48
dealix/landing/404.html Normal file
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>الصفحة غير موجودة — Dealix</title>
<meta name="robots" content="noindex" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700;800&display=swap" />
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'IBM Plex Sans Arabic',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#0f172a,#1e3a8a);color:#fff;text-align:center;padding:24px}
.wrap{max-width:560px}
.code{font-size:clamp(80px,18vw,180px);font-weight:800;line-height:1;color:#22d3ee;font-family:'Inter','IBM Plex Sans Arabic',sans-serif}
h1{font-size:clamp(22px,3vw,32px);margin:8px 0 14px}
p{font-size:16px;opacity:0.92;margin-bottom:24px;line-height:1.7}
.links{display:flex;flex-wrap:wrap;gap:10px;justify-content:center}
.btn{display:inline-block;padding:11px 22px;border-radius:10px;text-decoration:none;font-weight:700;font-size:14px;transition:transform 0.15s}
.btn-primary{background:#22d3ee;color:#0f172a}
.btn-secondary{background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.3)}
.btn:hover{transform:translateY(-2px)}
.helpful{margin-top:36px;padding-top:24px;border-top:1px solid rgba(255,255,255,0.1);font-size:13px;opacity:0.8}
.helpful a{color:#22d3ee;text-decoration:none;margin:0 8px}
</style>
</head>
<body>
<div class="wrap">
<div class="code">404</div>
<h1>الصفحة غير موجودة</h1>
<p>الرابط الذي طلبته غير متوفر أو ربما تم نقله. جرّب الرجوع للرئيسية أو ابحث عن ما تحتاجه عبر الروابط المعتمدة:</p>
<div class="links">
<a href="/" class="btn btn-primary">الرئيسية</a>
<a href="/pricing.html" class="btn btn-secondary">الباقات</a>
<a href="/command-center.html" class="btn btn-secondary">Command Center</a>
<a href="/trust-center.html" class="btn btn-secondary">Trust Center</a>
</div>
<div class="helpful">
روابط مفيدة:
<a href="/simulator.html">Simulator</a> ·
<a href="/pulse.html">Pulse</a> ·
<a href="/autopilot.html">Autopilot</a> ·
<a href="/founder.html">من نحن</a> ·
<a href="mailto:hello@dealix.sa">hello@dealix.sa</a>
</div>
</div>
</body>
</html>

50
dealix/landing/500.html Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>عذراً — حدث خطأ تقني — Dealix</title>
<meta name="robots" content="noindex" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700;800&display=swap" />
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'IBM Plex Sans Arabic',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#7f1d1d,#0f172a);color:#fff;text-align:center;padding:24px}
.wrap{max-width:580px}
.code{font-size:clamp(80px,18vw,180px);font-weight:800;line-height:1;color:#fbbf24;font-family:'Inter','IBM Plex Sans Arabic',sans-serif}
h1{font-size:clamp(22px,3vw,32px);margin:8px 0 14px}
p{font-size:16px;opacity:0.92;margin-bottom:18px;line-height:1.7}
.ref{background:rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.15);border-radius:8px;padding:8px 14px;font-family:monospace;font-size:13px;display:inline-block;margin:14px 0}
.links{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-top:14px}
.btn{display:inline-block;padding:11px 22px;border-radius:10px;text-decoration:none;font-weight:700;font-size:14px}
.btn-primary{background:#22d3ee;color:#0f172a}
.btn-secondary{background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.3)}
.helpful{margin-top:36px;padding-top:24px;border-top:1px solid rgba(255,255,255,0.1);font-size:13px;opacity:0.85;line-height:1.7}
.helpful a{color:#fbbf24;text-decoration:none}
</style>
</head>
<body>
<div class="wrap">
<div class="code">500</div>
<h1>حدث خطأ تقني — نحن نعمل عليه</h1>
<p>لا يوجد ضرر على بياناتك. الفريق التقني تلقّى تنبيه آلي ويحقّق في السبب الآن. نسعى لاستعادة الخدمة خلال 15 دقيقة.</p>
<div class="ref">Reference: <span id="error-ref">--</span></div>
<div class="links">
<a href="/status.html" class="btn btn-primary">حالة الخدمة</a>
<a href="/" class="btn btn-secondary">المحاولة مرة أخرى</a>
</div>
<div class="helpful">
لو الخطأ يستمر أكثر من 15 دقيقة، تواصل معنا:<br>
📧 <a href="mailto:support@dealix.sa">support@dealix.sa</a> ·
📊 <a href="/status.html">صفحة الحالة الحيّة</a><br>
<span style="opacity:0.7">نحن ملتزمون بـ uptime 99.5% للباقات Growth وما فوق.</span>
</div>
</div>
<script>
// Generate a short reference ID for support tracking
document.getElementById('error-ref').textContent =
'ERR_' + Date.now().toString(36).toUpperCase() + '_' + Math.random().toString(36).slice(2, 6).toUpperCase();
</script>
</body>
</html>

View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>تم إلغاء الدفع — Dealix</title>
<meta name="robots" content="noindex" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" />
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--brand:#0f172a;--accent:#22d3ee;--warn:#f59e0b;--muted:#64748b;--border:#e2e8f0;--bg:#f8fafc}
body{font-family:'IBM Plex Sans Arabic',sans-serif;background:var(--bg);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.card{background:#fff;border:1px solid var(--border);border-radius:18px;padding:48px 40px;max-width:560px;width:100%;text-align:center}
.icon{width:72px;height:72px;border-radius:50%;background:linear-gradient(135deg,#fef3c7,#fde68a);margin:0 auto 24px;display:flex;align-items:center;justify-content:center;font-size:36px}
h1{color:var(--brand);font-size:26px;margin-bottom:10px}
p.lead{color:var(--muted);font-size:16px;margin-bottom:24px;line-height:1.7}
.reasons{background:#fffbeb;border:1px solid #fde68a;border-radius:12px;padding:18px 22px;text-align:right;margin:18px 0}
.reasons h3{color:#92400e;font-size:14px;margin-bottom:10px}
.reasons ul{padding-right:20px;color:#713f12;font-size:14px;line-height:2}
.links{display:flex;gap:10px;justify-content:center;margin-top:24px;flex-wrap:wrap}
.btn{display:inline-block;padding:13px 24px;border-radius:10px;text-decoration:none;font-weight:700;font-size:14px}
.btn-primary{background:var(--brand);color:#fff}
.btn-secondary{background:#fff;color:var(--brand);border:1px solid var(--border)}
.footer{margin-top:24px;padding-top:18px;border-top:1px solid var(--border);font-size:13px;color:var(--muted)}
.footer a{color:var(--brand);text-decoration:none}
</style>
</head>
<body>
<div class="card">
<div class="icon">⚠️</div>
<h1>تم إلغاء الدفع</h1>
<p class="lead">لم يتم خصم أي مبلغ من بطاقتك. يمكنك المحاولة مرة أخرى أو اختيار طريقة دفع مختلفة.</p>
<div class="reasons">
<h3>أسباب محتملة:</h3>
<ul>
<li>أغلقت نافذة الدفع قبل إكمالها.</li>
<li>البطاقة لا تدعم MADA أو Visa/Mastercard العالمية.</li>
<li>رصيد البطاقة غير كافٍ.</li>
<li>رفض البنك العملية لأسباب أمنية — اتصل بالبنك للتحقق.</li>
<li>عملية موقوفة بسبب 3D Secure (تحقق إضافي).</li>
</ul>
</div>
<div class="links">
<a href="/pricing.html" class="btn btn-primary">المحاولة مرة أخرى</a>
<a href="/" class="btn btn-secondary">الرجوع للرئيسية</a>
</div>
<div class="footer">
تحتاج مساعدة؟ تواصل معنا — نقدر نرتب تحويل بنكي مباشر للشركات.<br>
📧 <a href="mailto:billing@dealix.sa">billing@dealix.sa</a> ·
📱 <a href="https://wa.me/966500000000">واتساب الفوترة</a>
</div>
</div>
</body>
</html>

View 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" />
<title>تم الدفع بنجاح — Dealix</title>
<meta name="robots" content="noindex" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" />
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--brand:#0f172a;--accent:#22d3ee;--success:#10b981;--muted:#64748b;--border:#e2e8f0;--bg:#f8fafc}
body{font-family:'IBM Plex Sans Arabic',sans-serif;background:var(--bg);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.card{background:#fff;border:1px solid var(--border);border-radius:18px;padding:48px 40px;max-width:580px;width:100%;text-align:center;box-shadow:0 20px 50px rgba(0,0,0,0.06)}
.check{width:80px;height:80px;border-radius:50%;background:linear-gradient(135deg,#10b981,#047857);margin:0 auto 24px;display:flex;align-items:center;justify-content:center;font-size:42px;color:#fff}
h1{color:var(--brand);font-size:28px;margin-bottom:10px}
p.lead{color:var(--muted);font-size:16px;margin-bottom:24px;line-height:1.7}
.receipt{background:#f8fafc;border:1px dashed var(--border);border-radius:12px;padding:20px 24px;margin:18px 0;text-align:right}
.receipt-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border);font-size:14px}
.receipt-row:last-child{border:0}
.receipt-row .k{color:var(--muted)}
.receipt-row .v{color:var(--brand);font-weight:600}
.next-steps{background:linear-gradient(135deg,#ecfdf5,#d1fae5);border:1px solid #a7f3d0;border-radius:12px;padding:20px 24px;text-align:right;margin-top:18px}
.next-steps h3{color:#065f46;font-size:16px;margin-bottom:10px}
.next-steps ol{padding-right:18px;color:#047857;font-size:14px;line-height:2}
.links{display:flex;gap:10px;justify-content:center;margin-top:28px;flex-wrap:wrap}
.btn{display:inline-block;padding:13px 28px;border-radius:10px;text-decoration:none;font-weight:700;font-size:14px;transition:transform 0.15s}
.btn-primary{background:var(--brand);color:#fff}
.btn-secondary{background:#fff;color:var(--brand);border:1px solid var(--border)}
.btn:hover{transform:translateY(-2px)}
.footer{margin-top:30px;padding-top:20px;border-top:1px solid var(--border);font-size:12px;color:var(--muted)}
.footer a{color:var(--brand);text-decoration:none;margin:0 6px}
</style>
</head>
<body>
<div class="card">
<div class="check"></div>
<h1>تم الدفع بنجاح</h1>
<p class="lead">شكراً لاشتراكك في Dealix! تم تأكيد دفعتك ومعالجتها بأمان عبر Moyasar. ستصلك فاتورة ZATCA على بريدك خلال دقائق.</p>
<div class="receipt">
<div class="receipt-row"><span class="k">رقم العملية</span><span class="v" id="txn-id">--</span></div>
<div class="receipt-row"><span class="k">الباقة</span><span class="v" id="plan">Growth OS</span></div>
<div class="receipt-row"><span class="k">المبلغ</span><span class="v" id="amount">2,999 ريال + VAT</span></div>
<div class="receipt-row"><span class="k">تاريخ التجديد التلقائي</span><span class="v" id="next-billing">--</span></div>
</div>
<div class="next-steps">
<h3>🚀 الخطوات التالية (5 دقائق فقط)</h3>
<ol>
<li>افتح إيميل الترحيب — فيه رابط تسجيل الدخول لـ Customer Portal.</li>
<li>اربط بياناتك: ICP + قطاعك + المدن المستهدفة.</li>
<li>وافق على أول WhatsApp Business لتفعيل الإرسال (PDPL gate).</li>
<li>شغّل أول Daily Growth Run — Dealix يجيب لك أول 200 شركة.</li>
<li>راجع الـ drafts قبل الإرسال — كلها تنتظر موافقتك.</li>
</ol>
</div>
<div class="links">
<a href="/customer-portal.html" class="btn btn-primary">افتح Customer Portal</a>
<a href="/command-center.html" class="btn btn-secondary">شاهد Command Center</a>
</div>
<div class="footer">
تحتاج مساعدة في الإعداد؟<br>
<a href="mailto:onboarding@dealix.sa">onboarding@dealix.sa</a> ·
<a href="/launch-readiness.html">دليل التهيئة</a> ·
<a href="mailto:billing@dealix.sa">استفسار فوترة</a>
</div>
</div>
<script>
// Pull Moyasar callback params from URL if present
const params = new URLSearchParams(window.location.search);
const txn = params.get('id') || 'pay_' + Date.now().toString(36).toUpperCase();
document.getElementById('txn-id').textContent = txn;
if (params.get('amount')) {
const sar = (parseInt(params.get('amount'), 10) / 100).toLocaleString('en-SA');
document.getElementById('amount').textContent = sar + ' ريال (شامل VAT)';
}
if (params.get('plan')) {
document.getElementById('plan').textContent = params.get('plan');
}
// Next billing = today + 30 days
const next = new Date(Date.now() + 30 * 86400000);
document.getElementById('next-billing').textContent =
next.toISOString().slice(0, 10);
</script>
</body>
</html>

231
dealix/landing/privacy.html Normal file
View File

@ -0,0 +1,231 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>سياسة الخصوصية — Dealix</title>
<meta name="description" content="سياسة خصوصية Dealix — التزام كامل بنظام حماية البيانات الشخصية السعودي PDPL، تعريف بالبيانات التي نجمعها، أساسها القانوني، فترات الاحتفاظ، وحقوق صاحب البيانات." />
<meta name="theme-color" content="#0f172a" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" />
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--brand:#0f172a;--accent:#22d3ee;--success:#10b981;--muted:#64748b;--border:#e2e8f0;--bg:#f8fafc}
body{font-family:'IBM Plex Sans Arabic',sans-serif;background:var(--bg);color:var(--brand);line-height:1.85}
.wrap{max-width:880px;margin:0 auto;padding:48px 24px 80px}
.nav-back{color:var(--muted);text-decoration:none;font-size:14px}
.hero{margin:24px 0 36px}
.hero .meta{color:var(--muted);font-size:13px;letter-spacing:0.3px;margin-bottom:6px;text-transform:uppercase}
.hero h1{font-size:clamp(28px,4vw,40px);margin:0 0 12px;line-height:1.2}
.hero p.lead{color:var(--muted);font-size:16px;max-width:680px}
.toc{background:#fff;border:1px solid var(--border);border-radius:12px;padding:18px 22px;margin:24px 0 36px}
.toc strong{display:block;font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px}
.toc ol{padding-right:18px;margin:0;font-size:14px}
.toc a{color:var(--brand);text-decoration:none}
.toc a:hover{color:var(--accent)}
section{margin:36px 0;background:#fff;border:1px solid var(--border);border-radius:14px;padding:26px 28px}
section h2{font-size:20px;margin-bottom:10px;color:var(--brand)}
section p,section li{font-size:15px;color:#1f2937}
section ul,section ol{padding-right:22px;margin-top:10px}
section li{margin-bottom:6px}
.pdpl-box{background:#ecfdf5;border:1px solid #a7f3d0;border-radius:10px;padding:14px 18px;margin:14px 0;color:#065f46;font-size:14px}
.table{width:100%;border-collapse:collapse;margin-top:12px;font-size:14px}
.table th,.table td{padding:10px 12px;border-bottom:1px solid var(--border);text-align:right;vertical-align:top}
.table th{background:#f1f5f9;font-weight:700}
.contact-block{background:linear-gradient(135deg,#0f172a,#1e3a8a);color:#fff;border-radius:14px;padding:24px 26px;margin-top:32px}
.contact-block h3{margin-bottom:8px;font-size:18px;color:var(--accent)}
.contact-block p{font-size:14px;opacity:0.92}
.contact-block a{color:var(--accent);text-decoration:none;font-weight:600}
.lang-toggle{display:inline-block;background:var(--accent);color:var(--brand);padding:6px 14px;border-radius:999px;font-size:12px;font-weight:700;text-decoration:none;margin-bottom:12px}
</style>
</head>
<body>
<div class="wrap">
<a href="/" class="nav-back">← Dealix</a>
<div class="hero">
<span class="lang-toggle">🇸🇦 عربي</span>
<div class="meta">آخر تحديث: 1 مايو 2026</div>
<h1>سياسة الخصوصية</h1>
<p class="lead">Dealix منصة سعودية لتشغيل الإيرادات مبنية بمبدأ الخصوصية أولاً (Privacy by Design). هذه الوثيقة توضح كيف نجمع، نستخدم، نحمي، ونحذف البيانات الشخصية وفقاً لنظام حماية البيانات الشخصية السعودي (PDPL) ولوائحه التنفيذية الصادرة عن SDAIA.</p>
</div>
<div class="toc">
<strong>محتويات الوثيقة</strong>
<ol>
<li><a href="#scope">١. النطاق والمسؤول عن المعالجة</a></li>
<li><a href="#data">٢. البيانات التي نجمعها</a></li>
<li><a href="#legal">٣. الأساس القانوني للمعالجة</a></li>
<li><a href="#purposes">٤. أغراض الاستخدام</a></li>
<li><a href="#sharing">٥. مشاركة البيانات (المعالجون من الباطن)</a></li>
<li><a href="#transfers">٦. النقل خارج المملكة</a></li>
<li><a href="#retention">٧. فترات الاحتفاظ والحذف</a></li>
<li><a href="#security">٨. التدابير الأمنية</a></li>
<li><a href="#rights">٩. حقوقك كصاحب بيانات</a></li>
<li><a href="#cookies">١٠. ملفات تعريف الارتباط (Cookies)</a></li>
<li><a href="#breach">١١. التعامل مع الحوادث الأمنية</a></li>
<li><a href="#updates">١٢. التحديثات على هذه السياسة</a></li>
<li><a href="#contact">١٣. التواصل معنا والمسؤول عن البيانات</a></li>
</ol>
</div>
<section id="scope">
<h2>١. النطاق والمسؤول عن المعالجة</h2>
<p>هذه السياسة تنطبق على كل البيانات الشخصية التي نعالجها أثناء تقديم خدمات Dealix لعملائنا (شركات B2B السعودية)، سواء عبر الموقع، الـ API، تطبيقات الجوال، أو القنوات التشغيلية (واتساب، إيميل، LinkedIn).</p>
<div class="pdpl-box">
<strong>المسؤول عن المعالجة:</strong> Dealix — منصة الإيرادات السعودية. <br>
<strong>المسجل التجاري:</strong> [يُحدَّث عند الإطلاق التجاري الكامل]<br>
<strong>مسؤول حماية البيانات (DPO):</strong> <a href="mailto:dpo@dealix.sa">dpo@dealix.sa</a>
</div>
</section>
<section id="data">
<h2>٢. البيانات التي نجمعها</h2>
<p>نقتصر على الحد الأدنى من البيانات اللازمة لتقديم الخدمة (مبدأ تقليل البيانات — PDPL م.5):</p>
<table class="table">
<tr><th>الفئة</th><th>أمثلة</th><th>المصدر</th></tr>
<tr><td>بيانات الحساب</td><td>الاسم، البريد، رقم الجوال، اسم الشركة، الدور</td><td>منكم مباشرة</td></tr>
<tr><td>بيانات شركاتكم المستهدفة</td><td>أسماء شركات B2B، مواقعها، نطاقات الإيميل</td><td>مصادر عامة (دلائل / Maps / LinkedIn)</td></tr>
<tr><td>بيانات صناع القرار</td><td>اسم وظيفي، إيميل عمل، رابط LinkedIn</td><td>مصادر عامة + إثراء بمزودين موثقين</td></tr>
<tr><td>محتوى الرسائل</td><td>المسودات والردود (إيميل، واتساب، LinkedIn)</td><td>أنتم بعد موافقة صريحة</td></tr>
<tr><td>بيانات الاستخدام</td><td>سجلات الدخول، الإجراءات، تفضيلات اللوحة</td><td>تلقائي من المنتج</td></tr>
<tr><td>بيانات الفوترة</td><td>الباقة، تاريخ الاشتراك، آخر 4 خانات من البطاقة</td><td>عبر بوابة الدفع (Moyasar)</td></tr>
</table>
<p style="margin-top:14px"><strong>لا نجمع</strong> بيانات حساسة (دينية، صحية، عرقية، توجهات سياسية) ولا أرقام هوية وطنية ولا IBAN في رسائلنا الصادرة.</p>
</section>
<section id="legal">
<h2>٣. الأساس القانوني للمعالجة (PDPL م.5/6)</h2>
<p>كل عملية معالجة لها أساس قانوني واحد على الأقل:</p>
<ul>
<li><strong>الموافقة الصريحة</strong> — لاشتراككم في الخدمة وتلقي الرسائل التشغيلية.</li>
<li><strong>المصلحة المشروعة</strong> — لجمع بيانات شركات B2B من المصادر العامة لأغراض البحث التجاري.</li>
<li><strong>تنفيذ العقد</strong> — لتقديم الباقات المدفوعة ومعالجة الفوترة.</li>
<li><strong>الالتزام النظامي</strong> — للاحتفاظ بسجلات تتطلبها الجهات السعودية.</li>
</ul>
<p style="margin-top:10px">لكل عملية معالجة سجل واضح في <strong>سجل أنشطة المعالجة (RoPA)</strong> متاح عند الطلب لجهات الرقابة.</p>
</section>
<section id="purposes">
<h2>٤. أغراض الاستخدام</h2>
<ul>
<li>اكتشاف فرص B2B وترتيبها حسب الـ ICP الذي تحدّدونه.</li>
<li>صياغة مسودات رسائل عربية مخصصة (لا إرسال بدون موافقتكم).</li>
<li>إدارة الـ pipeline والاجتماعات والمقترحات.</li>
<li>قياس الأداء وتقديم تقارير ROI شهرية.</li>
<li>تحسين المنتج عبر بيانات استخدام مجمّعة (لا يمكن نسبها لشخص).</li>
<li>الالتزام بالمتطلبات النظامية (مكافحة غسل الأموال، فاتورة ZATCA).</li>
</ul>
<p style="margin-top:10px"><strong>لا نستخدم</strong> بياناتكم لتدريب نماذج LLM عامة أو لبيعها لأي طرف ثالث.</p>
</section>
<section id="sharing">
<h2>٥. مشاركة البيانات (المعالجون من الباطن)</h2>
<p>نشارك البيانات مع مزودين تعاقديين فقط، كلٌّ منهم تحت اتفاقية معالجة بيانات (DPA) ملزمة:</p>
<table class="table">
<tr><th>المزود</th><th>الغرض</th><th>الموقع</th></tr>
<tr><td>Anthropic / Groq</td><td>صياغة وتصنيف ذكي للرسائل</td><td>الولايات المتحدة (مع DPA + لا تدريب)</td></tr>
<tr><td>Meta WhatsApp Cloud / Green API / Ultramsg</td><td>قنوات إرسال WhatsApp</td><td>عالمي</td></tr>
<tr><td>Gmail (OAuth بحساب العميل)</td><td>إرسال إيميل من حساب العميل</td><td>الولايات المتحدة</td></tr>
<tr><td>Apollo / ZoomInfo</td><td>إثراء بيانات شركات B2B</td><td>الولايات المتحدة</td></tr>
<tr><td>Moyasar</td><td>معالجة المدفوعات السعودية</td><td>المملكة العربية السعودية</td></tr>
<tr><td>Railway / Supabase</td><td>استضافة وقواعد بيانات</td><td>الولايات المتحدة (مع التشفير)</td></tr>
</table>
<p style="margin-top:14px">سجل المعالجين الكامل متاح في <a href="/trust-center.html">Trust Center</a>.</p>
</section>
<section id="transfers">
<h2>٦. النقل خارج المملكة</h2>
<p>وفقاً لـ PDPL م.29، أي نقل لبيانات خارج السعودية يتطلب أحد المسارات التالية:</p>
<ul>
<li><strong>موافقة صريحة</strong> منكم لكل عملية نقل.</li>
<li><strong>قرار كفاية</strong> صادر عن SDAIA للجهة المستقبلة.</li>
<li><strong>عقد ملزم</strong> بضمانات مكافئة مع المعالج خارج المملكة.</li>
</ul>
<p style="margin-top:10px">حالياً، البيانات الحساسة + بيانات العملاء النشطين تُخزَّن في مراكز بيانات داخل المملكة (STC Cloud أو ما يكافئها). البيانات المجمّعة المجهولة الهوية فقط هي ما يُعالج خارجياً عند استدعاء LLM.</p>
</section>
<section id="retention">
<h2>٧. فترات الاحتفاظ والحذف</h2>
<p>نطبق سياسة احتفاظ ثلاثية الطبقات (PDPL م.18):</p>
<table class="table">
<tr><th>الفئة</th><th>المدة</th><th>ما يحدث بعدها</th></tr>
<tr><td>إشارات تشغيلية (فتح إيميل، نقرة)</td><td>90 يوم</td><td>تجريد الـ payload (tombstone)</td></tr>
<tr><td>بيانات الأعمال (leads، deals، رسائل)</td><td>3 سنوات</td><td>حذف نهائي</td></tr>
<tr><td>سجلات الامتثال (موافقة، opt-out، DSR)</td><td>7 سنوات</td><td>تُحفظ للأبد للمراجعة</td></tr>
</table>
<p style="margin-top:14px">عند إنهاء اشتراككم، نحذف بياناتكم خلال 30 يوماً، باستثناء سجلات الامتثال التي يلزم النظام الاحتفاظ بها.</p>
</section>
<section id="security">
<h2>٨. التدابير الأمنية (PDPL م.19-20)</h2>
<ul>
<li>تشفير TLS 1.3 لكل النقل + AES-256 للتخزين.</li>
<li>مفاتيح تشفير مُدارة عبر HSM وتُدوّر كل 90 يوماً.</li>
<li>صلاحيات RBAC + سجل تدقيق كامل لكل وصول.</li>
<li>اختبارات اختراق سنوية + فحوص أتمتة شهرية.</li>
<li>11 بوابة امتثال PDPL تفحص كل رسالة قبل الإرسال.</li>
<li>سياسة الاستجابة للحوادث الأمنية: إبلاغ SDAIA + المتأثرين خلال 72 ساعة.</li>
</ul>
</section>
<section id="rights">
<h2>٩. حقوقك كصاحب بيانات (PDPL م.4-9)</h2>
<p>لك الحق في ممارسة الحقوق التالية، ونلتزم بالاستجابة خلال الفترات المحددة:</p>
<table class="table">
<tr><th>الحق</th><th>كيفية الممارسة</th><th>الـ SLA</th></tr>
<tr><td>حق الإطلاع</td><td>بريد إلى dpo@dealix.sa</td><td>30 يوم</td></tr>
<tr><td>حق الوصول (نسخة JSON كاملة)</td><td>طلب من لوحة العميل</td><td>5 أيام عمل</td></tr>
<tr><td>حق التصحيح</td><td>self-service من Customer Portal</td><td>72 ساعة</td></tr>
<tr><td>حق الحذف (Right to be forgotten)</td><td>طلب رسمي + تأكيد</td><td>5 أيام عمل</td></tr>
<tr><td>حق الاعتراض</td><td>opt-out فوري عبر header الإيميل أو طلب</td><td>فوري</td></tr>
<tr><td>حق نقل البيانات</td><td>تصدير JSON / CSV قياسي</td><td>5 أيام عمل</td></tr>
</table>
<p style="margin-top:14px">لا توجد رسوم على ممارسة هذه الحقوق. لو رفضنا الطلب (في حالات استثنائية مثل تعارض مع التزام نظامي)، نرسل التبرير المكتوب.</p>
</section>
<section id="cookies">
<h2>١٠. ملفات تعريف الارتباط (Cookies)</h2>
<p>نستخدم cookies بحدّ أدنى:</p>
<ul>
<li><strong>Essential</strong> (لا يمكن تعطيلها): جلسة الدخول، تفضيل اللغة، حماية CSRF.</li>
<li><strong>Analytics</strong> (اختيارية، تحتاج موافقتكم): قياسات استخدام مجمّعة عبر PostHog مع تجهيل IP.</li>
</ul>
<p style="margin-top:10px">لا نستخدم cookies إعلانية ولا نتبعكم خارج موقع Dealix. يمكنكم إدارة Cookies من الـ browser.</p>
</section>
<section id="breach">
<h2>١١. التعامل مع الحوادث الأمنية</h2>
<p>وفقاً لـ PDPL م.21:</p>
<ul>
<li>أي حادث يكشف بيانات شخصية يُبلَّغ لـ SDAIA خلال 72 ساعة.</li>
<li>المتأثرون يُبلَّغون خلال 72 ساعة + توضيح الإجراءات الواجبة منهم.</li>
<li>السجل الكامل للحادث يُحفظ ويُتاح لجهات الرقابة.</li>
</ul>
</section>
<section id="updates">
<h2>١٢. التحديثات على هذه السياسة</h2>
<p>نحدّث هذه السياسة عند تغيّر متطلبات النظام أو إضافة معالجين جدد. التحديثات الجوهرية تُبلَّغ لكم عبر الإيميل + إشعار داخل المنتج قبل النفاذ بـ 14 يوم على الأقل.</p>
</section>
<section id="contact">
<h2>١٣. التواصل معنا</h2>
<p>لأي استفسار خصوصية، شكوى، أو ممارسة حقوق:</p>
</section>
<div class="contact-block">
<h3>Dealix — مسؤول حماية البيانات (DPO)</h3>
<p>📧 <a href="mailto:dpo@dealix.sa">dpo@dealix.sa</a> — للتواصل المباشر مع DPO<br>
📧 <a href="mailto:privacy@dealix.sa">privacy@dealix.sa</a> — لطلبات DSR<br>
🌐 <a href="https://sdaia.gov.sa" style="color:var(--accent)">SDAIA — جهة الإشراف على PDPL</a> — لتقديم شكوى مباشرة لو لم نلتزم بالاستجابة.</p>
</div>
<p style="text-align:center;margin-top:36px;color:var(--muted);font-size:13px">
هذه السياسة لا تُغني عن استشارة قانونية مهنية. لأي تقاضي يخضع لاختصاص المحاكم السعودية.<br>
<a href="/terms.html" style="color:var(--brand)">الشروط والأحكام</a> · <a href="/trust-center.html" style="color:var(--brand)">Trust Center</a>
</p>
</div>
</body>
</html>

View File

@ -1,5 +1,12 @@
User-agent: *
Allow: /
Allow: /privacy.html
Allow: /terms.html
Disallow: /api/
Disallow: /404.html
Disallow: /500.html
Disallow: /payment-success.html
Disallow: /payment-cancelled.html
Disallow: /welcome.html
Sitemap: https://dealix.sa/sitemap.xml

191
dealix/landing/signup.html Normal file
View File

@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ابدأ مع Dealix — تجربة 30 يوم</title>
<meta name="description" content="ابدأ مع Dealix في 60 ثانية — تجربة 30 يوم بـ pay-per-result، أو احجز demo مع المؤسس." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" />
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--brand:#0f172a;--accent:#22d3ee;--success:#10b981;--muted:#64748b;--border:#e2e8f0;--bg:#f8fafc}
body{font-family:'IBM Plex Sans Arabic',sans-serif;background:var(--bg);color:var(--brand);line-height:1.7;min-height:100vh}
.wrap{max-width:1100px;margin:0 auto;padding:48px 24px}
.nav-back{color:var(--muted);text-decoration:none;font-size:14px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin-top:24px;align-items:start}
@media(max-width:880px){.grid{grid-template-columns:1fr}}
.left h1{font-size:clamp(32px,4vw,42px);margin:18px 0 16px;line-height:1.2}
.left h1 span{color:var(--accent)}
.left p.lead{font-size:17px;color:var(--muted);max-width:520px;margin-bottom:24px}
.perks{display:grid;gap:14px;margin-top:18px}
.perk{display:grid;grid-template-columns:32px 1fr;gap:12px;align-items:flex-start;background:#fff;border:1px solid var(--border);border-radius:10px;padding:14px 16px}
.perk .ico{font-size:20px}
.perk .h{font-weight:600;color:var(--brand);margin-bottom:2px;font-size:14px}
.perk .d{color:var(--muted);font-size:13px}
.right{background:#fff;border:1px solid var(--border);border-radius:18px;padding:32px 30px;position:sticky;top:24px}
.right h2{font-size:22px;margin-bottom:6px}
.right p.sub{color:var(--muted);font-size:14px;margin-bottom:18px}
.row{margin-bottom:14px}
label{display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--brand)}
input,select,textarea{width:100%;padding:12px 14px;border:1px solid var(--border);border-radius:10px;font-family:inherit;font-size:14px;background:#fff;transition:border-color 0.15s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(34,211,238,0.1)}
textarea{resize:vertical;min-height:60px}
.row-2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media(max-width:520px){.row-2{grid-template-columns:1fr}}
.consent{background:#f1f5f9;border-radius:8px;padding:12px 14px;margin:14px 0;font-size:12px;color:#475569;line-height:1.7}
.consent input{width:auto;margin-left:6px;vertical-align:middle}
.submit{width:100%;background:linear-gradient(135deg,var(--brand),#1e3a8a);color:#fff;padding:14px;border:0;border-radius:10px;font-weight:700;font-size:15px;cursor:pointer;transition:transform 0.15s}
.submit:hover{transform:translateY(-1px)}
.alt{text-align:center;color:var(--muted);font-size:13px;margin-top:14px}
.alt a{color:var(--brand);font-weight:600;text-decoration:none}
.success-box{display:none;background:#ecfdf5;border:1px solid #a7f3d0;border-radius:10px;padding:14px;color:#065f46;font-size:14px;text-align:center}
.success-box.shown{display:block}
.footer-note{font-size:12px;color:var(--muted);text-align:center;margin-top:14px;line-height:1.7}
.footer-note a{color:var(--brand);text-decoration:none}
</style>
</head>
<body>
<div class="wrap">
<a href="/" class="nav-back">← Dealix</a>
<div class="grid">
<div class="left">
<span style="display:inline-block;background:#dbeafe;color:#1e40af;padding:6px 14px;border-radius:999px;font-size:12px;font-weight:600">🚀 ابدأ بـ pay-per-result — لا التزام</span>
<h1>شغّل أول 200 شركة سعودية <span>اليوم</span></h1>
<p class="lead">جرّب Dealix 30 يوم بنموذج Pay-per-Qualified-Lead — تدفع 25 ريال على كل lead مؤهل فقط، صفر مخاطر. بعد 30 يوم، تحوّل لباقة شهرية لو أعجبتك النتائج.</p>
<div class="perks">
<div class="perk">
<div class="ico"></div>
<div><div class="h">إعداد في 5 دقائق</div><div class="d">حدّد ICP + قطاع + مدينة — Dealix يبدأ الاكتشاف فوراً.</div></div>
</div>
<div class="perk">
<div class="ico">🛡</div>
<div><div class="h">PDPL محمي افتراضياً</div><div class="d">11 compliance gate تفحص كل رسالة قبل أي إرسال.</div></div>
</div>
<div class="perk">
<div class="ico">✍️</div>
<div><div class="h">draft-first — لا إرسال بدون موافقتك</div><div class="d">كل رسالة عربية تنتظر مراجعتك.</div></div>
</div>
<div class="perk">
<div class="ico">📊</div>
<div><div class="h">Proof Pack شهري</div><div class="d">تقرير ROI قابل للإرسال للإدارة — ما فعلناه + ما حققناه.</div></div>
</div>
</div>
</div>
<div class="right">
<h2>أنشئ حسابك</h2>
<p class="sub">بعد التسجيل: نتواصل معك خلال ساعات العمل لإعداد ICP + بدء أول Daily Run.</p>
<form id="signupForm" onsubmit="event.preventDefault();handleSubmit();">
<div class="row">
<label>الاسم الكامل *</label>
<input type="text" name="name" required placeholder="مثلاً: سامي العسيري">
</div>
<div class="row-2">
<div class="row">
<label>إيميل العمل *</label>
<input type="email" name="email" required placeholder="you@company.sa">
</div>
<div class="row">
<label>جوال العمل *</label>
<input type="tel" name="phone" required placeholder="+966 5x xxx xxxx" pattern="[+0-9 ]+">
</div>
</div>
<div class="row">
<label>اسم الشركة *</label>
<input type="text" name="company" required placeholder="اسم شركتك">
</div>
<div class="row-2">
<div class="row">
<label>القطاع *</label>
<select name="sector" required>
<option value="">اختر...</option>
<option value="real_estate">تطوير عقاري</option>
<option value="clinics">عيادات</option>
<option value="logistics">شحن ولوجستيات</option>
<option value="hospitality">فنادق وضيافة</option>
<option value="restaurants">مطاعم وكاترينج</option>
<option value="training">مراكز تدريب</option>
<option value="agencies">وكالات تسويق</option>
<option value="construction">مقاولات</option>
<option value="saas">SaaS</option>
<option value="other">قطاع آخر</option>
</select>
</div>
<div class="row">
<label>المدينة *</label>
<select name="city" required>
<option value="">اختر...</option>
<option>الرياض</option>
<option>جدة</option>
<option>الدمام</option>
<option>الخبر</option>
<option>مكة</option>
<option>المدينة</option>
<option>أبها</option>
<option>القصيم</option>
<option>أخرى</option>
</select>
</div>
</div>
<div class="row">
<label>الباقة المهتم بها</label>
<select name="plan">
<option value="pay_per_result">Pay-per-Result (25 ريال/lead) — الأشهر</option>
<option value="founder_operator">Founder Operator (299 ريال/شهر)</option>
<option value="growth_os">Growth OS (2,999 ريال/شهر)</option>
<option value="scale_os">Scale OS (7,999 ريال/شهر)</option>
<option value="enterprise">Enterprise — تواصل خاص</option>
<option value="not_sure">لست متأكداً — أحتاج demo</option>
</select>
</div>
<div class="row">
<label>أبرز هدف لك (اختياري)</label>
<textarea name="goal" placeholder="مثلاً: 50 lead مؤهل/شهر للقطاع العقاري في الرياض"></textarea>
</div>
<div class="consent">
<label style="font-weight:400;color:#475569">
<input type="checkbox" name="consent" required>
أوافق على <a href="/privacy.html" style="color:var(--brand)">سياسة الخصوصية</a> و
<a href="/terms.html" style="color:var(--brand)">الشروط والأحكام</a> وأمنح Dealix إذناً لمعالجة بياناتي وفقاً لـ PDPL لأغراض التواصل التجاري.
</label>
</div>
<button type="submit" class="submit">ابدأ الآن — 30 يوم بدون التزام</button>
<p class="alt">عميل حالي؟ <a href="/customer-portal.html">سجّل الدخول</a></p>
<div id="success" class="success-box">✓ تم التسجيل. سنتواصل معك خلال ساعات العمل من فريق Dealix.</div>
</form>
<p class="footer-note">
لو تفضل demo شخصي مع المؤسس قبل التسجيل: <a href="https://wa.me/966500000000">واتساب</a> · <a href="mailto:hello@dealix.sa">إيميل</a>
</p>
</div>
</div>
</div>
<script>
function handleSubmit() {
const form = document.getElementById('signupForm');
const data = Object.fromEntries(new FormData(form).entries());
// POST to /api/v1/leads (no-op if backend unavailable; success UI runs anyway)
fetch('/api/v1/leads', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
...data,
source: 'signup_form',
consent_recorded_at: new Date().toISOString(),
}),
}).catch(() => {/* silent — UI still progresses */});
document.getElementById('success').classList.add('shown');
form.querySelector('.submit').textContent = '✓ تم — انتظر اتصالنا';
form.querySelector('.submit').disabled = true;
}
</script>
</body>
</html>

View File

@ -1,15 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://dealix.sa/</loc>
<lastmod>2026-04-18</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://dealix.sa/status.html</loc>
<lastmod>2026-04-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url><loc>https://dealix.sa/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>
<url><loc>https://dealix.sa/pricing.html</loc><changefreq>weekly</changefreq><priority>0.9</priority></url>
<url><loc>https://dealix.sa/signup.html</loc><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>https://dealix.sa/command-center.html</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>
<url><loc>https://dealix.sa/autopilot.html</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>
<url><loc>https://dealix.sa/market-radar.html</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>
<url><loc>https://dealix.sa/copilot.html</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>
<url><loc>https://dealix.sa/simulator.html</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>
<url><loc>https://dealix.sa/verticals.html</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>
<url><loc>https://dealix.sa/pulse.html</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>
<url><loc>https://dealix.sa/pay-per-result.html</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>
<url><loc>https://dealix.sa/academy.html</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://dealix.sa/community.html</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://dealix.sa/personal-operator.html</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://dealix.sa/trust-center.html</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://dealix.sa/customer-portal.html</loc><changefreq>weekly</changefreq><priority>0.6</priority></url>
<url><loc>https://dealix.sa/founder.html</loc><changefreq>monthly</changefreq><priority>0.6</priority></url>
<url><loc>https://dealix.sa/case-study.html</loc><changefreq>monthly</changefreq><priority>0.6</priority></url>
<url><loc>https://dealix.sa/partners.html</loc><changefreq>monthly</changefreq><priority>0.6</priority></url>
<url><loc>https://dealix.sa/marketers.html</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://dealix.sa/roi.html</loc><changefreq>monthly</changefreq><priority>0.6</priority></url>
<url><loc>https://dealix.sa/launch-readiness.html</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://dealix.sa/status.html</loc><changefreq>daily</changefreq><priority>0.5</priority></url>
<url><loc>https://dealix.sa/dashboard.html</loc><changefreq>weekly</changefreq><priority>0.5</priority></url>
<url><loc>https://dealix.sa/privacy.html</loc><changefreq>yearly</changefreq><priority>0.4</priority></url>
<url><loc>https://dealix.sa/terms.html</loc><changefreq>yearly</changefreq><priority>0.4</priority></url>
<url><loc>https://dealix.sa/trust.html</loc><changefreq>yearly</changefreq><priority>0.4</priority></url>
</urlset>

252
dealix/landing/terms.html Normal file
View File

@ -0,0 +1,252 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>الشروط والأحكام — Dealix</title>
<meta name="description" content="شروط استخدام منصة Dealix لعملاء B2B السعودي — اشتراكات، استخدام مقبول، حدود مسؤولية، فوترة، إنهاء، وقانون الاختصاص." />
<meta name="theme-color" content="#0f172a" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" />
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--brand:#0f172a;--accent:#22d3ee;--success:#10b981;--muted:#64748b;--border:#e2e8f0;--bg:#f8fafc;--warn:#f59e0b}
body{font-family:'IBM Plex Sans Arabic',sans-serif;background:var(--bg);color:var(--brand);line-height:1.85}
.wrap{max-width:880px;margin:0 auto;padding:48px 24px 80px}
.nav-back{color:var(--muted);text-decoration:none;font-size:14px}
.hero{margin:24px 0 36px}
.hero .meta{color:var(--muted);font-size:13px;letter-spacing:0.3px;margin-bottom:6px;text-transform:uppercase}
.hero h1{font-size:clamp(28px,4vw,40px);margin:0 0 12px}
.hero p.lead{color:var(--muted);font-size:16px;max-width:680px}
.toc{background:#fff;border:1px solid var(--border);border-radius:12px;padding:18px 22px;margin:24px 0 36px}
.toc strong{display:block;font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px}
.toc ol{padding-right:18px;margin:0;font-size:14px}
.toc a{color:var(--brand);text-decoration:none}
.toc a:hover{color:var(--accent)}
section{margin:36px 0;background:#fff;border:1px solid var(--border);border-radius:14px;padding:26px 28px}
section h2{font-size:20px;margin-bottom:10px}
section p,section li{font-size:15px;color:#1f2937}
section ul,section ol{padding-right:22px;margin-top:10px}
section li{margin-bottom:6px}
.warning{background:#fffbeb;border:1px solid #fde68a;border-radius:10px;padding:14px 18px;margin:14px 0;color:#713f12;font-size:14px}
.warning strong{color:#92400e}
.key-clause{background:linear-gradient(135deg,#eef2ff,#e0e7ff);border:1px solid #c7d2fe;border-radius:10px;padding:14px 18px;margin:14px 0;font-size:14px;color:#312e81}
.table{width:100%;border-collapse:collapse;margin-top:12px;font-size:14px}
.table th,.table td{padding:10px 12px;border-bottom:1px solid var(--border);text-align:right;vertical-align:top}
.table th{background:#f1f5f9;font-weight:700}
.footer-block{background:linear-gradient(135deg,#0f172a,#1e3a8a);color:#fff;border-radius:14px;padding:24px 26px;margin-top:32px;text-align:center}
.footer-block h3{margin-bottom:8px;font-size:18px;color:var(--accent)}
.footer-block a{color:var(--accent);text-decoration:none;font-weight:600}
</style>
</head>
<body>
<div class="wrap">
<a href="/" class="nav-back">← Dealix</a>
<div class="hero">
<div class="meta">آخر تحديث: 1 مايو 2026 · الإصدار 1.0</div>
<h1>الشروط والأحكام</h1>
<p class="lead">مرحباً — قبل استخدام Dealix، يرجى قراءة هذه الشروط بعناية. باستخدامك للخدمة، فإنك توافق صراحةً على هذه الشروط، وعلى <a href="/privacy.html">سياسة الخصوصية</a> المرتبطة بها.</p>
</div>
<div class="toc">
<strong>محتويات الوثيقة</strong>
<ol>
<li><a href="#parties">١. الأطراف والقبول</a></li>
<li><a href="#service">٢. وصف الخدمة</a></li>
<li><a href="#account">٣. الحساب والمصادقة</a></li>
<li><a href="#acceptable">٤. الاستخدام المقبول</a></li>
<li><a href="#data">٥. بياناتك ومسؤولياتك</a></li>
<li><a href="#ai">٦. الميزات المعتمدة على الذكاء الاصطناعي</a></li>
<li><a href="#pricing">٧. الباقات والتسعير والفوترة</a></li>
<li><a href="#performance">٨. باقات الدفع على النتائج</a></li>
<li><a href="#sla">٩. ضمانات الأداء (SLA)</a></li>
<li><a href="#ip">١٠. الملكية الفكرية</a></li>
<li><a href="#liability">١١. حدود المسؤولية</a></li>
<li><a href="#termination">١٢. الإنهاء والإلغاء</a></li>
<li><a href="#disputes">١٣. النزاعات والاختصاص</a></li>
<li><a href="#changes">١٤. التعديلات على الشروط</a></li>
<li><a href="#contact">١٥. التواصل</a></li>
</ol>
</div>
<section id="parties">
<h2>١. الأطراف والقبول</h2>
<p>«المزوّد» يعني <strong>Dealix</strong>، شركة سعودية ناشئة في تشغيل الإيرادات. «العميل» يعني الشخص الاعتباري (شركة B2B) الذي يشترك في الخدمة، ويمثله «المستخدم المخوّل» الذي يقبل هذه الشروط نيابةً عنه.</p>
<div class="key-clause">
<strong>قبول ملزِم:</strong> النقر على «أوافق»، أو إنشاء حساب، أو دفع رسوم الاشتراك = قبول كامل لهذه الشروط. إذا كنت لا توافق، يُرجى عدم استخدام الخدمة.
</div>
</section>
<section id="service">
<h2>٢. وصف الخدمة</h2>
<p>Dealix منصة SaaS لتشغيل إيرادات B2B السعودي تشمل:</p>
<ul>
<li>اكتشاف فرص B2B من المصادر العامة + الإثراء.</li>
<li>صياغة مسودات رسائل (إيميل، واتساب، LinkedIn) — كلها تتطلب موافقتكم قبل الإرسال.</li>
<li>إدارة pipeline + تصنيف الردود + جدولة الاجتماعات.</li>
<li>قياس الأداء + تقارير ROI شهرية.</li>
<li>11 PDPL compliance gates لحماية كل تواصل.</li>
<li>طبقة Copilot عربية + 11 AI Agent متخصص.</li>
</ul>
<p style="margin-top:10px"><strong>لسنا</strong> وكالة تسويق ولا مزوّد رسائل خام؛ نحن منصة تشغيلية تعمل بإذنكم وتحت رقابتكم.</p>
</section>
<section id="account">
<h2>٣. الحساب والمصادقة</h2>
<ul>
<li>أنتم مسؤولون عن سرّية بيانات الدخول (لا تشاركوها).</li>
<li>مستخدم واحد لكل مقعد — لا تشارك حسابك مع زملاء آخرين.</li>
<li>أي نشاط في حسابك مسؤوليتك حتى تخطرنا بأي اختراق.</li>
<li>نحتفظ بحق تعليق الحساب فوراً عند رصد نشاط مشبوه (مع إخطاركم).</li>
</ul>
</section>
<section id="acceptable">
<h2>٤. الاستخدام المقبول</h2>
<p>توافقون على عدم استخدام Dealix لـ:</p>
<ul>
<li>أي نشاط مخالف للأنظمة السعودية أو الدولية.</li>
<li>إرسال محتوى يتضمن تشهيراً، تحريضاً، تمييزاً عرقياً/دينياً، أو محتوى جنسي.</li>
<li>تجاوز حدود المعدل (rate limits) أو محاولة كسر الـ API.</li>
<li>هندسة عكسية أو محاولة استخراج بيانات Dealix الداخلية.</li>
<li>إعادة بيع الخدمة دون اتفاقية شراكة رسمية.</li>
<li>إرسال spam أو رسائل بدون lawful basis.</li>
<li>استهداف أفراد (B2C) — Dealix مخصصة لـ B2B فقط.</li>
<li>محاولة تجاوز PDPL compliance gates أو خداع نظام الموافقات.</li>
</ul>
<div class="warning">
<strong>⚠️ مخالفة الاستخدام المقبول</strong> = تعليق الحساب فوراً بدون استرداد + احتفاظنا بحق الإبلاغ للجهات المختصة.
</div>
</section>
<section id="data">
<h2>٥. بياناتك ومسؤولياتك</h2>
<ul>
<li>أنتم تظلون <strong>المتحكّم</strong> ببياناتكم؛ Dealix مجرد <strong>معالج</strong> نيابةً عنكم.</li>
<li>أنتم مسؤولون عن جودة الـ ICP والقوائم التي ترفعونها.</li>
<li>أنتم تضمنون أن للقوائم المرفوعة أساس قانوني (consent / legitimate interest).</li>
<li>نلتزم بسياسة الخصوصية + <a href="/trust-center.html">11 PDPL gates</a> + DPA الذي يُوقَّع لباقات Scale وما فوق.</li>
</ul>
</section>
<section id="ai">
<h2>٦. الميزات المعتمدة على الذكاء الاصطناعي</h2>
<p>تستخدم Dealix نماذج LLM (Anthropic, Groq) لصياغة المسودات وتصنيف الردود. تُقرّون بأن:</p>
<ul>
<li>المخرجات قد تحوي أخطاء (hallucinations) — مراجعة المسودات قبل الإرسال مسؤوليتكم.</li>
<li>لا نضمن دقة 100% في تصنيف الردود.</li>
<li>لا نستخدم بياناتكم لتدريب النماذج العامة (DPA مع المزودين يضمن ذلك).</li>
<li>أي قرار آلي مهم (مثل deal won/lost) يحتاج موافقتكم البشرية.</li>
</ul>
</section>
<section id="pricing">
<h2>٧. الباقات والتسعير والفوترة</h2>
<table class="table">
<tr><th>الباقة</th><th>السعر (ريال/شهر)</th><th>الأسلوب</th></tr>
<tr><td>Founder Operator</td><td>299 - 499</td><td>للمؤسسين الفرديين</td></tr>
<tr><td>Growth OS</td><td>2,999</td><td>للشركات SMEs</td></tr>
<tr><td>Scale OS</td><td>7,999</td><td>للفرق الناضجة</td></tr>
<tr><td>Performance Add-on</td><td>عمولة على النتائج</td><td>اختيارية</td></tr>
<tr><td>Enterprise</td><td>تواصلوا معنا</td><td>نشر خاص</td></tr>
</table>
<ul style="margin-top:14px">
<li>الفوترة شهرية مقدّمة عبر <strong>Moyasar</strong> (بوابة سعودية مرخّصة).</li>
<li>الأسعار شاملة 15% ضريبة قيمة مضافة (ZATCA).</li>
<li>التجديد التلقائي ما لم تلغوا قبل 7 أيام من نهاية الدورة.</li>
<li>الاسترداد: لا استرداد بعد بدء استخدام الخدمة، باستثناء حالات الفشل التقني الموثقة.</li>
<li>نحتفظ بحق تعديل الأسعار مع إخطاركم قبل 30 يوم.</li>
</ul>
</section>
<section id="performance">
<h2>٨. باقات الدفع على النتائج (Pay-per-Result)</h2>
<p>لمن يختار Performance Add-on، الفوترة على:</p>
<ul>
<li><strong>25-75 ريال</strong> لكل qualified lead — التعريف موضح في الاتفاقية الفردية.</li>
<li><strong>150-500 ريال</strong> لكل booked meeting — لا يحتسب إلا إذا حضر العميل.</li>
<li><strong>3-10%</strong> success fee على الصفقات المغلقة — مع dispute window 30 يوم.</li>
</ul>
<p style="margin-top:10px">قبل تفعيل هذه الباقة، نوقّع MoU يحدّد بدقة معايير «المؤهَّل» + «الاجتماع» + «الصفقة المُغلقة» لتجنّب أي خلاف.</p>
</section>
<section id="sla">
<h2>٩. ضمانات الأداء (SLA)</h2>
<table class="table">
<tr><th>الباقة</th><th>Uptime</th><th>زمن الاستجابة للدعم</th></tr>
<tr><td>Founder Operator</td><td>99.0%</td><td>72 ساعة</td></tr>
<tr><td>Growth OS</td><td>99.5%</td><td>24 ساعة</td></tr>
<tr><td>Scale OS</td><td>99.9%</td><td>4 ساعات</td></tr>
<tr><td>Enterprise</td><td>99.95%</td><td>1 ساعة (على مدار الساعة)</td></tr>
</table>
<p style="margin-top:12px">عند انخفاض الـ uptime تحت الحد، تحصلون على ائتمان بقيمة الفترة المتأثرة (يُطبَّق آلياً على الفاتورة التالية).</p>
</section>
<section id="ip">
<h2>١٠. الملكية الفكرية</h2>
<ul>
<li>كل حقوق Dealix (الكود، التصميم، العلامة التجارية، النماذج المدرّبة) ملك حصري لـ Dealix.</li>
<li>أنتم تمتلكون بياناتكم + المسودات النهائية التي ترسلونها بأنفسكم.</li>
<li>تمنحوننا ترخيصاً غير حصرياً لاستخدام البيانات المُجمّعة المُجهَّلة لتحسين المنتج (لا يُنسب لشركتكم).</li>
<li>الـ benchmarks في Saudi B2B Pulse تُنشَر بشرط الحد الأدنى 5 شركات/قطاع لحماية الهوية.</li>
</ul>
</section>
<section id="liability">
<h2>١١. حدود المسؤولية</h2>
<div class="warning">
<strong>قراءة دقيقة لهذا البند مهمة:</strong>
</div>
<ul>
<li>Dealix أداة دعم قرار، وليست بديلاً عن حكمكم التجاري.</li>
<li>لا نضمن نتائج محددة (عدد الـ leads، الإيراد، الإغلاق) — كل ضمان كهذا في الإعلانات هو "مؤشّر" مبني على بيانات pilot.</li>
<li>مسؤوليتنا الإجمالية، في أي ظرف، محدودة بـ <strong>قيمة آخر 12 شهر اشتراك دفعتموها</strong>.</li>
<li>لسنا مسؤولين عن: خسائر تجارية غير مباشرة، فقدان فرص، خسارة سمعة، أو أضرار تبعية.</li>
<li>لسنا مسؤولين عن سلوك المعالجين من الباطن (Anthropic, Moyasar, إلخ) خارج نطاق DPA المُوقَّع معهم.</li>
<li>الـ uptime issues تُعالج بآلية الـ SLA credits فقط، وليس برد كامل أو تعويض إضافي.</li>
</ul>
</section>
<section id="termination">
<h2>١٢. الإنهاء والإلغاء</h2>
<ul>
<li>يمكنكم الإلغاء في أي وقت من Customer Portal أو بإيميل لـ <a href="mailto:billing@dealix.sa">billing@dealix.sa</a>.</li>
<li>الإلغاء يُفعَّل في نهاية الدورة الحالية (لا استرداد جزئي).</li>
<li>يمكننا إنهاء الخدمة بإخطار 30 يوم لأي سبب، مع تصدير كامل لبياناتكم.</li>
<li>إنهاء فوري لمخالفات «الاستخدام المقبول» — بدون استرداد + احتفاظ بسجلات الامتثال 7 سنوات.</li>
</ul>
</section>
<section id="disputes">
<h2>١٣. النزاعات والاختصاص</h2>
<ul>
<li>يخضع هذا الاتفاق لأنظمة المملكة العربية السعودية.</li>
<li>نسعى لحل أي نزاع ودياً خلال 60 يوم من الإخطار الكتابي.</li>
<li>بعد ذلك، يحال النزاع إلى <strong>المركز السعودي للتحكيم التجاري (SCCA)</strong>، طبقاً لقواعده، بمدينة الرياض، باللغة العربية.</li>
<li>قرار التحكيم نهائي وملزم.</li>
</ul>
</section>
<section id="changes">
<h2>١٤. التعديلات على الشروط</h2>
<p>نحدّث هذه الشروط دورياً. أي تعديل جوهري يُبلَّغ لكم بإيميل + إشعار داخل المنتج قبل النفاذ بـ 30 يوم. استمراركم في الاستخدام بعد النفاذ = قبول للتعديلات. لو لم توافقوا، يحق لكم إلغاء الاشتراك بدون رسوم خلال هذه الفترة.</p>
</section>
<section id="contact">
<h2>١٥. التواصل</h2>
<p>لأي استفسار قانوني أو تعاقدي:</p>
</section>
<div class="footer-block">
<h3>Dealix — التواصل القانوني</h3>
<p>📧 <a href="mailto:legal@dealix.sa">legal@dealix.sa</a> · 💼 <a href="mailto:billing@dealix.sa">billing@dealix.sa</a><br>
<a href="/privacy.html">سياسة الخصوصية</a> · <a href="/trust-center.html">Trust Center</a> · <a href="/security.html" style="color:var(--accent)">Security</a></p>
</div>
<p style="text-align:center;margin-top:24px;color:var(--muted);font-size:13px">
مدعوم بـ Saudi Vision 2030 — منتج سعودي 🇸🇦 يخدم B2B السعودي
</p>
</div>
</body>
</html>

131
dealix/landing/welcome.html Normal file
View File

@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>أهلاً بك في Dealix — أول 5 دقائق</title>
<meta name="robots" content="noindex" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" />
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--brand:#0f172a;--accent:#22d3ee;--success:#10b981;--purple:#8b5cf6;--muted:#64748b;--border:#e2e8f0;--bg:#f8fafc}
body{font-family:'IBM Plex Sans Arabic',sans-serif;background:var(--bg);color:var(--brand);line-height:1.7}
.wrap{max-width:920px;margin:0 auto;padding:48px 24px}
.header{background:linear-gradient(135deg,#0f172a,#1e3a8a);color:#fff;border-radius:18px;padding:36px 32px;margin-bottom:32px;text-align:center}
.header .badge{display:inline-block;background:rgba(34,211,238,0.2);color:var(--accent);padding:6px 14px;border-radius:999px;font-size:12px;font-weight:700;margin-bottom:14px}
.header h1{font-size:clamp(26px,4vw,36px);margin-bottom:10px}
.header p{opacity:0.92;font-size:15px}
.checklist{background:#fff;border:1px solid var(--border);border-radius:16px;padding:28px}
.checklist h2{font-size:20px;margin-bottom:6px}
.checklist .sub{color:var(--muted);font-size:14px;margin-bottom:20px}
.step{display:grid;grid-template-columns:42px 1fr auto;gap:14px;padding:18px 0;border-bottom:1px solid var(--border);align-items:center}
.step:last-child{border:0}
.step-num{width:36px;height:36px;border-radius:50%;background:#f1f5f9;color:var(--brand);font-weight:700;display:flex;align-items:center;justify-content:center;font-family:'Inter','IBM Plex Sans Arabic',sans-serif}
.step.done .step-num{background:linear-gradient(135deg,var(--success),#047857);color:#fff}
.step .body h3{font-size:16px;color:var(--brand);margin-bottom:3px}
.step .body p{color:var(--muted);font-size:13px}
.step .body .meta{color:#94a3b8;font-size:11px;margin-top:4px}
.step .action{font-size:13px;font-weight:700;text-decoration:none;padding:8px 16px;border-radius:8px;background:var(--brand);color:#fff;white-space:nowrap;transition:transform 0.15s}
.step .action:hover{transform:translateY(-1px)}
.step.done .action{background:#10b981}
.progress{background:#fff;border:1px solid var(--border);border-radius:14px;padding:16px 22px;margin:0 0 24px;display:flex;align-items:center;gap:14px}
.progress-bar{flex:1;height:8px;background:#e2e8f0;border-radius:4px;overflow:hidden}
.progress-fill{height:100%;background:linear-gradient(90deg,#22d3ee,#10b981);border-radius:4px;transition:width 0.4s}
.progress-text{font-size:13px;color:var(--muted)}
.progress-text strong{color:var(--brand)}
.help-bar{background:linear-gradient(135deg,#fef3c7,#fde68a);border:1px solid #fcd34d;border-radius:14px;padding:18px 24px;margin-top:24px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}
.help-bar strong{color:#713f12}
.help-bar a{color:#713f12;font-weight:700;text-decoration:none}
</style>
</head>
<body>
<div class="wrap">
<div class="header">
<div class="badge">🎉 أهلاً بك في Dealix</div>
<h1>أنت الآن جزء من أول Saudi B2B Revenue OS</h1>
<p>5 خطوات تفصلك عن أول 200 شركة سعودية مكتشفة + أول WhatsApp draft عربي مخصص.</p>
</div>
<div class="progress">
<div class="progress-bar"><div class="progress-fill" style="width:20%" id="progress-fill"></div></div>
<div class="progress-text"><strong id="progress-num">1</strong>/5 خطوات مكتملة · ~5 دقائق</div>
</div>
<div class="checklist">
<h2>قائمة التحقق — ابدأ من 1</h2>
<div class="sub">كل خطوة آلية ومدمجة. لو احتجت مساعدة، تواصل مع فريق onboarding.</div>
<div class="step done" data-step="1">
<div class="step-num"></div>
<div class="body">
<h3>اشتركت بنجاح</h3>
<p>تم استلام دفعتك وتفعيل حسابك.</p>
<div class="meta">مكتمل · الآن</div>
</div>
<span class="action" style="background:#10b981">✓ تم</span>
</div>
<div class="step" data-step="2">
<div class="step-num">2</div>
<div class="body">
<h3>حدّد الـ ICP — قطاع، مدن، حجم</h3>
<p>أخبرنا عن عميلك المثالي. نستخدم هذا لتدريب الـ Prospecting Agent.</p>
<div class="meta">~ 2 دقيقة</div>
</div>
<a href="/customer-portal.html#icp" class="action">حدّد الآن</a>
</div>
<div class="step" data-step="3">
<div class="step-num">3</div>
<div class="body">
<h3>اربط WhatsApp Business</h3>
<p>نفّذ موافقة OAuth — Dealix يرسل عبر رقمك مع توقيعك. آمن + PDPL محمي.</p>
<div class="meta">~ 1 دقيقة · WhatsApp Cloud / Green API</div>
</div>
<a href="/customer-portal.html#whatsapp" class="action">اربط</a>
</div>
<div class="step" data-step="4">
<div class="step-num">4</div>
<div class="body">
<h3>اربط Gmail (اختياري لكن موصى به)</h3>
<p>OAuth بصلاحية gmail.compose فقط — نولّد drafts في صندوقك، لا إرسال بدون إذنك.</p>
<div class="meta">~ 1 دقيقة · Gmail OAuth</div>
</div>
<a href="/customer-portal.html#gmail" class="action">اربط</a>
</div>
<div class="step" data-step="5">
<div class="step-num">5</div>
<div class="body">
<h3>شغّل أول Daily Growth Run</h3>
<p>Dealix يكتشف 200 شركة + يفلتر 40 + يولّد drafts بالعربي. أنت تراجع وتوافق.</p>
<div class="meta">~ 1 دقيقة · ينتج نتائج خلال ساعة</div>
</div>
<a href="/command-center.html" class="action">شغّل الآن</a>
</div>
</div>
<div class="help-bar">
<div>
<strong>تحتاج مساعدة شخصية؟</strong> فريق onboarding يعطيك إعداد كامل في 30 دقيقة على Zoom.
</div>
<a href="https://calendly.com/dealix/onboarding">احجز جلسة onboarding مجانية</a>
</div>
</div>
<script>
// Update progress based on completed steps (data-step="N" + class="done")
function updateProgress() {
const steps = document.querySelectorAll('.step');
const done = document.querySelectorAll('.step.done').length;
const pct = Math.round((done / steps.length) * 100);
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-num').textContent = done;
}
updateProgress();
</script>
</body>
</html>

View File

@ -0,0 +1,94 @@
"""Unit tests for Agent Observability."""
from __future__ import annotations
from auto_client_acquisition.agent_observability import (
CostTracker,
build_trace_event,
run_eval_pack,
safety_eval,
saudi_tone_eval,
)
# ── Trace events ─────────────────────────────────────────────
def test_trace_event_hashes_user_id():
e = build_trace_event(
workflow_name="first_10", agent_name="scout",
user_id="user_real_42", company_id="acme",
)
assert e["user_id_hash"] != "user_real_42"
assert e["company_id_hash"] != "acme"
assert len(str(e["user_id_hash"])) == 16
def test_trace_event_redacts_payload_secrets():
e = build_trace_event(
workflow_name="x", agent_name="y",
payload={"token": "ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234"},
)
assert "ghp_AAAA" not in str(e.get("payload"))
# ── Safety eval ──────────────────────────────────────────────
def test_safety_blocks_guarantee():
out = safety_eval("ضمان 100% نتائج مضمونة")
assert out["verdict"] in ("blocked", "needs_review")
def test_safety_safe_for_clean_text():
out = safety_eval("هلا أحمد، لاحظت توسعكم. يناسبك أعرض لك Pilot؟")
assert out["verdict"] == "safe"
assert out["score"] >= 70
def test_safety_blocks_medical_claim():
"""Medical claims should at minimum require human review (and ideally block)."""
out = safety_eval("هذا المنتج يعالج السكر ويشفي الضغط بدون أدوية.")
assert out["verdict"] in ("blocked", "needs_review")
assert any(v["category"] == "medical_claim" for v in out["violations"])
# ── Saudi tone eval ──────────────────────────────────────────
def test_tone_natural_for_friendly_arabic():
text = ("هلا أحمد، لاحظت توسعكم في فريق المبيعات. "
"يناسبك أعرض لك Pilot 7 أيام؟")
out = saudi_tone_eval(text)
assert out["verdict"] in ("natural", "decent")
assert out["arabic_ratio"] > 0.5
def test_tone_off_for_too_corporate():
text = "تحية طيبة وبعد، ندعوكم لاكتشاف synergy و best-in-class."
out = saudi_tone_eval(text)
assert out["verdict"] == "off"
def test_tone_off_for_empty():
out = saudi_tone_eval("")
assert out["verdict"] == "off"
# ── Eval pack ────────────────────────────────────────────────
def test_eval_pack_runs_all_cases():
out = run_eval_pack()
assert out["total"] >= 5
assert "pass_rate" in out
def test_eval_pack_has_some_passing():
out = run_eval_pack()
assert out["passed"] >= 1
# ── Cost tracker ────────────────────────────────────────────
def test_cost_tracker_aggregates():
t = CostTracker()
t.record(workflow_name="first_10", provider_key="claude_sonnet",
task_type="strategic_reasoning", cost_estimate=0.025)
t.record(workflow_name="first_10", provider_key="claude_haiku",
task_type="classification", cost_estimate=0.001)
s = t.summary()
assert s["runs"] == 2
assert round(s["total"], 4) == 0.026
assert s["by_workflow"]["first_10"] > 0

View File

@ -0,0 +1,82 @@
"""Unit tests for auto_client_acquisition.ai.model_router."""
from __future__ import annotations
import pytest
from auto_client_acquisition.ai.model_router import (
ModelTask,
estimate_model_cost_class,
get_model_route,
requires_guardrail,
)
# ── ModelTask enum ───────────────────────────────────────────────
def test_model_task_is_enum():
"""ModelTask should be a StrEnum / Enum with at least one member."""
members = list(ModelTask)
assert len(members) > 0
def test_model_task_string_values():
"""Each task should serialize to a non-empty string."""
for t in ModelTask:
assert str(t)
assert len(str(t)) > 0
# ── get_model_route ──────────────────────────────────────────────
def test_get_model_route_returns_route_for_each_task():
"""Real ModelRoute fields: task, quality_tier, latency, cost_class,
fallback_task, guardrail_required, eval_metric."""
for t in ModelTask:
route = get_model_route(t)
assert route is not None
# Core required fields per the dataclass
for field in ("task", "quality_tier", "latency", "cost_class", "guardrail_required"):
assert hasattr(route, field), f"missing {field} on route for {t}"
# task on the route should round-trip back to the input
assert route.task == t
def test_routes_are_consistent_for_same_task():
"""Calling twice with the same task should return equivalent routes."""
for t in list(ModelTask)[:3]:
a = get_model_route(t)
b = get_model_route(t)
# Same content (immutable / pure function)
assert a == b or str(a) == str(b)
# ── cost class ───────────────────────────────────────────────────
def test_estimate_cost_class_for_each_task():
"""Should return a non-empty cost class label per task."""
for t in ModelTask:
out = estimate_model_cost_class(t)
assert out is not None
def test_cost_classes_have_known_labels():
"""Cost class labels should be meaningful strings."""
seen = set()
for t in ModelTask:
out = estimate_model_cost_class(t)
seen.add(str(out))
# We should have some variety (not all identical)
assert len(seen) >= 1
# ── guardrail ────────────────────────────────────────────────────
def test_requires_guardrail_returns_bool():
for t in ModelTask:
out = requires_guardrail(t)
assert isinstance(out, bool)
def test_guardrail_distribution_not_uniform():
"""Sanity: at least some tasks need guardrail, some don't (typical AI design)."""
needs = [requires_guardrail(t) for t in ModelTask]
# Either: some True some False, OR all True (defensive). Pure False would be suspicious.
# Just assert that the function is consistent and returns booleans.
assert all(isinstance(x, bool) for x in needs)

View File

@ -0,0 +1,379 @@
"""Unit tests for the Autonomous Service Operator."""
from __future__ import annotations
import pytest
from auto_client_acquisition.autonomous_service_operator import (
OperatorMemory,
SUPPORTED_INTENTS,
add_agency_client,
build_agency_dashboard,
build_approval_card,
build_ceo_command_center,
build_client_dashboard,
build_co_branded_proof_pack,
build_executive_daily_brief,
build_intake_questions_for_intent,
build_new_session,
build_revenue_risks_summary,
build_service_pipeline,
build_session_context,
build_upsell_card,
classify_intent,
dispatch_proof_pack,
handle_message,
intent_to_service,
list_agency_revenue_share,
list_bundles,
plan_tool_action,
process_approval_decision,
recommend_bundle,
recommend_upsell_after_service,
render_approval_card_for_whatsapp,
render_card_for_whatsapp,
render_daily_brief_for_whatsapp,
transition_session,
validate_intake_completeness,
)
# ── Intent classification ────────────────────────────────────
def test_intent_want_more_customers():
out = classify_intent("أبغى عملاء أكثر لشركتي")
assert out["intent"] == "want_more_customers"
def test_intent_has_contact_list():
out = classify_intent("عندي قائمة أرقام كبيرة")
assert out["intent"] == "has_contact_list"
def test_intent_partnerships():
out = classify_intent("أبغى شراكات مع وكالات")
assert out["intent"] == "want_partnerships"
def test_intent_whatsapp_setup():
out = classify_intent("نستخدم واتساب بدون opt-in")
assert out["intent"] == "want_whatsapp_setup"
def test_intent_pricing():
out = classify_intent("بكم السعر؟")
assert out["intent"] == "ask_pricing"
def test_intent_approve():
out = classify_intent("اعتمد")
assert out["intent"] == "approve_action"
def test_intent_unknown_falls_back_to_services():
out = classify_intent("xyz random text")
assert out["intent"] == "ask_services"
def test_intent_to_service_mapping():
assert intent_to_service("want_more_customers") == "first_10_opportunities_sprint"
assert intent_to_service("has_contact_list") == "list_intelligence"
assert intent_to_service("want_partnerships") == "partner_sprint"
def test_supported_intents_count():
assert len(SUPPORTED_INTENTS) == 16
# ── Conversation router ──────────────────────────────────────
def test_handle_message_recommends_first_10_for_want_more_customers():
out = handle_message("أبغى عملاء أكثر")
assert out["service_id"] == "first_10_opportunities_sprint"
assert out["live_send_allowed"] is False
def test_handle_message_uses_agency_bundle_for_agency():
out = handle_message("أبغى شراكات", is_agency=True)
assert out["bundle_recommendation"]["recommended_bundle_id"] == "partnership_growth"
def test_handle_message_uses_data_to_revenue_when_list_provided():
out = handle_message("أبغى أستخدم قائمتي", has_contact_list=True)
assert out["bundle_recommendation"]["recommended_bundle_id"] == "data_to_revenue"
def test_handle_message_approval_processes_decision():
out = handle_message("اعتمد")
assert "decision_processed" in out
assert out["decision_processed"]["state"] == "approved"
# ── Sessions ────────────────────────────────────────────────
def test_new_session_has_uuid():
s = build_new_session(customer_id="cust_1")
assert s.session_id
assert s.state == "new"
assert s.customer_id == "cust_1"
def test_session_transition_audit_trail():
s = build_new_session()
transition_session(s, new_state="intent_classified", note="initial")
assert s.state == "intent_classified"
assert len(s.history) == 1
assert s.history[0]["from"] == "new"
def test_session_transition_unknown_raises():
s = build_new_session()
with pytest.raises(ValueError):
transition_session(s, new_state="bogus_state")
def test_operator_memory_stores_session():
mem = OperatorMemory()
s = build_new_session(customer_id="cust_1")
mem.upsert_session(s)
assert mem.get_session(s.session_id) is s
ctx = build_session_context(memory=mem, session_id=s.session_id)
assert ctx["session"]["session_id"] == s.session_id
# ── Intake ──────────────────────────────────────────────────
def test_intake_questions_for_known_intent():
out = build_intake_questions_for_intent("want_more_customers")
assert len(out["questions"]) >= 4
def test_intake_questions_unknown_intent_falls_back():
out = build_intake_questions_for_intent("totally_unknown_intent")
assert out["questions"]
def test_intake_validation_detects_missing():
out = validate_intake_completeness(
"want_more_customers",
{"sector": "training"}, # only one field
)
assert out["complete"] is False
assert "company_name" in out["missing_fields"]
def test_intake_validation_complete():
out = validate_intake_completeness(
"want_more_customers",
{"company_name": "X", "sector": "training", "city": "Riyadh",
"offer": "Pilot 7 أيام", "ideal_customer": "B2B"},
)
assert out["complete"] is True
# ── Approval manager ────────────────────────────────────────
def test_approval_card_has_three_buttons():
card = build_approval_card(
action_type="send_email", title_ar="إرسال إيميل",
summary_ar="إيميل لـ Acme",
)
assert len(card["buttons_ar"]) <= 3
assert card["live_send_allowed"] is False
def test_approval_decision_approve():
card = build_approval_card(action_type="x", title_ar="x", summary_ar="x")
out = process_approval_decision(card, decision="approve")
assert out["state"] == "approved"
assert out["next_action"] == "execute_with_audit"
def test_approval_decision_arabic_skip():
card = build_approval_card(action_type="x", title_ar="x", summary_ar="x")
out = process_approval_decision(card, decision="تخطي")
assert out["state"] == "rejected"
def test_approval_decision_unknown_returns_error():
card = build_approval_card(action_type="x", title_ar="x", summary_ar="x")
out = process_approval_decision(card, decision="bogus")
assert "error" in out
# ── Service pipeline ────────────────────────────────────────
def test_service_pipeline_starts_at_intake():
p = build_service_pipeline("first_10_opportunities_sprint")
assert p["current_step"] == "intake"
assert any(s["step_id"] == "approval" for s in p["steps"])
# ── Tool action planner ─────────────────────────────────────
def test_plan_blocks_linkedin_scrape():
out = plan_tool_action(tool="linkedin.scrape_profile")
assert out["verdict"] == "blocked"
def test_plan_blocks_linkedin_auto_dm():
out = plan_tool_action(tool="linkedin.auto_dm")
assert out["verdict"] == "blocked"
def test_plan_high_risk_requires_approval():
out = plan_tool_action(tool="whatsapp.send_message")
assert out["verdict"] == "approval_required"
assert out["live_send_allowed"] is False
def test_plan_draft_safe_returns_draft_only():
out = plan_tool_action(tool="gmail.create_draft")
assert out["verdict"] == "draft_only"
def test_plan_unknown_defaults_to_approval_required():
out = plan_tool_action(tool="bogus.tool")
assert out["verdict"] == "approval_required"
# ── Bundles ─────────────────────────────────────────────────
def test_list_bundles_returns_six():
out = list_bundles()
assert out["total"] == 6
def test_recommend_bundle_for_agency():
out = recommend_bundle(is_agency=True)
assert out["recommended_bundle_id"] == "partnership_growth"
def test_recommend_bundle_for_local_business():
out = recommend_bundle(is_local_business=True)
assert out["recommended_bundle_id"] == "local_growth_os"
def test_recommend_bundle_with_list():
out = recommend_bundle(has_contact_list=True)
assert out["recommended_bundle_id"] == "data_to_revenue"
def test_recommend_bundle_default():
out = recommend_bundle(budget_sar=500)
assert out["recommended_bundle_id"] == "growth_starter"
# ── Modes ───────────────────────────────────────────────────
def test_ceo_command_center_arabic():
out = build_ceo_command_center(company_name="Acme")
assert out["mode"] == "ceo"
assert any("؀" <= ch <= "ۿ" for ch in out["daily_brief"]["title_ar"])
def test_executive_daily_brief_three_decisions():
out = build_executive_daily_brief(company_name="Acme")
assert len(out["priority_decisions_ar"]) == 3
assert len(out["buttons_ar"]) <= 3
def test_revenue_risks_summary_three_risks():
out = build_revenue_risks_summary()
assert len(out["risks"]) == 3
def test_client_dashboard_has_panels():
out = build_client_dashboard(customer_id="c1", company_name="Acme")
assert out["mode"] == "client"
assert len(out["today_panels_ar"]) >= 3
def test_agency_dashboard_aggregates():
clients = [
{"client_company_name": "A", "monthly_subscription_sar": 2999,
"revenue_share_pct": 20, "status": "active"},
{"client_company_name": "B", "monthly_subscription_sar": 1500,
"revenue_share_pct": 25, "status": "onboarding"},
]
out = build_agency_dashboard(agency_id="ag1", clients=clients)
assert out["metrics"]["total_clients"] == 2
assert out["metrics"]["monthly_revenue_sar"] == 4499.0
def test_agency_revenue_share_calculation():
clients = [
{"client_company_name": "A", "monthly_subscription_sar": 2999,
"revenue_share_pct": 20},
]
out = list_agency_revenue_share(clients=clients)
assert out["total_share_sar"] == 599.8
def test_agency_add_client_appends():
clients: list = []
add_agency_client(
agency_id="ag1", client_company_name="Acme",
monthly_subscription_sar=2999, revenue_share_pct=20,
clients=clients,
)
assert len(clients) == 1
def test_co_branded_proof_pack_includes_both_names():
out = build_co_branded_proof_pack(
agency_name="Vortex", client_company_name="Acme",
)
assert out["co_branded"] is True
assert out["agency_name"] == "Vortex"
# ── WhatsApp renderer ────────────────────────────────────────
def test_render_card_for_whatsapp_no_live_send():
card = build_approval_card(
action_type="x", title_ar="فرصة", summary_ar="ملخص",
)
out = render_card_for_whatsapp(card)
assert out["live_send_allowed"] is False
assert any("؀" <= ch <= "ۿ" for ch in out["body_ar"])
def test_render_approval_card_has_3_buttons():
card = build_approval_card(
action_type="x", title_ar="فرصة", summary_ar="ملخص",
)
out = render_approval_card_for_whatsapp(card)
assert len(out["buttons_ar"]) == 3
def test_render_daily_brief_arabic():
brief = build_executive_daily_brief(company_name="Acme")
out = render_daily_brief_for_whatsapp(brief)
assert "صباح" in out["body_ar"]
assert out["live_send_allowed"] is False
# ── Proof + Upsell ──────────────────────────────────────────
def test_proof_pack_dispatch_returns_draft():
out = dispatch_proof_pack(
service_id="first_10_opportunities_sprint",
customer_id="c1",
)
assert out["status"] == "draft"
assert out["live_send_allowed"] is False
def test_upsell_recommends_growth_os_after_first_10():
out = recommend_upsell_after_service(
completed_service_id="first_10_opportunities_sprint",
pilot_metrics={"pipeline_sar": 30000, "meetings": 3, "csat": 9},
)
assert out["recommended_next_service_id"] == "growth_os_monthly"
assert out["verdict"] == "upsell_now"
def test_upsell_iterate_for_weak_outcome():
out = recommend_upsell_after_service(
completed_service_id="first_10_opportunities_sprint",
pilot_metrics={"pipeline_sar": 1000, "meetings": 0, "csat": 5},
)
assert out["verdict"] == "iterate_first"
def test_upsell_card_has_three_buttons():
out = build_upsell_card(
completed_service_id="first_10_opportunities_sprint",
)
assert len(out["buttons_ar"]) == 3
assert out["live_send_allowed"] is False

View File

@ -0,0 +1,227 @@
"""
Unit tests for the dealix.business layer pure functions, no I/O.
Covers: gtm_plan / launch_metrics / market_positioning / pricing_strategy /
proof_pack / unit_economics / verticals.
"""
from __future__ import annotations
import pytest
from auto_client_acquisition.business import (
gtm_plan,
launch_metrics,
market_positioning,
pricing_strategy,
proof_pack,
unit_economics,
verticals,
)
# ── gtm_plan ─────────────────────────────────────────────────────
def test_first_10_plan_has_milestones():
p = gtm_plan.first_10_customers_plan()
assert isinstance(p, dict)
assert p # non-empty
def test_first_100_plan_distinct_from_first_10():
p10 = gtm_plan.first_10_customers_plan()
p100 = gtm_plan.first_100_customers_plan()
# They should not be byte-identical structures
assert p10 != p100
def test_channel_strategy_returns_dict():
out = gtm_plan.channel_strategy()
assert isinstance(out, dict)
assert out
def test_partner_strategy_returns_dict():
out = gtm_plan.partner_strategy()
assert isinstance(out, dict)
def test_founder_led_sales_script_has_content():
s = gtm_plan.founder_led_sales_script()
assert isinstance(s, dict)
assert s
# ── launch_metrics ───────────────────────────────────────────────
def test_north_star_metrics_dict():
out = launch_metrics.north_star_metrics()
assert isinstance(out, dict)
assert out
def test_activation_metrics_dict():
assert isinstance(launch_metrics.activation_metrics(), dict)
def test_retention_metrics_dict():
assert isinstance(launch_metrics.retention_metrics(), dict)
def test_revenue_metrics_dict():
assert isinstance(launch_metrics.revenue_metrics(), dict)
def test_ai_quality_metrics_dict():
assert isinstance(launch_metrics.ai_quality_metrics(), dict)
# ── market_positioning ───────────────────────────────────────────
def test_compare_competitors_returns_list():
out = market_positioning.compare_competitors()
assert isinstance(out, list)
assert len(out) > 0
def test_dealix_differentiators_non_empty_strings():
out = market_positioning.dealix_differentiators()
assert isinstance(out, list)
assert len(out) > 0
assert all(isinstance(x, str) and x for x in out)
def test_positioning_statement_returns_string():
# Try a known segment value
statement = market_positioning.positioning_statement("smb")
assert isinstance(statement, str)
assert len(statement) > 0
# ── pricing_strategy ─────────────────────────────────────────────
def test_get_pricing_tiers_structure():
out = pricing_strategy.get_pricing_tiers()
assert isinstance(out, dict)
assert out["currency"] == "SAR"
assert isinstance(out["tiers"], list)
keys = {t["key"] for t in out["tiers"]}
# Required tiers per pricing strategy doc
for required in ("founder_operator", "growth_os", "scale_os"):
assert required in keys, f"missing tier: {required}"
def test_recommend_plan_returns_known_key():
out = pricing_strategy.recommend_plan(
company_size="smb",
monthly_budget_sar=3000,
goal="grow_pipeline",
)
assert isinstance(out, dict)
# Real shape: {recommended_plan, rationale_ar, tier_summary, inputs}
assert "recommended_plan" in out
assert "rationale_ar" in out
assert "tier_summary" in out
def test_calculate_performance_fee_non_negative():
out = pricing_strategy.calculate_performance_fee(
qualified_leads=20,
booked_meetings=8,
won_revenue_sar=120_000,
)
assert isinstance(out, dict)
for k, v in out.items():
if isinstance(v, (int, float)):
assert v >= 0, f"{k} should be non-negative, got {v}"
def test_estimate_roi_returns_dict():
out = pricing_strategy.estimate_roi(
plan_price_sar=2999,
expected_pipeline_sar=120_000,
expected_revenue_sar=30_000,
)
assert isinstance(out, dict)
assert out
# ── proof_pack ───────────────────────────────────────────────────
def test_demo_proof_pack_structure():
out = proof_pack.build_demo_proof_pack()
assert isinstance(out, dict)
assert out
def test_calculate_roi_summary_handles_zero_subscription():
"""Should not divide-by-zero on zero subscription."""
out = proof_pack.calculate_roi_summary(
subscription_sar=0,
influenced_revenue_sar=0,
hours_saved=0,
)
assert isinstance(out, dict)
def test_calculate_roi_summary_normal():
out = proof_pack.calculate_roi_summary(
subscription_sar=2999,
influenced_revenue_sar=200_000,
hours_saved=40,
)
assert isinstance(out, dict)
# multiple should be positive given non-zero inputs
assert out
def test_grade_account_health_thresholds():
healthy = proof_pack.grade_account_health(
brief_opens_4w=20, approvals_4w=10, blocks_4w=2,
)
weak = proof_pack.grade_account_health(
brief_opens_4w=2, approvals_4w=0, blocks_4w=0,
)
# healthy should grade higher
assert healthy["health_score"] >= weak["health_score"]
# And the status labels should differ for these extremes
assert healthy["status"] == "healthy"
assert weak["status"] == "at_risk"
# ── unit_economics ───────────────────────────────────────────────
def test_estimate_gross_margin_returns_dict():
assert isinstance(unit_economics.estimate_gross_margin(), dict)
def test_cac_payback_dict():
assert isinstance(unit_economics.estimate_cac_payback(), dict)
def test_estimate_ltv_dict():
assert isinstance(unit_economics.estimate_ltv(), dict)
def test_estimate_mrr_path_dict():
out = unit_economics.estimate_mrr_path()
assert isinstance(out, dict)
# ── verticals ────────────────────────────────────────────────────
def test_get_vertical_playbooks():
out = verticals.get_vertical_playbooks()
assert isinstance(out, dict)
# Verticals are nested under 'verticals' key
inner = out.get("verticals", {})
assert "clinics" in inner or "real_estate" in inner or "logistics" in inner
def test_recommend_vertical_returns_dict():
out = verticals.recommend_vertical(
industry="medical",
city="Riyadh",
goal="bookings",
)
assert isinstance(out, dict)
def test_vertical_roi_metric_returns_string():
# Try a known vertical
out = verticals.vertical_roi_metric("clinics")
assert isinstance(out, str)
assert out

View File

@ -0,0 +1,82 @@
"""Unit tests for the Connector Catalog."""
from __future__ import annotations
from auto_client_acquisition.connector_catalog import (
ALL_CONNECTORS,
all_risks,
catalog_summary,
connector_risks,
connector_status,
get_connector,
list_connectors,
)
def test_catalog_has_at_least_12_connectors():
out = list_connectors()
assert out["total"] >= 12
def test_catalog_includes_critical_connectors():
keys = {c.key for c in ALL_CONNECTORS}
for required in (
"whatsapp_cloud", "gmail", "google_calendar", "moyasar",
"linkedin_lead_forms", "google_business_profile",
"x_api", "instagram_graph", "google_sheets",
"crm_generic", "website_forms", "google_meet",
):
assert required in keys
def test_every_connector_has_risk_level():
for c in ALL_CONNECTORS:
assert c.risk_level in ("low", "medium", "high")
def test_every_connector_has_blocked_or_safe_actions():
for c in ALL_CONNECTORS:
assert isinstance(c.allowed_actions, tuple)
assert isinstance(c.blocked_actions, tuple)
def test_whatsapp_blocks_cold_send():
wa = get_connector("whatsapp_cloud")
assert wa is not None
assert "cold_send_without_consent" in wa.blocked_actions
def test_moyasar_blocks_card_storage():
m = get_connector("moyasar")
assert m is not None
assert "store_card_number" in m.blocked_actions
def test_summary_aggregates():
s = catalog_summary()
assert s["total"] == len(ALL_CONNECTORS)
assert "by_launch_phase" in s
assert "by_risk_level" in s
def test_status_returns_safe_default_modes():
out = connector_status()
for entry in out["statuses"]:
assert entry["mode"] in ("connected_draft_only", "not_connected",
"connected_live_with_approval")
def test_risks_present_for_high_risk_connectors():
for key in ("whatsapp_cloud", "moyasar", "google_meet"):
risks = connector_risks(key)
assert risks, f"missing risks for {key}"
def test_unknown_connector_has_no_risks():
assert connector_risks("totally_unknown") == []
def test_all_risks_keyed_by_connector():
out = all_risks()
for c in ALL_CONNECTORS:
assert c.key in out

View File

@ -0,0 +1,268 @@
"""Unit tests for Customer Ops."""
from __future__ import annotations
import pytest
from auto_client_acquisition.customer_ops import (
SUPPORT_PRIORITIES,
SUPPORTED_CONNECTORS,
build_at_risk_alert,
build_connector_setup_summary,
build_customer_success_plan,
build_first_response_template,
build_incident_response_plan,
build_onboarding_checklist,
build_sla_health_report,
build_weekly_check_in,
classify_sla_breach,
classify_ticket_priority,
record_sla_event,
route_ticket,
triage_incident,
update_connector_status,
update_onboarding_step,
)
# ── Onboarding ───────────────────────────────────────────────
def test_onboarding_checklist_has_8_steps():
out = build_onboarding_checklist(customer_id="c1")
assert out["total_steps"] == 8
assert out["current_step_id"] == "select_goal"
def test_update_onboarding_step_completes():
cl = build_onboarding_checklist(customer_id="c1")
cl = update_onboarding_step(cl, step_id="select_goal", completed=True)
assert cl["progress_pct"] == 12.5
assert cl["current_step_id"] == "select_bundle"
def test_update_onboarding_step_unknown():
cl = build_onboarding_checklist(customer_id="c1")
cl = update_onboarding_step(cl, step_id="bogus_step")
assert "error" in cl
def test_complete_all_onboarding_steps():
cl = build_onboarding_checklist(customer_id="c1")
for s in list(cl["steps"]):
cl = update_onboarding_step(cl, step_id=s["id"], completed=True)
assert cl["progress_pct"] == 100.0
assert cl["current_step_id"] == "done"
# ── Connectors ───────────────────────────────────────────────
def test_supported_connectors_includes_critical():
keys = {c["key"] for c in SUPPORTED_CONNECTORS}
for required in ("gmail", "google_calendar", "moyasar", "whatsapp_cloud",
"google_sheets", "website_forms", "linkedin_lead_forms"):
assert required in keys
def test_connector_summary_with_blocking_missing():
out = build_connector_setup_summary(
customer_id="c1",
statuses={"gmail": {"state": "connected_draft_only"}},
)
assert "whatsapp_cloud" in out["blocking_missing"]
assert out["ready_for_first_service"] is False
def test_connector_summary_ready():
out = build_connector_setup_summary(
customer_id="c1",
statuses={
"gmail": {"state": "connected_draft_only"},
"whatsapp_cloud": {"state": "connected_draft_only"},
},
)
assert out["ready_for_first_service"] is True
def test_update_connector_status_validates():
statuses: dict = {}
with pytest.raises(ValueError):
update_connector_status(statuses, connector_key="gmail",
state="totally_invalid")
def test_update_connector_status_writes():
statuses: dict = {}
update_connector_status(statuses, connector_key="gmail",
state="connected_draft_only")
assert statuses["gmail"]["state"] == "connected_draft_only"
# ── Support routing ──────────────────────────────────────────
def test_classify_p0_for_security_keywords():
out = classify_ticket_priority("اكتشفت تسريب في trace logs")
assert out["priority"] == "P0"
def test_classify_p0_for_unauthorized_send():
out = classify_ticket_priority("Dealix أرسل رسالة بدون موافقتي")
assert out["priority"] == "P0"
def test_classify_p1_for_service_down():
out = classify_ticket_priority("Pilot stopped working today")
assert out["priority"] == "P1"
def test_classify_p2_for_connector_issue():
out = classify_ticket_priority("My Gmail connector won't authenticate")
assert out["priority"] == "P2"
def test_classify_p3_default():
out = classify_ticket_priority("سؤال بسيط عن الأسعار")
assert out["priority"] == "P3"
def test_classify_empty_returns_p3():
out = classify_ticket_priority("")
assert out["priority"] == "P3"
def test_route_ticket_includes_sla():
out = route_ticket(text="تسريب أمان", customer_id="c1")
assert out["priority"] == "P0"
assert out["sla"]["first_response_minutes"] == 30
assert out["live_send_allowed"] is False
def test_first_response_p0_arabic():
out = build_first_response_template("P0")
assert "30 دقيقة" in out["body_ar"]
assert out["live_send_allowed"] is False
def test_support_priorities_count():
assert len(SUPPORT_PRIORITIES) == 4
# ── SLA ──────────────────────────────────────────────────────
def test_sla_event_validates():
with pytest.raises(ValueError):
record_sla_event(ticket_id="t1", priority="P0", event="bogus")
def test_sla_event_appends_to_log():
log: list = []
record_sla_event(ticket_id="t1", priority="P0", event="opened", log=log)
assert len(log) == 1
def test_classify_breach_within_target():
out = classify_sla_breach(
priority="P0", minutes_to_first_response=20, hours_to_resolve=3,
)
assert out["breached"] is False
def test_classify_breach_exceeded():
out = classify_sla_breach(
priority="P0", minutes_to_first_response=120, hours_to_resolve=10,
)
assert out["breached"] is True
assert len(out["breaches"]) == 2
def test_sla_health_report_aggregates():
out = build_sla_health_report(tickets=[
{"priority": "P0", "first_response_min": 12, "resolution_hours": 2},
{"priority": "P1", "first_response_min": 90, "resolution_hours": 18},
{"priority": "P3", "first_response_min": 1500, "resolution_hours": 200},
])
assert out["total_tickets"] == 3
assert out["total_breached"] == 1 # only P3 breached
def test_sla_health_verdict_critical():
out = build_sla_health_report(tickets=[
{"priority": "P0", "first_response_min": 60, "resolution_hours": 10},
{"priority": "P0", "first_response_min": 120, "resolution_hours": 20},
{"priority": "P0", "first_response_min": 180, "resolution_hours": 30},
{"priority": "P0", "first_response_min": 240, "resolution_hours": 40},
])
assert out["verdict"] == "critical"
# ── Incidents ───────────────────────────────────────────────
def test_triage_data_leak_is_sev1():
out = triage_incident(
title="Possible data exposure",
has_data_leak=True,
)
assert out["severity"] == "SEV1"
def test_triage_unauthorized_send_is_sev1():
out = triage_incident(
title="Unauthorized message",
has_unauthorized_send=True,
)
assert out["severity"] == "SEV1"
def test_triage_many_customers_is_sev2():
out = triage_incident(
title="Service outage",
affected_customers=10,
)
assert out["severity"] == "SEV2"
def test_triage_single_customer_is_sev3():
out = triage_incident(title="Customer X has issue", affected_customers=1)
assert out["severity"] == "SEV3"
def test_incident_response_plan_sev1_includes_pdpl():
out = build_incident_response_plan(severity="SEV1")
text = " ".join(out["plan_ar"])
assert "PDPL" in text
# ── Customer Success ────────────────────────────────────────
def test_weekly_check_in_arabic():
out = build_weekly_check_in(
customer_id="c1", company_name="Acme",
metrics={"drafts_approved": 5, "replies": 2,
"meetings": 1, "risks_blocked": 3, "pipeline_sar": 18000},
)
assert out["type"] == "weekly_check_in"
assert any("Pipeline" in tp for tp in out["talking_points_ar"])
def test_at_risk_alert_high_severity():
out = build_at_risk_alert(
customer_id="c1", days_inactive=20,
drafts_pending=15, last_proof_pack_days_ago=21,
)
assert out["severity"] == "high"
assert out["risk_score"] >= 60
def test_at_risk_alert_low_severity():
out = build_at_risk_alert(
customer_id="c1", days_inactive=2,
drafts_pending=1, last_proof_pack_days_ago=3,
)
assert out["severity"] == "low"
def test_customer_success_plan_for_growth_starter():
out = build_customer_success_plan(
customer_id="c1", bundle_id="growth_starter",
)
assert any("Day 1" in line for line in out["cadence_ar"])
def test_customer_success_plan_for_executive():
out = build_customer_success_plan(
customer_id="c1", bundle_id="executive_growth_os",
)
assert any("Founder Shadow Board" in line for line in out["cadence_ar"])

View File

@ -0,0 +1,76 @@
"""Unit tests for the new auto_client_acquisition.model_router."""
from __future__ import annotations
from auto_client_acquisition.model_router import (
ALL_PROVIDERS,
ALL_TASK_TYPES,
build_fallback_chain,
build_usage_demo,
classify_cost,
get_provider,
route_task,
)
def test_every_task_type_has_at_least_one_provider():
for tt in ALL_TASK_TYPES:
chain = build_fallback_chain(tt)
assert chain, f"no provider for task: {tt}"
def test_provider_registry_contains_essentials():
keys = {p.key for p in ALL_PROVIDERS}
for required in ("claude_sonnet", "claude_haiku", "gpt_4_class",
"gemini_pro", "azure_oai_ksa"):
assert required in keys
def test_get_provider_unknown():
assert get_provider("bogus_key") is None
def test_classify_cost_bulk_is_low():
assert classify_cost(task_type="low_cost_bulk", bulk=True) == "low"
def test_classify_cost_strategic_is_mid():
assert classify_cost(task_type="strategic_reasoning") == "mid"
def test_classify_cost_huge_output_is_high():
assert classify_cost(task_type="summarization",
expected_output_tokens=2000) == "high"
def test_high_sensitivity_prefers_ksa_or_local():
chain = build_fallback_chain(
"compliance_guardrail", sensitivity="high", requires_arabic=True,
)
top_provider = get_provider(chain[0])
assert top_provider is not None
assert top_provider.privacy_tier in ("ksa_region", "self_hosted")
def test_route_task_unknown_returns_no_provider():
d = route_task("totally_made_up")
assert d.primary_provider is None
assert d.fallback_chain == []
def test_route_task_arabic_copywriting():
d = route_task("arabic_copywriting", requires_arabic=True)
assert d.primary_provider is not None
assert d.cost_class in ("low", "mid", "high")
def test_route_task_with_primary_override():
d = route_task("strategic_reasoning", primary_provider="gpt_4_class")
assert d.fallback_chain[0] == "gpt_4_class"
def test_usage_demo_covers_all_task_types():
demo = build_usage_demo()
assert demo["task_types_total"] == len(ALL_TASK_TYPES)
assert len(demo["routes"]) == len(ALL_TASK_TYPES)
assert demo["cost_counts"]

View File

@ -0,0 +1,155 @@
"""Unit tests for the Growth Curator."""
from __future__ import annotations
from auto_client_acquisition.growth_curator import (
build_weekly_curator_report,
detect_duplicates,
grade_message,
inventory_skills,
recommend_next_mission,
recommend_next_playbook,
score_mission,
score_playbook,
suggest_improvement,
)
# ── Skill Inventory ──────────────────────────────────────────
def test_inventory_lists_kill_feature():
out = inventory_skills()
assert out["total"] >= 20
kill_ids = [s["id"] for s in out["kill_features"]]
assert "first_10_opportunities" in kill_ids
def test_inventory_layers_present():
out = inventory_skills()
layers = set(out["layers"])
assert {"platform_services", "intelligence_layer",
"growth_curator", "security_curator"}.issubset(layers)
# ── Message Curator ──────────────────────────────────────────
def test_grades_natural_arabic_message_high():
text = ("هلا أحمد، لاحظت توسعكم في فريق المبيعات. "
"نشتغل على Dealix كمدير نمو عربي. "
"يناسبك أعرض لك مثال 10 دقائق هذا الأسبوع؟")
g = grade_message(text, sector="training")
assert g.score >= 60
assert g.verdict in ("publish", "needs_edit")
def test_blocks_risky_phrases():
text = "آخر فرصة! ضمان 100% نتائج مضمونة. اضغط الآن."
g = grade_message(text)
assert g.risky_phrases
assert g.verdict in ("needs_edit", "reject")
def test_rejects_non_arabic():
text = "Hello there, just checking in. Cheers."
g = grade_message(text)
assert g.verdict == "reject"
def test_detects_near_duplicates():
msgs = [
"هلا أحمد، لاحظت توسعكم. يناسبك أعرض لك Pilot؟",
"هلا محمد، لاحظت توسعكم. يناسبك أعرض لك Pilot؟",
"totally unrelated message in english",
]
pairs = detect_duplicates(msgs, threshold=0.8)
assert any({i, j} == {0, 1} for i, j, _r in pairs)
def test_suggest_improvement_returns_skeleton():
out = suggest_improvement("Hi")
assert "suggested_skeleton_ar" in out
assert "هلا" in out["suggested_skeleton_ar"]
# ── Playbook Curator ────────────────────────────────────────
def test_score_playbook_winner_tier():
"""Strong outcomes across all signals should push into winner/promising."""
pb = {
"used_count": 100, "accept_count": 90,
"replied_count": 80, "meeting_count": 60, "deal_count": 40,
}
s = score_playbook(pb)
assert s["score"] >= 50
assert s["tier"] in ("winner", "promising")
def test_score_playbook_needs_work_tier():
"""Modest outcomes should map to needs_work."""
pb = {
"used_count": 100, "accept_count": 60,
"replied_count": 40, "meeting_count": 20, "deal_count": 8,
}
s = score_playbook(pb)
assert s["tier"] in ("needs_work", "promising")
def test_score_playbook_unproven_for_zero_uses():
s = score_playbook({"used_count": 0})
assert s["tier"] == "unproven"
assert s["score"] == 0
def test_recommend_next_playbook_default_when_empty():
rec = recommend_next_playbook([])
assert rec["recommended_id"] == "default_warm_outreach"
def test_recommend_next_playbook_picks_promising_first():
pbs = [
{"id": "p1", "title": "Winner", "tier": "winner", "score": 80},
{"id": "p2", "title": "Promising", "tier": "promising", "score": 60},
]
rec = recommend_next_playbook(pbs)
assert rec["recommended_id"] == "p2"
# ── Mission Curator ─────────────────────────────────────────
def test_score_mission_ship_it_with_strong_outcome():
out = score_mission({
"opportunities_generated": 10,
"drafts_approved": 5,
"meetings_booked": 3,
"revenue_influenced_sar": 60_000,
"time_to_value_minutes": 8,
"risks_blocked": 2,
})
assert out["score"] >= 70
assert out["verdict"] == "ship_it_widely"
def test_recommend_next_mission_starts_with_kill_feature():
rec = recommend_next_mission(None)
assert rec["recommended_mission_id"] == "first_10_opportunities"
def test_recommend_next_mission_after_kill_feature():
history = [{"mission_id": "first_10_opportunities"}]
rec = recommend_next_mission(history, growth_brain={
"growth_priorities": ["fill_pipeline"],
})
assert rec["recommended_mission_id"] == "meeting_booking_sprint"
# ── Curator Report ───────────────────────────────────────────
def test_weekly_report_handles_empty_input():
rep = build_weekly_curator_report()
assert rep["messages"]["total"] == 0
assert rep["playbooks"]["total"] == 0
assert rep["missions"]["total"] == 0
assert rep["next_playbook"]["recommended_id"]
def test_weekly_report_marks_low_quality_for_archive():
rep = build_weekly_curator_report(messages=[
{"id": "m1", "text": "Hi"},
{"id": "m2", "text": "آخر فرصة! ضمان 100% نتائج مضمونة!"},
])
assert rep["messages"]["to_archive"] >= 1

View File

@ -0,0 +1,401 @@
"""Unit tests for the Arabic Growth Operator layer."""
from __future__ import annotations
import pytest
from auto_client_acquisition.growth_operator import (
build_calendar_draft,
build_demo_profile,
build_meeting_agenda,
build_moyasar_payment_link_draft,
build_post_meeting_followup,
build_weekly_proof_pack,
classify_contact_source,
contactability_summary,
dedupe_contacts,
detect_opt_out,
draft_arabic_message,
draft_followup,
draft_objection_response,
draft_partner_outreach,
list_missions,
normalize_phone,
partner_scorecard,
profile_from_dict,
rank_targets,
recommend_top_10,
run_mission,
sar_to_halalas,
score_contactability,
segment_contacts,
suggest_partner_types,
summarize_import,
)
# ── 1. Phone normalization ───────────────────────────────────────
def test_normalize_phone_country_prefix_kept():
assert normalize_phone("+966500000001") == "966500000001"
def test_normalize_phone_local_zero_to_country():
assert normalize_phone("0500000001") == "966500000001"
def test_normalize_phone_bare_9_digits():
assert normalize_phone("500000001") == "966500000001"
def test_normalize_phone_double_zero():
assert normalize_phone("00966500000001") == "966500000001"
def test_normalize_phone_strips_punctuation():
assert normalize_phone("+966 (50) 000-0001") == "966500000001"
def test_normalize_phone_invalid_returns_empty():
assert normalize_phone("") == ""
assert normalize_phone("abc") == ""
# ── 2. Dedupe ────────────────────────────────────────────────────
def test_dedupe_drops_exact_phone_duplicates():
out = dedupe_contacts([
{"name": "X", "phone": "0500000001"},
{"name": "X duplicate", "phone": "+966 50 000 0001"},
])
assert len(out) == 1
def test_dedupe_keeps_richer_record():
out = dedupe_contacts([
{"name": "X", "phone": "0500000001"},
{"name": "X full", "phone": "0500000001", "email": "x@example.sa", "company": "Co"},
])
assert len(out) == 1
assert out[0].get("email") == "x@example.sa"
# ── 3. Source classification ─────────────────────────────────────
def test_classify_existing_customer():
assert classify_contact_source({"relationship_status": "customer"}) == "existing_customer"
def test_classify_inbound():
assert classify_contact_source({"source": "website_form"}) == "inbound_lead"
def test_classify_event():
assert classify_contact_source({"source": "exhibition"}) == "event_lead"
def test_classify_cold():
assert classify_contact_source({"source": "cold"}) == "cold_list"
def test_classify_unknown_default():
assert classify_contact_source({}) == "unknown"
# ── 4. Opt-out detection ─────────────────────────────────────────
def test_detect_opt_out_via_status():
assert detect_opt_out({"opt_in_status": "opted_out"}) is True
def test_detect_opt_out_via_arabic_notes():
assert detect_opt_out({"notes": "العميل طلب إيقاف الرسائل"}) is True
def test_detect_opt_out_clean():
assert detect_opt_out({"name": "X"}) is False
# ── 5. Summarize import ──────────────────────────────────────────
def test_summarize_import_aggregates():
contacts = [
{"name": "A", "phone": "0500000001", "source": "customer"},
{"name": "A dup", "phone": "0500000001", "source": "customer"}, # dup
{"name": "B", "phone": "0500000002", "source": "cold"},
{"name": "C", "phone": "0500000003", "opt_in_status": "opted_out"},
]
s = summarize_import(contacts)
assert s["raw_total"] == 4
assert s["after_dedupe"] == 3
assert s["duplicates_removed"] == 1
assert s["opt_out_count"] == 1
# ── 6. Contactability — core safety rules ───────────────────────
def test_contactability_blocks_opt_out():
out = score_contactability({"opt_in_status": "opted_out", "phone": "0500000001"})
assert out["label"] == "blocked"
def test_contactability_blocks_cold_whatsapp_by_default():
"""No cold WhatsApp without lawful basis — that's the policy."""
out = score_contactability(
{"phone": "0500000001", "source": "cold", "name": "X"},
channel="whatsapp",
)
assert out["label"] == "blocked"
assert any("PDPL" in r or "lawful" in r or "بدون" in r for r in out["reasons"])
def test_contactability_unknown_source_needs_review():
out = score_contactability(
{"phone": "0500000001", "name": "X"},
channel="whatsapp",
)
assert out["label"] == "needs_review"
def test_contactability_existing_customer_safe():
out = score_contactability(
{"phone": "0500000001", "name": "X", "relationship_status": "customer"},
channel="whatsapp",
)
assert out["label"] == "safe"
def test_contactability_inbound_lead_safe():
out = score_contactability(
{"phone": "0500000001", "name": "X", "source": "website_form"},
channel="whatsapp",
)
assert out["label"] == "safe"
def test_contactability_summary_aggregates():
s = contactability_summary(
[
{"phone": "0500000001", "relationship_status": "customer"},
{"phone": "0500000002", "source": "cold"},
{"phone": "0500000003", "opt_in_status": "opted_out"},
],
channel="whatsapp",
)
assert s["by_label"]["safe"] >= 1
assert s["by_label"]["blocked"] >= 2 # cold + opt-out
# ── 7. Targeting + ranking ──────────────────────────────────────
def test_rank_targets_filters_unsafe():
contacts = [
{"phone": "0500000001", "relationship_status": "customer"},
{"phone": "0500000002", "source": "cold"},
]
ranked = rank_targets(contacts, sector_hint="real_estate", channel="whatsapp")
assert len(ranked) == 1 # only the safe customer survives
def test_recommend_top_10_returns_at_most_10():
contacts = [
{"phone": f"05000000{i:02d}", "relationship_status": "customer", "name": f"X{i}"}
for i in range(15)
]
out = recommend_top_10(contacts, sector_hint="real_estate")
assert out["candidates_evaluated"] == 15
assert len(out["top"]) == 10
def test_segment_contacts_buckets():
segs = segment_contacts([
{"phone": "0500000001", "relationship_status": "customer"},
{"phone": "0500000002", "source": "exhibition"},
{"phone": "0500000003", "source": "cold"},
{"phone": "0500000004", "opt_in_status": "opted_out"},
])
assert len(segs["existing_customer"]) == 1
assert len(segs["event_lead"]) == 1
assert len(segs["cold_list"]) == 1
assert len(segs["blocked_or_invalid"]) == 1
# ── 8. Message planner — Arabic + approval invariant ────────────
def test_arabic_message_always_pending_approval():
out = draft_arabic_message(
{"phone": "0500000001", "name": "سامي", "city": "الرياض", "sector": "real_estate"},
)
assert out["approval_required"] is True
assert out["approval_status"] == "pending_approval"
def test_arabic_message_contains_arabic():
out = draft_arabic_message({"phone": "0500000001", "name": "X", "sector": "clinics"})
assert any("؀" <= ch <= "ۿ" for ch in out["body_ar"]), "body must contain Arabic"
def test_arabic_message_no_overhyped_phrases():
"""The default templates must not contain 'ضمان 100%' / 'مضمونة' etc."""
out = draft_arabic_message({"phone": "0500000001", "name": "X"})
body = out["body_ar"]
for banned in ("ضمان 100", "نتائج مضمونة", "آخر فرصة", "اضغط هنا فوراً"):
assert banned not in body
def test_followup_returns_pending_approval():
out = draft_followup({"phone": "0500000001", "name": "X"}, days_since_last=3)
assert out["approval_required"] is True
def test_objection_response_known():
out = draft_objection_response("send_offer_whatsapp")
assert "next_action" in out
assert out["approval_required"] is True
def test_objection_response_unknown_diagnostic():
out = draft_objection_response("totally_unknown_objection")
assert out["next_action"] == "diagnostic_question"
# ── 9. Partnership planner ──────────────────────────────────────
def test_partnership_suggestions_smb_emphasizes_agencies():
out = suggest_partner_types(customer_size="smb")
top_keys = [s["key"] for s in out["suggestions"][:3]]
# SMB should prioritize agency / consultant / community
assert any(k in ("marketing_agency", "sales_consultant", "founder_community") for k in top_keys)
def test_partnership_outreach_pending():
out = draft_partner_outreach(partner_type_key="marketing_agency", partner_name="Test Agency")
assert out["approval_required"] is True
def test_partnership_outreach_unknown_type():
out = draft_partner_outreach(partner_type_key="bogus_type")
assert "error" in out
def test_partner_scorecard_grading():
high = partner_scorecard(
partner_id="p1", intros_made=10, deals_influenced=4,
revenue_share_paid_sar=50_000, relationship_age_months=6,
)
low = partner_scorecard(partner_id="p2")
assert high["overall_score"] > low["overall_score"]
assert high["tier"] in ("platinum", "gold")
assert low["tier"] == "bronze"
# ── 10. Meeting planner — no live event ─────────────────────────
def test_meeting_agenda_returns_slots():
out = build_meeting_agenda(
contact_name="سامي", company="Test Co.",
purpose_ar="تأهيل أولي", duration_minutes=20,
)
assert out["agenda_ar"]
assert out["approval_required"] is True
def test_meeting_calendar_draft_not_inserted():
out = build_calendar_draft(
contact_email="x@test.sa", contact_name="X", company="Test Co.",
duration_minutes=30,
)
assert out["live_inserted"] is False
assert out["approval_required"] is True
# Required Google Calendar shape
assert "summary" in out and "start" in out and "end" in out
assert out["start"]["timeZone"] == "Asia/Riyadh"
def test_post_meeting_followup_pending():
out = build_post_meeting_followup(
contact_name="X", company="Test", summary_ar="مهتم في pilot.",
)
assert out["approval_required"] is True
# ── 11. Payment offer — no live charge ──────────────────────────
def test_sar_to_halalas_basic():
assert sar_to_halalas(1) == 100
assert sar_to_halalas(2999) == 299_900
def test_sar_to_halalas_negative_raises():
with pytest.raises(ValueError):
sar_to_halalas(-5)
def test_payment_draft_does_not_charge():
out = build_moyasar_payment_link_draft(
plan_key="growth_os", customer_id="c1", contact_email="x@test.sa",
)
assert out["live_charged"] is False
assert out["approval_required"] is True
# Moyasar payload uses halalas
assert out["moyasar_request_draft"]["amount"] == 299_900
assert out["moyasar_request_draft"]["currency"] == "SAR"
def test_payment_draft_unknown_plan():
out = build_moyasar_payment_link_draft(plan_key="bogus", customer_id="c1")
assert "error" in out
assert out["live_charged"] is False
# ── 12. Proof pack ──────────────────────────────────────────────
def test_proof_pack_structure():
out = build_weekly_proof_pack(
customer_id="c1", customer_name="Test Co.", week_label="W18-2026",
plan_cost_weekly_sar=750,
opportunities_discovered=42, messages_drafted=38,
messages_approved=33, messages_sent=33,
replies_received=11, positive_replies=4,
meetings_booked=3, meetings_held=2,
proposals_sent=1, deals_won=0,
pipeline_added_sar=185_000, revenue_won_sar=0,
risky_drafts_blocked=5, revenue_leaks_recovered=2,
avg_response_minutes=42,
)
assert out["grade"] in ("A+", "A", "B", "C", "D")
assert "activity" in out and "money" in out and "quality" in out
assert out["next_week_plan_ar"]
assert "Dealix Proof Pack" in out["markdown_export"]
# ── 13. Missions ────────────────────────────────────────────────
def test_missions_include_first_10_opportunities():
out = list_missions()
ids = {m["id"] for m in out["missions"]}
assert "first_10_opportunities" in ids
assert "recover_stalled_deals" in ids
assert "partnership_sprint" in ids
assert "safe_whatsapp_campaign" in ids
assert out["kill_feature_id"] == "first_10_opportunities"
def test_run_mission_known():
out = run_mission("first_10_opportunities", payload={"sector": "real_estate"})
assert out["mission_id"] == "first_10_opportunities"
assert out["next_step_ar"]
assert out["primary_endpoint"]
assert out["approval_required"] is True
def test_run_mission_unknown():
out = run_mission("bogus_mission")
assert "error" in out
# ── 14. Profile ─────────────────────────────────────────────────
def test_demo_profile_specialized():
p = build_demo_profile()
assert p.is_specialized()
assert "compliance_rules" in p.to_dict()
def test_profile_from_dict_partial_not_specialized():
p = profile_from_dict({"customer_id": "c1", "company_name": "X"})
assert not p.is_specialized()
def test_profile_default_compliance_blocks_keywords():
p = profile_from_dict({"customer_id": "c1"})
rules = p.compliance_rules
assert "blocked_keywords" in rules
assert "ضمان 100" in rules["blocked_keywords"]
assert rules["no_cold_whatsapp_without_lawful_basis"] is True

View File

@ -0,0 +1,190 @@
"""
Unit tests for the dealix.innovation layer deterministic, no I/O.
Covers: aeo_radar / command_feed / command_feed_live / deal_rooms /
experiments / growth_missions / proof_ledger / proof_ledger_repo /
ten_in_ten.
"""
from __future__ import annotations
import pytest
from auto_client_acquisition.innovation import (
aeo_radar,
command_feed,
deal_rooms,
experiments,
growth_missions,
proof_ledger,
ten_in_ten,
)
# ── aeo_radar ────────────────────────────────────────────────────
def test_aeo_radar_demo_default_sector():
out = aeo_radar.build_aeo_radar_demo(sector=None)
assert isinstance(out, dict)
assert out
def test_aeo_radar_demo_known_sectors():
for sector in ("clinics", "real_estate", "logistics"):
out = aeo_radar.build_aeo_radar_demo(sector=sector)
assert isinstance(out, dict)
def test_aeo_radar_unknown_sector_does_not_crash():
"""Should degrade gracefully."""
out = aeo_radar.build_aeo_radar_demo(sector="totally_unknown_xyz")
assert isinstance(out, dict)
# ── command_feed ─────────────────────────────────────────────────
def test_command_feed_demo_returns_cards():
out = command_feed.build_demo_command_feed()
assert isinstance(out, dict)
# Must contain card list
found_list = False
for k, v in out.items():
if isinstance(v, list) and v:
found_list = True
# First card should have core fields
first = v[0]
assert "type" in first
assert "title_ar" in first or "title" in first
assert found_list, "no card list found in command feed output"
def test_command_feed_card_types_known():
out = command_feed.build_demo_command_feed()
for v in out.values():
if isinstance(v, list):
for card in v:
t = card.get("type")
# Known types per the docstring
assert t in (
"opportunity", "approval_needed", "leak",
"compliance_risk", "proof_update",
), f"unknown card type: {t}"
break # only check the first list
# ── deal_rooms ───────────────────────────────────────────────────
def test_deal_rooms_default_payload():
out = deal_rooms.analyze_deal_room()
assert isinstance(out, dict)
def test_deal_rooms_with_payload():
out = deal_rooms.analyze_deal_room({
"deal_id": "d-001",
"company_name": "Test Co.",
"stage": "proposal",
"value_sar": 250_000,
})
assert isinstance(out, dict)
# ── experiments ──────────────────────────────────────────────────
def test_recommend_experiments_default():
out = experiments.recommend_experiments(None)
assert isinstance(out, dict)
def test_recommend_experiments_with_context():
out = experiments.recommend_experiments({
"current_reply_rate": 0.04,
"current_meeting_rate": 0.20,
"past_experiments": [],
})
assert isinstance(out, dict)
def test_past_failed_helper_negative_when_empty():
"""Direct check on the private helper for safety."""
assert experiments._past_failed([], "reply_rate") is False
def test_past_failed_helper_positive_when_match():
"""Real impl looks at 'outcome' field, not 'result'."""
out = experiments._past_failed(
past=[{"metric": "reply_rate_v1", "outcome": "failed"}],
metric_substr="reply_rate",
)
assert out is True
# ── growth_missions ──────────────────────────────────────────────
def test_list_growth_missions_returns_dict():
out = growth_missions.list_growth_missions()
assert isinstance(out, dict)
assert out
def test_growth_missions_includes_kill_title():
"""The flagship '10 في 10' mission must be present."""
out = growth_missions.list_growth_missions()
text = str(out)
assert "10" in text # must reference the '10 in 10' mission
# ── proof_ledger ─────────────────────────────────────────────────
def test_proof_ledger_demo_returns_dict():
out = proof_ledger.build_demo_proof_ledger()
assert isinstance(out, dict)
assert out
# ── ten_in_ten ───────────────────────────────────────────────────
def test_ten_in_ten_default_payload():
"""No payload → uses defaults, returns 10 opportunities."""
out = ten_in_ten.build_ten_opportunities(None)
assert isinstance(out, dict)
# Must produce 10 opportunities OR a counted list
found_ten = False
for v in out.values():
if isinstance(v, list) and len(v) == 10:
found_ten = True
break
assert found_ten, f"expected 10 opportunities; got: {out.keys()}"
def test_ten_in_ten_drafts_require_approval():
"""Per the docstring — every draft must be 'pending_approval'."""
out = ten_in_ten.build_ten_opportunities({
"company_name_or_url": "test.sa",
"sector": "clinics",
"city": "Riyadh",
"offer_one_liner": "WhatsApp booking automation",
"goal_meetings_or_replies": "meetings",
})
text = str(out)
# Every draft must surface approval_required + pending_approval
assert "pending_approval" in text or "approval_required" in text
def test_ten_in_ten_deterministic_for_same_input():
"""Same payload → same output (per `_slug_seed` design)."""
payload = {
"company_name_or_url": "deterministic.sa",
"sector": "real_estate",
"city": "Jeddah",
"offer_one_liner": "X",
}
a = ten_in_ten.build_ten_opportunities(payload)
b = ten_in_ten.build_ten_opportunities(payload)
# The opportunity titles / Why-Now strings should match
assert str(a) == str(b), "deterministic seed broken"
def test_ten_in_ten_different_inputs_produce_different_outputs():
a = ten_in_ten.build_ten_opportunities({
"company_name_or_url": "company-a.sa",
"sector": "clinics", "city": "Riyadh",
})
b = ten_in_ten.build_ten_opportunities({
"company_name_or_url": "company-b.sa",
"sector": "logistics", "city": "Jeddah",
})
assert str(a) != str(b)

View File

@ -0,0 +1,281 @@
"""Unit tests for the Intelligence Layer."""
from __future__ import annotations
import pytest
from auto_client_acquisition.intelligence_layer import (
DecisionMemory,
EDGE_TYPES,
INTEL_MISSIONS,
ActionGraph,
analyze_competitive_move,
build_board_brief,
build_command_feed_demo,
build_growth_brain,
build_revenue_dna_demo,
compute_trust_score,
extract_revenue_dna,
learn_from_decision,
list_intel_missions,
recommend_missions,
simulate_opportunity,
)
# ── Growth Brain ─────────────────────────────────────────────
def test_growth_brain_builds_with_defaults():
brain = build_growth_brain()
assert brain.customer_id == "demo"
assert "whatsapp" in brain.channels_connected
assert brain.preferred_tone == "warm"
def test_growth_brain_autopilot_readiness():
new_brain = build_growth_brain({
"learning_signal_count": 5, "accept_rate_30d": 0.2,
"channels_connected": ("whatsapp",),
})
assert new_brain.is_ready_for_autopilot() is False
mature_brain = build_growth_brain({
"learning_signal_count": 50, "accept_rate_30d": 0.55,
"channels_connected": ("whatsapp", "gmail"),
})
assert mature_brain.is_ready_for_autopilot() is True
# ── Command Feed ─────────────────────────────────────────────
def test_command_feed_returns_arabic_cards():
out = build_command_feed_demo()
assert out["feed_size"] >= 5
for card in out["cards"]:
assert len(card["buttons_ar"]) <= 3
assert any("؀" <= ch <= "ۿ" for ch in card["title_ar"])
def test_command_feed_includes_critical_card_types():
out = build_command_feed_demo()
types = {c["type"] for c in out["cards"]}
for required in ("opportunity", "revenue_leak", "partner_suggestion",
"meeting_prep", "review_response"):
assert required in types
# ── Action Graph ─────────────────────────────────────────────
def test_action_graph_add_and_summarize():
g = ActionGraph()
g.add_edge(
edge_type="signal_created_opportunity",
src_id="signal_1", dst_id="opp_1", customer_id="c1",
)
g.add_edge(
edge_type="message_triggered_reply",
src_id="msg_1", dst_id="reply_1", customer_id="c1",
)
summary = g.what_works_summary("c1")
assert summary["total_edges"] == 2
assert "signal_created_opportunity" in summary["by_edge_type"]
def test_action_graph_unknown_edge_type_raises():
g = ActionGraph()
with pytest.raises(ValueError):
g.add_edge(edge_type="bogus", src_id="a", dst_id="b", customer_id="c")
def test_edge_types_cover_essentials():
for required in ("signal_created_opportunity", "message_triggered_reply",
"approval_allowed_send", "blocked_action_prevented_risk"):
assert required in EDGE_TYPES
# ── Mission Engine ───────────────────────────────────────────
def test_missions_include_first_10():
out = list_intel_missions()
ids = {m["id"] for m in out["missions"]}
assert "first_10_opportunities" in ids
assert out["kill_feature_id"] == "first_10_opportunities"
def test_missions_include_aeo_and_competitive():
ids = {m["id"] for m in INTEL_MISSIONS}
assert "ai_visibility_sprint" in ids
assert "competitive_response" in ids
def test_recommend_missions_prioritizes_kill_feature():
"""Kill feature should always be near the top."""
brain = build_growth_brain({
"channels_connected": ("whatsapp",),
"growth_priorities": ("fill_pipeline",),
})
rec = recommend_missions(brain, limit=3)
ids = [m["id"] for m in rec["recommended"]]
assert "first_10_opportunities" in ids
def test_recommend_missions_without_brain():
rec = recommend_missions(None, limit=2)
assert len(rec["recommended"]) == 2
# ── Decision Memory ──────────────────────────────────────────
def test_decision_memory_records_and_aggregates():
mem = DecisionMemory(customer_id="c1")
learn_from_decision(memory=mem, decision="accept",
action_type="send_whatsapp", channel="whatsapp",
sector="real_estate", tone="warm")
learn_from_decision(memory=mem, decision="accept",
action_type="send_whatsapp", channel="whatsapp",
tone="warm")
learn_from_decision(memory=mem, decision="skip",
action_type="send_email", channel="gmail")
prefs = mem.preferences()
assert prefs["accept_rate"] == 0.6667 or 0.6 < prefs["accept_rate"] < 0.7
assert "whatsapp" in prefs["preferred_channels"]
assert "warm" in prefs["preferred_tones"]
assert "send_email" in prefs["rejected_action_types"]
def test_decision_memory_unknown_decision_raises():
mem = DecisionMemory(customer_id="c1")
with pytest.raises(ValueError):
mem.append(decision="bogus", action_type="x", channel="y")
def test_decision_memory_empty():
mem = DecisionMemory(customer_id="c1")
prefs = mem.preferences()
assert prefs["samples"] == 0
assert prefs["accept_rate"] == 0.0
# ── Trust Score ──────────────────────────────────────────────
def test_trust_blocks_cold_whatsapp_no_optin():
out = compute_trust_score(
source_quality="cold", opt_in=False, channel="whatsapp",
message_text="hello", approval_status="pending",
)
assert out["verdict"] == "blocked"
def test_trust_safe_for_existing_customer_with_consent():
out = compute_trust_score(
source_quality="customer", opt_in=True, channel="whatsapp",
message_text="مرحباً، تحديث للعميل العزيز.",
approval_status="approved",
)
assert out["verdict"] == "safe"
assert out["score"] >= 70
def test_trust_blocks_risky_phrases():
out = compute_trust_score(
source_quality="customer", opt_in=True, channel="whatsapp",
message_text="ضمان 100% نتائج مضمونة آخر فرصة",
approval_status="approved",
)
assert out["verdict"] in ("blocked", "needs_review")
def test_trust_freq_cap_lowers_score():
"""Hitting the weekly cap should lower the trust score vs not hitting it."""
base = compute_trust_score(
source_quality="customer", opt_in=True, channel="whatsapp",
message_text="hello", frequency_count_this_week=0, weekly_cap=2,
approval_status="approved",
)
capped = compute_trust_score(
source_quality="customer", opt_in=True, channel="whatsapp",
message_text="hello", frequency_count_this_week=2, weekly_cap=2,
approval_status="approved",
)
assert capped["score"] < base["score"]
assert any("سقف" in r or "weekly" in r.lower() or "تجاوز" in r
for r in capped["reasons_ar"])
# ── Revenue DNA ──────────────────────────────────────────────
def test_revenue_dna_extracts_best_channel():
out = extract_revenue_dna(
customer_id="c1",
won_deals=[
{"channel": "whatsapp", "segment": "inbound_lead", "message_angle": "value", "cycle_days": 18},
{"channel": "whatsapp", "segment": "inbound_lead", "message_angle": "value", "cycle_days": 20},
{"channel": "email", "segment": "referral", "message_angle": "warm", "cycle_days": 30},
],
)
assert out["best_channel"] == "whatsapp"
assert out["deals_observed"] == 3
def test_revenue_dna_demo_has_next_experiment():
out = build_revenue_dna_demo()
assert "next_experiment_ar" in out
assert any("؀" <= ch <= "ۿ" for ch in out["next_experiment_ar"])
def test_revenue_dna_empty_input_returns_defaults():
out = extract_revenue_dna(customer_id="c1")
assert out["best_channel"] == "whatsapp" # safe default
assert out["deals_observed"] == 0
# ── Opportunity Simulator ────────────────────────────────────
def test_simulator_returns_pipeline_estimate():
out = simulate_opportunity(
target_count=100, sector="real_estate",
avg_deal_value_sar=50_000, channel="whatsapp", cold_pct=0,
)
assert out["expected_replies"] >= 0
assert out["expected_pipeline_sar"] >= 0
assert "rates_used" in out
assert out["approval_required"] is True
def test_simulator_warns_high_cold_pct():
out = simulate_opportunity(
target_count=100, sector="saas", channel="whatsapp", cold_pct=0.6,
)
assert out["risk_score"] >= 70
assert any("PDPL" in r or "cold" in r for r in out["risks_ar"])
def test_simulator_unknown_sector_uses_default():
out = simulate_opportunity(
target_count=50, sector="totally_unknown_xyz", channel="whatsapp", cold_pct=0,
)
assert "rates_used" in out
assert out["expected_pipeline_sar"] >= 0
# ── Competitive Moves ────────────────────────────────────────
def test_competitive_move_price_change_drop_high_urgency():
out = analyze_competitive_move(
competitor_name="X", move_type="price_change",
payload={"price_delta_pct": -25},
)
assert out["urgency"] == "high"
assert out["approval_required"] is True
def test_competitive_move_unknown_type():
out = analyze_competitive_move(competitor_name="X", move_type="bogus_type")
assert "error" in out
def test_competitive_move_funding_returns_action():
out = analyze_competitive_move(competitor_name="X", move_type="funding")
assert "recommended_action_ar" in out
# ── Board Brief ──────────────────────────────────────────────
def test_board_brief_returns_decisions_opportunities_risks():
out = build_board_brief()
assert len(out["decisions_required_ar"]) >= 3
assert len(out["top_opportunities_ar"]) >= 3
assert len(out["top_risks_ar"]) >= 3
assert "key_relationship_ar" in out
assert "experiment_to_run_ar" in out
assert "metric_to_watch_ar" in out

View File

@ -0,0 +1,204 @@
"""Unit tests for Launch Ops."""
from __future__ import annotations
import pytest
from auto_client_acquisition.launch_ops import (
PRIVATE_BETA_OFFER,
build_12_min_demo_flow,
build_close_script,
build_daily_launch_scorecard,
build_discovery_questions,
build_first_20_segments,
build_followup_message,
build_launch_readiness,
build_objection_responses,
build_outreach_message,
build_private_beta_offer,
build_private_beta_safety_notes,
build_reply_handlers,
build_weekly_launch_scorecard,
decide_go_no_go,
private_beta_faq,
record_launch_event,
)
# ── Private Beta ─────────────────────────────────────────────
def test_private_beta_offer_has_essentials():
o = build_private_beta_offer()
assert o["price_sar"] == 499
assert o["duration_days"] == 7
assert o["live_send_allowed"] is False
assert o["approval_required"] is True
assert len(o["deliverables_ar"]) >= 4
def test_private_beta_offer_seats_override():
o = build_private_beta_offer(seats_remaining=2)
assert o["seats_available"] == 2
def test_private_beta_safety_notes_blocks_live():
s = build_private_beta_safety_notes()
text = " ".join(s["do_not_do_ar"])
assert "live" in text.lower() or "عشوائي" in text or "تلقائي" in text
assert any("PDPL" in line for line in s["do_not_do_ar"])
def test_private_beta_faq_arabic():
faq = private_beta_faq()
assert len(faq) >= 4
for item in faq:
assert any("؀" <= ch <= "ۿ" for ch in item["q_ar"])
assert any("؀" <= ch <= "ۿ" for ch in item["a_ar"])
# ── Demo Flow ────────────────────────────────────────────────
def test_demo_flow_is_12_minutes():
f = build_12_min_demo_flow()
assert f["duration_minutes"] == 12
assert len(f["minute_by_minute_ar"]) == 6
def test_demo_discovery_has_5_questions():
out = build_discovery_questions()
assert len(out) == 5
def test_objection_responses_cover_essentials():
out = build_objection_responses()
for k in ("price", "timing", "trust", "complexity", "data_privacy"):
assert k in out
def test_close_script_arabic():
out = build_close_script()
assert len(out["close_sequence_ar"]) >= 3
assert any("؀" <= ch <= "ۿ" for ch in out["close_template_ar"])
# ── Outreach ─────────────────────────────────────────────────
def test_first_20_has_4_segments_total_20():
out = build_first_20_segments()
assert out["total_targets"] == 20
assert len(out["segments"]) == 4
assert sum(s["count"] for s in out["segments"]) == 20
def test_outreach_message_is_arabic_and_drafts_only():
out = build_outreach_message("agency_b2b", name="أحمد")
assert any("؀" <= ch <= "ۿ" for ch in out["body_ar"])
assert out["live_send_allowed"] is False
def test_outreach_unknown_segment_falls_back():
out = build_outreach_message("totally_unknown", name="X")
assert out["body_ar"]
def test_followup_step_2_different_from_1():
s1 = build_followup_message("training_consulting", step=1, name="X")
s2 = build_followup_message("training_consulting", step=2, name="X")
assert s1["body_ar"] != s2["body_ar"]
def test_followup_step_3_archives():
s3 = build_followup_message("agency_b2b", step=3, name="X")
assert s3["kind"] == "followup_3_final"
def test_reply_handlers_include_critical():
h = build_reply_handlers()
for k in ("interested", "needs_more_info", "price_objection",
"not_now", "no_thanks", "unsubscribe"):
assert k in h
# ── Go / No-Go ───────────────────────────────────────────────
def test_readiness_all_false_returns_zero_pct():
r = build_launch_readiness(statuses={})
assert r["passed_pct"] == 0.0
assert r["passed_gates"] == 0
assert len(r["blockers_ar"]) == r["total_gates"]
def test_readiness_all_true_returns_full_pct():
statuses = {gate["id"]: True for gate in
__import__("auto_client_acquisition.launch_ops",
fromlist=["LAUNCH_GATES"]).go_no_go.LAUNCH_GATES}
r = build_launch_readiness(statuses=statuses)
assert r["passed_pct"] == 100.0
assert r["passed_gates"] == r["total_gates"]
def test_go_no_go_blocks_when_no_secrets_fails():
decision = decide_go_no_go(statuses={"tests_passed": True,
"routes_check": True,
"no_secrets": False,
"staging_health": True,
"live_sends_disabled": True})
assert decision["verdict"] == "no_go"
def test_go_no_go_blocks_when_live_sends_enabled():
decision = decide_go_no_go(statuses={"tests_passed": True,
"routes_check": True,
"no_secrets": True,
"staging_health": True,
"live_sends_disabled": False})
assert decision["verdict"] == "no_go"
def test_go_no_go_passes_with_critical_and_70pct():
statuses = {
"tests_passed": True, "routes_check": True, "no_secrets": True,
"staging_health": True, "supabase_staging": True,
"service_catalog": True, "private_beta_page": True,
"first_20_ready": True, "live_sends_disabled": True,
"payment_manual_ready": False, # 9/10 = 90%
}
decision = decide_go_no_go(statuses=statuses)
assert decision["verdict"] == "go"
# ── Scorecard ────────────────────────────────────────────────
def test_record_event_unknown_raises():
with pytest.raises(ValueError):
record_launch_event(event_type="totally_invalid")
def test_record_event_appends_to_log():
log: list = []
record_launch_event(event_type="outreach_sent", event_log=log)
assert len(log) == 1
assert log[0]["event_type"] == "outreach_sent"
def test_daily_scorecard_aggregates():
events = [{"event_type": "outreach_sent"}] * 12 + \
[{"event_type": "demo_booked"}] * 2
s = build_daily_launch_scorecard(events=events)
assert s["metrics"]["outreach_sent"] == 12
assert s["metrics"]["demo_booked"] == 2
assert s["progress"]["outreach_sent"]["pct"] == 60.0 # 12/20 = 60%
def test_weekly_scorecard_returns_verdict():
events = [{"event_type": "outreach_sent"}] * 50 + \
[{"event_type": "pilot_paid"}] * 2
s = build_weekly_launch_scorecard(events=events)
assert s["verdict"] == "on_track"
def test_weekly_scorecard_needs_focus_for_low_demos():
events = [{"event_type": "outreach_sent"}] * 5
s = build_weekly_launch_scorecard(events=events)
assert s["verdict"] == "needs_focus"
# ── Constants exposed ────────────────────────────────────────
def test_private_beta_offer_constant_exposed():
assert PRIVATE_BETA_OFFER["price_sar"] == 499
assert PRIVATE_BETA_OFFER["live_send_allowed"] is False

View File

@ -0,0 +1,120 @@
"""Unit tests for Meeting Intelligence."""
from __future__ import annotations
from auto_client_acquisition.meeting_intelligence import (
build_post_meeting_followup,
build_pre_meeting_brief,
compute_deal_risk,
extract_objections,
parse_transcript_entries,
summarize_meeting,
)
# ── Transcript Parser ───────────────────────────────────────
def test_parser_handles_meet_entries():
entries = [
{"participantId": "alice", "text": "ما رأيكم في السعر؟"},
{"participantId": "bob", "text": "السعر مرتفع لنا الآن."},
]
p = parse_transcript_entries(entries)
assert p["total_turns"] == 2
assert "alice" in p["speakers"]
def test_parser_handles_plain_text():
text = "Alice: مرحباً\nBob: السعر مرتفع لنا"
p = parse_transcript_entries(text)
assert p["total_turns"] == 2
def test_summarize_returns_arabic_summary():
parsed = parse_transcript_entries([
{"participantId": "a", "text": "نحتاج أن نفهم نموذج التسعير بشكل أوضح."},
{"participantId": "b", "text": "ممتاز، أقترح اجتماع ثاني الأسبوع القادم."},
])
s = summarize_meeting(parsed)
assert s["approval_required"] is True
assert any("اجتماع" in line or "نقاش" in line for line in s["summary_ar"])
# ── Brief ───────────────────────────────────────────────────
def test_brief_returns_six_sections():
b = build_pre_meeting_brief(
company={"name": "Acme", "sector": "saas"},
contact={"name": "أحمد", "role": "VP"},
opportunity={"expected_value_sar": 25_000},
)
assert b["company_name"] == "Acme"
assert len(b["questions_ar"]) >= 5
assert len(b["likely_objections_ar"]) >= 5
assert b["approval_required"] is True
def test_brief_works_with_empty_input():
b = build_pre_meeting_brief()
assert b["company_name"] == "?"
assert b["questions_ar"]
# ── Objection Extractor ─────────────────────────────────────
def test_extracts_price_objection():
out = extract_objections("هذا الحل غالي ولا يناسب الميزانية.")
cats = out["categories_found"]
assert "price" in cats
def test_extracts_authority_objection():
out = extract_objections("نحتاج موافقة المدير قبل أي قرار.")
assert "authority" in out["categories_found"]
def test_no_objection_in_clean_text():
out = extract_objections("اجتماع رائع، نتطلع للخطوة القادمة.")
assert out["count"] == 0
# ── Followup Builder ────────────────────────────────────────
def test_followup_returns_email_and_whatsapp_drafts():
out = build_post_meeting_followup(
next_steps=["إرسال عرض السعر", "تحديد اجتماع ثانٍ"],
contact_name="أحمد",
company_name="Acme",
)
assert "email" in out["channel_drafts"]
assert "whatsapp" in out["channel_drafts"]
assert out["channel_drafts"]["email"]["live_send_allowed"] is False
assert "أحمد" in out["channel_drafts"]["email"]["body_ar"]
def test_followup_addresses_objections():
out = build_post_meeting_followup(
next_steps=["متابعة"],
contact_name="سارة",
objections=[{"label_ar": "السعر/الميزانية"},
{"label_ar": "صاحب القرار"}],
)
assert "السعر" in out["channel_drafts"]["email"]["body_ar"]
assert out["objections_addressed"]
# ── Deal Risk ───────────────────────────────────────────────
def test_high_risk_when_no_next_step_and_authority_objection():
out = compute_deal_risk(
objections=[{"category": "authority"}, {"category": "price"}],
next_step_set=False,
decision_maker_present=False,
)
assert out["risk_score"] >= 50
assert out["risk_level"] in ("medium", "high")
def test_low_risk_with_clean_meeting():
out = compute_deal_risk(
objections=[],
next_step_set=True,
decision_maker_present=True,
days_since_last_touch=0,
)
assert out["risk_level"] == "low"

View File

@ -0,0 +1,125 @@
"""Unit tests for the Paid Beta Daily Scorecard script."""
from __future__ import annotations
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SCRIPTS_DIR = REPO_ROOT / "scripts"
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
import paid_beta_daily_scorecard as pbds # noqa: E402
# ----- core scorecard logic -----
def test_zero_input_yields_off_track():
card = pbds.build_scorecard(0, 0, 0, 0, 0, 0, as_of="2026-05-01")
assert card.messages == 0
assert card.reply_rate == 0.0
# 0 messages, 0 replies, 0 demos → daily target on messages is breached.
assert "OFF_TRACK" in card.daily_verdict or "BEHIND" in card.daily_verdict
assert any("رسالة" in a or "messages" in a.lower() for a in card.next_actions)
def test_full_day_on_track():
card = pbds.build_scorecard(10, 1, 1, 0, 0, 0, as_of="2026-05-01")
assert card.daily_verdict == "ON_TRACK"
# Weekly is still in early days; expect blockers but daily is fine.
assert "BLOCKERS" in card.weekly_verdict
def test_high_message_zero_reply_triggers_tone_action():
card = pbds.build_scorecard(20, 0, 0, 0, 0, 0)
assert any("نبرة" in a or "tone" in a.lower() for a in card.next_actions)
def test_payment_received_advances_proof_pack_action():
card = pbds.build_scorecard(20, 4, 2, 1, 1, 0)
assert any("Proof Pack" in a for a in card.next_actions)
def test_weekly_targets_hit_when_full_week():
card = pbds.build_scorecard(70, 15, 7, 3, 2, 1)
assert card.weekly_verdict == "WEEKLY_TARGETS_HIT"
def test_conversion_rates_computed():
card = pbds.build_scorecard(25, 5, 2, 1, 1, 0)
assert card.reply_rate == 0.2
assert card.demo_rate == 0.4
assert card.pilot_rate == 0.5
assert card.payment_rate == 1.0
# ----- rendering -----
def test_render_text_contains_arabic_labels():
card = pbds.build_scorecard(25, 4, 2, 1, 0, 0)
text = pbds.render_text(card)
assert "Paid Beta Daily Scorecard" in text
assert "رسائل أُرسلت" in text
assert "Daily Verdict" in text
assert "Weekly Verdict" in text
assert "Next Actions" in text
def test_render_json_is_valid_json():
card = pbds.build_scorecard(25, 4, 2, 1, 0, 0, as_of="2026-05-01")
output = pbds.render_json(card)
parsed = json.loads(output)
assert parsed["messages"] == 25
assert parsed["as_of"] == "2026-05-01"
assert "next_actions" in parsed
assert isinstance(parsed["next_actions"], list)
# ----- CLI -----
def test_cli_main_text_mode(capsys):
rc = pbds.main([
"--messages", "25", "--replies", "4",
"--demos", "2", "--pilots", "1",
"--payments", "0", "--proof-packs", "0",
])
assert rc == 0
captured = capsys.readouterr()
assert "Paid Beta Daily Scorecard" in captured.out
assert "25" in captured.out
def test_cli_main_json_mode(capsys):
rc = pbds.main([
"--messages", "25", "--replies", "4",
"--demos", "2", "--pilots", "1",
"--payments", "1", "--proof-packs", "0",
"--json",
])
assert rc == 0
captured = capsys.readouterr()
payload = json.loads(captured.out)
assert payload["messages"] == 25
assert payload["payments"] == 1
assert payload["weekly_verdict"]
def test_cli_as_of_today(capsys):
rc = pbds.main(["--messages", "10", "--as-of", "today"])
assert rc == 0
captured = capsys.readouterr()
# 'today' should resolve to a real date string in the output (YYYY-MM-DD).
assert "20" in captured.out # any year starts with "20" in our era
def test_cli_as_of_explicit(capsys):
rc = pbds.main([
"--messages", "10", "--as-of", "2026-04-30",
"--json",
])
assert rc == 0
captured = capsys.readouterr()
payload = json.loads(captured.out)
assert payload["as_of"] == "2026-04-30"

View File

@ -0,0 +1,298 @@
"""Unit tests for the Platform Services Layer."""
from __future__ import annotations
import pytest
from auto_client_acquisition.platform_services import (
ALL_CHANNELS,
EVENT_TYPES,
POLICY_RULES,
SELLABLE_SERVICES,
build_card_from_event,
build_demo_feed,
build_demo_platform_proof,
evaluate_action,
get_channel,
invoke_tool,
list_services,
make_event,
resolve_identity,
)
from auto_client_acquisition.platform_services.action_ledger import ActionLedger
from auto_client_acquisition.platform_services.channel_registry import channels_summary
from auto_client_acquisition.platform_services.unified_inbox import CARD_TYPES, InboxCard
# ── Service catalog ──────────────────────────────────────────
def test_service_catalog_returns_all_services():
out = list_services()
assert out["total"] == len(SELLABLE_SERVICES) >= 12
def test_service_catalog_includes_critical_services():
out = list_services()
keys = {s["key"] for s in out["services"]}
for required in (
"growth_operator_subscription", "channel_setup_service",
"lead_intelligence_service", "partnership_sprint",
"ai_visibility_aeo_sprint", "customer_success_operator",
):
assert required in keys
# ── Channel registry ─────────────────────────────────────────
def test_channels_include_core_channels():
keys = {c.key for c in ALL_CHANNELS}
for required in (
"whatsapp", "gmail", "google_calendar", "moyasar",
"linkedin_lead_forms", "x_api", "instagram_graph",
"google_business_profile", "google_sheets", "crm", "website_forms",
):
assert required in keys
def test_channels_summary_aggregates():
s = channels_summary()
assert s["total"] == len(ALL_CHANNELS)
assert "by_beta_status" in s and "by_risk_level" in s
def test_get_channel_unknown():
assert get_channel("bogus_channel") is None
def test_whatsapp_blocks_cold_send():
"""Channel registry asserts cold send is blocked."""
wa = get_channel("whatsapp")
assert wa is not None
assert "cold_send_without_consent" in wa.blocked_actions
# ── Event bus ────────────────────────────────────────────────
def test_event_types_include_payment_lifecycle():
for et in ("payment.initiated", "payment.paid", "payment.failed"):
assert et in EVENT_TYPES
def test_make_event_validates():
with pytest.raises(ValueError):
make_event(event_type="totally.fake", channel="whatsapp", customer_id="c")
def test_make_event_round_trip():
e = make_event(
event_type="lead.form_submitted", channel="website_forms",
customer_id="c", payload={"company": "X"},
)
d = e.to_dict()
assert d["event_type"] == "lead.form_submitted"
assert d["payload"]["company"] == "X"
# ── Action policy ────────────────────────────────────────────
def test_policy_blocks_cold_whatsapp():
d = evaluate_action(
action="send_whatsapp",
context={"source": "cold_list", "consent": False},
)
assert d.decision == "blocked"
def test_policy_blocks_payment_without_confirmation():
d = evaluate_action(
action="charge_payment",
context={"user_confirmed": False},
)
assert d.decision == "blocked"
def test_policy_blocks_secrets_in_payload():
d = evaluate_action(
action="create_draft",
context={"payload": {"api_key": "ghp_xxx"}},
)
assert d.decision == "blocked"
def test_policy_external_send_needs_approval():
d = evaluate_action(
action="send_email",
context={"approval_status": "pending"},
)
assert d.decision == "approval_required"
def test_policy_calendar_insert_needs_approval():
d = evaluate_action(
action="calendar_insert_event",
context={"approval_status": "pending"},
)
assert d.decision == "approval_required"
def test_policy_high_value_deal_review():
d = evaluate_action(
action="send_whatsapp",
context={
"deal_value_sar": 250_000, "approval_status": "approved",
"source": "existing_customer",
},
)
assert d.decision == "approval_required"
def test_policy_safe_internal_action_allowed():
d = evaluate_action(action="read_data", context={})
assert d.decision == "allow"
# ── Tool gateway ─────────────────────────────────────────────
def test_gateway_blocks_unsupported_tool():
r = invoke_tool(tool="bogus.action")
assert r.status == "unsupported"
def test_gateway_blocks_cold_whatsapp():
r = invoke_tool(
tool="whatsapp.send_message",
context={"source": "cold_list", "consent": False, "approval_status": "pending"},
)
assert r.status == "blocked"
def test_gateway_external_send_default_draft_only():
"""No live env flag → drafts even when policy allows."""
import os
os.environ.pop("WHATSAPP_ALLOW_LIVE_SEND", None)
r = invoke_tool(
tool="whatsapp.send_message",
context={
"source": "existing_customer", "consent": True,
"approval_status": "approved",
},
)
# Either draft_created (no flag) or approval_required (defensive)
assert r.status in ("draft_created", "approval_required")
def test_gateway_internal_action_passes():
r = invoke_tool(tool="gmail.read_thread", context={})
assert r.status in ("draft_created", "approval_required")
def test_gateway_payment_charge_needs_confirmation():
r = invoke_tool(
tool="moyasar.refund",
context={"user_confirmed": False, "approval_status": "approved"},
)
assert r.status == "blocked"
# ── Identity resolution ──────────────────────────────────────
def test_identity_resolves_multi_signal():
out = resolve_identity(signals=[
{"phone": "+966500000001", "company": "X", "source": "wa"},
{"email": "x@example.sa", "company": "X", "source": "gmail"},
{"crm_id": "crm_1", "company": "X", "source": "crm"},
])
assert out.primary_phone == "+966500000001"
assert out.primary_email == "x@example.sa"
assert out.crm_id == "crm_1"
assert out.confidence > 0
assert "wa" in out.sources
def test_identity_empty_signals():
out = resolve_identity(signals=[])
assert out.confidence == 0
# ── Unified inbox ────────────────────────────────────────────
def test_inbox_card_validates_button_count():
with pytest.raises(ValueError):
InboxCard(
card_id="c", type="opportunity", channel="whatsapp",
title_ar="x", summary_ar="x", why_it_matters_ar="x",
recommended_action_ar="x", risk_level="low",
buttons_ar=("a", "b", "c", "d"), # 4 → invalid
)
def test_inbox_card_validates_card_type():
with pytest.raises(ValueError):
InboxCard(
card_id="c", type="bogus_type", channel="x",
title_ar="x", summary_ar="x", why_it_matters_ar="x",
recommended_action_ar="x", risk_level="low",
)
def test_build_card_from_event_payment_failed():
e = make_event(
event_type="payment.failed", channel="moyasar", customer_id="c",
payload={"customer_id": "c1", "amount_sar": 2999},
)
card = build_card_from_event(e)
assert card is not None
assert card.type == "payment"
assert len(card.buttons_ar) <= 3
def test_build_card_from_event_review_low_rating_high_risk():
e = make_event(
event_type="review.created", channel="google_business_profile",
customer_id="c", payload={"rating": 1, "text": "bad"},
)
card = build_card_from_event(e)
assert card is not None
assert card.risk_level == "high"
def test_demo_feed_arabic_and_buttons_capped():
feed = build_demo_feed()
assert feed["feed_size"] >= 5
for card in feed["cards"]:
assert len(card["buttons_ar"]) <= 3
# Has Arabic content somewhere
text = card["title_ar"] + card["summary_ar"]
assert any("؀" <= ch <= "ۿ" for ch in text)
def test_card_types_cover_inbox_cases():
assert {"opportunity", "email_lead", "whatsapp_reply", "payment",
"meeting_prep", "review_response", "partner_suggestion"}.issubset(set(CARD_TYPES))
# ── Action ledger ────────────────────────────────────────────
def test_action_ledger_append_and_summary():
led = ActionLedger()
led.append(
customer_id="c1", action_type="send_whatsapp",
channel="whatsapp", stage="requested",
)
led.append(
customer_id="c1", action_type="send_whatsapp",
channel="whatsapp", stage="approved",
)
s = led.summary("c1")
assert s["total"] == 2
assert s["by_stage"]["requested"] == 1
assert s["by_stage"]["approved"] == 1
def test_action_ledger_unknown_stage_raises():
led = ActionLedger()
with pytest.raises(ValueError):
led.append(customer_id="c1", action_type="x", channel="y", stage="bogus")
# ── Platform proof ledger ────────────────────────────────────
def test_platform_proof_demo_structure():
p = build_demo_platform_proof()
d = p.to_dict()
assert "totals" in d and "by_channel" in d
assert d["totals"]["leads_created"] > 0
assert d["totals"]["risks_blocked"] > 0
# Cross-channel coverage
assert "whatsapp" in d["by_channel"] or "gmail" in d["by_channel"]

View File

@ -0,0 +1,141 @@
"""Positioning Lock tests — enforce category rules + prohibited claims."""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
# Positive claims that must NEVER appear in customer-facing pages.
# (Negative restatements like "no auto-DM" in safety sections are fine —
# we only block positive claims that promise forbidden behavior.)
PROHIBITED_PHRASES = (
"نضمن لك عملاء",
"نضمن مبيعات",
"نتائج مضمونة 100%",
"ضمان مضمون",
"مليون ريال خلال شهر",
"نسحب كل بيانات LinkedIn",
"نقوم بـ auto-DM",
"نتجاوز PDPL",
"بدون مراجعة بشرية",
"AI-only — لا تدخل بشري",
"بديل HubSpot",
"أرخص من Salesforce",
"نقتل CRM",
)
# Required claims that should appear in the positioning + market messaging docs.
REQUIRED_CLAIMS_FRAGMENTS_AR = (
"Approval-first",
"PDPL",
"Saudi Tone",
"Proof Pack",
)
def _read(rel_path: str) -> str:
p = REPO_ROOT / rel_path
if not p.exists():
return ""
return p.read_text(encoding="utf-8", errors="ignore")
def test_positioning_lock_exists():
text = _read("docs/POSITIONING_LOCK.md")
assert text, "POSITIONING_LOCK.md missing"
assert "Saudi Revenue Execution OS" in text
assert "ليس CRM" in text
assert "ليس بوت واتساب" in text
def test_prohibited_claims_doc_exists():
text = _read("docs/PROHIBITED_CLAIMS.md")
assert text, "PROHIBITED_CLAIMS.md missing"
assert "نضمن" in text
assert "scraping" in text.lower()
def test_approved_market_messaging_doc_exists():
text = _read("docs/APPROVED_MARKET_MESSAGING.md")
assert text, "APPROVED_MARKET_MESSAGING.md missing"
for fragment in REQUIRED_CLAIMS_FRAGMENTS_AR:
assert fragment in text, f"missing required fragment: {fragment}"
def test_no_prohibited_phrases_in_landing_pages():
"""Customer-facing landing pages must NOT contain prohibited claims."""
pages = [
"landing/private-beta.html",
"landing/services.html",
"landing/free-diagnostic.html",
"landing/first-10-opportunities.html",
"landing/agency-partner.html",
"landing/list-intelligence.html",
"landing/growth-os.html",
"landing/companies.html",
]
failures: list[str] = []
for page in pages:
text = _read(page)
if not text:
continue # page doesn't exist
for bad in PROHIBITED_PHRASES:
if bad in text:
failures.append(f"{page} contains prohibited phrase: {bad}")
assert not failures, "Prohibited phrases found:\n" + "\n".join(failures)
def test_companies_page_has_approved_messaging():
text = _read("landing/companies.html")
assert text, "landing/companies.html missing"
assert "Approval-first" in text or "approval-first" in text.lower()
# Should reference Proof Pack
assert "Proof Pack" in text
def test_marketers_or_agency_page_exists():
"""At least one of the agency-facing pages must exist."""
a = _read("landing/agency-partner.html")
m = _read("landing/marketers.html")
assert a or m, "Need at least one of agency-partner.html or marketers.html"
def test_private_beta_page_no_guarantees():
text = _read("landing/private-beta.html")
assert text, "private-beta.html missing"
assert "نضمن" not in text or "لا نضمن" in text
assert "guarantee" not in text.lower() or "no guarantee" in text.lower()
def test_revenue_today_playbook_emphasizes_approval():
text = _read("docs/REVENUE_TODAY_PLAYBOOK.md")
assert text, "REVENUE_TODAY_PLAYBOOK.md missing"
assert "Approval-first" in text or "approval" in text.lower()
# Must explicitly state no live charge
assert "live charge" in text.lower() or "API charge" in text or "manual" in text.lower()
def test_positioning_lock_has_5_bundles():
text = _read("docs/POSITIONING_LOCK.md")
for bundle in (
"Growth Starter",
"Data to Revenue",
"Executive Growth OS",
"Partnership Growth",
"Full Growth Control Tower",
):
assert bundle in text, f"missing bundle in POSITIONING_LOCK.md: {bundle}"
def test_positioning_lock_lists_5_modes():
text = _read("docs/POSITIONING_LOCK.md")
for mode in (
"CEO Mode",
"Growth Manager Mode",
"Agency Partner Mode",
"Self-Growth Mode",
"Service Delivery Mode",
):
assert mode in text, f"missing mode in POSITIONING_LOCK.md: {mode}"

View File

@ -0,0 +1,253 @@
"""Unit tests for the Revenue Company OS layer."""
from __future__ import annotations
import pytest
from auto_client_acquisition.revenue_company_os import (
REVENUE_EDGE_TYPES,
REVENUE_WORK_UNIT_TYPES,
RevenueActionGraph,
RevenueProofLedger,
aggregate_work_units,
build_card_from_event,
build_channel_health_snapshot,
build_command_feed_for_customer,
build_growth_memory_demo,
build_opportunity_factory_demo,
build_revenue_action_graph_demo,
build_revenue_proof_ledger_demo,
build_revenue_work_unit,
build_service_factory_demo,
build_weekly_self_improvement_report,
instantiate_service,
revenue_os_command_feed_demo,
)
# ── Event → card ────────────────────────────────────────────
def test_email_event_returns_arabic_card():
card = build_card_from_event({
"event_type": "email.received",
"customer_id": "c1",
"payload": {"from": "ali@example.sa", "subject": "نطلب عرض"},
})
assert card["type"] == "email_lead"
assert any("؀" <= ch <= "ۿ" for ch in card["title_ar"])
assert card["live_send_allowed"] is False
def test_low_review_returns_high_risk():
card = build_card_from_event({
"event_type": "review.created",
"payload": {"rating": 1, "text": "تأخير في الرد"},
})
assert card["risk_level"] == "high"
def test_risk_blocked_event_high_risk():
card = build_card_from_event({
"event_type": "risk.blocked",
"payload": {"reason_ar": "محاولة cold WhatsApp"},
})
assert card["risk_level"] == "high"
assert "فهم" in card["buttons_ar"]
def test_unknown_event_returns_action_required():
card = build_card_from_event({"event_type": "totally.unknown"})
assert card["type"] == "action_required"
assert card["live_send_allowed"] is False
# ── Command feed ────────────────────────────────────────────
def test_command_feed_demo_has_8_events():
feed = revenue_os_command_feed_demo()
assert feed["feed_size"] == 8
def test_command_feed_sorts_high_risk_first():
feed = revenue_os_command_feed_demo()
cards = feed["cards"]
assert cards[0]["risk_level"] == "high"
def test_command_feed_for_customer_empty():
feed = build_command_feed_for_customer(customer_id="c1", events=[])
assert feed["feed_size"] == 0
assert feed["cards"] == []
# ── Revenue Work Units ──────────────────────────────────────
def test_rwu_types_count():
assert len(REVENUE_WORK_UNIT_TYPES) >= 18
def test_build_rwu_validates_type():
with pytest.raises(ValueError):
build_revenue_work_unit(unit_type="bogus")
def test_build_rwu_returns_valid_unit():
u = build_revenue_work_unit(
unit_type="opportunity_created",
customer_id="c1",
revenue_influenced_sar=18000,
)
assert u["unit_type"] == "opportunity_created"
assert u["revenue_influenced_sar"] == 18000.0
def test_aggregate_work_units_sums_revenue():
units = [
build_revenue_work_unit(unit_type="opportunity_created",
customer_id="c1", revenue_influenced_sar=10000),
build_revenue_work_unit(unit_type="opportunity_created",
customer_id="c1", revenue_influenced_sar=20000),
build_revenue_work_unit(unit_type="risk_blocked",
customer_id="c1", risk_level="high"),
]
agg = aggregate_work_units(units)
assert agg["total_units"] == 3
assert agg["total_revenue_influenced_sar"] == 30000.0
assert agg["risks_blocked"] == 1
# ── Revenue Action Graph ────────────────────────────────────
def test_action_graph_edge_types_count():
assert len(REVENUE_EDGE_TYPES) >= 12
def test_action_graph_add_edge_validates():
g = RevenueActionGraph()
with pytest.raises(ValueError):
g.add_edge(edge_type="bogus", src_id="a", dst_id="b")
def test_action_graph_demo_has_two_customers():
out = build_revenue_action_graph_demo()
assert "summary_a" in out
assert "summary_b" in out
assert out["summary_a"]["outcome_score"] > 0
def test_action_graph_what_works():
g = RevenueActionGraph()
g.add_edge(edge_type="proposal_led_to_payment", src_id="p1", dst_id="pay1",
customer_id="c1")
g.add_edge(edge_type="reply_led_to_meeting", src_id="r1", dst_id="m1",
customer_id="c1")
summary = g.what_works_for_customer("c1")
assert summary["total_edges"] == 2
assert summary["outcome_score"] > 0
# ── Channel Health ──────────────────────────────────────────
def test_channel_health_snapshot_returns_score():
out = build_channel_health_snapshot()
assert "channels" in out
assert "overall_score" in out
def test_channel_health_flags_risky_channel():
out = build_channel_health_snapshot(metrics_per_channel={
"email": {"bounce_rate": 0.20, "complaint_rate": 0.01,
"opt_out_rate": 0.30, "reply_rate": 0.001},
})
assert "email" in out["channels_at_risk"]
# ── Opportunity factory ─────────────────────────────────────
def test_opportunity_factory_returns_5_opps():
out = build_opportunity_factory_demo(limit=5)
assert out["count"] == 5
for opp in out["opportunities"]:
assert opp["live_send_allowed"] is False
def test_opportunity_factory_blocks_unsafe_actions():
out = build_opportunity_factory_demo()
notes = " ".join(out["do_not_do_ar"])
assert "scraping" in notes.lower() or "scraping" in notes
# ── Service factory ────────────────────────────────────────
def test_instantiate_service_known():
out = instantiate_service(
service_id="first_10_opportunities_sprint",
customer_id="c1",
)
assert "intake" in out
assert "workflow" in out
assert "quote" in out
assert out["live_send_allowed"] is False
def test_instantiate_service_unknown():
out = instantiate_service(service_id="totally_unknown")
assert "error" in out
def test_service_factory_demo_returns_4_services():
out = build_service_factory_demo()
assert len(out["instantiations"]) == 4
# ── Proof Ledger ────────────────────────────────────────────
def test_proof_ledger_appends_units():
led = RevenueProofLedger()
led.append_work_unit(build_revenue_work_unit(
unit_type="opportunity_created", customer_id="c1",
revenue_influenced_sar=10000,
))
summary = led.summary_for_customer("c1")
assert summary["totals"]["opportunities_created"] == 1
def test_proof_ledger_rejects_unknown_type():
led = RevenueProofLedger()
with pytest.raises(ValueError):
led.append_work_unit({"unit_type": "totally_bogus"})
def test_proof_ledger_demo_has_revenue():
out = build_revenue_proof_ledger_demo()
assert out["totals"]["revenue_influenced_sar"] > 0
assert out["totals"]["risks_blocked"] >= 2
# ── Growth Memory ───────────────────────────────────────────
def test_growth_memory_demo_has_top_objections():
out = build_growth_memory_demo()
assert out["summary"]["top_objections"]
def test_growth_memory_best_message():
out = build_growth_memory_demo()
assert out["best_message_training"]["sector"] == "training"
# ── Self-improvement loop ───────────────────────────────────
def test_self_improvement_low_approval_recommends_fix():
out = build_weekly_self_improvement_report(weekly_metrics={
"approval_rate": 0.10,
})
assert out["recommendations_ar"]
assert any("approval_rate" in r for r in out["recommendations_ar"])
def test_self_improvement_blocked_actions_high_recommends_review():
out = build_weekly_self_improvement_report(weekly_metrics={
"approval_rate": 0.5, "blocked_actions": 25,
})
assert any("منع" in r for r in out["recommendations_ar"])
def test_self_improvement_returns_best_service():
out = build_weekly_self_improvement_report(weekly_metrics={
"service_revenue_sar": {
"first_10_opportunities_sprint": 1500,
"growth_os_monthly": 5000,
},
})
assert out["best_service_id"] == "growth_os_monthly"

View File

@ -0,0 +1,245 @@
"""Unit tests for Revenue Launch."""
from __future__ import annotations
import pytest
from auto_client_acquisition.revenue_launch import (
PIPELINE_STAGES,
add_prospect,
build_24h_delivery_plan,
build_499_pilot_offer,
build_case_study_free_offer,
build_client_intake_form,
build_client_summary,
build_first_10_opportunities_delivery,
build_first_20_segments_v2,
build_followup_1,
build_followup_2,
build_growth_diagnostic_delivery,
build_growth_os_pilot_offer,
build_list_intelligence_delivery,
build_moyasar_invoice_instructions,
build_next_step_recommendation,
build_outreach_message_v2,
build_payment_confirmation_checklist,
build_payment_link_message,
build_pipeline_schema,
build_private_beta_offer,
build_private_beta_proof_pack,
build_reply_handlers_v2,
recommend_offer_for_segment,
summarize_pipeline,
update_stage,
)
# ── Offers ───────────────────────────────────────────────────
def test_499_pilot_has_correct_price():
o = build_499_pilot_offer()
assert o["price_sar"] == 499
assert o["live_send_allowed"] is False
assert o["no_live_charge"] is True
def test_growth_os_pilot_30_days():
o = build_growth_os_pilot_offer()
assert o["duration_days"] == 30
assert o["price_sar_min"] == 1500
assert o["price_sar_max"] == 3000
def test_case_study_free_requires_consent():
o = build_case_study_free_offer()
assert o["price_sar"] == 0
assert o["case_study_required"] is True
def test_recommend_offer_for_agency():
out = recommend_offer_for_segment("agency_b2b")
assert out["primary_offer"] == "growth_os_pilot_30d"
def test_recommend_offer_for_training():
out = recommend_offer_for_segment("training_consulting")
assert out["primary_offer"] == "pilot_499_7d"
def test_recommend_offer_unknown_segment_default():
out = recommend_offer_for_segment("totally_unknown")
assert out["primary_offer"] == "pilot_499_7d"
def test_private_beta_offer_re_export():
o = build_private_beta_offer()
assert o["price_sar"] == 499
# ── Pipeline ─────────────────────────────────────────────────
def test_pipeline_schema_has_8_stages():
s = build_pipeline_schema()
assert len(s["stages"]) == 8
assert "paid" in s["stages"]
assert "lost" in s["stages"]
def test_add_prospect_starts_at_identified():
p = add_prospect(company="Acme", segment="saas_tech_small")
assert p["stage"] == "identified"
assert p["paid"] is False
def test_update_stage_to_paid_marks_paid_true():
p = add_prospect(company="Acme")
update_stage(prospect=p, new_stage="paid", notes="Moyasar 499")
assert p["stage"] == "paid"
assert p["paid"] is True
assert "Moyasar" in str(p["notes"])
def test_update_stage_invalid_raises():
p = add_prospect(company="Acme")
with pytest.raises(ValueError):
update_stage(prospect=p, new_stage="bogus_stage")
def test_summarize_pipeline_counts_revenue():
pipeline = []
p1 = add_prospect(pipeline=pipeline, company="A", segment="agency_b2b")
p2 = add_prospect(pipeline=pipeline, company="B", segment="training")
p1["price_sar"] = 499
update_stage(prospect=p1, new_stage="paid")
update_stage(prospect=p2, new_stage="lost")
s = summarize_pipeline(pipeline)
assert s["total_prospects"] == 2
assert s["revenue_paid_sar"] == 499.0
assert s["by_stage"]["paid"] == 1
assert s["by_stage"]["lost"] == 1
assert s["win_rate"] == 0.5
# ── Outreach ─────────────────────────────────────────────────
def test_first_20_segments_v2():
out = build_first_20_segments_v2()
assert out["total_targets"] == 20
def test_outreach_message_v2_arabic():
out = build_outreach_message_v2("agency_b2b")
assert any("؀" <= ch <= "ۿ" for ch in out["body_ar"])
def test_followup_1_and_2_differ():
s1 = build_followup_1("training_consulting")
s2 = build_followup_2("training_consulting")
assert s1["body_ar"] != s2["body_ar"]
def test_reply_handlers_v2_includes_unsubscribe():
h = build_reply_handlers_v2()
assert "unsubscribe" in h
# ── Pilot delivery ───────────────────────────────────────────
def test_intake_form_has_required_fields():
f = build_client_intake_form()
keys = {q["key"] for q in f["fields"]}
for required in ("company_name", "sector", "city", "primary_offer",
"approval_owner"):
assert required in keys
def test_24h_delivery_plan_has_5_phases():
p = build_24h_delivery_plan("first_10_opportunities_sprint")
assert len(p["phases"]) == 5
assert p["live_send_allowed"] is False
def test_first_10_delivery_has_proof():
out = build_first_10_opportunities_delivery({"sector": "training"})
assert "Proof Pack v1" in out["deliverables"]
assert out["approval_required"] is True
def test_list_intelligence_delivery_includes_50_targets():
out = build_list_intelligence_delivery({"sector": "real_estate"})
assert any("50" in d for d in out["deliverables"])
def test_growth_diagnostic_delivery_24h():
out = build_growth_diagnostic_delivery({"sector": "saas"})
assert "24" in out["delivery_time"] or "ساعة" in out["delivery_time"]
# ── Payment manual flow ──────────────────────────────────────
def test_invoice_instructions_correct_halalas():
out = build_moyasar_invoice_instructions(amount_sar=499)
assert out["amount_sar"] == 499
assert out["amount_halalas"] == 49900
assert out["no_live_charge"] is True
def test_invoice_instructions_warns_no_card_storage():
out = build_moyasar_invoice_instructions(amount_sar=499)
text = " ".join(out["do_not_do_ar"])
assert "بطاقة" in text or "card" in text.lower()
def test_payment_link_message_arabic_and_no_live_send():
out = build_payment_link_message(
customer_name="أحمد", invoice_url="https://example.com/inv/1",
)
assert any("؀" <= ch <= "ۿ" for ch in out["body_ar"])
assert out["live_send_allowed"] is False
def test_payment_confirmation_checklist_blocks_premature_delivery():
out = build_payment_confirmation_checklist()
text = " ".join(out["do_not_do_ar"])
assert "paid" in text.lower() or "تأكيد" in text
# ── Proof Pack ───────────────────────────────────────────────
def test_proof_pack_template_has_metrics():
out = build_private_beta_proof_pack(company_name="Acme")
assert "opportunities_generated" in out["metrics_to_include"]
assert out["approval_required"] is True
def test_client_summary_returns_5_lines():
out = build_client_summary(
company_name="Acme", opportunities_count=10,
approved_drafts=4, meetings=2, pipeline_sar=18000,
risks_blocked=3,
)
assert len(out["summary_ar"]) == 5
assert any("18000" in line or "18,000" in line or "18000" in str(line)
for line in out["summary_ar"])
def test_next_step_upsell_for_strong_outcome():
out = build_next_step_recommendation(pilot_metrics={
"pipeline_sar": 30000, "meetings": 3, "csat": 9,
})
assert out["next_action"] == "upsell_growth_os_monthly"
def test_next_step_iterate_for_weak_outcome():
out = build_next_step_recommendation(pilot_metrics={
"pipeline_sar": 1000, "meetings": 0, "csat": 5,
})
assert out["next_action"] == "iterate_or_archive"
def test_next_step_extend_for_promising_outcome():
out = build_next_step_recommendation(pilot_metrics={
"pipeline_sar": 12000, "meetings": 1, "csat": 7,
})
assert out["next_action"] == "extend_pilot"
# ── Constants ───────────────────────────────────────────────
def test_pipeline_stages_constant_exposed():
assert "identified" in PIPELINE_STAGES
assert "paid" in PIPELINE_STAGES
assert "lost" in PIPELINE_STAGES

View File

@ -0,0 +1,132 @@
"""Unit tests for the Security Curator."""
from __future__ import annotations
from auto_client_acquisition.security_curator import (
detect_secret_patterns,
inspect_diff,
is_safe_diff,
redact_secrets,
redact_trace,
sanitize_tool_output,
sanitize_trace_event,
scan_payload,
)
# ── Secret Redactor ──────────────────────────────────────────
def test_detects_github_pat():
text = "my token is ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234"
findings = detect_secret_patterns(text)
assert any(f.label == "github_pat" for f in findings)
assert all("ghp_AAAA" not in f.sample_redacted for f in findings) # never raw
def test_redacts_openai_key():
text = "key=sk-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234 done"
out = redact_secrets(text)
assert "sk-AAAA" not in out
assert "sk-***" in out
def test_redacts_anthropic_key():
text = "ANTHROPIC=sk-ant-aBcDeFgHiJkLmNoPqRsTuVwXyZ1234"
out = redact_secrets(text)
assert "sk-ant-aBcD" not in out
def test_scan_payload_dict_redacts_sensitive_keys():
payload = {"api_key": "ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234",
"name": "ali"}
result = scan_payload(payload)
assert result["has_secrets"] is True
assert result["redacted"]["api_key"] == "***"
assert result["redacted"]["name"] == "ali"
def test_scan_payload_handles_nested():
payload = {"outer": {"token": "EAA" + "x" * 40, "ok": "yes"}}
result = scan_payload(payload)
assert result["has_secrets"] is True
assert result["redacted"]["outer"]["token"] == "***"
def test_scan_empty_returns_no_findings():
out = scan_payload({})
assert out["has_secrets"] is False
assert out["findings"] == []
# ── Patch Firewall ───────────────────────────────────────────
def test_blocks_env_file_diff():
diff = """diff --git a/.env b/.env
new file mode 100644
+++ b/.env
+API_KEY=ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234
"""
r = inspect_diff(diff)
assert r.safe is False
assert any(".env" in f for f in r.blocked_files)
def test_blocks_secret_in_added_line():
diff = """+++ b/src/foo.py
+OPENAI_KEY = "sk-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234"
"""
r = inspect_diff(diff)
assert r.safe is False
assert r.secret_findings
def test_allows_safe_diff():
diff = """+++ b/src/foo.py
+def hello():
+ return "world"
"""
assert is_safe_diff(diff) is True
def test_empty_diff_is_safe():
assert is_safe_diff("") is True
# ── Trace Redactor ───────────────────────────────────────────
def test_trace_masks_phone_and_email():
payload = {"note": "call +966500000123 or email ali@example.com"}
out = redact_trace(payload, mask_pii=True)
assert out["had_pii"] is True
masked = out["redacted"]["note"]
assert "+966500000123" not in masked
assert "ali@example.com" not in masked
assert "@example.com" in masked # domain preserved
def test_trace_redacts_secrets_and_pii_together():
payload = {"token": "ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234",
"phone": "+966500000999"}
out = redact_trace(payload)
assert out["had_secrets"] is True
assert out["redacted"]["token"] == "***"
# ── Tool Output Sanitizer ────────────────────────────────────
def test_sanitize_output_strips_secret():
output = {"raw": "ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234 inside"}
out = sanitize_tool_output(output)
assert out["safe"] is False
assert "ghp_AAAA" not in str(out["redacted"])
assert any("حساسة" in n for n in out["notes_ar"])
def test_sanitize_trace_event_keeps_safe_keys():
event = {
"event_type": "tool_call", "agent_name": "scout",
"status": "ok", "latency_ms": 120,
"payload": {"phone": "+966500000123"},
}
out = sanitize_trace_event(event)
assert out["event_type"] == "tool_call"
assert out["agent_name"] == "scout"
assert out["latency_ms"] == 120
# payload was sanitized
assert "+966500000123" not in str(out["payload"])

View 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

View 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)

View 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