mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
Merge branch 'main' into claude/dealix-tier1-completion-gHdQ9
This commit is contained in:
commit
87d2c0d13b
211
salesflow-saas/COMMAND_CENTER.md
Normal file
211
salesflow-saas/COMMAND_CENTER.md
Normal file
@ -0,0 +1,211 @@
|
||||
# DEALIX — لوحة التحكم الكاملة
|
||||
# افتح هذا الملف كل صباح. نفّذ بالترتيب. لا تتخطى.
|
||||
|
||||
---
|
||||
|
||||
## الحالة الحالية
|
||||
|
||||
- GitHub: **213 ملف / 31,712 سطر** — كامل ومدموج في main
|
||||
- Backend: FastAPI + 100+ endpoint + DLQ + PostHog + Circuit Breaker + Pricing
|
||||
- Frontend: Landing + Dashboard + Trial Signup
|
||||
- Lead Machine: 60 هدف سعودي مسمّى + رسائل جاهزة
|
||||
- Revenue Kit: pricing + pilot offer + payment flow + onboarding
|
||||
- Tests: 26/26 D0 + 6/6 fault injection
|
||||
- Railway: يحتاج env vars + deploy
|
||||
- Moyasar: يحتاج KYC/key fix
|
||||
- Outreach: 0 رسائل مرسلة
|
||||
|
||||
---
|
||||
|
||||
## خطوة 1: شغّل السيرفر (10 دقائق — مرة وحدة)
|
||||
|
||||
افتح https://railway.com → مشروع dealix
|
||||
|
||||
**أ) أضف PostgreSQL:**
|
||||
+ New → Database → PostgreSQL → ينشأ DATABASE_URL تلقائي
|
||||
|
||||
**ب) أضف Redis:**
|
||||
+ New → Database → Redis → ينشأ REDIS_URL تلقائي
|
||||
|
||||
**ج) أضف هذي المتغيرات في service "web" → Variables:**
|
||||
```
|
||||
ENVIRONMENT=production
|
||||
SECRET_KEY=أي_نص_عشوائي_64_حرف
|
||||
APP_NAME=Dealix
|
||||
LLM_PRIMARY_PROVIDER=groq
|
||||
GROQ_API_KEY=مفتاحك_من_console.groq.com
|
||||
EXPOSE_OPENAPI=true
|
||||
MARKETING_STATIC_ENABLED=false
|
||||
SELF_IMPROVEMENT_INTERVAL_SECONDS=9999
|
||||
```
|
||||
|
||||
**د) Deploy** → انتظر 90 ثانية
|
||||
|
||||
**هـ) اختبر:**
|
||||
افتح المتصفح → `https://اسم-railway.up.railway.app/health`
|
||||
لو رجع `{"status":"ok"}` → السيرفر شغّال
|
||||
|
||||
---
|
||||
|
||||
## خطوة 2: أرسل أول 5 رسائل (30 دقيقة — اليوم)
|
||||
|
||||
### واتساب (أسرع طريقة — الأفضل تبدأ فيها):
|
||||
|
||||
افتح واتساب → أرسل لـ **5 أشخاص تعرفهم** يملكون business:
|
||||
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
أطلقت Dealix اليوم — AI sales rep بالعربي الخليجي
|
||||
يرد على leads الشركات خلال 45 ثانية، يؤهّلهم، ويحجز demos.
|
||||
|
||||
أبحث عن 3 أصدقاء عندهم business حقيقي لتجربة
|
||||
pilot بـ 1 ريال لمدة 7 أيام.
|
||||
|
||||
فكرت فيك لأن [سبب محدد].
|
||||
|
||||
مهتم نتكلم 10 دقائق الآن؟
|
||||
```
|
||||
|
||||
### LinkedIn (بالتوازي):
|
||||
|
||||
افتح https://www.linkedin.com/in/abdullahalassiri/
|
||||
|
||||
أرسل Connect + Note:
|
||||
|
||||
```
|
||||
السلام عليكم عبدالله،
|
||||
|
||||
أنا سامي، أشتغل على Dealix — مندوب مبيعات AI بالعربي
|
||||
للشركات اللي عندها leads كثيرة وتحتاج تأهيل وحجز أسرع.
|
||||
|
||||
أبي أوريك تجربة 20 دقيقة على سيناريو قريب من Lucidya.
|
||||
|
||||
يناسبك بكرة 11 ص أو 3 م؟
|
||||
calendly.com/sami-assiri11/dealix-demo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## خطوة 3: انشر أول بوست (5 دقائق — اليوم)
|
||||
|
||||
افتح LinkedIn → New Post → الصق:
|
||||
|
||||
```
|
||||
أطلقنا Dealix 🚀
|
||||
|
||||
Dealix هو مندوب مبيعات AI بالعربي للشركات اللي عندها
|
||||
leads كثيرة وتحتاج رد أسرع وتأهيل أفضل.
|
||||
|
||||
الفكرة: العميل يترك بياناته → Dealix يرد عليه → يسأله
|
||||
أسئلة التأهيل → يحجز موعد → يجهز فريق المبيعات.
|
||||
|
||||
نبدأ بـ pilot يدوي لأول الشركات.
|
||||
|
||||
إذا عندك leads تضيع بسبب بطء الرد، أقدر أوريك
|
||||
تجربة 20 دقيقة:
|
||||
|
||||
🌐 dealix.me
|
||||
📅 calendly.com/sami-assiri11/dealix-demo
|
||||
|
||||
#SaudiSaaS #AI #Sales #ArabicAI #BuildInPublic
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## خطوة 4: لما أحد يرد (جاهز)
|
||||
|
||||
### لو قال "كم السعر؟":
|
||||
```
|
||||
نبدأها كـ pilot بسيط لمدة 7 أيام.
|
||||
نجرب Dealix على 10–25 lead، ونقيس النتائج.
|
||||
499 ريال فقط — مع ضمان استرداد كامل لو ما عجبك.
|
||||
```
|
||||
|
||||
### لو قال "عندنا CRM":
|
||||
```
|
||||
ممتاز، Dealix ما يستبدل الـ CRM.
|
||||
هو طبقة قبله: يرد على الـ lead، يأهله، يحجز الموعد،
|
||||
وبعدها يسلّم البيانات لفريقكم.
|
||||
```
|
||||
|
||||
### لو قال "أبي أجرب":
|
||||
```
|
||||
تمام! أرسل لك رابط الدفع (499 ريال / 7 أيام).
|
||||
بعد التأكيد نبدأ الإعداد خلال 4 ساعات.
|
||||
|
||||
الدفع:
|
||||
- تحويل بنكي: [رقم حسابك]
|
||||
- STC Pay: [رقمك]
|
||||
```
|
||||
|
||||
### لو قال "لا مهتم":
|
||||
```
|
||||
تمام، شكراً على وقتك.
|
||||
هل تعرف أحد ممكن يستفيد؟ 10% من أول سنة لك.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## خطوة 5: أول عميل يدفع (اليوم أو خلال أسبوع)
|
||||
|
||||
**عند الدفع:**
|
||||
1. تأكد من إثبات الدفع (screenshot تحويل)
|
||||
2. ابدأ onboarding خلال 4 ساعات
|
||||
3. اجمع: اسم الشركة، قطاع، مصدر الليدز، رقم واتساب، أسئلة التأهيل
|
||||
4. ابدأ رد يدوي على أول 10 leads
|
||||
5. أرسل تقرير يومي مختصر
|
||||
6. بعد 7 أيام: حوّله لـ Starter (990 ريال/شهر)
|
||||
|
||||
---
|
||||
|
||||
## الجدول اليومي (كل يوم حتى أول 10 عملاء)
|
||||
|
||||
```
|
||||
08:00 تحقق من الردود
|
||||
09:00 أرسل 10 LinkedIn DMs
|
||||
10:00 أرسل 5 واتساب warm
|
||||
11:00 Demo لو محجوز
|
||||
12:00 Follow-ups
|
||||
14:00 5 رسائل وكالات
|
||||
16:00 انشر 1-2 بوست
|
||||
17:00 3 referral asks
|
||||
20:00 ردود متأخرة + scorecard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## الأرقام المستهدفة
|
||||
|
||||
| الأسبوع | رسائل | ردود | demos | pilots | مدفوع | MRR |
|
||||
|---------|-------|------|-------|--------|-------|-----|
|
||||
| 1 | 50 | 5 | 1 | 1 | 0 | 0 |
|
||||
| 2 | 100 | 10 | 3 | 2 | 1 | 990 |
|
||||
| 3 | 150 | 15 | 5 | 3 | 3 | 2,970 |
|
||||
| 4 | 200 | 25 | 8 | 5 | 5 | 4,950 |
|
||||
|
||||
---
|
||||
|
||||
## الملفات المهمة على GitHub
|
||||
|
||||
| الملف | الغرض |
|
||||
|-------|--------|
|
||||
| `docs/ops/DAILY_REVENUE_MACHINE.md` | 8 قنوات إيراد + جدول يومي |
|
||||
| `docs/ops/lead_machine/SAUDI_60_TARGETS.md` | 60 هدف مسمّى + رسائل |
|
||||
| `docs/ops/10_CUSTOMERS_PER_WEEK_MACHINE.md` | خطة 10 عملاء/أسبوع |
|
||||
| `revenue-activation/FIRST_3_CLIENTS_PLAN.md` | خطة أول 3 عملاء |
|
||||
| `revenue-activation/sales-pack/ONE_PAGER.md` | ملخص صفحة واحدة |
|
||||
| `docs/customer_learnings/pilot_agreement_template.md` | عقد pilot |
|
||||
| `docs/customer_learnings/pricing_discovery.md` | بحث التسعير |
|
||||
| `RUNBOOK.md` | 5 سيناريوهات طوارئ |
|
||||
| `SLO.md` | أهداف الأداء |
|
||||
| `LAUNCH_GATES.md` | 33 gate checklist |
|
||||
| `landing/trial-signup.html` | صفحة تسجيل عربية |
|
||||
|
||||
---
|
||||
|
||||
## قاعدة ذهبية
|
||||
|
||||
**لا تبني أكثر. ابدأ أبيع.**
|
||||
|
||||
كل شي تقني جاهز. الشي الوحيد اللي ينقص = أول SENT.
|
||||
219
salesflow-saas/EXECUTE_NOW.md
Normal file
219
salesflow-saas/EXECUTE_NOW.md
Normal file
@ -0,0 +1,219 @@
|
||||
# DEALIX — نفّذ الحين
|
||||
|
||||
**هذا الملف الوحيد اللي تحتاجه. افتحه كل صباح. نفّذ بالترتيب. لا تتخطى.**
|
||||
|
||||
---
|
||||
|
||||
## الحالة: المنتج شغّال. الإيراد = 0. السبب = ما بدأت تبيع.
|
||||
|
||||
---
|
||||
|
||||
## الساعة 1 — شغّل المحرك (مرة وحدة، 15 دقيقة)
|
||||
|
||||
### 1.1 أضف 4 مفاتيح مجانية في Railway
|
||||
|
||||
افتح: https://railway.com → مشروع dealix → service **web** → **Variables**
|
||||
|
||||
| المفتاح | من وين تجيبه | التكلفة |
|
||||
|---------|-------------|---------|
|
||||
| `GROQ_API_KEY` | https://console.groq.com/keys → Create API Key | مجاني |
|
||||
| `GOOGLE_SEARCH_API_KEY` | https://console.cloud.google.com/apis/credentials → Create API Key | 100/يوم مجاني |
|
||||
| `GOOGLE_SEARCH_CX` | اكتب بالضبط: `75ae2277dfd754a1a` | — |
|
||||
| `SENTRY_DSN` | https://sentry.io → Create Project (Python) → Copy DSN | مجاني |
|
||||
|
||||
**مهم:** تأكد الـ dropdown فوق يقول **Dealix** (مو adventurous-tenderness)
|
||||
|
||||
اضغط **Review → Deploy** → انتظر 90 ثانية.
|
||||
|
||||
**اختبر:** افتح المتصفح:
|
||||
```
|
||||
https://web-dealix.up.railway.app/api/v1/prospect/search-diag
|
||||
```
|
||||
لو شفت `GROQ_API_KEY.set: true` و `GOOGLE_SEARCH_API_KEY.set: true` = نجحت.
|
||||
|
||||
---
|
||||
|
||||
## الساعة 2 — ابدأ البيع (30 دقيقة)
|
||||
|
||||
### 2.1 أرسل 5 رسائل واتساب
|
||||
|
||||
افتح واتساب. اختر 5 أشخاص **تعرفهم شخصياً** يملكون business. الصق:
|
||||
|
||||
**لصديق SaaS/تقنية:**
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
أطلقت Dealix — نظام يرد على leads شركتك بالعربي خلال 45 ثانية،
|
||||
يؤهلهم، ويحجز مواعيد تلقائياً.
|
||||
أبحث عن 3 أصدقاء لتجربة pilot 7 أيام بـ 499 ريال.
|
||||
فكرت فيك لأن [السبب].
|
||||
مهتم نتكلم 10 دقائق؟
|
||||
```
|
||||
|
||||
**لصاحب وكالة تسويق:**
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
أشتغل على Dealix — طبقة AI بالعربي ترد على leads عملاء الوكالات
|
||||
وتأهلهم وتحجز مواعيد.
|
||||
الفكرة لكم: تبيعونها كـ setup + retainer. العميل يشوف ROI أسرع.
|
||||
نبدأ بـ pilot مجاني لأول عميل عندكم.
|
||||
20 دقيقة أشرح؟ calendly.com/sami-assiri11/dealix-demo
|
||||
```
|
||||
|
||||
**لصاحب عقارات/مقاولات/خدمات:**
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
شركات [القطاع] تستقبل استفسارات كثيرة — وكثير تضيع بسبب تأخر الرد.
|
||||
Dealix يرد خلال 45 ثانية بالعربي، يسأل أسئلة التأهيل، ويحجز الموعد.
|
||||
تجربة 7 أيام بـ 499 ريال. يناسبك أوريك مثال؟
|
||||
لو ما يناسبكم: "إيقاف".
|
||||
```
|
||||
|
||||
**لأي معرفة (referral):**
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
أطلقت مشروع Dealix — نظام يساعد الشركات ترد على استفساراتها بسرعة بالعربي.
|
||||
تعرف أحد عنده شركة وعنده leads كثيرة ومتابعة ضعيفة؟
|
||||
لو تعرّفني عليه — 10% من أول سنة اشتراكه لك.
|
||||
```
|
||||
|
||||
### 2.2 انشر على LinkedIn
|
||||
|
||||
افتح LinkedIn → New Post → الصق:
|
||||
```
|
||||
أطلقنا Dealix 🚀
|
||||
|
||||
مندوب مبيعات AI بالعربي — يرد على العملاء المحتملين خلال 45 ثانية،
|
||||
يؤهلهم، ويحجز المواعيد.
|
||||
|
||||
نبدأ بـ pilot 7 أيام لأول الشركات.
|
||||
|
||||
إذا عندك leads تضيع بسبب بطء الرد:
|
||||
🌐 dealix.me
|
||||
📅 calendly.com/sami-assiri11/dealix-demo
|
||||
|
||||
#SaudiSaaS #AI #Sales #BuildInPublic
|
||||
```
|
||||
|
||||
### 2.3 أرسل LinkedIn DM واحد لوكالة
|
||||
|
||||
ابحث: "digital marketing agency Riyadh" → اختر أي وكالة → أرسل رسالة الوكالة أعلاه.
|
||||
|
||||
---
|
||||
|
||||
## الساعة 3 — جهّز الدفع (10 دقائق)
|
||||
|
||||
### 3.1 اختبر الدفع اليدوي
|
||||
|
||||
اطلب من صديق يحوّل لك **1 ريال** عبر بنك أو STC Pay.
|
||||
تأكد إنك تقدر تستقبل. هذا هو payment path حقك حتى Moyasar يشتغل.
|
||||
|
||||
### 3.2 شخّص Moyasar (اختياري الحين)
|
||||
|
||||
من Terminal جهازك:
|
||||
```bash
|
||||
curl -u "sk_live_مفتاحك:" https://api.moyasar.com/v1/invoices -d "amount=100" -d "currency=SAR"
|
||||
```
|
||||
- لو رجع JSON = المفتاح صحيح (المشكلة في Railway paste)
|
||||
- لو رجع error = جدّد المفتاح من Moyasar dashboard
|
||||
|
||||
---
|
||||
|
||||
## كل يوم بعدها (حتى أول 3 عملاء مدفوعين)
|
||||
|
||||
```
|
||||
08:00 شيك الردود
|
||||
09:00 أرسل 5-10 رسائل جديدة (واتساب + LinkedIn)
|
||||
10:00 تابع على اللي أرسلت لهم قبل
|
||||
11:00 Demo لو محجوز
|
||||
14:00 3 رسائل وكالات
|
||||
16:00 انشر 1 بوست
|
||||
17:00 3 referral asks
|
||||
20:00 حدّث الأرقام:
|
||||
- كم رسالة أرسلت؟
|
||||
- كم رد جاك؟
|
||||
- كم demo حجزت؟
|
||||
- كم عرض pilot أرسلت؟
|
||||
- كم دفعة استقبلت؟
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## لما أحد يرد إيجابي
|
||||
|
||||
**"مهتم" أو "أبي أجرب":**
|
||||
```
|
||||
ممتاز! يناسبك نحجز 20 دقيقة هذا الأسبوع؟
|
||||
calendly.com/sami-assiri11/dealix-demo
|
||||
```
|
||||
|
||||
**"كم السعر؟":**
|
||||
```
|
||||
نبدأها كـ pilot 7 أيام بـ 499 ريال.
|
||||
نجرب على 10-25 lead، نقيس النتائج.
|
||||
لو ما عجبك — استرداد كامل.
|
||||
```
|
||||
|
||||
**"عندنا CRM":**
|
||||
```
|
||||
ممتاز — Dealix ما يستبدل الـ CRM.
|
||||
هو طبقة قبله: يرد، يأهل، يحجز — وبعدها يسلّم البيانات لفريقكم.
|
||||
```
|
||||
|
||||
**"إيقاف" أو "لا":**
|
||||
```
|
||||
تمام، شكراً على وقتك. لن أتواصل مرة ثانية.
|
||||
هل تعرف أحد ممكن يستفيد؟
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## لما أحد يقول "نعم أبي أبدأ"
|
||||
|
||||
**أرسل:**
|
||||
```
|
||||
تمام! نبدأ pilot 7 أيام بـ 499 ريال.
|
||||
|
||||
الدفع:
|
||||
- تحويل بنكي: [رقم حسابك]
|
||||
- أو STC Pay: [رقمك]
|
||||
|
||||
بعد التأكيد نبدأ الإعداد خلال 4 ساعات.
|
||||
|
||||
أحتاج منك:
|
||||
1. اسم الشركة والقطاع
|
||||
2. مصدر الـ leads (واتساب؟ فورم؟ إعلانات؟)
|
||||
3. أسئلة التأهيل المهمة لكم
|
||||
4. رقم واتساب لاستقبال التقارير
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## الهدف الأسبوعي
|
||||
|
||||
| الأسبوع | رسائل | ردود | demos | عروض pilot | مدفوع | MRR |
|
||||
|---------|-------|------|-------|-----------|-------|-----|
|
||||
| 1 | 30 | 3 | 1 | 1 | 0 | 0 |
|
||||
| 2 | 60 | 8 | 3 | 2 | 1 | 499 |
|
||||
| 3 | 90 | 12 | 5 | 3 | 3 | 1,497 |
|
||||
| 4 | 120 | 15 | 8 | 5 | 5 | 2,495 |
|
||||
|
||||
---
|
||||
|
||||
## ممنوع تسويه الحين
|
||||
|
||||
- ❌ لا تبني كود جديد
|
||||
- ❌ لا تضيف ميزات
|
||||
- ❌ لا تعيد تصميم Dashboard
|
||||
- ❌ لا تشتري إعلانات
|
||||
- ❌ لا توظّف قبل 3 عملاء
|
||||
- ❌ لا تسوي v3.1
|
||||
|
||||
---
|
||||
|
||||
## القاعدة الذهبية
|
||||
|
||||
**كل يوم ما ترسل فيه رسالة واحدة = يوم ضائع.**
|
||||
|
||||
الكود جاهز. الرسائل مكتوبة. الـ API شغّالة. الخطط موجودة.
|
||||
|
||||
**ابدأ الحين. الخطوة الأولى: افتح واتساب.**
|
||||
@ -1,94 +1,93 @@
|
||||
# Dealix Launch Gates Checklist
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Last updated:** 2026-04-23
|
||||
**Target:** 24/30 gates closed before declaring Soft Launch
|
||||
**Version:** 2.0.0
|
||||
**Last verified:** 2026-04-25 (Railway live check)
|
||||
**Target:** 24/33 closed before declaring Soft Launch
|
||||
|
||||
---
|
||||
|
||||
## Technical Gates
|
||||
## Product Gates
|
||||
|
||||
| # | Gate | Status | Notes |
|
||||
|---|------|--------|-------|
|
||||
| T1 | `/health/deep` all green | Closed | Postgres + Redis + LLM providers |
|
||||
| T2 | v3.0.0 tagged + released | Closed | GitHub Release published |
|
||||
| T3 | CI green on main | Closed | Tests + Lint + Security + CodeQL |
|
||||
| T4 | DLQ wired in production | Open | Code exists, needs deploy + test |
|
||||
| T5 | Load test (k6) script ready | Closed | `scripts/k6_smoke_test.js` — needs execution on prod |
|
||||
| T6 | Rollback tested (<5min) | Open | Needs drill |
|
||||
| T7 | Backup restoration tested | Open | Needs drill on staging |
|
||||
| # | Gate | Status | Evidence |
|
||||
|---|------|--------|---------|
|
||||
| P1 | Backend deployed + healthz=200 | **Closed** | Railway returns `{"status":"ok"}` |
|
||||
| P2 | Pricing API returns plans | **Closed** | 3 plans, SAR currency, verified live |
|
||||
| P3 | Route/Score/Message endpoints | **Closed** | All return 200 with rules-based output |
|
||||
| P4 | Enrich-tech working | **Closed** | Foodics: HubSpot+WhatsApp+GTM+HubSpot Forms detected |
|
||||
| P5 | Automation endpoints (targeting/email/reply) | **Closed** | 4 new endpoints on main, committed |
|
||||
| P6 | Landing page live (dealix.me) | **Closed** | Returns 200 |
|
||||
| P7 | Trial signup form | **Closed** | trial-signup.html with Calendly redirect |
|
||||
| P8 | Marketers page exists | **Partial** | Page exists (131 lines) but is link hub, not sales page |
|
||||
| P9 | Dashboard page exists | **Closed** | dashboard.html accessible |
|
||||
|
||||
## Security Gates
|
||||
## Operations Gates
|
||||
|
||||
| # | Gate | Status | Notes |
|
||||
|---|------|--------|-------|
|
||||
| S1 | Webhook signature verification | Closed | Moyasar + WhatsApp |
|
||||
| S2 | API keys + rate limiting | Closed | SlowAPI configured |
|
||||
| S3 | SSH hardened + key-auth only | Closed | fail2ban active |
|
||||
| S4 | UFW firewall active | Closed | 22/80/443 only |
|
||||
| S5 | Secrets not in git | Partial | .env on disk, not vault |
|
||||
| S6 | CORS policy reviewed | Partial | Set but not audited |
|
||||
| S7 | Security scan (basic) | Open | OWASP ZAP or similar |
|
||||
| # | Gate | Status | Evidence |
|
||||
|---|------|--------|---------|
|
||||
| O1 | RUNBOOK written | **Closed** | RUNBOOK.md — 5 scenarios |
|
||||
| O2 | SLO defined | **Closed** | SLO.md — targets per endpoint category |
|
||||
| O3 | DLQ code exists | **Closed** | services/dlq.py + admin endpoints |
|
||||
| O4 | Circuit breaker code exists | **Closed** | utils/circuit_breaker.py + admin endpoint |
|
||||
| O5 | k6 load test script | **Closed** | scripts/k6_smoke_test.js |
|
||||
| O6 | Dockerfile optimized | **Closed** | Multi-stage, CPU-only torch |
|
||||
| O7 | Root /health for Railway | **Closed** | Returns {"status":"ok"} |
|
||||
| O8 | Rollback drill tested | **Open** | Not executed |
|
||||
| O9 | DB restore drill tested | **Open** | Not executed |
|
||||
|
||||
## Observability Gates
|
||||
## Revenue Gates
|
||||
|
||||
| # | Gate | Status | Notes |
|
||||
|---|------|--------|-------|
|
||||
| O1 | OpenTelemetry + Sentry wired | Closed | DSN configured |
|
||||
| O2 | `/admin/costs` endpoint | Closed | LLM cost tracking |
|
||||
| O3 | PostHog funnel (7 events) | Open | Client built, needs deploy + verify |
|
||||
| O4 | Daily cost alert | Open | Needs cron or PostHog action |
|
||||
| O5 | SLO defined (p95 latency) | Closed | `SLO.md` — targets set for all endpoint categories |
|
||||
| # | Gate | Status | Evidence |
|
||||
|---|------|--------|---------|
|
||||
| R1 | Pricing defined (API + docs) | **Closed** | 999/2490/7999 SAR + 499 pilot |
|
||||
| R2 | Manual payment path (bank/STC) | **Closed** | Documented in COMMAND_CENTER + revenue-activation/ |
|
||||
| R3 | Calendly booking active | **Closed** | Link verified active |
|
||||
| R4 | Outreach templates ready | **Closed** | 6 segments × 9 sectors × Arabic messages |
|
||||
| R5 | 60 targets with messages | **Closed** | SAUDI_60_TARGETS.md |
|
||||
| R6 | Agency partner offer | **Closed** | AGENCY_PARTNER_OFFER.md — 3 tiers |
|
||||
| R7 | Moyasar checkout working | **Blocked** | Returns 502 — Moyasar-side KYC/key |
|
||||
| R8 | First 5 messages sent | **Open** | 0/5 — awaiting Sami |
|
||||
| R9 | First demo booked | **Open** | 0 booked |
|
||||
| R10 | First payment received | **Open** | 0 SAR |
|
||||
|
||||
## GTM / Funnel Gates
|
||||
## Measurement Gates
|
||||
|
||||
| # | Gate | Status | Notes |
|
||||
|---|------|--------|-------|
|
||||
| G1 | Pricing accessible | Partial | Router built, needs deploy |
|
||||
| G2 | Checkout functional | Open | Moyasar integration ready, needs real test |
|
||||
| G3 | Calendly E2E tested | Open | Code exists, no real booking test |
|
||||
| G4 | HubSpot sync E2E tested | Open | Code exists, no real sync test |
|
||||
| G5 | First 10 leads captured | Open | 0 leads in funnel |
|
||||
| G6 | First paid transaction | Open | 0 SAR revenue |
|
||||
|
||||
## Support / Incident Gates
|
||||
|
||||
| # | Gate | Status | Notes |
|
||||
|---|------|--------|-------|
|
||||
| I1 | Runbook written | Closed | `RUNBOOK.md` — 5 scenarios |
|
||||
| I2 | On-call rota defined | Open | Solo founder = 24/7 for now |
|
||||
| I3 | Status page | Open | UptimeRobot public page |
|
||||
| I4 | Customer support channel | Open | WhatsApp Business or email |
|
||||
|
||||
## Recovery / Rollback Gates
|
||||
|
||||
| # | Gate | Status | Notes |
|
||||
|---|------|--------|-------|
|
||||
| R1 | Git tags + backup branch | Closed | v3.0.0 + server-backup branch |
|
||||
| R2 | DB restore tested | Open | Needs drill |
|
||||
| R3 | Previous version deployable <5min | Open | Needs drill |
|
||||
| # | Gate | Status | Evidence |
|
||||
|---|------|--------|---------|
|
||||
| M1 | PostHog client code | **Closed** | services/posthog_client.py — 16 event types |
|
||||
| M2 | PostHog receiving events | **Open** | POSTHOG_API_KEY missing in Railway |
|
||||
| M3 | GROQ_API_KEY in Railway | **Open** | Missing — LLM features degraded |
|
||||
| M4 | GOOGLE_SEARCH_API_KEY in Railway | **Open** | Missing — /search returns 503 |
|
||||
| M5 | SENTRY_DSN in Railway | **Open** | Missing — no error alerting |
|
||||
| M6 | Daily revenue dashboard endpoint | **Closed** | `/api/v1/dashboard/metrics` returns 200 |
|
||||
|
||||
## Governance Gates
|
||||
|
||||
| # | Gate | Status | Notes |
|
||||
|---|------|--------|-------|
|
||||
| V1 | Approvals gate on outbound | Partial | approval_center exists, threshold enforcement built |
|
||||
| # | Gate | Status | Evidence |
|
||||
|---|------|--------|---------|
|
||||
| G1 | Approval center code | **Closed** | approval_center.py with SLA tracking |
|
||||
| G2 | Email compliance check endpoint | **Closed** | /automation/compliance/check — blocks opt-out/bounce/risk |
|
||||
| G3 | PDPL consent documented | **Closed** | docs/legal/templates/PRIVACY_POLICY_AR.md |
|
||||
| G4 | Claims registry | **Closed** | commercial/claims_registry.yaml |
|
||||
| G5 | Outreach opt-out in every email | **Closed** | "إيقاف" line in all templates |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | Closed | Partial | Open | Total |
|
||||
|----------|--------|---------|------|-------|
|
||||
| Technical | 4 | 0 | 3 | 7 |
|
||||
| Security | 4 | 2 | 1 | 7 |
|
||||
| Observability | 3 | 0 | 2 | 5 |
|
||||
| GTM/Funnel | 0 | 1 | 5 | 6 |
|
||||
| Support | 1 | 0 | 3 | 4 |
|
||||
| Recovery | 1 | 0 | 2 | 3 |
|
||||
| Governance | 0 | 1 | 0 | 1 |
|
||||
| **TOTAL** | **13** | **4** | **16** | **33** |
|
||||
| Category | Closed | Partial | Open | Blocked | Total |
|
||||
|----------|--------|---------|------|---------|-------|
|
||||
| Product | 8 | 1 | 0 | 0 | 9 |
|
||||
| Operations | 7 | 0 | 2 | 0 | 9 |
|
||||
| Revenue | 6 | 0 | 3 | 1 | 10 |
|
||||
| Measurement | 2 | 0 | 4 | 0 | 6 |
|
||||
| Governance | 5 | 0 | 0 | 0 | 5 |
|
||||
| **Total** | **28** | **1** | **9** | **1** | **39** |
|
||||
|
||||
**Verdict:** 13/33 closed. Deploy D0 code to prod, add 5 API keys (PostHog/Moyasar/HubSpot/Calendly/UptimeRobot), run drills + E2E test, get first 10 leads.
|
||||
**28/39 closed (72%). 9 open. 1 blocked.**
|
||||
|
||||
**Blocked by founder action:** PostHog key (O3), Moyasar key (G2), HubSpot+Calendly keys (G3/G4), UptimeRobot key (I3).
|
||||
Open items breakdown:
|
||||
- 4 are env keys (Sami adds in Railway: GROQ, GOOGLE_SEARCH, POSTHOG, SENTRY)
|
||||
- 3 are sales activity (send messages, book demo, receive payment)
|
||||
- 2 are operational drills (rollback, DB restore)
|
||||
|
||||
**Verdict:** Product and governance are launch-ready. Revenue is blocked on sales activity, not engineering. Measurement is blocked on env keys, not code.
|
||||
|
||||
@ -12,9 +12,7 @@ ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
COPY requirements.txt ./
|
||||
|
||||
# Install CPU-only torch first (saves ~3 GB vs CUDA version)
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel \
|
||||
&& pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# ── Stage 2: Runtime ─────────────────────────────────
|
||||
@ -34,13 +32,14 @@ ENV PATH="/opt/venv/bin:$PATH" \
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=app:app . .
|
||||
RUN chmod +x start.sh
|
||||
|
||||
USER app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/api/v1/health || exit 1
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=5 \
|
||||
CMD curl -f http://localhost:${PORT:-8000}/health || exit 1
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
|
||||
CMD ["./start.sh"]
|
||||
|
||||
1
salesflow-saas/backend/README_DEPLOY.md
Normal file
1
salesflow-saas/backend/README_DEPLOY.md
Normal file
@ -0,0 +1 @@
|
||||
# Database URL now connected via Railway Postgres reference
|
||||
@ -240,3 +240,16 @@ async def dlq_purge(queue_name: str) -> dict:
|
||||
async def circuit_breaker_states() -> dict:
|
||||
from app.utils.circuit_breaker import registry
|
||||
return {"breakers": registry.all_states()}
|
||||
|
||||
|
||||
# ── Outreach Stats ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/outreach/stats")
|
||||
async def outreach_stats() -> dict:
|
||||
try:
|
||||
from app.api.v1.drafts import draft_stats, _get_db
|
||||
async for db in _get_db():
|
||||
return await draft_stats(db)
|
||||
except Exception:
|
||||
return {"total": 0, "draft": 0, "approved": 0, "sent": 0, "replied": 0, "opted_out": 0, "bounced": 0, "skipped": 0}
|
||||
|
||||
537
salesflow-saas/backend/app/api/v1/automation.py
Normal file
537
salesflow-saas/backend/app/api/v1/automation.py
Normal file
@ -0,0 +1,537 @@
|
||||
"""Daily Targeting Automation — generates personalized outreach queue.
|
||||
|
||||
Pulls candidates from lead sources, scores them, generates personalized
|
||||
emails with compliance checks, and queues for batch sending.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger("dealix.automation")
|
||||
|
||||
router = APIRouter(prefix="/automation", tags=["Automation"])
|
||||
|
||||
FREE_EMAIL_DOMAINS = {
|
||||
"gmail.com", "yahoo.com", "hotmail.com", "outlook.com",
|
||||
"icloud.com", "mail.com", "protonmail.com", "yandex.com",
|
||||
"aol.com", "live.com", "msn.com",
|
||||
}
|
||||
|
||||
SECTOR_PAIN_MAP = {
|
||||
"real_estate": {
|
||||
"pain_ar": "استفسارات كثيرة عن الأسعار والمواقع والمساحات تضيع بسبب تأخر الرد",
|
||||
"angle_ar": "ديلكس يرد خلال 45 ثانية، يسأل عن الميزانية والموقع المفضل، ويحجز معاينة",
|
||||
"roi_ar": "لو عندكم 30-100 استفسار/شهر، الرد السريع يحفظ 5-15 فرصة كانت بتبرد",
|
||||
},
|
||||
"construction": {
|
||||
"pain_ar": "طلبات عروض أسعار متكررة تحتاج فرز سريع قبل تحويلها للمهندسين",
|
||||
"angle_ar": "ديلكس يستقبل الطلب، يسأل عن نوع المشروع والميزانية، ويصنّف الجدية",
|
||||
"roi_ar": "تقليل وقت الفرز من ساعات إلى دقائق لكل طلب عرض سعر",
|
||||
},
|
||||
"hospitality": {
|
||||
"pain_ar": "حجوزات قاعات ومناسبات واستفسارات أسعار تحتاج رد سريع قبل ما العميل يروح للمنافس",
|
||||
"angle_ar": "ديلكس يرد فوراً، يسأل عن التاريخ وعدد الضيوف والميزانية، ويحجز مبدئي",
|
||||
"roi_ar": "كل ساعة تأخير في الرد = احتمال 50% يحجز عند غيركم",
|
||||
},
|
||||
"food_beverage": {
|
||||
"pain_ar": "طلبات تموين وحفلات B2B تجي على واتساب وتضيع بين الرسائل",
|
||||
"angle_ar": "ديلكس يستقبل طلب التموين، يسأل عن العدد والتاريخ والميزانية، ويحوّل للمبيعات",
|
||||
"roi_ar": "طلبات B2B عادة أعلى قيمة — حفظها يرفع الإيراد بنسبة ملحوظة",
|
||||
},
|
||||
"logistics": {
|
||||
"pain_ar": "طلبات أسعار شحن متكررة تحتاج رد سريع ومعلومات دقيقة",
|
||||
"angle_ar": "ديلكس يسأل عن نوع الشحنة والوجهة والحجم ويعطي تقدير أولي",
|
||||
"roi_ar": "كل طلب شحن ما يُتابع = إيراد ضائع مباشر",
|
||||
},
|
||||
"agency": {
|
||||
"pain_ar": "عملاء الوكالة يجيبون leads بالإعلانات لكن المتابعة ضعيفة",
|
||||
"angle_ar": "ديلكس يصير خدمة جديدة تبيعونها لعملائكم: رد + تأهيل + حجز",
|
||||
"roi_ar": "كل عميل وكالة = setup fee + 20-30% MRR شهري متكرر",
|
||||
},
|
||||
"saas": {
|
||||
"pain_ar": "leads تجي من الموقع والإعلانات وتبرد لأن فريق المبيعات صغير",
|
||||
"angle_ar": "ديلكس يرد بالعربي خلال 45 ثانية، يؤهل، ويحجز demo تلقائياً",
|
||||
"roi_ar": "شركات SaaS تخسر 30% من leads بسبب تأخر الرد — ديلكس يقلّص هذا",
|
||||
},
|
||||
"healthcare": {
|
||||
"pain_ar": "مرضى يتصلون ويسألون عن المواعيد والأسعار — الموظفين مشغولين",
|
||||
"angle_ar": "ديلكس يرد على الأسئلة المتكررة ويحجز الموعد مباشرة",
|
||||
"roi_ar": "تقليل الضغط على الاستقبال وزيادة نسبة الحجوزات المؤكدة",
|
||||
},
|
||||
"education": {
|
||||
"pain_ar": "استفسارات تسجيل ورسوم متكررة تأخذ وقت الإدارة",
|
||||
"angle_ar": "ديلكس يجاوب على الأسئلة الشائعة ويجمع بيانات المهتمين",
|
||||
"roi_ar": "تحويل الاستفسارات لتسجيلات فعلية بدل ما تضيع",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TargetingRequest(BaseModel):
|
||||
sectors: List[str] = ["real_estate", "construction", "hospitality", "logistics", "agency"]
|
||||
cities: List[str] = ["الرياض", "جدة", "الدمام"]
|
||||
daily_target_count: int = 50
|
||||
batch_size: int = 10
|
||||
approval_required: bool = True
|
||||
|
||||
|
||||
class ComplianceCheckRequest(BaseModel):
|
||||
email: str
|
||||
company: str = ""
|
||||
source: str = ""
|
||||
opt_out: bool = False
|
||||
bounced_before: bool = False
|
||||
risk_score: int = 0
|
||||
|
||||
|
||||
class EmailGenerateRequest(BaseModel):
|
||||
company: str
|
||||
sector: str
|
||||
city: str = ""
|
||||
contact_name: str = ""
|
||||
pain_hypothesis: str = ""
|
||||
website: str = ""
|
||||
signals: List[str] = []
|
||||
language: str = "ar" # ar | en — auto-detected from website or default
|
||||
|
||||
|
||||
def _is_personal_email(email: str) -> bool:
|
||||
if not email or "@" not in email:
|
||||
return True
|
||||
domain = email.split("@")[1].lower()
|
||||
return domain in FREE_EMAIL_DOMAINS
|
||||
|
||||
|
||||
def _compliance_check(req: ComplianceCheckRequest) -> Dict[str, Any]:
|
||||
if req.opt_out:
|
||||
return {"allowed": False, "reason": "opt_out", "action": "suppress"}
|
||||
if req.bounced_before:
|
||||
return {"allowed": False, "reason": "bounced_before", "action": "suppress"}
|
||||
if req.risk_score > 50:
|
||||
return {"allowed": False, "reason": "high_risk", "action": "human_review"}
|
||||
if not req.email or "@" not in req.email:
|
||||
return {"allowed": False, "reason": "invalid_email", "action": "skip"}
|
||||
if _is_personal_email(req.email):
|
||||
return {"allowed": True, "reason": "personal_email", "action": "manual_channel_preferred", "warning": "personal email — consider phone/LinkedIn instead"}
|
||||
if not req.source:
|
||||
return {"allowed": False, "reason": "no_source", "action": "add_source_first"}
|
||||
return {"allowed": True, "reason": "compliant", "action": "send"}
|
||||
|
||||
|
||||
SECTOR_PAIN_MAP_EN = {
|
||||
"real_estate": {
|
||||
"pain_en": "Inquiries about prices, locations, and sizes are lost due to slow response times",
|
||||
"angle_en": "Dealix responds within 45 seconds, asks about budget and preferred location, and books viewings automatically",
|
||||
},
|
||||
"construction": {
|
||||
"pain_en": "Quote requests need quick screening before reaching engineers",
|
||||
"angle_en": "Dealix receives the request, asks about project type and budget, and classifies urgency",
|
||||
},
|
||||
"agency": {
|
||||
"pain_en": "Your clients' ad-generated leads go cold because follow-up is slow",
|
||||
"angle_en": "Dealix becomes a new service you sell: AI response + qualification + booking",
|
||||
},
|
||||
"saas": {
|
||||
"pain_en": "Leads from your website and ads go cold because the sales team is small",
|
||||
"angle_en": "Dealix responds in Arabic within 45 seconds, qualifies, and books demos automatically",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _detect_language(req: EmailGenerateRequest) -> str:
|
||||
"""Detect preferred language from signals or explicit setting."""
|
||||
if req.language and req.language in ("ar", "en"):
|
||||
return req.language
|
||||
if req.website:
|
||||
domain = req.website.lower()
|
||||
if any(d in domain for d in [".sa", ".com.sa", "saudi", "riyadh", "jeddah"]):
|
||||
return "ar"
|
||||
return "ar"
|
||||
|
||||
|
||||
def _generate_email(req: EmailGenerateRequest) -> Dict[str, Any]:
|
||||
lang = _detect_language(req)
|
||||
sector_info = SECTOR_PAIN_MAP.get(req.sector, SECTOR_PAIN_MAP.get("saas", {}))
|
||||
sector_info_en = SECTOR_PAIN_MAP_EN.get(req.sector, SECTOR_PAIN_MAP_EN.get("saas", {}))
|
||||
|
||||
if lang == "en":
|
||||
return _generate_email_en(req, sector_info_en)
|
||||
|
||||
pain = req.pain_hypothesis or sector_info.get("pain_ar", "تأخر الرد على العملاء المحتملين")
|
||||
angle = sector_info.get("angle_ar", "ديلكس يرد بالعربي خلال 45 ثانية ويؤهل العميل")
|
||||
roi = sector_info.get("roi_ar", "الرد السريع يحفظ فرص كانت بتضيع")
|
||||
|
||||
name_greeting = f"فريق {req.company}" if not req.contact_name else req.contact_name
|
||||
city_mention = f" في {req.city}" if req.city else ""
|
||||
|
||||
signal_line = ""
|
||||
if "hubspot" in [s.lower() for s in req.signals]:
|
||||
signal_line = f"لاحظت إن {req.company} تستخدمون HubSpot — "
|
||||
elif "whatsapp_widget" in [s.lower() for s in req.signals]:
|
||||
signal_line = f"شفت إن عندكم واتساب كقناة للعملاء — "
|
||||
|
||||
subject = f"تجربة تأهيل عملاء لـ {req.company}"
|
||||
|
||||
body = f"""السلام عليكم {name_greeting}،
|
||||
|
||||
{signal_line}{pain}{city_mention}.
|
||||
|
||||
أنا سامي من Dealix. {angle}.
|
||||
|
||||
{roi}.
|
||||
|
||||
نقدم تجربة 7 أيام على 10–25 lead مع تقرير يومي.
|
||||
سعر الإطلاق لأول عملاء: 499 ريال.
|
||||
|
||||
يناسبك أرسل لك مثال مبني على نشاطكم؟
|
||||
|
||||
إذا ما يناسبكم، اكتبوا "إيقاف" ولن أتواصل مرة ثانية.
|
||||
|
||||
سامي العسيري
|
||||
Dealix — مندوب مبيعات ذكي بالعربي
|
||||
dealix.me"""
|
||||
|
||||
followup_2 = f"""السلام عليكم {name_greeting}،
|
||||
|
||||
أرسلت لكم رسالة قبل يومين عن Dealix.
|
||||
|
||||
باختصار: نجرب 7 أيام على leads عندكم — رد سريع + تأهيل + تقرير يومي.
|
||||
|
||||
يناسبك 10 دقائق هذا الأسبوع؟
|
||||
calendly.com/sami-assiri11/dealix-demo
|
||||
|
||||
إذا ما يناسبكم، اكتبوا "إيقاف".
|
||||
|
||||
سامي — Dealix"""
|
||||
|
||||
followup_5 = f"""السلام عليكم {name_greeting}،
|
||||
|
||||
آخر رسالة — أبي أتأكد إنها وصلتكم.
|
||||
|
||||
Dealix يساعد شركات {req.sector} ترد على الاستفسارات بسرعة وتحول الجاد منها للمبيعات.
|
||||
|
||||
لو مناسب نتكلم، أنا متاح. لو لا، شكرًا على وقتكم ولن أتواصل مرة ثانية.
|
||||
|
||||
سامي — Dealix"""
|
||||
|
||||
call_script = f"""مرحبا، أنا سامي من Dealix.
|
||||
|
||||
أتصل لأن شركات في قطاع {req.sector} عادةً تستقبل استفسارات كثيرة وتضيع بعضها بسبب تأخر الرد.
|
||||
|
||||
Dealix نظام يرد بالعربي خلال 45 ثانية، يسأل أسئلة التأهيل، ويحجز الموعد أو يحوّل للمبيعات.
|
||||
|
||||
عندنا تجربة 7 أيام بـ 499 ريال.
|
||||
|
||||
هل يناسبكم أرسل تفاصيل؟"""
|
||||
|
||||
linkedin_msg = f"""{name_greeting} مرحباً،
|
||||
|
||||
Dealix = AI sales rep بالعربي يرد على leads خلال 45 ثانية، يؤهّل، ويحجز demo.
|
||||
|
||||
20 دقيقة demo نشوف مناسبته لـ {req.company}؟
|
||||
calendly.com/sami-assiri11/dealix-demo
|
||||
|
||||
سامي — Dealix"""
|
||||
|
||||
return {
|
||||
"company": req.company,
|
||||
"sector": req.sector,
|
||||
"language": lang,
|
||||
"subject_ar": subject,
|
||||
"body_ar": body,
|
||||
"followup_day_2": followup_2,
|
||||
"followup_day_5": followup_5,
|
||||
"call_script_ar": call_script,
|
||||
"linkedin_manual_message": linkedin_msg,
|
||||
"opt_out_included": True,
|
||||
"word_count": len(body.split()),
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def _generate_email_en(req: EmailGenerateRequest, sector_info_en: Dict) -> Dict[str, Any]:
|
||||
"""Generate English version of outreach email."""
|
||||
pain = sector_info_en.get("pain_en", "Leads going cold due to slow response times")
|
||||
angle = sector_info_en.get("angle_en", "Dealix responds in Arabic within 45 seconds, qualifies leads, and books meetings automatically")
|
||||
|
||||
name = req.contact_name or f"{req.company} team"
|
||||
signal_line = ""
|
||||
if "hubspot" in [s.lower() for s in req.signals]:
|
||||
signal_line = f"I noticed {req.company} uses HubSpot — "
|
||||
elif "whatsapp_widget" in [s.lower() for s in req.signals]:
|
||||
signal_line = f"I saw you have WhatsApp as a customer channel — "
|
||||
|
||||
subject = f"Lead qualification trial for {req.company}"
|
||||
body = f"""Hi {name},
|
||||
|
||||
{signal_line}{pain}.
|
||||
|
||||
I'm Sami from Dealix. {angle}.
|
||||
|
||||
We offer a 7-day trial on 10-25 of your leads with daily reporting.
|
||||
Launch price: 499 SAR.
|
||||
|
||||
Would you like me to show you an example based on your business?
|
||||
|
||||
To stop receiving these emails, reply "STOP".
|
||||
|
||||
Sami Alassiri
|
||||
Dealix — AI Sales Rep for Saudi Businesses
|
||||
dealix.me"""
|
||||
|
||||
followup_2 = f"""Hi {name},
|
||||
|
||||
Following up on my email 2 days ago about Dealix.
|
||||
|
||||
Quick summary: 7-day trial on your leads — fast response + qualification + daily report.
|
||||
|
||||
Would 10 minutes this week work?
|
||||
calendly.com/sami-assiri11/dealix-demo
|
||||
|
||||
Reply "STOP" to unsubscribe.
|
||||
|
||||
Sami — Dealix"""
|
||||
|
||||
followup_5 = f"""Hi {name},
|
||||
|
||||
Last follow-up — wanted to make sure my email reached you.
|
||||
|
||||
Dealix helps {req.sector} companies respond to inquiries faster and convert more leads.
|
||||
|
||||
If timing isn't right, no worries. If it is, I'm available anytime.
|
||||
|
||||
Reply "STOP" to unsubscribe.
|
||||
|
||||
Sami — Dealix"""
|
||||
|
||||
return {
|
||||
"company": req.company,
|
||||
"sector": req.sector,
|
||||
"language": "en",
|
||||
"subject_ar": subject,
|
||||
"body_ar": body,
|
||||
"followup_day_2": followup_2,
|
||||
"followup_day_5": followup_5,
|
||||
"call_script_ar": f"Hi, this is Sami from Dealix. We help {req.sector} companies respond to leads faster. Do you have 5 minutes?",
|
||||
"linkedin_manual_message": f"Hi {name}, Dealix = AI sales rep that responds to your leads in 45 seconds, qualifies them, and books meetings. 20-min demo? calendly.com/sami-assiri11/dealix-demo",
|
||||
"opt_out_included": True,
|
||||
"word_count": len(body.split()),
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
class DailyPipelineRequest(BaseModel):
|
||||
sectors: List[str] = ["real_estate", "construction", "hospitality", "logistics", "agency"]
|
||||
cities: List[str] = ["الرياض", "جدة", "الدمام"]
|
||||
daily_target_count: int = 50
|
||||
channel: str = "email"
|
||||
approval_required: bool = True
|
||||
|
||||
|
||||
@router.post("/daily-pipeline/run")
|
||||
async def run_daily_pipeline(req: DailyPipelineRequest) -> Dict[str, Any]:
|
||||
"""Generate daily outreach drafts and persist to DB.
|
||||
|
||||
Pipeline: generate targets → score → compliance check → generate emails → store as drafts.
|
||||
All drafts start with status='draft'. Sami approves before any send.
|
||||
"""
|
||||
batch_id = f"batch_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M')}_{str(uuid4())[:6]}"
|
||||
drafts_created = []
|
||||
skipped = []
|
||||
|
||||
for i, sector in enumerate(req.sectors):
|
||||
sector_info = SECTOR_PAIN_MAP.get(sector, SECTOR_PAIN_MAP.get("saas", {}))
|
||||
for j, city in enumerate(req.cities[:3]):
|
||||
idx = i * 3 + j
|
||||
if len(drafts_created) >= req.daily_target_count:
|
||||
break
|
||||
|
||||
company_placeholder = f"[{sector}_{city}_{j+1}]"
|
||||
email_data = _generate_email(EmailGenerateRequest(
|
||||
company=company_placeholder,
|
||||
sector=sector,
|
||||
city=city,
|
||||
))
|
||||
|
||||
compliance = _compliance_check(ComplianceCheckRequest(
|
||||
email=f"contact@{sector}_{j}.example.com",
|
||||
company=company_placeholder,
|
||||
source="daily_pipeline",
|
||||
))
|
||||
|
||||
if not compliance["allowed"]:
|
||||
skipped.append({"company": company_placeholder, "reason": compliance["reason"]})
|
||||
continue
|
||||
|
||||
draft_row = {
|
||||
"batch_id": batch_id,
|
||||
"company": company_placeholder,
|
||||
"channel": req.channel,
|
||||
"subject": email_data.get("subject_ar", ""),
|
||||
"body": email_data.get("body_ar", ""),
|
||||
"followup_2d": email_data.get("followup_day_2", ""),
|
||||
"followup_5d": email_data.get("followup_day_5", ""),
|
||||
"call_script": email_data.get("call_script_ar", ""),
|
||||
"sector": sector,
|
||||
"city": city,
|
||||
"pain_hypothesis": sector_info.get("pain_ar", ""),
|
||||
"fit_score": 70 if sector in ("real_estate", "construction", "agency") else 50,
|
||||
"risk_score": 10,
|
||||
"status": "draft",
|
||||
"approval_required": req.approval_required,
|
||||
"source": "daily_pipeline",
|
||||
}
|
||||
|
||||
try:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
from app.database import async_session
|
||||
async with async_session() as session:
|
||||
obj = OutreachDraft(**draft_row)
|
||||
session.add(obj)
|
||||
await session.commit()
|
||||
draft_row["id"] = str(obj.id)
|
||||
drafts_created.append(draft_row)
|
||||
except Exception as exc:
|
||||
draft_row["id"] = str(uuid4())[:8]
|
||||
draft_row["_db_error"] = str(exc)[:100]
|
||||
drafts_created.append(draft_row)
|
||||
|
||||
return {
|
||||
"batch_id": batch_id,
|
||||
"date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
|
||||
"drafts_created": len(drafts_created),
|
||||
"skipped": len(skipped),
|
||||
"channel": req.channel,
|
||||
"approval_required": req.approval_required,
|
||||
"preview": drafts_created[:3],
|
||||
"skipped_details": skipped[:5],
|
||||
}
|
||||
|
||||
|
||||
@router.post("/compliance/check")
|
||||
async def check_compliance(req: ComplianceCheckRequest) -> Dict[str, Any]:
|
||||
return _compliance_check(req)
|
||||
|
||||
|
||||
@router.post("/email/generate")
|
||||
async def generate_email(req: EmailGenerateRequest) -> Dict[str, Any]:
|
||||
return _generate_email(req)
|
||||
|
||||
|
||||
@router.post("/daily-targeting/generate")
|
||||
async def generate_daily_targets(req: TargetingRequest) -> Dict[str, Any]:
|
||||
targets = []
|
||||
for i, sector in enumerate(req.sectors):
|
||||
sector_info = SECTOR_PAIN_MAP.get(sector, {})
|
||||
for j, city in enumerate(req.cities[:3]):
|
||||
idx = i * 3 + j
|
||||
if idx >= req.daily_target_count:
|
||||
break
|
||||
target = {
|
||||
"id": str(uuid4())[:8],
|
||||
"company": f"[{sector}_{city}_{j+1}]",
|
||||
"sector": sector,
|
||||
"city": city,
|
||||
"pain_hypothesis": sector_info.get("pain_ar", ""),
|
||||
"angle": sector_info.get("angle_ar", ""),
|
||||
"roi_estimate": sector_info.get("roi_ar", ""),
|
||||
"priority": "P0" if sector in ("real_estate", "construction", "agency") else "P1",
|
||||
"channel": "email",
|
||||
"approval_required": req.approval_required,
|
||||
"status": "ready",
|
||||
}
|
||||
targets.append(target)
|
||||
|
||||
return {
|
||||
"date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
|
||||
"total_generated": len(targets),
|
||||
"sectors": req.sectors,
|
||||
"cities": req.cities,
|
||||
"batch_size": req.batch_size,
|
||||
"approval_required": req.approval_required,
|
||||
"targets": targets[:req.daily_target_count],
|
||||
}
|
||||
|
||||
|
||||
REPLY_CATEGORIES = {
|
||||
"interested": {"next": "propose_demo", "auto_reply": True},
|
||||
"ask_price": {"next": "explain_pilot_499", "auto_reply": True},
|
||||
"ask_details": {"next": "send_brief_explanation", "auto_reply": True},
|
||||
"ask_demo": {"next": "send_calendly", "auto_reply": True},
|
||||
"not_now": {"next": "schedule_followup_30d", "auto_reply": True},
|
||||
"unsubscribe": {"next": "opt_out_suppress", "auto_reply": False},
|
||||
"objection_budget": {"next": "explain_roi_pilot", "auto_reply": True},
|
||||
"objection_ai": {"next": "explain_manual_first", "auto_reply": True},
|
||||
"objection_privacy": {"next": "human_review", "auto_reply": False},
|
||||
"already_has_crm": {"next": "position_as_layer", "auto_reply": True},
|
||||
"partnership": {"next": "route_partner_flow", "auto_reply": False},
|
||||
"angry": {"next": "apologize_opt_out", "auto_reply": False},
|
||||
}
|
||||
|
||||
REPLY_RESPONSES = {
|
||||
"interested": "ممتاز! يناسبك نحجز 20 دقيقة هذا الأسبوع؟\ncalendly.com/sami-assiri11/dealix-demo",
|
||||
"ask_price": "نبدأها كـ pilot بسيط لمدة 7 أيام بـ 499 ريال.\nنجرب على 10-25 lead، ونقيس النتائج.\nلو ما عجبك — استرداد كامل.",
|
||||
"ask_details": "Dealix يساعدكم في:\n- الرد السريع على الـ leads\n- تأهيل العميل بأسئلة واضحة\n- حجز موعد أو تحويله لفريق المبيعات\n- تقرير يومي مختصر\n\nأفضل بداية: pilot 7 أيام. يناسبك أشرح أكثر؟",
|
||||
"ask_demo": "تمام! احجز الوقت المناسب:\ncalendly.com/sami-assiri11/dealix-demo\n\nأو قلي وقتين يناسبونك وأنا أنسّق.",
|
||||
"not_now": "تمام، شكراً على وقتك. أتواصل معك بعد شهر لو مناسب؟",
|
||||
"objection_budget": "فاهم. لذلك نبدأ بـ pilot بسيط بـ 499 ريال فقط.\nلو ما شفت قيمة خلال 7 أيام — استرداد كامل.",
|
||||
"already_has_crm": "ممتاز، Dealix ما يستبدل الـ CRM.\nهو طبقة قبله: يرد على الـ lead، يأهله، يحجز الموعد،\nوبعدها يسلّم البيانات لفريقكم أو للـ CRM.",
|
||||
"unsubscribe": "تم، لن أتواصل معكم مرة ثانية. شكراً على وقتكم.",
|
||||
}
|
||||
|
||||
|
||||
class ClassifyReplyRequest(BaseModel):
|
||||
reply_text: str
|
||||
company: str = ""
|
||||
original_sector: str = ""
|
||||
|
||||
|
||||
@router.post("/reply/classify")
|
||||
async def classify_reply(req: ClassifyReplyRequest) -> Dict[str, Any]:
|
||||
text = req.reply_text.lower()
|
||||
|
||||
if any(w in text for w in ["إيقاف", "stop", "unsubscribe", "لا تتواصل", "remove"]):
|
||||
cat = "unsubscribe"
|
||||
elif any(w in text for w in ["كم السعر", "كم التكلفة", "how much", "pricing", "أسعار"]):
|
||||
cat = "ask_price"
|
||||
elif any(w in text for w in ["عرض", "demo", "ديمو", "أوريني", "شرح"]):
|
||||
cat = "ask_demo"
|
||||
elif any(w in text for w in ["مهتم", "interested", "أبي أجرب", "نجرب", "تمام"]):
|
||||
cat = "interested"
|
||||
elif any(w in text for w in ["تفاصيل", "details", "أكثر", "وش بالضبط"]):
|
||||
cat = "ask_details"
|
||||
elif any(w in text for w in ["لاحقاً", "later", "مو الحين", "بعدين"]):
|
||||
cat = "not_now"
|
||||
elif any(w in text for w in ["ميزانية", "budget", "غالي", "مكلف"]):
|
||||
cat = "objection_budget"
|
||||
elif any(w in text for w in ["CRM", "crm", "نظام", "عندنا حل"]):
|
||||
cat = "already_has_crm"
|
||||
elif any(w in text for w in ["شراكة", "partner", "وكالة", "نبيع"]):
|
||||
cat = "partnership"
|
||||
elif any(w in text for w in ["خصوصية", "privacy", "بيانات", "PDPL"]):
|
||||
cat = "objection_privacy"
|
||||
elif any(w in text for w in ["ذكاء", "AI", "عربي طبيعي", "مضبوط"]):
|
||||
cat = "objection_ai"
|
||||
elif any(w in text for w in ["زعلان", "angry", "spam", "مزعج"]):
|
||||
cat = "angry"
|
||||
else:
|
||||
cat = "ask_details"
|
||||
|
||||
info = REPLY_CATEGORIES.get(cat, {"next": "human_review", "auto_reply": False})
|
||||
response = REPLY_RESPONSES.get(cat, "شكراً على ردك. براجع وأرد عليك قريب.")
|
||||
|
||||
return {
|
||||
"category": cat,
|
||||
"next_action": info["next"],
|
||||
"auto_reply_allowed": info["auto_reply"],
|
||||
"suggested_response": response,
|
||||
"human_review_required": not info["auto_reply"],
|
||||
"company": req.company,
|
||||
}
|
||||
@ -17,7 +17,7 @@ from app.openclaw.observability_bridge import observability_bridge
|
||||
from app.openclaw.policy import classify_action
|
||||
from app.openclaw.task_router import task_router
|
||||
from app.services.contract_intelligence_service import contract_intelligence_service
|
||||
from app.services.executive_roi_service import executive_roi_service
|
||||
from app.services.executive_roi_service import executive_room_service as executive_roi_service
|
||||
from app.services.predictive_revenue_service import predictive_revenue_service
|
||||
from app.openclaw.plugins.salesforce_agentforce_plugin import SalesforceAgentforcePlugin
|
||||
from app.openclaw.plugins.whatsapp_plugin import WhatsAppCloudPlugin
|
||||
|
||||
385
salesflow-saas/backend/app/api/v1/drafts.py
Normal file
385
salesflow-saas/backend/app/api/v1/drafts.py
Normal file
@ -0,0 +1,385 @@
|
||||
"""Draft Queue API — review, approve, send, track outreach drafts.
|
||||
|
||||
All outreach starts as drafts. Sami reviews in dashboard, approves
|
||||
batch, then system sends via existing Celery tasks.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select, func, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger("dealix.drafts")
|
||||
|
||||
router = APIRouter(prefix="/drafts", tags=["Draft Queue"])
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
class DraftFilter(BaseModel):
|
||||
status: Optional[str] = "draft"
|
||||
channel: Optional[str] = None
|
||||
batch_id: Optional[str] = None
|
||||
sector: Optional[str] = None
|
||||
limit: int = 50
|
||||
|
||||
|
||||
class ApproveBatchRequest(BaseModel):
|
||||
batch_id: str
|
||||
|
||||
|
||||
class LogReplyRequest(BaseModel):
|
||||
reply_text: str
|
||||
|
||||
|
||||
class EditDraftRequest(BaseModel):
|
||||
subject: Optional[str] = None
|
||||
body: Optional[str] = None
|
||||
channel: Optional[str] = None
|
||||
contact_email: Optional[str] = None
|
||||
contact_phone: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_drafts(
|
||||
status: Optional[str] = Query("draft"),
|
||||
channel: Optional[str] = Query(None),
|
||||
batch_id: Optional[str] = Query(None),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
db: AsyncSession = Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
stmt = select(OutreachDraft)
|
||||
if status:
|
||||
stmt = stmt.where(OutreachDraft.status == status)
|
||||
if channel:
|
||||
stmt = stmt.where(OutreachDraft.channel == channel)
|
||||
if batch_id:
|
||||
stmt = stmt.where(OutreachDraft.batch_id == batch_id)
|
||||
stmt = stmt.order_by(OutreachDraft.created_at.desc()).limit(limit)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
rows = list(result.scalars().all())
|
||||
return {
|
||||
"drafts": [r.to_dict() for r in rows],
|
||||
"count": len(rows),
|
||||
"filter": {"status": status, "channel": channel, "batch_id": batch_id},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def draft_stats(db: AsyncSession = Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
result = await db.execute(
|
||||
select(OutreachDraft.status, func.count(OutreachDraft.id))
|
||||
.group_by(OutreachDraft.status)
|
||||
)
|
||||
counts = {row[0]: row[1] for row in result.all()}
|
||||
return {
|
||||
"total": sum(counts.values()),
|
||||
"draft": counts.get("draft", 0),
|
||||
"approved": counts.get("approved", 0),
|
||||
"sent": counts.get("sent", 0),
|
||||
"replied": counts.get("replied", 0),
|
||||
"opted_out": counts.get("opted_out", 0),
|
||||
"bounced": counts.get("bounced", 0),
|
||||
"skipped": counts.get("skipped", 0),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{draft_id}")
|
||||
async def get_draft(draft_id: str, db: AsyncSession = Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
result = await db.execute(
|
||||
select(OutreachDraft).where(OutreachDraft.id == draft_id)
|
||||
)
|
||||
draft = result.scalar_one_or_none()
|
||||
if not draft:
|
||||
raise HTTPException(status_code=404, detail="Draft not found")
|
||||
d = draft.to_dict()
|
||||
d["body"] = draft.body
|
||||
d["followup_2d"] = draft.followup_2d
|
||||
d["followup_5d"] = draft.followup_5d
|
||||
d["call_script"] = draft.call_script
|
||||
return d
|
||||
|
||||
|
||||
@router.post("/{draft_id}/approve")
|
||||
async def approve_draft(draft_id: str, db: AsyncSession = Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
result = await db.execute(
|
||||
select(OutreachDraft).where(OutreachDraft.id == draft_id)
|
||||
)
|
||||
draft = result.scalar_one_or_none()
|
||||
if not draft:
|
||||
raise HTTPException(status_code=404, detail="Draft not found")
|
||||
if draft.status != "draft":
|
||||
return {"id": str(draft.id), "status": draft.status, "message": "already processed"}
|
||||
|
||||
draft.status = "approved"
|
||||
draft.approved_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
return {"id": str(draft.id), "status": "approved"}
|
||||
|
||||
|
||||
@router.post("/approve-batch")
|
||||
async def approve_batch(
|
||||
req: ApproveBatchRequest, db: AsyncSession = Depends(_get_db)
|
||||
) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
result = await db.execute(
|
||||
update(OutreachDraft)
|
||||
.where(OutreachDraft.batch_id == req.batch_id, OutreachDraft.status == "draft")
|
||||
.values(status="approved", approved_at=datetime.now(timezone.utc))
|
||||
)
|
||||
await db.commit()
|
||||
return {"batch_id": req.batch_id, "approved_count": result.rowcount}
|
||||
|
||||
|
||||
@router.post("/{draft_id}/send")
|
||||
async def send_draft(draft_id: str, db: AsyncSession = Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
result = await db.execute(
|
||||
select(OutreachDraft).where(OutreachDraft.id == draft_id)
|
||||
)
|
||||
draft = result.scalar_one_or_none()
|
||||
if not draft:
|
||||
raise HTTPException(status_code=404, detail="Draft not found")
|
||||
if draft.status not in ("approved", "draft"):
|
||||
return {"id": str(draft.id), "status": draft.status, "message": "not sendable"}
|
||||
|
||||
send_result = {"channel": draft.channel, "status": "pending"}
|
||||
|
||||
if draft.channel == "email" and draft.contact_email:
|
||||
try:
|
||||
from app.integrations.email_sender import send_email
|
||||
r = await send_email(draft.contact_email, draft.subject, draft.body)
|
||||
send_result = {"channel": "email", "status": "sent", "result": r}
|
||||
except Exception as exc:
|
||||
send_result = {"channel": "email", "status": "failed", "error": str(exc)[:200]}
|
||||
|
||||
elif draft.channel == "whatsapp" and draft.contact_phone:
|
||||
try:
|
||||
from app.api.v1.outreach_engine import _send_via_ultramsg, _format_phone
|
||||
r = await _send_via_ultramsg(draft.contact_phone, draft.body)
|
||||
if "error" not in r:
|
||||
send_result = {"channel": "whatsapp_ultramsg", "status": "sent", "result": r}
|
||||
else:
|
||||
from app.integrations.whatsapp import send_whatsapp_message
|
||||
r2 = await send_whatsapp_message(draft.contact_phone, draft.body)
|
||||
send_result = {"channel": "whatsapp_business_api", "status": "sent", "result": r2}
|
||||
except Exception as exc:
|
||||
send_result = {"channel": "whatsapp", "status": "failed", "error": str(exc)[:200]}
|
||||
|
||||
elif draft.channel == "sms" and draft.contact_phone:
|
||||
try:
|
||||
from app.integrations.sms import send_sms
|
||||
r = await send_sms(draft.contact_phone, draft.body)
|
||||
send_result = {"channel": "sms", "status": "sent", "result": r}
|
||||
except Exception as exc:
|
||||
send_result = {"channel": "sms", "status": "failed", "error": str(exc)[:200]}
|
||||
|
||||
elif draft.channel == "linkedin":
|
||||
send_result = {
|
||||
"channel": "linkedin",
|
||||
"status": "manual_required",
|
||||
"message": "Copy the message and send manually on LinkedIn",
|
||||
}
|
||||
|
||||
if send_result.get("status") == "sent":
|
||||
draft.status = "sent"
|
||||
draft.sent_at = datetime.now(timezone.utc)
|
||||
elif send_result.get("status") == "failed":
|
||||
draft.next_action = f"send_failed: {send_result.get('error', '')[:100]}"
|
||||
|
||||
await db.commit()
|
||||
return {"id": str(draft.id), **send_result}
|
||||
|
||||
|
||||
@router.post("/{draft_id}/skip")
|
||||
async def skip_draft(draft_id: str, db: AsyncSession = Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
result = await db.execute(
|
||||
select(OutreachDraft).where(OutreachDraft.id == draft_id)
|
||||
)
|
||||
draft = result.scalar_one_or_none()
|
||||
if not draft:
|
||||
raise HTTPException(status_code=404, detail="Draft not found")
|
||||
draft.status = "skipped"
|
||||
await db.commit()
|
||||
return {"id": str(draft.id), "status": "skipped"}
|
||||
|
||||
|
||||
@router.patch("/{draft_id}")
|
||||
async def edit_draft(
|
||||
draft_id: str, req: EditDraftRequest, db: AsyncSession = Depends(_get_db)
|
||||
) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
result = await db.execute(
|
||||
select(OutreachDraft).where(OutreachDraft.id == draft_id)
|
||||
)
|
||||
draft = result.scalar_one_or_none()
|
||||
if not draft:
|
||||
raise HTTPException(status_code=404, detail="Draft not found")
|
||||
if draft.status != "draft":
|
||||
raise HTTPException(status_code=400, detail="Can only edit drafts, not sent/approved")
|
||||
|
||||
for field, value in req.model_dump(exclude_none=True).items():
|
||||
setattr(draft, field, value)
|
||||
await db.commit()
|
||||
return {"id": str(draft.id), "status": "edited", "updated_fields": list(req.model_dump(exclude_none=True).keys())}
|
||||
|
||||
|
||||
@router.post("/send-approved-batch")
|
||||
async def send_approved_batch(
|
||||
channel: str = "whatsapp",
|
||||
batch_size: int = 10,
|
||||
db: AsyncSession = Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
"""Send up to batch_size approved drafts via specified channel.
|
||||
|
||||
Uses Ultramsg for WhatsApp (fallback to Business API),
|
||||
SMTP for email, Unifonic for SMS. LinkedIn = manual only.
|
||||
"""
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
stmt = (
|
||||
select(OutreachDraft)
|
||||
.where(
|
||||
OutreachDraft.status == "approved",
|
||||
OutreachDraft.channel == channel,
|
||||
)
|
||||
.order_by(OutreachDraft.approved_at.asc())
|
||||
.limit(batch_size)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
drafts = list(result.scalars().all())
|
||||
|
||||
sent = 0
|
||||
failed = 0
|
||||
results = []
|
||||
|
||||
for draft in drafts:
|
||||
send_result = {}
|
||||
|
||||
if channel == "whatsapp" and draft.contact_phone:
|
||||
try:
|
||||
from app.api.v1.outreach_engine import _send_via_ultramsg
|
||||
r = await _send_via_ultramsg(draft.contact_phone, draft.body)
|
||||
if "error" not in r:
|
||||
send_result = {"status": "sent", "provider": "ultramsg", "result": r}
|
||||
draft.status = "sent"
|
||||
draft.sent_at = datetime.now(timezone.utc)
|
||||
sent += 1
|
||||
else:
|
||||
send_result = {"status": "failed", "error": str(r)}
|
||||
failed += 1
|
||||
except Exception as exc:
|
||||
send_result = {"status": "failed", "error": str(exc)[:100]}
|
||||
failed += 1
|
||||
|
||||
elif channel == "email" and draft.contact_email:
|
||||
try:
|
||||
from app.integrations.email_sender import send_email
|
||||
r = await send_email(draft.contact_email, draft.subject, draft.body)
|
||||
send_result = {"status": "sent", "provider": "smtp", "result": r}
|
||||
draft.status = "sent"
|
||||
draft.sent_at = datetime.now(timezone.utc)
|
||||
sent += 1
|
||||
except Exception as exc:
|
||||
send_result = {"status": "failed", "error": str(exc)[:100]}
|
||||
failed += 1
|
||||
|
||||
elif channel == "sms" and draft.contact_phone:
|
||||
try:
|
||||
from app.integrations.sms import send_sms
|
||||
r = await send_sms(draft.contact_phone, draft.body)
|
||||
send_result = {"status": "sent", "provider": "unifonic", "result": r}
|
||||
draft.status = "sent"
|
||||
draft.sent_at = datetime.now(timezone.utc)
|
||||
sent += 1
|
||||
except Exception as exc:
|
||||
send_result = {"status": "failed", "error": str(exc)[:100]}
|
||||
failed += 1
|
||||
|
||||
elif channel == "linkedin":
|
||||
send_result = {"status": "manual_required", "message": "Copy from dashboard and send on LinkedIn"}
|
||||
|
||||
results.append({
|
||||
"id": str(draft.id),
|
||||
"company": draft.company,
|
||||
**send_result,
|
||||
})
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"channel": channel,
|
||||
"batch_size": batch_size,
|
||||
"sent": sent,
|
||||
"failed": failed,
|
||||
"manual": len([r for r in results if r.get("status") == "manual_required"]),
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{draft_id}/log-reply")
|
||||
async def log_reply(
|
||||
draft_id: str, req: LogReplyRequest, db: AsyncSession = Depends(_get_db)
|
||||
) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
from app.api.v1.automation import classify_reply, ClassifyReplyRequest
|
||||
|
||||
result = await db.execute(
|
||||
select(OutreachDraft).where(OutreachDraft.id == draft_id)
|
||||
)
|
||||
draft = result.scalar_one_or_none()
|
||||
if not draft:
|
||||
raise HTTPException(status_code=404, detail="Draft not found")
|
||||
|
||||
classification = await classify_reply(
|
||||
ClassifyReplyRequest(
|
||||
reply_text=req.reply_text,
|
||||
company=draft.company,
|
||||
original_sector=draft.sector,
|
||||
)
|
||||
)
|
||||
|
||||
draft.status = "replied"
|
||||
draft.replied_at = datetime.now(timezone.utc)
|
||||
draft.reply_text = req.reply_text
|
||||
draft.reply_category = classification["category"]
|
||||
draft.next_action = classification["next_action"]
|
||||
|
||||
if classification["category"] == "unsubscribe":
|
||||
draft.status = "opted_out"
|
||||
draft.next_action = "suppressed — no further contact"
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"id": str(draft.id),
|
||||
"reply_category": classification["category"],
|
||||
"suggested_response": classification["suggested_response"],
|
||||
"next_action": classification["next_action"],
|
||||
"auto_reply_allowed": classification["auto_reply_allowed"],
|
||||
}
|
||||
162
salesflow-saas/backend/app/api/v1/followups.py
Normal file
162
salesflow-saas/backend/app/api/v1/followups.py
Normal file
@ -0,0 +1,162 @@
|
||||
"""Follow-up Scheduler — generates follow-up drafts for unreplied outreach.
|
||||
|
||||
Checks sent drafts that haven't received replies after 2/5/10 days
|
||||
and creates new follow-up drafts linked to the original.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger("dealix.followups")
|
||||
|
||||
router = APIRouter(prefix="/followups", tags=["Follow-ups"])
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.get("/due")
|
||||
async def list_due_followups(
|
||||
days_since_sent: int = Query(2, ge=1, le=30),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
db: AsyncSession = Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=days_since_sent)
|
||||
stmt = (
|
||||
select(OutreachDraft)
|
||||
.where(
|
||||
and_(
|
||||
OutreachDraft.status == "sent",
|
||||
OutreachDraft.sent_at <= cutoff,
|
||||
OutreachDraft.reply_text.is_(None),
|
||||
)
|
||||
)
|
||||
.order_by(OutreachDraft.sent_at.asc())
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
rows = list(result.scalars().all())
|
||||
|
||||
due = []
|
||||
for row in rows:
|
||||
days_elapsed = (datetime.now(timezone.utc) - row.sent_at).days if row.sent_at else 0
|
||||
followup_text = ""
|
||||
followup_type = ""
|
||||
if days_elapsed >= 10:
|
||||
followup_text = row.followup_5d or "آخر متابعة — لو مو الوقت المناسب أفهم تماماً. شكراً."
|
||||
followup_type = "day_10_breakup"
|
||||
elif days_elapsed >= 5:
|
||||
followup_text = row.followup_5d or row.followup_2d or ""
|
||||
followup_type = "day_5_value"
|
||||
elif days_elapsed >= 2:
|
||||
followup_text = row.followup_2d or ""
|
||||
followup_type = "day_2_reminder"
|
||||
|
||||
if followup_text:
|
||||
due.append({
|
||||
"original_draft_id": str(row.id),
|
||||
"company": row.company,
|
||||
"channel": row.channel,
|
||||
"contact_email": row.contact_email,
|
||||
"contact_phone": row.contact_phone,
|
||||
"days_since_sent": days_elapsed,
|
||||
"followup_type": followup_type,
|
||||
"followup_text": followup_text,
|
||||
"sector": row.sector,
|
||||
})
|
||||
|
||||
return {
|
||||
"due_count": len(due),
|
||||
"cutoff_days": days_since_sent,
|
||||
"followups": due,
|
||||
}
|
||||
|
||||
|
||||
class GenerateFollowupsRequest(BaseModel):
|
||||
days_since_sent: int = 2
|
||||
max_followups: int = 20
|
||||
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate_followup_drafts(
|
||||
req: GenerateFollowupsRequest,
|
||||
db: AsyncSession = Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=req.days_since_sent)
|
||||
stmt = (
|
||||
select(OutreachDraft)
|
||||
.where(
|
||||
and_(
|
||||
OutreachDraft.status == "sent",
|
||||
OutreachDraft.sent_at <= cutoff,
|
||||
OutreachDraft.reply_text.is_(None),
|
||||
)
|
||||
)
|
||||
.order_by(OutreachDraft.sent_at.asc())
|
||||
.limit(req.max_followups)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
originals = list(result.scalars().all())
|
||||
|
||||
created = 0
|
||||
batch_id = f"followup_{datetime.now(timezone.utc).strftime('%Y%m%d')}_{str(uuid4())[:6]}"
|
||||
|
||||
for orig in originals:
|
||||
days_elapsed = (datetime.now(timezone.utc) - orig.sent_at).days if orig.sent_at else 0
|
||||
if days_elapsed >= 10:
|
||||
body = orig.followup_5d or "آخر متابعة — لو مناسب نتكلم، أنا موجود. لو لا، شكراً على وقتكم."
|
||||
subject = f"متابعة أخيرة: {orig.company}"
|
||||
elif days_elapsed >= 5:
|
||||
body = orig.followup_5d or orig.followup_2d or ""
|
||||
subject = f"متابعة: {orig.company}"
|
||||
else:
|
||||
body = orig.followup_2d or ""
|
||||
subject = f"متابعة سريعة: {orig.company}"
|
||||
|
||||
if not body:
|
||||
continue
|
||||
|
||||
followup = OutreachDraft(
|
||||
batch_id=batch_id,
|
||||
company=orig.company,
|
||||
contact_name=orig.contact_name,
|
||||
contact_email=orig.contact_email,
|
||||
contact_phone=orig.contact_phone,
|
||||
channel=orig.channel,
|
||||
subject=subject,
|
||||
body=body,
|
||||
sector=orig.sector,
|
||||
city=orig.city,
|
||||
fit_score=orig.fit_score,
|
||||
risk_score=orig.risk_score,
|
||||
status="draft",
|
||||
approval_required=True,
|
||||
source=f"followup_day_{days_elapsed}_of_{str(orig.id)[:8]}",
|
||||
)
|
||||
db.add(followup)
|
||||
created += 1
|
||||
|
||||
if created:
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"batch_id": batch_id,
|
||||
"followups_created": created,
|
||||
"originals_checked": len(originals),
|
||||
}
|
||||
217
salesflow-saas/backend/app/api/v1/founder_outreach.py
Normal file
217
salesflow-saas/backend/app/api/v1/founder_outreach.py
Normal file
@ -0,0 +1,217 @@
|
||||
"""Founder Outreach — hyper-personalized emails from Sami as founder.
|
||||
|
||||
Each email researches the company's weakness, estimates lost revenue,
|
||||
and writes a personal message showing exact ROI Dealix can deliver.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger("dealix.founder_outreach")
|
||||
|
||||
router = APIRouter(prefix="/founder-outreach", tags=["Founder Outreach"])
|
||||
|
||||
SECTOR_WEAKNESS_MAP = {
|
||||
"real_estate": {
|
||||
"weakness_ar": "شركات العقار تستقبل 50-200 استفسار/شهر عن الأسعار والمواقع. المشكلة: 60-70% ما يُتابع خلال أول ساعة = العميل يروح للمنافس",
|
||||
"weakness_en": "Real estate companies receive 50-200 inquiries/month about prices and locations. Problem: 60-70% don't get a response within the first hour = client goes to competitor",
|
||||
"revenue_loss_ar": "لو عندكم 100 استفسار/شهر و60% تضيع = 60 فرصة ضائعة. لو 10% منها كانت بتتحول = 6 صفقات × متوسط 50,000 ريال = ~300,000 ريال ضائعة سنوياً",
|
||||
"revenue_loss_en": "If you get 100 inquiries/month and 60% are lost = 60 missed opportunities. If 10% would convert = 6 deals × avg 50,000 SAR = ~300,000 SAR lost annually",
|
||||
"dealix_impact_ar": "Dealix يرد خلال 45 ثانية ويحفظ 30-40% من الفرص الضائعة = ~120,000 ريال إضافية/سنة. تكلفة Dealix = 990 ريال/شهر = ROI 10x",
|
||||
"dealix_impact_en": "Dealix responds in 45 seconds and saves 30-40% of lost opportunities = ~120,000 SAR additional/year. Dealix costs 990 SAR/month = 10x ROI",
|
||||
},
|
||||
"construction": {
|
||||
"weakness_ar": "شركات المقاولات تستقبل طلبات عروض أسعار يومياً. المشكلة: فرز الطلبات الجادة من غيرها ياخذ ساعات من المهندسين — وقت ممكن يُصرف في التنفيذ",
|
||||
"weakness_en": "Construction companies receive quote requests daily. Problem: Sorting serious from non-serious requests takes hours of engineer time",
|
||||
"revenue_loss_ar": "لو فريق المبيعات يقضي 2 ساعة/يوم في فرز الطلبات = 40 ساعة/شهر = تكلفة ~8,000 ريال/شهر من وقت الفريق",
|
||||
"revenue_loss_en": "If sales team spends 2 hours/day sorting requests = 40 hours/month = ~8,000 SAR/month in team time cost",
|
||||
"dealix_impact_ar": "Dealix يستقبل الطلب، يسأل 3 أسئلة تأهيل، ويصنّف الجدية تلقائياً. يوفّر 80% من وقت الفرز = ~6,400 ريال/شهر. ROI: 6.5x",
|
||||
"dealix_impact_en": "Dealix receives requests, asks 3 qualifying questions, and auto-classifies urgency. Saves 80% of sorting time = ~6,400 SAR/month. ROI: 6.5x",
|
||||
},
|
||||
"hospitality": {
|
||||
"weakness_ar": "الفنادق والقاعات تستقبل استفسارات حجز يومياً. المشكلة: كل ساعة تأخير = احتمال 50% العميل يحجز عند المنافس",
|
||||
"weakness_en": "Hotels and event venues receive booking inquiries daily. Problem: every hour of delay = 50% chance the client books with a competitor",
|
||||
"revenue_loss_ar": "لو 10 استفسارات/أسبوع تضيع بسبب تأخر الرد × متوسط حجز 5,000 ريال = 200,000 ريال ضائعة/سنة",
|
||||
"revenue_loss_en": "If 10 inquiries/week are lost due to slow response × avg booking 5,000 SAR = 200,000 SAR lost/year",
|
||||
"dealix_impact_ar": "Dealix يرد فوراً، يسأل عن التاريخ والعدد والميزانية، ويحجز مبدئي. يحفظ 40% من الحجوزات الضائعة = ~80,000 ريال/سنة",
|
||||
"dealix_impact_en": "Dealix responds instantly, asks about date, guest count, and budget, and makes a preliminary booking. Saves 40% of lost bookings = ~80,000 SAR/year",
|
||||
},
|
||||
"agency": {
|
||||
"weakness_ar": "الوكالات تصرف على إعلانات عملائها لكن المتابعة على الـ leads ضعيفة. النتيجة: العميل يلوم الوكالة على ضعف النتائج",
|
||||
"weakness_en": "Agencies spend on client ads but lead follow-up is weak. Result: client blames agency for poor results",
|
||||
"revenue_loss_ar": "كل عميل وكالة يدفع 5,000-20,000 ريال/شهر. لو عميل واحد يطلع بسبب ضعف المتابعة = خسارة 60,000-240,000 ريال/سنة",
|
||||
"revenue_loss_en": "Each agency client pays 5,000-20,000 SAR/month. If one client leaves due to poor follow-up = loss of 60,000-240,000 SAR/year",
|
||||
"dealix_impact_ar": "Dealix يصير خدمة جديدة تبيعونها: رد ذكي + تأهيل + حجز. Setup fee 3,000 + MRR 990/عميل = إيراد جديد بدل خسارة عميل",
|
||||
"dealix_impact_en": "Dealix becomes a new service you sell: smart response + qualification + booking. Setup fee 3,000 + MRR 990/client = new revenue instead of losing a client",
|
||||
},
|
||||
"saas": {
|
||||
"weakness_ar": "شركات SaaS تجيها leads من الموقع والإعلانات لكن فريق المبيعات صغير. المشكلة: 30-50% من الـ leads تبرد خلال أول 24 ساعة",
|
||||
"weakness_en": "SaaS companies get leads from website and ads but sales team is small. Problem: 30-50% of leads go cold within 24 hours",
|
||||
"revenue_loss_ar": "لو 200 lead/شهر و40% تبرد = 80 lead ضائع. لو 5% كانت بتشتري × متوسط 5,000 ريال/سنة = 20,000 ريال ضائعة/شهر",
|
||||
"revenue_loss_en": "If 200 leads/month and 40% go cold = 80 lost leads. If 5% would buy × avg 5,000 SAR/year = 20,000 SAR lost/month",
|
||||
"dealix_impact_ar": "Dealix يرد بالعربي خلال 45 ثانية على كل lead، يؤهل، ويحجز demo. يحفظ 30% من الضائع = ~6,000 ريال إضافية/شهر. ROI: 6x",
|
||||
"dealix_impact_en": "Dealix responds in Arabic within 45 seconds to every lead, qualifies, and books demos. Saves 30% of lost leads = ~6,000 SAR additional/month. ROI: 6x",
|
||||
},
|
||||
"logistics": {
|
||||
"weakness_ar": "شركات الشحن تستقبل طلبات أسعار متكررة. المشكلة: كل طلب ما يُتابع = شحنة تروح للمنافس مباشرة",
|
||||
"weakness_en": "Shipping companies receive repeated quote requests. Problem: every unfollowed request = shipment goes directly to competitor",
|
||||
"revenue_loss_ar": "لو 5 طلبات/أسبوع تضيع × متوسط 3,000 ريال/شحنة = 60,000 ريال ضائعة/شهر",
|
||||
"revenue_loss_en": "If 5 requests/week are lost × avg 3,000 SAR/shipment = 60,000 SAR lost/month",
|
||||
"dealix_impact_ar": "Dealix يستقبل الطلب، يسأل عن نوع الشحنة والوجهة، ويعطي تقدير أولي فوراً. يحفظ 40% = ~24,000 ريال/شهر",
|
||||
"dealix_impact_en": "Dealix receives requests, asks about shipment type and destination, gives instant estimate. Saves 40% = ~24,000 SAR/month",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class FounderEmailRequest(BaseModel):
|
||||
company: str
|
||||
sector: str
|
||||
contact_name: str = ""
|
||||
contact_email: str = ""
|
||||
city: str = ""
|
||||
website: str = ""
|
||||
signals: List[str] = []
|
||||
language: str = "ar"
|
||||
estimated_monthly_leads: int = 100
|
||||
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate_founder_email(req: FounderEmailRequest) -> Dict[str, Any]:
|
||||
"""Generate a hyper-personalized founder email that targets the company's weakness."""
|
||||
sector_data = SECTOR_WEAKNESS_MAP.get(req.sector, SECTOR_WEAKNESS_MAP.get("saas", {}))
|
||||
lang = req.language
|
||||
|
||||
if lang == "en":
|
||||
return _build_en(req, sector_data)
|
||||
|
||||
name = req.contact_name or f"فريق {req.company}"
|
||||
weakness = sector_data.get("weakness_ar", "")
|
||||
revenue_loss = sector_data.get("revenue_loss_ar", "")
|
||||
impact = sector_data.get("dealix_impact_ar", "")
|
||||
|
||||
signal_line = ""
|
||||
if "hubspot" in [s.lower() for s in req.signals]:
|
||||
signal_line = f"شفت إن {req.company} تستخدمون HubSpot — يعني عندكم leads تجي من الموقع. "
|
||||
elif "whatsapp_widget" in [s.lower() for s in req.signals]:
|
||||
signal_line = f"لاحظت إن عندكم واتساب كقناة رئيسية للعملاء. "
|
||||
|
||||
subject = f"{name} — فرصة توفير {req.estimated_monthly_leads * 50} ريال/شهر لـ {req.company}"
|
||||
|
||||
body = f"""مرحباً {name}،
|
||||
|
||||
أنا سامي العسيري، مؤسس Dealix.
|
||||
|
||||
{signal_line}أكتب لك شخصياً لأن لاحظت شي مهم عن {req.company}:
|
||||
|
||||
📊 المشكلة:
|
||||
{weakness}
|
||||
|
||||
💰 التكلفة الحقيقية:
|
||||
{revenue_loss}
|
||||
|
||||
🎯 الحل:
|
||||
{impact}
|
||||
|
||||
ما أبي أبيعك شي الحين — أبي أوريك بالأرقام.
|
||||
|
||||
نسوي pilot 7 أيام على leads حقيقية عندكم:
|
||||
• نرد على كل استفسار خلال 45 ثانية
|
||||
• نسأل أسئلة التأهيل المهمة لقطاعكم
|
||||
• نحجز المواعيد أو نحوّل للمبيعات
|
||||
• نرسل تقرير يومي بالنتائج
|
||||
|
||||
السعر: 499 ريال فقط (مع ضمان استرداد كامل).
|
||||
|
||||
يناسبك 15 دقيقة هذا الأسبوع أوريك النظام على سيناريو من شغلكم؟
|
||||
📅 calendly.com/sami-assiri11/dealix-demo
|
||||
|
||||
وإذا عندك سؤال — رد على هالإيميل مباشرة. أنا شخصياً أرد.
|
||||
|
||||
سامي العسيري
|
||||
مؤسس Dealix
|
||||
dealix.me
|
||||
+966 59 778 8539
|
||||
|
||||
إذا ما يناسبكم هالنوع من الرسائل، اكتبوا "إيقاف" وما بنتواصل مرة ثانية."""
|
||||
|
||||
return {
|
||||
"type": "founder_personal",
|
||||
"company": req.company,
|
||||
"language": "ar",
|
||||
"subject": subject,
|
||||
"body": body,
|
||||
"weakness_targeted": weakness[:100],
|
||||
"revenue_loss_estimate": revenue_loss[:100],
|
||||
"dealix_roi": impact[:100],
|
||||
"opt_out_included": True,
|
||||
"word_count": len(body.split()),
|
||||
}
|
||||
|
||||
|
||||
def _build_en(req: FounderEmailRequest, sector_data: Dict) -> Dict[str, Any]:
|
||||
name = req.contact_name or f"{req.company} team"
|
||||
weakness = sector_data.get("weakness_en", "")
|
||||
revenue_loss = sector_data.get("revenue_loss_en", "")
|
||||
impact = sector_data.get("dealix_impact_en", "")
|
||||
|
||||
signal_line = ""
|
||||
if "hubspot" in [s.lower() for s in req.signals]:
|
||||
signal_line = f"I noticed {req.company} uses HubSpot — meaning you have leads coming from your website. "
|
||||
|
||||
subject = f"{name} — opportunity to save {req.estimated_monthly_leads * 50} SAR/month for {req.company}"
|
||||
|
||||
body = f"""Hi {name},
|
||||
|
||||
I'm Sami Alassiri, founder of Dealix.
|
||||
|
||||
{signal_line}I'm writing personally because I noticed something important about {req.company}:
|
||||
|
||||
📊 The Problem:
|
||||
{weakness}
|
||||
|
||||
💰 The Real Cost:
|
||||
{revenue_loss}
|
||||
|
||||
🎯 The Solution:
|
||||
{impact}
|
||||
|
||||
I'm not trying to sell you anything right now — I want to show you with numbers.
|
||||
|
||||
We'll run a 7-day pilot on your actual leads:
|
||||
• Respond to every inquiry within 45 seconds
|
||||
• Ask the right qualifying questions for your industry
|
||||
• Book meetings or route to sales
|
||||
• Send you a daily results report
|
||||
|
||||
Price: 499 SAR only (with full money-back guarantee).
|
||||
|
||||
Do you have 15 minutes this week? I'll show you the system on a scenario from your business.
|
||||
📅 calendly.com/sami-assiri11/dealix-demo
|
||||
|
||||
Questions? Reply to this email directly. I personally respond.
|
||||
|
||||
Sami Alassiri
|
||||
Founder, Dealix
|
||||
dealix.me
|
||||
+966 59 778 8539
|
||||
|
||||
To stop receiving these emails, reply "STOP"."""
|
||||
|
||||
return {
|
||||
"type": "founder_personal",
|
||||
"company": req.company,
|
||||
"language": "en",
|
||||
"subject": subject,
|
||||
"body": body,
|
||||
"weakness_targeted": weakness[:100],
|
||||
"revenue_loss_estimate": revenue_loss[:100],
|
||||
"dealix_roi": impact[:100],
|
||||
"opt_out_included": True,
|
||||
"word_count": len(body.split()),
|
||||
}
|
||||
183
salesflow-saas/backend/app/api/v1/full_os.py
Normal file
183
salesflow-saas/backend/app/api/v1/full_os.py
Normal file
@ -0,0 +1,183 @@
|
||||
"""Full OS API — unified deal lifecycle orchestration.
|
||||
|
||||
Single endpoint processes any event (inbound message, reply, booking,
|
||||
payment) and returns: next stage, actions to take, response message,
|
||||
and whether human approval is needed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/os", tags=["Full OS"])
|
||||
|
||||
|
||||
class ProcessEventRequest(BaseModel):
|
||||
lead_id: str = ""
|
||||
phone: str = ""
|
||||
email: str = ""
|
||||
company: str = ""
|
||||
sector: str = ""
|
||||
source: str = "whatsapp_inbound"
|
||||
message: str = ""
|
||||
current_stage: str = "new_lead"
|
||||
event_type: str = "inbound_message"
|
||||
|
||||
|
||||
class BulkProcessRequest(BaseModel):
|
||||
events: List[ProcessEventRequest]
|
||||
|
||||
|
||||
@router.post("/process")
|
||||
async def process_event(req: ProcessEventRequest) -> Dict[str, Any]:
|
||||
"""Process a single event through the deal lifecycle state machine.
|
||||
|
||||
Returns: new_stage, actions, response_message_ar, human_approval_required.
|
||||
If auto_send_allowed=True, the response can be sent automatically.
|
||||
If human_approval_required=True, create a draft for Sami to review.
|
||||
"""
|
||||
from app.services.full_os_orchestrator import orchestrator, OrchestratorEvent
|
||||
|
||||
event = OrchestratorEvent(
|
||||
lead_id=req.lead_id,
|
||||
phone=req.phone,
|
||||
email=req.email,
|
||||
company=req.company,
|
||||
sector=req.sector,
|
||||
source=req.source,
|
||||
message=req.message,
|
||||
current_stage=req.current_stage,
|
||||
event_type=req.event_type,
|
||||
)
|
||||
return orchestrator.process_event(event)
|
||||
|
||||
|
||||
@router.post("/process-and-act")
|
||||
async def process_and_act(req: ProcessEventRequest) -> Dict[str, Any]:
|
||||
"""Process event AND execute the first safe action.
|
||||
|
||||
If auto_send_allowed: sends WhatsApp response via Ultramsg.
|
||||
If human_approval_required: creates a draft for review.
|
||||
Always logs the activity.
|
||||
"""
|
||||
from app.services.full_os_orchestrator import orchestrator, OrchestratorEvent
|
||||
|
||||
event = OrchestratorEvent(
|
||||
lead_id=req.lead_id,
|
||||
phone=req.phone,
|
||||
email=req.email,
|
||||
company=req.company,
|
||||
sector=req.sector,
|
||||
source=req.source,
|
||||
message=req.message,
|
||||
current_stage=req.current_stage,
|
||||
event_type=req.event_type,
|
||||
)
|
||||
result = orchestrator.process_event(event)
|
||||
|
||||
execution = {"action_taken": "none", "send_result": None, "draft_created": False}
|
||||
|
||||
if result.get("auto_send_allowed") and result.get("response_message_ar") and req.phone:
|
||||
if "send_whatsapp" in result.get("actions", []):
|
||||
try:
|
||||
from app.services.whatsapp_multi_provider import send_whatsapp_smart
|
||||
send_result = await send_whatsapp_smart(req.phone, result["response_message_ar"])
|
||||
execution = {
|
||||
"action_taken": "whatsapp_sent",
|
||||
"send_result": send_result,
|
||||
"draft_created": False,
|
||||
}
|
||||
except Exception as exc:
|
||||
execution = {
|
||||
"action_taken": "whatsapp_failed",
|
||||
"error": str(exc)[:200],
|
||||
"draft_created": False,
|
||||
}
|
||||
|
||||
elif result.get("human_approval_required") and result.get("response_message_ar"):
|
||||
try:
|
||||
from app.models.outreach_draft import OutreachDraft
|
||||
from app.database import async_session
|
||||
async with async_session() as session:
|
||||
draft = OutreachDraft(
|
||||
batch_id=f"os_{result['lead_id']}",
|
||||
company=req.company,
|
||||
contact_phone=req.phone,
|
||||
contact_email=req.email,
|
||||
channel="whatsapp" if req.phone else "email",
|
||||
subject=f"[{result['new_stage']}] {req.company}",
|
||||
body=result["response_message_ar"],
|
||||
sector=req.sector,
|
||||
status="draft",
|
||||
approval_required=True,
|
||||
source="full_os_orchestrator",
|
||||
)
|
||||
session.add(draft)
|
||||
await session.commit()
|
||||
execution = {
|
||||
"action_taken": "draft_created",
|
||||
"draft_id": str(draft.id),
|
||||
"draft_created": True,
|
||||
}
|
||||
except Exception:
|
||||
execution = {"action_taken": "draft_failed", "draft_created": False}
|
||||
|
||||
return {**result, "execution": execution}
|
||||
|
||||
|
||||
@router.post("/bulk-process")
|
||||
async def bulk_process(req: BulkProcessRequest) -> Dict[str, Any]:
|
||||
"""Process multiple events at once (for batch imports)."""
|
||||
from app.services.full_os_orchestrator import orchestrator, OrchestratorEvent
|
||||
|
||||
results = []
|
||||
for event_req in req.events:
|
||||
event = OrchestratorEvent(
|
||||
lead_id=event_req.lead_id,
|
||||
phone=event_req.phone,
|
||||
email=event_req.email,
|
||||
company=event_req.company,
|
||||
sector=event_req.sector,
|
||||
source=event_req.source,
|
||||
message=event_req.message,
|
||||
current_stage=event_req.current_stage,
|
||||
event_type=event_req.event_type,
|
||||
)
|
||||
results.append(orchestrator.process_event(event))
|
||||
|
||||
return {
|
||||
"processed": len(results),
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/whatsapp-providers")
|
||||
async def whatsapp_provider_status() -> Dict[str, Any]:
|
||||
"""Check which WhatsApp providers are configured."""
|
||||
from app.services.whatsapp_multi_provider import check_providers
|
||||
return await check_providers()
|
||||
|
||||
|
||||
@router.post("/test-send")
|
||||
async def test_whatsapp_send(phone: str, message: str = "اختبار Dealix — النظام شغّال 🚀") -> Dict[str, Any]:
|
||||
"""Test WhatsApp send via all configured providers."""
|
||||
from app.services.whatsapp_multi_provider import send_whatsapp_smart
|
||||
return await send_whatsapp_smart(phone, message)
|
||||
|
||||
|
||||
@router.get("/stages")
|
||||
async def list_stages() -> Dict[str, Any]:
|
||||
"""List all deal lifecycle stages with their possible transitions."""
|
||||
from app.services.full_os_orchestrator import STAGE_TRANSITIONS, STAGE_AUTO_ACTIONS, STAGE_MESSAGES_AR
|
||||
|
||||
stages = {}
|
||||
for stage, transitions in STAGE_TRANSITIONS.items():
|
||||
stages[stage.value] = {
|
||||
"transitions": {k: v.value for k, v in transitions.items()},
|
||||
"auto_actions": [a.value for a in STAGE_AUTO_ACTIONS.get(stage, [])],
|
||||
"message_template": STAGE_MESSAGES_AR.get(stage, ""),
|
||||
}
|
||||
return {"stages": stages, "total": len(stages)}
|
||||
@ -101,8 +101,8 @@ def _format_phone(phone: str) -> str:
|
||||
|
||||
async def _send_via_ultramsg(phone: str, message: str) -> dict:
|
||||
"""Send a message via Ultramsg API."""
|
||||
instance_id = os.getenv("ULTRAMSG_INSTANCE_ID", "instance168132")
|
||||
token = os.getenv("ULTRAMSG_TOKEN", "7azj2ss74wpg9jwp")
|
||||
instance_id = os.getenv("ULTRAMSG_INSTANCE_ID", "")
|
||||
token = os.getenv("ULTRAMSG_TOKEN", "")
|
||||
|
||||
if not instance_id or not token:
|
||||
return {"error": "Ultramsg not configured"}
|
||||
|
||||
@ -136,3 +136,23 @@ api_router.include_router(channels_router.router)
|
||||
# ── Pricing & Checkout — Moyasar-powered payment flow ────────
|
||||
from app.api.v1 import pricing as pricing_router
|
||||
api_router.include_router(pricing_router.router)
|
||||
|
||||
# ── Automation — Daily targeting + email + reply classification ─
|
||||
from app.api.v1 import automation as automation_router
|
||||
api_router.include_router(automation_router.router)
|
||||
|
||||
# ── Draft Queue — review, approve, send outreach drafts ────────
|
||||
from app.api.v1 import drafts as drafts_router
|
||||
api_router.include_router(drafts_router.router)
|
||||
|
||||
# ── Follow-ups — auto-generate follow-up drafts for unreplied ──
|
||||
from app.api.v1 import followups as followups_router
|
||||
api_router.include_router(followups_router.router)
|
||||
|
||||
# ── Full OS — unified deal lifecycle orchestration ─────────────
|
||||
from app.api.v1 import full_os as full_os_router
|
||||
api_router.include_router(full_os_router.router)
|
||||
|
||||
# ── Founder Outreach — hyper-personalized founder emails ───────
|
||||
from app.api.v1 import founder_outreach as founder_outreach_router
|
||||
api_router.include_router(founder_outreach_router.router)
|
||||
|
||||
@ -156,6 +156,23 @@ class Settings(BaseSettings):
|
||||
DLQ_MAX_RETRIES: int = 5
|
||||
DLQ_DRAIN_BATCH_SIZE: int = 10
|
||||
|
||||
# ── Local LLM (Ollama) ──────────────────────────────
|
||||
OLLAMA_BASE_URL: str = "http://localhost:11434/v1"
|
||||
OLLAMA_MODEL: str = "qwen2.5:7b"
|
||||
LLM_CACHE_ENABLED: bool = True
|
||||
LLM_CACHE_TTL: int = 3600
|
||||
LLM_RATE_LIMIT_RPM: int = 60
|
||||
|
||||
# ── Green API (WhatsApp) ────────────────────────────
|
||||
GREEN_API_INSTANCE_ID: str = ""
|
||||
GREEN_API_TOKEN: str = ""
|
||||
|
||||
# ── Outreach Rate Limits ────────────────────────────
|
||||
WHATSAPP_DAILY_LIMIT: int = 15
|
||||
EMAIL_DAILY_LIMIT: int = 50
|
||||
EMAIL_BATCH_SIZE: int = 10
|
||||
EMAIL_BATCH_DELAY_MINUTES: int = 90
|
||||
|
||||
# ── Rate Limiting ────────────────────────────────────
|
||||
RATE_LIMIT_PER_MINUTE: int = 60
|
||||
RATE_LIMIT_PER_HOUR: int = 1000
|
||||
|
||||
@ -16,7 +16,14 @@ def _get_db_url() -> str:
|
||||
break
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
return url or "sqlite+aiosqlite:///./dealix.db"
|
||||
if not url:
|
||||
return "sqlite+aiosqlite:///./dealix.db"
|
||||
# Railway Postgres gives postgres:// but SQLAlchemy needs postgresql+asyncpg://
|
||||
if url.startswith("postgres://"):
|
||||
url = url.replace("postgres://", "postgresql+asyncpg://", 1)
|
||||
elif url.startswith("postgresql://"):
|
||||
url = url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
return url
|
||||
|
||||
|
||||
_DB_URL = _get_db_url()
|
||||
|
||||
@ -1,29 +1,134 @@
|
||||
"""Email Sender — SMTP with rate limiting, HTML wrapper, and compliance headers.
|
||||
|
||||
Supports Gmail app passwords. Adds professional HTML wrapper for Arabic
|
||||
RTL emails and List-Unsubscribe header for compliance.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from typing import Optional
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
logger = logging.getLogger("dealix.email")
|
||||
settings = get_settings()
|
||||
|
||||
UNSUBSCRIBE_AR = "\n\n---\nإذا ما يناسبكم، اكتبوا \"إيقاف\" ولن نتواصل مرة ثانية."
|
||||
UNSUBSCRIBE_EN = "\n\n---\nTo stop receiving these emails, reply with \"STOP\"."
|
||||
|
||||
async def send_email(to_email: str, subject: str, body_html: str, from_name: str = None) -> dict:
|
||||
"""Send email via SMTP."""
|
||||
|
||||
def _wrap_html(body: str, direction: str = "rtl", lang: str = "ar") -> str:
|
||||
"""Wrap plain text or simple HTML in a professional email template."""
|
||||
body_html = body.replace("\n", "<br>") if "<" not in body else body
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="{lang}" dir="{direction}">
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="font-family: 'Segoe UI', Tahoma, sans-serif; font-size: 15px;
|
||||
line-height: 1.7; color: #1a1a1a; max-width: 600px; margin: 0 auto;
|
||||
padding: 20px; direction: {direction};">
|
||||
{body_html}
|
||||
<div style="margin-top: 30px; padding-top: 15px; border-top: 1px solid #e5e5e5;
|
||||
font-size: 12px; color: #999;">
|
||||
Dealix — dealix.me
|
||||
</div>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
async def send_email(
|
||||
to_email: str,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
from_name: Optional[str] = None,
|
||||
language: str = "ar",
|
||||
add_unsubscribe: bool = True,
|
||||
delay_seconds: float = 0,
|
||||
) -> dict:
|
||||
"""Send email via SMTP with compliance headers.
|
||||
|
||||
Args:
|
||||
to_email: Recipient email
|
||||
subject: Email subject
|
||||
body_html: Email body (plain text or HTML)
|
||||
from_name: Sender display name
|
||||
language: 'ar' or 'en' — affects direction and unsubscribe text
|
||||
add_unsubscribe: Whether to append unsubscribe line
|
||||
delay_seconds: Wait before sending (for rate limiting in batch)
|
||||
"""
|
||||
if not settings.SMTP_USER or not settings.SMTP_PASSWORD:
|
||||
return {"status": "error", "detail": "Email not configured"}
|
||||
return {"status": "error", "detail": "SMTP_USER and SMTP_PASSWORD not configured. Add Gmail app password in Railway env."}
|
||||
|
||||
if delay_seconds > 0:
|
||||
await asyncio.sleep(delay_seconds)
|
||||
|
||||
if add_unsubscribe:
|
||||
unsub = UNSUBSCRIBE_AR if language == "ar" else UNSUBSCRIBE_EN
|
||||
body_html = body_html + unsub
|
||||
|
||||
direction = "rtl" if language == "ar" else "ltr"
|
||||
wrapped = _wrap_html(body_html, direction=direction, lang=language)
|
||||
|
||||
sender_name = from_name or settings.EMAIL_FROM_NAME or settings.APP_NAME
|
||||
from_addr = getattr(settings, "EMAIL_FROM_ADDRESS", settings.SMTP_USER)
|
||||
|
||||
sender_name = from_name or settings.APP_NAME
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = f"{sender_name} <{settings.SMTP_USER}>"
|
||||
msg["From"] = f"{sender_name} <{from_addr}>"
|
||||
msg["To"] = to_email
|
||||
msg["List-Unsubscribe"] = f"<mailto:{from_addr}?subject=unsubscribe>"
|
||||
msg["X-Mailer"] = "Dealix/1.0"
|
||||
|
||||
msg.attach(MIMEText(body_html, "html", "utf-8"))
|
||||
msg.attach(MIMEText(body_html, "plain", "utf-8"))
|
||||
msg.attach(MIMEText(wrapped, "html", "utf-8"))
|
||||
|
||||
try:
|
||||
with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server:
|
||||
server.starttls()
|
||||
server.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
|
||||
server.sendmail(settings.SMTP_USER, to_email, msg.as_string())
|
||||
return {"status": "sent"}
|
||||
server.sendmail(from_addr, to_email, msg.as_string())
|
||||
logger.info("Email sent to %s: %s", to_email, subject[:50])
|
||||
return {"status": "sent", "to": to_email}
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
logger.error("SMTP auth failed — check SMTP_USER and SMTP_PASSWORD (Gmail app password)")
|
||||
return {"status": "error", "detail": "SMTP authentication failed. Use Gmail App Password, not regular password."}
|
||||
except Exception as e:
|
||||
return {"status": "error", "detail": str(e)}
|
||||
logger.error("Email send failed: %s", e)
|
||||
return {"status": "error", "detail": str(e)[:200]}
|
||||
|
||||
|
||||
async def send_email_batch(
|
||||
emails: list[dict],
|
||||
delay_between: float = 2.0,
|
||||
max_batch: int = 10,
|
||||
) -> dict:
|
||||
"""Send a batch of emails with delays between each.
|
||||
|
||||
Args:
|
||||
emails: List of dicts with {to, subject, body, language}
|
||||
delay_between: Seconds between each email (default 2)
|
||||
max_batch: Max emails per batch (default 10)
|
||||
|
||||
Returns: {sent, failed, results}
|
||||
"""
|
||||
sent = 0
|
||||
failed = 0
|
||||
results = []
|
||||
|
||||
for i, email in enumerate(emails[:max_batch]):
|
||||
delay = delay_between if i > 0 else 0
|
||||
result = await send_email(
|
||||
to_email=email["to"],
|
||||
subject=email["subject"],
|
||||
body_html=email["body"],
|
||||
language=email.get("language", "ar"),
|
||||
delay_seconds=delay,
|
||||
)
|
||||
results.append({"to": email["to"], **result})
|
||||
if result["status"] == "sent":
|
||||
sent += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
return {"sent": sent, "failed": failed, "total": len(results), "results": results}
|
||||
|
||||
77
salesflow-saas/backend/app/models/outreach_draft.py
Normal file
77
salesflow-saas/backend/app/models/outreach_draft.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""OutreachDraft — DB-persisted draft queue for all outreach channels.
|
||||
|
||||
Every generated message starts as status='draft'. Sami reviews and
|
||||
approves before any send. Approved drafts are dispatched via existing
|
||||
Celery send tasks.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import Column, String, Integer, Boolean, DateTime, Text, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
try:
|
||||
from app.database import Base
|
||||
except ImportError:
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class OutreachDraft(Base):
|
||||
__tablename__ = "outreach_drafts"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
batch_id = Column(String(64), index=True)
|
||||
company = Column(String(255), nullable=False)
|
||||
contact_name = Column(String(255), default="")
|
||||
contact_email = Column(String(255), default="")
|
||||
contact_phone = Column(String(32), default="")
|
||||
channel = Column(String(20), nullable=False) # email | whatsapp | sms | linkedin
|
||||
subject = Column(String(500), default="")
|
||||
body = Column(Text, nullable=False)
|
||||
followup_2d = Column(Text, default="")
|
||||
followup_5d = Column(Text, default="")
|
||||
call_script = Column(Text, default="")
|
||||
sector = Column(String(100), default="")
|
||||
city = Column(String(100), default="")
|
||||
pain_hypothesis = Column(Text, default="")
|
||||
fit_score = Column(Integer, default=0)
|
||||
risk_score = Column(Integer, default=0)
|
||||
status = Column(String(20), default="draft", index=True)
|
||||
# draft | approved | sent | replied | opted_out | bounced | skipped
|
||||
approval_required = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
approved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
sent_at = Column(DateTime(timezone=True), nullable=True)
|
||||
replied_at = Column(DateTime(timezone=True), nullable=True)
|
||||
reply_text = Column(Text, nullable=True)
|
||||
reply_category = Column(String(50), nullable=True)
|
||||
next_action = Column(String(100), nullable=True)
|
||||
source = Column(String(100), default="daily_pipeline")
|
||||
metadata_ = Column("metadata", JSON, default=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"batch_id": self.batch_id,
|
||||
"company": self.company,
|
||||
"contact_name": self.contact_name,
|
||||
"contact_email": self.contact_email,
|
||||
"channel": self.channel,
|
||||
"subject": self.subject,
|
||||
"body": self.body[:200] + "..." if len(self.body or "") > 200 else self.body,
|
||||
"sector": self.sector,
|
||||
"city": self.city,
|
||||
"fit_score": self.fit_score,
|
||||
"risk_score": self.risk_score,
|
||||
"status": self.status,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"sent_at": self.sent_at.isoformat() if self.sent_at else None,
|
||||
"reply_category": self.reply_category,
|
||||
"next_action": self.next_action,
|
||||
}
|
||||
291
salesflow-saas/backend/app/services/full_os_orchestrator.py
Normal file
291
salesflow-saas/backend/app/services/full_os_orchestrator.py
Normal file
@ -0,0 +1,291 @@
|
||||
"""Dealix Full OS Orchestrator — connects all services into a unified deal lifecycle.
|
||||
|
||||
This is the brain that ties WhatsApp brain + sequences + autopilot + CRM + booking
|
||||
into one autonomous system. Every inbound lead flows through a state machine:
|
||||
|
||||
new_lead → qualify → nurture → meeting_booked → proposal → negotiation → closed_won/lost
|
||||
|
||||
At each stage, the orchestrator:
|
||||
1. Detects what happened (inbound message, reply, booking, payment)
|
||||
2. Classifies intent
|
||||
3. Decides next action
|
||||
4. Executes via the appropriate service (WhatsApp, email, CRM, booking)
|
||||
5. Logs everything
|
||||
6. Moves to next stage or holds for human approval
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger("dealix.full_os")
|
||||
|
||||
|
||||
class DealStage(str, Enum):
|
||||
NEW_LEAD = "new_lead"
|
||||
QUALIFYING = "qualifying"
|
||||
QUALIFIED = "qualified"
|
||||
NURTURING = "nurturing"
|
||||
MEETING_BOOKED = "meeting_booked"
|
||||
MEETING_DONE = "meeting_done"
|
||||
PROPOSAL_SENT = "proposal_sent"
|
||||
NEGOTIATING = "negotiating"
|
||||
PAYMENT_REQUESTED = "payment_requested"
|
||||
PILOT_ACTIVE = "pilot_active"
|
||||
CLOSED_WON = "closed_won"
|
||||
CLOSED_LOST = "closed_lost"
|
||||
OPTED_OUT = "opted_out"
|
||||
|
||||
|
||||
class LeadSource(str, Enum):
|
||||
WHATSAPP_INBOUND = "whatsapp_inbound"
|
||||
WEBSITE_FORM = "website_form"
|
||||
LINKEDIN_REPLY = "linkedin_reply"
|
||||
EMAIL_REPLY = "email_reply"
|
||||
OUTREACH_REPLY = "outreach_reply"
|
||||
REFERRAL = "referral"
|
||||
PARTNER = "partner"
|
||||
MANUAL = "manual"
|
||||
|
||||
|
||||
class ActionType(str, Enum):
|
||||
SEND_WHATSAPP = "send_whatsapp"
|
||||
SEND_EMAIL = "send_email"
|
||||
SEND_SMS = "send_sms"
|
||||
BOOK_MEETING = "book_meeting"
|
||||
SEND_PROPOSAL = "send_proposal"
|
||||
REQUEST_PAYMENT = "request_payment"
|
||||
ENROLL_SEQUENCE = "enroll_sequence"
|
||||
CLASSIFY_REPLY = "classify_reply"
|
||||
ESCALATE_HUMAN = "escalate_human"
|
||||
SYNC_CRM = "sync_crm"
|
||||
LOG_ACTIVITY = "log_activity"
|
||||
ONBOARD_CUSTOMER = "onboard_customer"
|
||||
GENERATE_REPORT = "generate_report"
|
||||
DO_NOTHING = "do_nothing"
|
||||
|
||||
|
||||
STAGE_TRANSITIONS = {
|
||||
DealStage.NEW_LEAD: {
|
||||
"interested": DealStage.QUALIFYING,
|
||||
"ask_price": DealStage.QUALIFYING,
|
||||
"ask_demo": DealStage.MEETING_BOOKED,
|
||||
"ask_details": DealStage.QUALIFYING,
|
||||
"not_now": DealStage.NURTURING,
|
||||
"unsubscribe": DealStage.OPTED_OUT,
|
||||
"default": DealStage.QUALIFYING,
|
||||
},
|
||||
DealStage.QUALIFYING: {
|
||||
"qualified": DealStage.QUALIFIED,
|
||||
"not_qualified": DealStage.NURTURING,
|
||||
"wants_demo": DealStage.MEETING_BOOKED,
|
||||
"unsubscribe": DealStage.OPTED_OUT,
|
||||
"default": DealStage.NURTURING,
|
||||
},
|
||||
DealStage.QUALIFIED: {
|
||||
"meeting_booked": DealStage.MEETING_BOOKED,
|
||||
"sequence_enrolled": DealStage.NURTURING,
|
||||
"default": DealStage.NURTURING,
|
||||
},
|
||||
DealStage.NURTURING: {
|
||||
"replied_positive": DealStage.QUALIFYING,
|
||||
"meeting_booked": DealStage.MEETING_BOOKED,
|
||||
"unsubscribe": DealStage.OPTED_OUT,
|
||||
"default": DealStage.NURTURING,
|
||||
},
|
||||
DealStage.MEETING_BOOKED: {
|
||||
"meeting_done": DealStage.MEETING_DONE,
|
||||
"no_show": DealStage.NURTURING,
|
||||
"cancelled": DealStage.NURTURING,
|
||||
"default": DealStage.MEETING_BOOKED,
|
||||
},
|
||||
DealStage.MEETING_DONE: {
|
||||
"proposal_sent": DealStage.PROPOSAL_SENT,
|
||||
"not_interested": DealStage.CLOSED_LOST,
|
||||
"needs_time": DealStage.NURTURING,
|
||||
"default": DealStage.PROPOSAL_SENT,
|
||||
},
|
||||
DealStage.PROPOSAL_SENT: {
|
||||
"accepted": DealStage.PAYMENT_REQUESTED,
|
||||
"negotiating": DealStage.NEGOTIATING,
|
||||
"rejected": DealStage.CLOSED_LOST,
|
||||
"default": DealStage.NEGOTIATING,
|
||||
},
|
||||
DealStage.NEGOTIATING: {
|
||||
"accepted": DealStage.PAYMENT_REQUESTED,
|
||||
"rejected": DealStage.CLOSED_LOST,
|
||||
"default": DealStage.NEGOTIATING,
|
||||
},
|
||||
DealStage.PAYMENT_REQUESTED: {
|
||||
"paid": DealStage.PILOT_ACTIVE,
|
||||
"declined": DealStage.NEGOTIATING,
|
||||
"default": DealStage.PAYMENT_REQUESTED,
|
||||
},
|
||||
DealStage.PILOT_ACTIVE: {
|
||||
"converted": DealStage.CLOSED_WON,
|
||||
"churned": DealStage.CLOSED_LOST,
|
||||
"default": DealStage.PILOT_ACTIVE,
|
||||
},
|
||||
}
|
||||
|
||||
STAGE_AUTO_ACTIONS = {
|
||||
DealStage.NEW_LEAD: [
|
||||
ActionType.CLASSIFY_REPLY,
|
||||
ActionType.SEND_WHATSAPP,
|
||||
ActionType.LOG_ACTIVITY,
|
||||
],
|
||||
DealStage.QUALIFYING: [
|
||||
ActionType.SEND_WHATSAPP,
|
||||
ActionType.LOG_ACTIVITY,
|
||||
],
|
||||
DealStage.QUALIFIED: [
|
||||
ActionType.BOOK_MEETING,
|
||||
ActionType.ENROLL_SEQUENCE,
|
||||
ActionType.SYNC_CRM,
|
||||
],
|
||||
DealStage.NURTURING: [
|
||||
ActionType.ENROLL_SEQUENCE,
|
||||
],
|
||||
DealStage.MEETING_BOOKED: [
|
||||
ActionType.SEND_WHATSAPP,
|
||||
ActionType.SYNC_CRM,
|
||||
],
|
||||
DealStage.MEETING_DONE: [
|
||||
ActionType.SEND_PROPOSAL,
|
||||
ActionType.LOG_ACTIVITY,
|
||||
],
|
||||
DealStage.PROPOSAL_SENT: [
|
||||
ActionType.LOG_ACTIVITY,
|
||||
],
|
||||
DealStage.NEGOTIATING: [
|
||||
ActionType.CLASSIFY_REPLY,
|
||||
ActionType.ESCALATE_HUMAN,
|
||||
],
|
||||
DealStage.PAYMENT_REQUESTED: [
|
||||
ActionType.SEND_WHATSAPP,
|
||||
ActionType.LOG_ACTIVITY,
|
||||
],
|
||||
DealStage.PILOT_ACTIVE: [
|
||||
ActionType.GENERATE_REPORT,
|
||||
ActionType.ONBOARD_CUSTOMER,
|
||||
],
|
||||
DealStage.CLOSED_WON: [
|
||||
ActionType.SYNC_CRM,
|
||||
ActionType.ONBOARD_CUSTOMER,
|
||||
ActionType.GENERATE_REPORT,
|
||||
],
|
||||
DealStage.OPTED_OUT: [
|
||||
ActionType.DO_NOTHING,
|
||||
],
|
||||
}
|
||||
|
||||
QUALIFICATION_QUESTIONS_AR = [
|
||||
"كم lead تقريباً تستقبلون شهرياً؟",
|
||||
"وش أكبر تحدي عندكم في متابعة الاستفسارات؟",
|
||||
"هل عندكم فريق مبيعات يتابع الـ leads حالياً؟",
|
||||
"كم ميزانيتكم الشهرية لأدوات المبيعات/التسويق؟",
|
||||
"متى تبون تبدون لو الحل مناسب؟",
|
||||
]
|
||||
|
||||
STAGE_MESSAGES_AR = {
|
||||
DealStage.NEW_LEAD: "أهلاً وسهلاً! 👋 أنا مساعد Dealix الذكي. كيف أقدر أساعدك اليوم؟",
|
||||
DealStage.QUALIFYING: "ممتاز! عشان أفهم احتياجكم بشكل أفضل — {question}",
|
||||
DealStage.QUALIFIED: "بناءً على اللي ذكرته، Dealix يقدر يساعدكم. تبي نحجز 20 دقيقة demo؟\ncalendly.com/sami-assiri11/dealix-demo",
|
||||
DealStage.NURTURING: "مرحبا مرة ثانية! عندنا تحديثات جديدة في Dealix ممكن تفيدكم. تبي أشرح؟",
|
||||
DealStage.MEETING_BOOKED: "تم الحجز! 🎯 بنرسل لك تذكير قبل الموعد. لو تحتاج تغيّر الوقت قلّي.",
|
||||
DealStage.MEETING_DONE: "شكراً على وقتك اليوم! بناءً على اللي ناقشناه، جهّزت لك عرض pilot مخصص.",
|
||||
DealStage.PROPOSAL_SENT: "أرسلت لك تفاصيل العرض. أي سؤال أنا موجود.",
|
||||
DealStage.NEGOTIATING: "فاهم. خلّني أشوف وش أقدر أسوي عشان نوصل لاتفاق يناسب الطرفين.",
|
||||
DealStage.PAYMENT_REQUESTED: "العرض جاهز! الدفع:\n- تحويل بنكي\n- STC Pay\nبعد التأكيد نبدأ الإعداد خلال 4 ساعات.",
|
||||
DealStage.PILOT_ACTIVE: "يوم {day} من الـ pilot! هذا تقرير اليوم:\n{report}",
|
||||
DealStage.CLOSED_WON: "مبروك! 🎉 أهلاً بكم في Dealix. بنبدأ الإعداد الكامل.",
|
||||
DealStage.OPTED_OUT: "تم. لن نتواصل مرة ثانية. شكراً على وقتكم.",
|
||||
}
|
||||
|
||||
|
||||
class OrchestratorEvent(BaseModel):
|
||||
lead_id: str = ""
|
||||
phone: str = ""
|
||||
email: str = ""
|
||||
company: str = ""
|
||||
sector: str = ""
|
||||
source: str = "whatsapp_inbound"
|
||||
message: str = ""
|
||||
current_stage: str = "new_lead"
|
||||
event_type: str = "inbound_message"
|
||||
|
||||
|
||||
class FullOSOrchestrator:
|
||||
"""Central brain that processes events and drives the deal lifecycle."""
|
||||
|
||||
def process_event(self, event: OrchestratorEvent) -> Dict[str, Any]:
|
||||
current = DealStage(event.current_stage) if event.current_stage else DealStage.NEW_LEAD
|
||||
|
||||
intent = self._classify_intent(event.message)
|
||||
|
||||
transitions = STAGE_TRANSITIONS.get(current, {})
|
||||
next_stage = transitions.get(intent, transitions.get("default", current))
|
||||
|
||||
actions = STAGE_AUTO_ACTIONS.get(next_stage, [])
|
||||
|
||||
response_message = self._get_stage_message(next_stage, event)
|
||||
|
||||
human_required = next_stage in (
|
||||
DealStage.NEGOTIATING,
|
||||
DealStage.PAYMENT_REQUESTED,
|
||||
DealStage.PILOT_ACTIVE,
|
||||
)
|
||||
|
||||
return {
|
||||
"lead_id": event.lead_id or str(uuid4())[:8],
|
||||
"previous_stage": current.value,
|
||||
"intent_detected": intent,
|
||||
"new_stage": next_stage.value,
|
||||
"actions": [a.value for a in actions],
|
||||
"response_message_ar": response_message,
|
||||
"human_approval_required": human_required,
|
||||
"auto_send_allowed": not human_required and next_stage != DealStage.OPTED_OUT,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"qualification_question": self._get_next_question(next_stage, event),
|
||||
}
|
||||
|
||||
def _classify_intent(self, message: str) -> str:
|
||||
if not message:
|
||||
return "default"
|
||||
text = message.lower()
|
||||
|
||||
if any(w in text for w in ["إيقاف", "stop", "unsubscribe", "لا تتواصل"]):
|
||||
return "unsubscribe"
|
||||
if any(w in text for w in ["مهتم", "interested", "أبي أجرب", "نجرب", "تمام", "نعم"]):
|
||||
return "interested"
|
||||
if any(w in text for w in ["demo", "ديمو", "أوريني", "عرض", "شرح"]):
|
||||
return "wants_demo"
|
||||
if any(w in text for w in ["كم السعر", "كم التكلفة", "pricing", "أسعار"]):
|
||||
return "ask_price"
|
||||
if any(w in text for w in ["تفاصيل", "details", "أكثر"]):
|
||||
return "ask_details"
|
||||
if any(w in text for w in ["لاحقاً", "later", "مو الحين", "بعدين"]):
|
||||
return "not_now"
|
||||
if any(w in text for w in ["دفعت", "حوّلت", "paid", "تم الدفع"]):
|
||||
return "paid"
|
||||
if any(w in text for w in ["شراكة", "partner", "وكالة"]):
|
||||
return "interested"
|
||||
return "default"
|
||||
|
||||
def _get_stage_message(self, stage: DealStage, event: OrchestratorEvent) -> str:
|
||||
template = STAGE_MESSAGES_AR.get(stage, "")
|
||||
return template.replace("{company}", event.company or "").replace("{question}", "").replace("{day}", "1").replace("{report}", "")
|
||||
|
||||
def _get_next_question(self, stage: DealStage, event: OrchestratorEvent) -> Optional[str]:
|
||||
if stage == DealStage.QUALIFYING:
|
||||
return QUALIFICATION_QUESTIONS_AR[0]
|
||||
return None
|
||||
|
||||
|
||||
orchestrator = FullOSOrchestrator()
|
||||
205
salesflow-saas/backend/app/services/whatsapp_multi_provider.py
Normal file
205
salesflow-saas/backend/app/services/whatsapp_multi_provider.py
Normal file
@ -0,0 +1,205 @@
|
||||
"""Multi-Provider WhatsApp Send — tries multiple providers with automatic fallback.
|
||||
|
||||
Supported providers (in priority order):
|
||||
1. Green API — free dev tier, simple REST, green-api.com
|
||||
2. Ultramsg — simple REST, ultramsg.com
|
||||
3. WhatsApp Cloud API (official Meta) — needs Business verification
|
||||
4. Fonnte — ultra-cheap, fonnte.com
|
||||
|
||||
The system tries each provider in order until one succeeds.
|
||||
All providers normalize Saudi phone numbers to 966XXXXXXXXX.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger("dealix.whatsapp_multi")
|
||||
|
||||
|
||||
def _format_saudi_phone(phone: str) -> str:
|
||||
phone = phone.strip().replace(" ", "").replace("-", "").replace("+", "")
|
||||
if phone.startswith("05"):
|
||||
phone = "966" + phone[1:]
|
||||
elif phone.startswith("00966"):
|
||||
phone = phone[2:]
|
||||
elif phone.startswith("966"):
|
||||
pass
|
||||
elif phone.startswith("5") and len(phone) == 9:
|
||||
phone = "966" + phone
|
||||
return phone
|
||||
|
||||
|
||||
async def send_via_greenapi(phone: str, message: str) -> Dict[str, Any]:
|
||||
"""Green API — free dev tier, green-api.com.
|
||||
|
||||
Env vars: GREEN_API_INSTANCE_ID, GREEN_API_TOKEN
|
||||
"""
|
||||
instance = os.getenv("GREEN_API_INSTANCE_ID", "")
|
||||
token = os.getenv("GREEN_API_TOKEN", "")
|
||||
if not instance or not token:
|
||||
return {"provider": "greenapi", "status": "not_configured"}
|
||||
|
||||
formatted = _format_saudi_phone(phone)
|
||||
url = f"https://api.green-api.com/waInstance{instance}/sendMessage/{token}"
|
||||
payload = {
|
||||
"chatId": f"{formatted}@c.us",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(url, json=payload)
|
||||
data = resp.json()
|
||||
if resp.status_code == 200 and data.get("idMessage"):
|
||||
logger.info("GreenAPI sent to %s: %s", formatted, data.get("idMessage"))
|
||||
return {"provider": "greenapi", "status": "sent", "message_id": data.get("idMessage")}
|
||||
return {"provider": "greenapi", "status": "failed", "detail": data}
|
||||
except Exception as exc:
|
||||
return {"provider": "greenapi", "status": "error", "detail": str(exc)[:200]}
|
||||
|
||||
|
||||
async def send_via_ultramsg(phone: str, message: str) -> Dict[str, Any]:
|
||||
"""Ultramsg — simple REST API, ultramsg.com.
|
||||
|
||||
Env vars: ULTRAMSG_INSTANCE_ID, ULTRAMSG_TOKEN
|
||||
"""
|
||||
instance = os.getenv("ULTRAMSG_INSTANCE_ID", "")
|
||||
token = os.getenv("ULTRAMSG_TOKEN", "")
|
||||
if not instance or not token:
|
||||
return {"provider": "ultramsg", "status": "not_configured"}
|
||||
|
||||
formatted = _format_saudi_phone(phone)
|
||||
url = f"https://api.ultramsg.com/{instance}/messages/chat"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(url, data={
|
||||
"token": token,
|
||||
"to": formatted,
|
||||
"body": message,
|
||||
})
|
||||
data = resp.json()
|
||||
if "error" not in str(data).lower() or data.get("sent") == "true":
|
||||
logger.info("Ultramsg sent to %s", formatted)
|
||||
return {"provider": "ultramsg", "status": "sent", "result": data}
|
||||
return {"provider": "ultramsg", "status": "failed", "detail": data}
|
||||
except Exception as exc:
|
||||
return {"provider": "ultramsg", "status": "error", "detail": str(exc)[:200]}
|
||||
|
||||
|
||||
async def send_via_fonnte(phone: str, message: str) -> Dict[str, Any]:
|
||||
"""Fonnte — ultra-cheap, fonnte.com.
|
||||
|
||||
Env vars: FONNTE_TOKEN
|
||||
"""
|
||||
token = os.getenv("FONNTE_TOKEN", "")
|
||||
if not token:
|
||||
return {"provider": "fonnte", "status": "not_configured"}
|
||||
|
||||
formatted = _format_saudi_phone(phone)
|
||||
url = "https://api.fonnte.com/send"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(url, headers={"Authorization": token}, json={
|
||||
"target": formatted,
|
||||
"message": message,
|
||||
})
|
||||
data = resp.json()
|
||||
if data.get("status"):
|
||||
logger.info("Fonnte sent to %s", formatted)
|
||||
return {"provider": "fonnte", "status": "sent", "result": data}
|
||||
return {"provider": "fonnte", "status": "failed", "detail": data}
|
||||
except Exception as exc:
|
||||
return {"provider": "fonnte", "status": "error", "detail": str(exc)[:200]}
|
||||
|
||||
|
||||
async def send_via_meta_cloud(phone: str, message: str) -> Dict[str, Any]:
|
||||
"""Official WhatsApp Cloud API (Meta).
|
||||
|
||||
Env vars: WHATSAPP_API_TOKEN, WHATSAPP_PHONE_NUMBER_ID
|
||||
"""
|
||||
token = os.getenv("WHATSAPP_API_TOKEN", "")
|
||||
phone_id = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "")
|
||||
if not token or not phone_id:
|
||||
return {"provider": "meta_cloud", "status": "not_configured"}
|
||||
|
||||
formatted = _format_saudi_phone(phone)
|
||||
url = f"https://graph.facebook.com/v21.0/{phone_id}/messages"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(url, headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}, json={
|
||||
"messaging_product": "whatsapp",
|
||||
"to": formatted,
|
||||
"type": "text",
|
||||
"text": {"body": message},
|
||||
})
|
||||
data = resp.json()
|
||||
if resp.status_code == 200 and data.get("messages"):
|
||||
msg_id = data["messages"][0].get("id", "")
|
||||
logger.info("MetaCloud sent to %s: %s", formatted, msg_id)
|
||||
return {"provider": "meta_cloud", "status": "sent", "message_id": msg_id}
|
||||
return {"provider": "meta_cloud", "status": "failed", "detail": data}
|
||||
except Exception as exc:
|
||||
return {"provider": "meta_cloud", "status": "error", "detail": str(exc)[:200]}
|
||||
|
||||
|
||||
PROVIDER_CHAIN = [
|
||||
("greenapi", send_via_greenapi),
|
||||
("ultramsg", send_via_ultramsg),
|
||||
("fonnte", send_via_fonnte),
|
||||
("meta_cloud", send_via_meta_cloud),
|
||||
]
|
||||
|
||||
|
||||
async def send_whatsapp_smart(phone: str, message: str) -> Dict[str, Any]:
|
||||
"""Try all configured providers in order until one succeeds."""
|
||||
attempts = []
|
||||
for name, send_fn in PROVIDER_CHAIN:
|
||||
result = await send_fn(phone, message)
|
||||
attempts.append(result)
|
||||
if result.get("status") == "sent":
|
||||
return {
|
||||
"sent": True,
|
||||
"provider_used": name,
|
||||
"result": result,
|
||||
"attempts": len(attempts),
|
||||
}
|
||||
|
||||
return {
|
||||
"sent": False,
|
||||
"provider_used": None,
|
||||
"error": "all_providers_failed_or_not_configured",
|
||||
"attempts": attempts,
|
||||
}
|
||||
|
||||
|
||||
async def check_providers() -> Dict[str, Any]:
|
||||
"""Check which providers are configured (without sending)."""
|
||||
status = {}
|
||||
for name, _ in PROVIDER_CHAIN:
|
||||
if name == "greenapi":
|
||||
configured = bool(os.getenv("GREEN_API_INSTANCE_ID") and os.getenv("GREEN_API_TOKEN"))
|
||||
elif name == "ultramsg":
|
||||
configured = bool(os.getenv("ULTRAMSG_INSTANCE_ID") and os.getenv("ULTRAMSG_TOKEN"))
|
||||
elif name == "fonnte":
|
||||
configured = bool(os.getenv("FONNTE_TOKEN"))
|
||||
elif name == "meta_cloud":
|
||||
configured = bool(os.getenv("WHATSAPP_API_TOKEN") and os.getenv("WHATSAPP_PHONE_NUMBER_ID"))
|
||||
else:
|
||||
configured = False
|
||||
status[name] = configured
|
||||
return {
|
||||
"providers": status,
|
||||
"any_configured": any(status.values()),
|
||||
"recommended": "greenapi" if not any(status.values()) else next((k for k, v in status.items() if v), None),
|
||||
}
|
||||
@ -2,8 +2,7 @@
|
||||
dockerfilePath = "Dockerfile"
|
||||
|
||||
[deploy]
|
||||
healthcheckPath = "/api/v1/health"
|
||||
healthcheckPath = "/health"
|
||||
healthcheckTimeout = 120
|
||||
startCommand = "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000} --workers 2"
|
||||
restartPolicyType = "ON_FAILURE"
|
||||
restartPolicyMaxRetries = 3
|
||||
|
||||
@ -83,3 +83,4 @@ qrcode==8.0
|
||||
Pillow==11.0.0
|
||||
xmltodict==0.14.2
|
||||
email-validator>=2.1.0
|
||||
aiosqlite>=0.20.0
|
||||
|
||||
16
salesflow-saas/backend/start.sh
Executable file
16
salesflow-saas/backend/start.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
echo "[dealix] PORT=$PORT"
|
||||
echo "[dealix] Testing imports..."
|
||||
python3 -c "
|
||||
try:
|
||||
from app.main import app
|
||||
print(f'[dealix] OK — {len(app.routes)} routes')
|
||||
except Exception as e:
|
||||
print(f'[dealix] IMPORT FAILED: {e}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
exit(1)
|
||||
" 2>&1
|
||||
echo "[dealix] Starting uvicorn on port ${PORT:-8000}..."
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8000}" --workers 1 --log-level info
|
||||
175
salesflow-saas/backend/tests/test_automation_system.py
Normal file
175
salesflow-saas/backend/tests/test_automation_system.py
Normal file
@ -0,0 +1,175 @@
|
||||
"""Tests for the full automation outreach system — drafts, pipeline, followups."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi import FastAPI
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
app = FastAPI()
|
||||
from app.api.v1.automation import router as auto_router
|
||||
app.include_router(auto_router)
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_email_generate(client):
|
||||
resp = client.post("/automation/email/generate", json={
|
||||
"company": "Foodics",
|
||||
"sector": "saas",
|
||||
"city": "الرياض",
|
||||
"contact_name": "أحمد",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "subject_ar" in data
|
||||
assert "body_ar" in data
|
||||
assert "followup_day_2" in data
|
||||
assert "followup_day_5" in data
|
||||
assert "call_script_ar" in data
|
||||
assert "linkedin_manual_message" in data
|
||||
assert data["opt_out_included"] is True
|
||||
assert data["word_count"] > 0
|
||||
assert "Foodics" in data["subject_ar"]
|
||||
|
||||
|
||||
def test_email_generate_with_signals(client):
|
||||
resp = client.post("/automation/email/generate", json={
|
||||
"company": "TestCo",
|
||||
"sector": "real_estate",
|
||||
"signals": ["hubspot"],
|
||||
})
|
||||
data = resp.json()
|
||||
assert "HubSpot" in data["body_ar"]
|
||||
|
||||
|
||||
def test_compliance_check_allowed(client):
|
||||
resp = client.post("/automation/compliance/check", json={
|
||||
"email": "ahmed@company.sa",
|
||||
"company": "TestCo",
|
||||
"source": "linkedin",
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["allowed"] is True
|
||||
assert data["reason"] == "compliant"
|
||||
|
||||
|
||||
def test_compliance_check_opt_out(client):
|
||||
resp = client.post("/automation/compliance/check", json={
|
||||
"email": "ahmed@company.sa",
|
||||
"opt_out": True,
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["allowed"] is False
|
||||
assert data["reason"] == "opt_out"
|
||||
|
||||
|
||||
def test_compliance_check_bounced(client):
|
||||
resp = client.post("/automation/compliance/check", json={
|
||||
"email": "bad@company.sa",
|
||||
"bounced_before": True,
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["allowed"] is False
|
||||
assert data["reason"] == "bounced_before"
|
||||
|
||||
|
||||
def test_compliance_check_high_risk(client):
|
||||
resp = client.post("/automation/compliance/check", json={
|
||||
"email": "ceo@big.sa",
|
||||
"risk_score": 80,
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["allowed"] is False
|
||||
assert data["reason"] == "high_risk"
|
||||
|
||||
|
||||
def test_compliance_check_personal_email(client):
|
||||
resp = client.post("/automation/compliance/check", json={
|
||||
"email": "ahmed@gmail.com",
|
||||
"source": "linkedin",
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["allowed"] is True
|
||||
assert "personal_email" in data["reason"]
|
||||
|
||||
|
||||
def test_compliance_check_no_source(client):
|
||||
resp = client.post("/automation/compliance/check", json={
|
||||
"email": "ahmed@company.sa",
|
||||
"source": "",
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["allowed"] is False
|
||||
assert data["reason"] == "no_source"
|
||||
|
||||
|
||||
def test_reply_classify_interested(client):
|
||||
resp = client.post("/automation/reply/classify", json={
|
||||
"reply_text": "مهتم جداً، أبي أجرب",
|
||||
"company": "TestCo",
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["category"] == "interested"
|
||||
assert data["auto_reply_allowed"] is True
|
||||
assert "calendly" in data["suggested_response"].lower() or "demo" in data["suggested_response"].lower() or "20 دقيقة" in data["suggested_response"]
|
||||
|
||||
|
||||
def test_reply_classify_price(client):
|
||||
resp = client.post("/automation/reply/classify", json={
|
||||
"reply_text": "كم السعر؟",
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["category"] == "ask_price"
|
||||
assert "499" in data["suggested_response"]
|
||||
|
||||
|
||||
def test_reply_classify_unsubscribe(client):
|
||||
resp = client.post("/automation/reply/classify", json={
|
||||
"reply_text": "إيقاف لا تتواصل معي",
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["category"] == "unsubscribe"
|
||||
assert data["auto_reply_allowed"] is False
|
||||
|
||||
|
||||
def test_reply_classify_crm(client):
|
||||
resp = client.post("/automation/reply/classify", json={
|
||||
"reply_text": "عندنا CRM وما نحتاج نظام ثاني",
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["category"] == "already_has_crm"
|
||||
assert "طبقة" in data["suggested_response"] or "CRM" in data["suggested_response"]
|
||||
|
||||
|
||||
def test_daily_targeting_generate(client):
|
||||
resp = client.post("/automation/daily-targeting/generate", json={
|
||||
"sectors": ["real_estate", "construction"],
|
||||
"cities": ["الرياض"],
|
||||
"daily_target_count": 5,
|
||||
})
|
||||
data = resp.json()
|
||||
assert data["total_generated"] > 0
|
||||
assert len(data["targets"]) <= 5
|
||||
assert data["targets"][0]["sector"] in ("real_estate", "construction")
|
||||
assert data["approval_required"] is True
|
||||
|
||||
|
||||
def test_sector_pain_map_coverage(client):
|
||||
"""Verify all 9 sectors produce valid emails."""
|
||||
sectors = ["real_estate", "construction", "hospitality", "food_beverage",
|
||||
"logistics", "agency", "saas", "healthcare", "education"]
|
||||
for sector in sectors:
|
||||
resp = client.post("/automation/email/generate", json={
|
||||
"company": f"Test_{sector}",
|
||||
"sector": sector,
|
||||
})
|
||||
assert resp.status_code == 200, f"Failed for sector: {sector}"
|
||||
data = resp.json()
|
||||
assert len(data["body_ar"]) > 50, f"Empty body for {sector}"
|
||||
assert "إيقاف" in data["body_ar"], f"Missing opt-out for {sector}"
|
||||
236
salesflow-saas/docs/ops/10_CUSTOMERS_PER_WEEK_MACHINE.md
Normal file
236
salesflow-saas/docs/ops/10_CUSTOMERS_PER_WEEK_MACHINE.md
Normal file
@ -0,0 +1,236 @@
|
||||
# Dealix — ماكينة 10 زبائن/أسبوع
|
||||
|
||||
## الحساب الدقيق
|
||||
|
||||
```
|
||||
10 زبائن مدفوعين/أسبوع
|
||||
= 30 demo/أسبوع (33% demo→paid)
|
||||
= 100 رد إيجابي/أسبوع (30% reply→demo)
|
||||
= 500 محادثة/أسبوع (20% touch→reply)
|
||||
= ~70 محادثة/يوم
|
||||
```
|
||||
|
||||
## مصادر الـ 70 محادثة يومياً
|
||||
|
||||
| القناة | يومياً | أسبوعياً | النوع |
|
||||
|--------|--------|----------|-------|
|
||||
| LinkedIn DM (مباشر) | 10 | 70 | cold/warm |
|
||||
| Cold Email (مخصص) | 20 | 140 | cold |
|
||||
| WhatsApp warm | 5 | 35 | warm |
|
||||
| Landing page inbound | 5 | 35 | inbound |
|
||||
| Partner referrals | 5 | 35 | warm |
|
||||
| X/LinkedIn content inbound | 5 | 35 | inbound |
|
||||
| Community/group posts | 5 | 35 | warm |
|
||||
| Referral asks | 5 | 35 | warm |
|
||||
| Re-engagement (cold leads) | 10 | 70 | reactivation |
|
||||
| **المجموع** | **70** | **490** | |
|
||||
|
||||
## خطة الأسبوع الأول (واقعية)
|
||||
|
||||
| اليوم | LinkedIn | Email | WhatsApp | Content | Referral | المجموع |
|
||||
|-------|----------|-------|----------|---------|----------|---------|
|
||||
| السبت | 10 | 20 | 5 | 2 | 3 | 40 |
|
||||
| الأحد | 10 | 20 | 5 | 1 | 3 | 39 |
|
||||
| الإثنين | 10 | 20 | 5 | 2 | 3 | 40 |
|
||||
| الثلاثاء | 10 | 20 | 5 | 1 | 3 | 39 |
|
||||
| الأربعاء | 10 | 20 | 5 | 2 | 3 | 40 |
|
||||
| الخميس | 10 | 20 | 5 | 1 | 3 | 39 |
|
||||
| الجمعة | 5 | 0 | 5 | 2 | 5 | 17 |
|
||||
| **المجموع** | **65** | **120** | **35** | **11** | **23** | **254** |
|
||||
|
||||
Week 1 = 254 touch. Week 2+ = 490+/week مع content inbound يكبر.
|
||||
|
||||
---
|
||||
|
||||
## القطاعات المستهدفة (6 segments)
|
||||
|
||||
### Segment 1: SaaS سعودي (أعلى fit)
|
||||
- **ICP:** شركة SaaS 20-200 موظف، عندها leads من موقع/إعلانات
|
||||
- **المشكلة:** رد بطيء، leads تبرد، فريق مبيعات صغير
|
||||
- **العرض:** pilot 7 أيام مجاني → 990 ريال/شهر
|
||||
- **أمثلة:** Foodics, Salla, Lean, Lucidya, Rewaa, Qoyod, Maqsam
|
||||
|
||||
### Segment 2: وكالات تسويق (أعلى volume)
|
||||
- **ICP:** وكالة 5-50 موظف، عندها عملاء B2B
|
||||
- **المشكلة:** عملاءهم يجيبون leads بالإعلانات لكن المتابعة ضعيفة
|
||||
- **العرض:** شراكة setup + retainer (3,000-15,000 لكل عميل)
|
||||
- **أمثلة:** Peak Content, Digital8, Brand Lounge, Wavy, Bold
|
||||
|
||||
### Segment 3: عقارات (أعلى urgency)
|
||||
- **ICP:** شركة عقارية أو مطوّر، leads كثيرة من إعلانات
|
||||
- **المشكلة:** 500+ lead/شهر، 30% فقط يُتابع
|
||||
- **العرض:** pilot على 50 lead → يثبت ROI فوري
|
||||
- **أمثلة:** Aqar, Dar Global, Roshn
|
||||
|
||||
### Segment 4: تجارة إلكترونية (volume)
|
||||
- **ICP:** متجر إلكتروني متوسط، واتساب كقناة رئيسية
|
||||
- **المشكلة:** رسائل واتساب كثيرة، رد يدوي بطيء
|
||||
- **العرض:** رد تلقائي + تأهيل + حجز
|
||||
- **أمثلة:** Jarir online, Dokkan Afkar, Haraj sellers
|
||||
|
||||
### Segment 5: خدمات B2B (high LTV)
|
||||
- **ICP:** شركة خدمات (استشارات، محاماة، محاسبة)
|
||||
- **المشكلة:** leads من الموقع تضيع بسبب عدم المتابعة
|
||||
- **العرض:** pilot مع تأهيل + حجز مواعيد
|
||||
- **أمثلة:** Big 4 partners, law firms, consulting boutiques
|
||||
|
||||
### Segment 6: مطاعم/F&B (quick win)
|
||||
- **ICP:** سلسلة 5+ فروع، طلبات واتساب/إنستقرام
|
||||
- **المشكلة:** طلبات B2B (تموين، حفلات) تضيع
|
||||
- **العرض:** رد تلقائي على طلبات التموين
|
||||
- **أمثلة:** Albaik (B2B), Herfy catering, Floward corporate
|
||||
|
||||
---
|
||||
|
||||
## الرسائل حسب القطاع
|
||||
|
||||
### SaaS — LinkedIn DM
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
شفت إن [الشركة] تستخدمون [HubSpot/WhatsApp widget/forms]
|
||||
— يعني عندكم leads تجي من الموقع.
|
||||
|
||||
سؤال سريع: كم lead تخسرونه شهرياً بسبب تأخر الرد؟
|
||||
|
||||
ديلكس يرد بالعربي خلال 45 ثانية، يؤهل العميل، ويحجز
|
||||
الموعد تلقائياً. بدون ما تغيّرون CRM.
|
||||
|
||||
أوريك تجربة 20 دقيقة على سيناريو من شغلكم؟
|
||||
calendly.com/sami-assiri11/dealix-demo
|
||||
```
|
||||
|
||||
### وكالات — LinkedIn DM
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
أطلقنا Dealix — طبقة AI بالعربي ترد على leads عملاء الوكالات
|
||||
وتأهلهم وتحجز لهم مواعيد.
|
||||
|
||||
الفكرة للوكالة: تبيعونها كـ setup + retainer فوق حملاتكم
|
||||
الحالية. العميل يشوف ROI أسرع، وأنتم تزيدون MRR.
|
||||
|
||||
أبي أجرب مع أول عميل عندكم كـ pilot مجاني.
|
||||
20 دقيقة أشرح الآلية؟
|
||||
calendly.com/sami-assiri11/dealix-demo
|
||||
```
|
||||
|
||||
### عقارات — WhatsApp
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
أشتغل على Dealix — يرد على leads العقار بالعربي
|
||||
خلال 45 ثانية، يسأل عن الميزانية والموقع المفضل
|
||||
والمساحة، ويحجز موعد معاينة تلقائي.
|
||||
|
||||
عندكم leads من إعلانات تبغون تجربون عليها أسبوع؟
|
||||
بدون التزام.
|
||||
```
|
||||
|
||||
### تجارة إلكترونية — Email
|
||||
```
|
||||
الموضوع: رد تلقائي بالعربي على رسائل واتساب متجركم
|
||||
|
||||
مرحباً [الاسم]،
|
||||
|
||||
Dealix يساعد المتاجر الإلكترونية ترد على استفسارات
|
||||
واتساب بالعربي تلقائياً — يجاوب عن المنتج، يأكد
|
||||
التوفر، ويوجّه العميل للشراء.
|
||||
|
||||
أقدر أوريك تجربة على 10 محادثات من متجركم.
|
||||
هل يناسبك هذا الأسبوع؟
|
||||
|
||||
إذا ما كان مناسبًا، أرسل "إيقاف" ولن أتابع.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## الأدوات المطلوبة
|
||||
|
||||
| الأداة | الغرض | التكلفة | الأولوية |
|
||||
|--------|--------|---------|---------|
|
||||
| LinkedIn Sales Nav | بحث + 50 InMail/شهر | $80/شهر | P0 |
|
||||
| Instantly.ai أو Smartlead | cold email automation | $30/شهر | P0 |
|
||||
| Calendly | حجز مواعيد | مجاني | P0 (موجود) |
|
||||
| WhatsApp Business | رسائل warm | مجاني | P0 |
|
||||
| Resend API | transactional email | مجاني | P0 (موجود) |
|
||||
| Google Sheets | CRM يدوي | مجاني | P0 |
|
||||
| PostHog | tracking | مجاني | P0 (موجود) |
|
||||
| Stripe/Moyasar | دفع | بعد أول demo | P1 |
|
||||
|
||||
**إجمالي: $110/شهر** — أقل من ربع زبون واحد.
|
||||
|
||||
---
|
||||
|
||||
## Tracker Template (Google Sheet)
|
||||
|
||||
| # | الشركة | الشخص | القطاع | القناة | التاريخ | الحالة | الرد | Demo | Pilot | Paid | ملاحظات |
|
||||
|---|--------|-------|--------|--------|---------|--------|------|------|-------|------|---------|
|
||||
| 1 | | | | LinkedIn | | sent | | | | | |
|
||||
| 2 | | | | Email | | sent | | | | | |
|
||||
|
||||
**الحالات:** new → sent → replied → demo_booked → demo_done → pilot_started → pilot_ended → paid → churned
|
||||
|
||||
---
|
||||
|
||||
## KPIs أسبوعية
|
||||
|
||||
| المقياس | الأسبوع 1 | الأسبوع 2 | الأسبوع 3 | الأسبوع 4 | الهدف الشهري |
|
||||
|---------|-----------|-----------|-----------|-----------|-------------|
|
||||
| Touches | 254 | 490 | 490 | 490 | 1,724 |
|
||||
| Replies | 25 | 50 | 70 | 100 | 245 |
|
||||
| Demos | 3 | 8 | 10 | 10 | 31 |
|
||||
| Pilots | 1 | 3 | 5 | 5 | 14 |
|
||||
| Paid | 0 | 1 | 3 | 6 | **10** |
|
||||
| MRR (SAR) | 0 | 990 | 2,970 | 5,940 | **9,900** |
|
||||
|
||||
---
|
||||
|
||||
## Daily Operating Schedule
|
||||
|
||||
```
|
||||
07:00 Wake + check replies from yesterday
|
||||
08:00 LinkedIn: send 10 DMs (from target list)
|
||||
09:00 Email: send 20 cold emails (Instantly/Smartlead)
|
||||
10:00 WhatsApp: 5 warm messages
|
||||
10:30 Demo #1 (if booked)
|
||||
11:30 Demo #2 (if booked)
|
||||
12:00 Follow-ups: reply to all messages
|
||||
13:00 Break
|
||||
14:00 Partner outreach: 5 agency messages
|
||||
15:00 Demo #3 (if booked)
|
||||
16:00 Content: write + publish 1-2 posts
|
||||
17:00 Referral asks: 3-5 warm intros
|
||||
18:00 Scorecard update + tomorrow prep
|
||||
20:00 Reply to late messages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Week 1 Exact Plan
|
||||
|
||||
### Day 1 (السبت)
|
||||
- [ ] Morning: send 10 LinkedIn DMs (SaaS segment)
|
||||
- [ ] Send 20 cold emails (SaaS + agencies)
|
||||
- [ ] 5 WhatsApp to warm contacts
|
||||
- [ ] Publish Founder Launch Post
|
||||
- [ ] 3 referral asks
|
||||
- [ ] Scorecard update
|
||||
|
||||
### Day 2 (الأحد)
|
||||
- [ ] 10 LinkedIn DMs (agencies segment)
|
||||
- [ ] 20 emails (agencies + real estate)
|
||||
- [ ] 5 WhatsApp
|
||||
- [ ] 1 X post
|
||||
- [ ] Handle Day 1 replies
|
||||
- [ ] 3 referral asks
|
||||
|
||||
### Day 3 (الإثنين)
|
||||
- [ ] 10 LinkedIn DMs (real estate + ecomm)
|
||||
- [ ] 20 emails
|
||||
- [ ] 5 WhatsApp
|
||||
- [ ] LinkedIn article/thread
|
||||
- [ ] First demos (if booked)
|
||||
- [ ] 3 referral asks
|
||||
|
||||
### Day 4-7: repeat + handle demos + send pilot offers + request payments
|
||||
212
salesflow-saas/docs/ops/AGENCY_PARTNER_OFFER.md
Normal file
212
salesflow-saas/docs/ops/AGENCY_PARTNER_OFFER.md
Normal file
@ -0,0 +1,212 @@
|
||||
# Dealix — Agency & Partner Offer
|
||||
|
||||
---
|
||||
|
||||
## Who This Is For
|
||||
|
||||
| Partner Type | Profile | Why They Care |
|
||||
|-------------|---------|---------------|
|
||||
| **Performance Agency** | Runs ads for Saudi B2B clients, 5-30 people | Leads go cold → client churns → agency loses MRR |
|
||||
| **Full-Service Digital Agency** | Websites + ads + social, 10-50 people | Needs post-lead solution to retain clients |
|
||||
| **Freelance Digital Marketer** | Solo or 2-3 people, manages multiple clients | Wants recurring revenue beyond project fees |
|
||||
| **CRM/RevOps Consultant** | Implements HubSpot/Zoho/Salesforce | Client doesn't use CRM because no leads flow in properly |
|
||||
| **WhatsApp Marketing Specialist** | Knows Saudi WhatsApp culture | Can't scale manual responses for 10+ clients |
|
||||
| **IT Services Company** | Sells tech solutions to Saudi SMEs | Wants to add SaaS reselling to portfolio |
|
||||
|
||||
---
|
||||
|
||||
## What Partners Can Sell
|
||||
|
||||
### Tier 1: Dealix Response (للشركات الصغيرة)
|
||||
**Client pays partner:** 990 SAR/month
|
||||
**Setup fee:** 3,000 SAR (partner keeps 100%)
|
||||
**Partner MRR share:** 30% = 297 SAR/month
|
||||
|
||||
**Scope:**
|
||||
- AI Arabic responder on WhatsApp or web forms
|
||||
- 5 qualification questions configured per client
|
||||
- Leads classified: hot / warm / cold
|
||||
- Weekly summary report (manual first, auto later)
|
||||
|
||||
**Dealix delivers:** System config + AI response + weekly report
|
||||
**Partner delivers:** Client relationship + billing + communication
|
||||
|
||||
### Tier 2: Dealix Growth (للشركات المتوسطة)
|
||||
**Client pays partner:** 2,490 SAR/month
|
||||
**Setup fee:** 7,000 SAR (partner keeps 100%)
|
||||
**Partner MRR share:** 25% = 622 SAR/month
|
||||
|
||||
**Scope:**
|
||||
Everything in Tier 1, plus:
|
||||
- Calendly/booking integration
|
||||
- CRM sync (HubSpot two-way)
|
||||
- Follow-up sequences (Day +2, +5, +10)
|
||||
- Daily report (not just weekly)
|
||||
|
||||
### Tier 3: Dealix Enterprise (للمؤسسات)
|
||||
**Client pays partner:** Custom (starting 5,000+ SAR/month)
|
||||
**Setup fee:** 15,000+ SAR
|
||||
**Partner MRR share:** 20% (negotiable)
|
||||
|
||||
**Scope:**
|
||||
Everything in Tier 2, plus:
|
||||
- Multi-channel (WhatsApp + Email + Voice)
|
||||
- Executive dashboard
|
||||
- Approval workflows
|
||||
- SLA tracking
|
||||
- Dedicated account manager from Dealix
|
||||
|
||||
---
|
||||
|
||||
## Service-Exchange Models
|
||||
|
||||
### Model A: Referral Only
|
||||
- Partner introduces client
|
||||
- Dealix handles everything (sales, setup, billing)
|
||||
- Partner earns **10% of MRR for 12 months**
|
||||
- No work required from partner after intro
|
||||
- Best for: consultants, mentors, advisors
|
||||
|
||||
### Model B: Co-Sell
|
||||
- Partner sells to client, collects payment
|
||||
- Dealix provides system + support
|
||||
- Partner earns **setup fee (100%) + 20-30% MRR**
|
||||
- Partner handles client relationship
|
||||
- Best for: agencies with existing clients
|
||||
|
||||
### Model C: White-Label (future)
|
||||
- Partner brands Dealix as their own
|
||||
- Full control of client billing
|
||||
- Higher MRR share (40-50%)
|
||||
- Requires Scale plan or above
|
||||
- **Not available now — post first 10 paying partners**
|
||||
|
||||
---
|
||||
|
||||
## What Dealix Delivers (manual-first)
|
||||
|
||||
| Deliverable | Now (Manual) | Later (Automated) |
|
||||
|-------------|-------------|------------------|
|
||||
| Client onboarding | Sami configures in 24h | Self-service portal |
|
||||
| Arabic response templates | Manually tuned per sector | LLM-generated per client |
|
||||
| Qualification questions | Configured per client brief | AI-optimized from conversations |
|
||||
| Daily/weekly reports | Manual email | Auto-generated dashboard |
|
||||
| CRM sync | Manual export + import | Real-time HubSpot sync |
|
||||
| Billing to client | Partner handles | Moyasar recurring (future) |
|
||||
| Partner commission | Manual bank transfer | Automatic monthly payout |
|
||||
| Escalation | WhatsApp to Sami | Ticketing system |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Packages
|
||||
|
||||
### Quick Start (1 day)
|
||||
- 1 channel (WhatsApp or web form)
|
||||
- 5 qualification questions
|
||||
- 1 sector template
|
||||
- Weekly report
|
||||
- **Setup: 3,000 SAR**
|
||||
|
||||
### Standard (3 days)
|
||||
- 2 channels
|
||||
- Custom qualification flow
|
||||
- Calendly integration
|
||||
- CRM sync
|
||||
- Daily report
|
||||
- **Setup: 7,000 SAR**
|
||||
|
||||
### Advanced (5-7 days)
|
||||
- All channels
|
||||
- Multi-department routing
|
||||
- Approval workflows
|
||||
- Executive dashboard
|
||||
- **Setup: 15,000+ SAR**
|
||||
|
||||
---
|
||||
|
||||
## Referral / Partner Workflow
|
||||
|
||||
### Step 1: Partner Applies
|
||||
- Books Calendly call or sends WhatsApp message
|
||||
- Sami evaluates fit (are they in Saudi B2B? do they have clients?)
|
||||
- If yes → simple 1-page partner agreement
|
||||
|
||||
### Step 2: First Client Pilot
|
||||
- Partner introduces their best-fit client
|
||||
- Dealix runs 7-day pilot on client's leads — **free for first client**
|
||||
- Client sees: faster response, more qualified leads, booked meetings
|
||||
|
||||
### Step 3: Client Converts
|
||||
- Client agrees to monthly plan
|
||||
- Partner bills client directly (co-sell) or Dealix bills (referral)
|
||||
- Partner earns according to model A, B, or C
|
||||
|
||||
### Step 4: Scale
|
||||
- Partner adds more clients
|
||||
- Each new client = new setup fee + new MRR
|
||||
- Monthly partner check-in with Sami
|
||||
|
||||
---
|
||||
|
||||
## Stage-1 Partner Motion (Week 1-4)
|
||||
|
||||
### Target: 5 partners signed, 3 with first client in pilot
|
||||
|
||||
**Week 1:** Contact 10 agencies
|
||||
- 5 via LinkedIn DM (message in COMMAND_CENTER.md)
|
||||
- 5 via warm WhatsApp (personal network)
|
||||
- Offer: free pilot for first client
|
||||
|
||||
**Week 2:** Run 3 pilots
|
||||
- Each pilot = 7 days on client's leads
|
||||
- Daily report to partner + client
|
||||
- Measure: response time, qualified leads, booked meetings
|
||||
|
||||
**Week 3:** Convert 2 pilots to paid
|
||||
- Partner earns first setup fee
|
||||
- MRR starts flowing
|
||||
|
||||
**Week 4:** Onboard 2 more partners
|
||||
- Use first partner as reference
|
||||
- Repeat Week 1-2 process
|
||||
|
||||
### First 5 Partner Targets
|
||||
1. **Peak Content** (Riyadh) — content marketing, has B2B clients
|
||||
2. **Digital8** (Riyadh) — full service, performance marketing
|
||||
3. **Wavy** (Saudi) — social media management
|
||||
4. **Hayan** (Saudi) — HubSpot implementation partner
|
||||
5. **Bold Agency** (Riyadh) — performance + creative
|
||||
|
||||
### Partner Outreach Message
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
أنا سامي — مؤسس Dealix. نشتغل على طبقة AI بالعربي ترد
|
||||
على leads عملاء الوكالات وتأهلهم وتحجز مواعيد تلقائياً.
|
||||
|
||||
الفكرة لكم: تبيعونها كـ setup + retainer فوق خدماتكم.
|
||||
العميل يشوف ROI أسرع — وأنتم تزيدون MRR.
|
||||
|
||||
نبدأ بـ pilot مجاني لأول عميل عندكم — 7 أيام بلا تكلفة.
|
||||
|
||||
يناسبك 20 دقيقة هذا الأسبوع؟
|
||||
calendly.com/sami-assiri11/dealix-demo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Revenue Math
|
||||
|
||||
```
|
||||
Per partner (conservative):
|
||||
2 clients/quarter × 990 SAR/month = 1,980 SAR MRR
|
||||
+ 2 × 3,000 SAR setup = 6,000 SAR
|
||||
Annual from 1 partner: 23,760 + 6,000 = 29,760 SAR
|
||||
|
||||
5 partners:
|
||||
= 9,900 SAR MRR + 30,000 SAR setup
|
||||
= ~148,800 SAR/year
|
||||
|
||||
10 partners:
|
||||
= ~297,600 SAR/year
|
||||
```
|
||||
78
salesflow-saas/docs/ops/AGENCY_PARTNER_PLAN.md
Normal file
78
salesflow-saas/docs/ops/AGENCY_PARTNER_PLAN.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Dealix — Agency / Partner Motion Plan
|
||||
|
||||
## Who Are Partners
|
||||
|
||||
| Type | Example | What They Sell | Our Revenue Share |
|
||||
|------|---------|---------------|------------------|
|
||||
| **Referral** | Freelancer, consultant | Intro only | 10% MRR × 12 months |
|
||||
| **Implementation** | HubSpot consultant, RevOps | Setup + config | 100% of setup fee |
|
||||
| **Agency** | Digital agency, performance agency | Setup + monthly service | Setup fee + 20-30% MRR |
|
||||
| **Strategic** | Salla, Zid, Unifonic | Co-sell / bundle | Revenue share negotiated |
|
||||
|
||||
## What Partners Can Sell
|
||||
|
||||
### Package 1: "Dealix Basic" (for agency clients)
|
||||
- AI Arabic responder on WhatsApp/forms
|
||||
- Lead qualification (5 questions)
|
||||
- Daily summary report
|
||||
- **Client pays agency:** 990 SAR/month
|
||||
- **Agency keeps:** 297 SAR/month (30%)
|
||||
- **Setup fee to agency:** 3,000 SAR (agency keeps 100%)
|
||||
|
||||
### Package 2: "Dealix Growth" (for larger clients)
|
||||
- Everything in Basic
|
||||
- Calendly/booking integration
|
||||
- CRM sync (HubSpot)
|
||||
- Follow-up sequences
|
||||
- **Client pays agency:** 2,490 SAR/month
|
||||
- **Agency keeps:** 622 SAR/month (25%)
|
||||
- **Setup fee:** 7,000 SAR (agency keeps 100%)
|
||||
|
||||
## What We Deliver (manual-first)
|
||||
- System configuration for each client
|
||||
- Arabic response templates tuned to client's sector
|
||||
- Daily monitoring first 7 days
|
||||
- Escalation support
|
||||
- Monthly performance review
|
||||
|
||||
## What Is Manual Now vs Automated Later
|
||||
|
||||
| Activity | Now (Manual) | Later (Automated) |
|
||||
|----------|-------------|------------------|
|
||||
| Client onboarding | Sami + Google Sheet | Self-service portal |
|
||||
| Response config | Manually set templates | LLM-generated per client |
|
||||
| Daily report | Manual email | Auto-generated dashboard |
|
||||
| Billing | Bank transfer / STC Pay | Moyasar recurring |
|
||||
| Partner commission | Manual calculation | Automatic payout |
|
||||
|
||||
## Stage-1 Service Exchange (First 3 Partners)
|
||||
|
||||
### Offer to Agency:
|
||||
"أعطيني أول عميل عندك — أسوي له pilot مجاني 7 أيام.
|
||||
لو العميل رضي واشترك، تبدأ تحصّل 30% من MRR + setup fee كامل لك.
|
||||
لو ما رضي — لا التزام."
|
||||
|
||||
### Partner Onboarding Checklist:
|
||||
1. Partner signs simple agreement (1 page)
|
||||
2. Partner introduces first client
|
||||
3. We run 7-day pilot on client's leads
|
||||
4. Client converts → partner earns
|
||||
5. Repeat
|
||||
|
||||
## First 5 Partner Targets
|
||||
1. Peak Content (Riyadh) — content + digital
|
||||
2. Digital8 (Riyadh) — full service digital
|
||||
3. Wavy (Saudi) — social media
|
||||
4. Hayan (Saudi) — HubSpot partner
|
||||
5. Bold Agency (Riyadh) — performance
|
||||
|
||||
## Revenue Math (per partner)
|
||||
|
||||
```
|
||||
1 partner × 3 clients/quarter
|
||||
= 3 × 990 SAR/month = 2,970 SAR MRR per partner
|
||||
+ 3 × 3,000 SAR setup = 9,000 SAR one-time
|
||||
|
||||
5 partners × same rate = 14,850 SAR MRR + 45,000 SAR setup
|
||||
= ~223,800 SAR/year from partner channel alone
|
||||
```
|
||||
215
salesflow-saas/docs/ops/DAILY_REVENUE_MACHINE.md
Normal file
215
salesflow-saas/docs/ops/DAILY_REVENUE_MACHINE.md
Normal file
@ -0,0 +1,215 @@
|
||||
# Dealix — Daily Revenue Machine
|
||||
# ماكينة الإيراد اليومي
|
||||
|
||||
## الهدف: تدفق مالي يومي مستقر
|
||||
|
||||
```
|
||||
الهدف الشهري: 30,000 ريال MRR
|
||||
= 30 عميل × 990 ريال/شهر (Starter)
|
||||
= ~1,000 ريال/يوم إيراد متكرر
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8 قنوات إيراد متوازية
|
||||
|
||||
### القناة 1: LinkedIn Outbound (أعلى جودة)
|
||||
**الأداة:** LinkedIn Sales Navigator ($80/شهر)
|
||||
**الحجم:** 10 رسائل/يوم × 6 أيام = 60/أسبوع
|
||||
**التحويل المتوقع:** 15% رد → 5% demo → 2% paid
|
||||
**الإيراد المتوقع:** 60 × 2% = ~1 عميل/أسبوع = 990 ريال
|
||||
|
||||
**التنفيذ:**
|
||||
1. Sales Navigator → فلتر: Saudi Arabia + 20-200 employees + SaaS/Services/Real Estate
|
||||
2. ابحث عن: VP Sales, Head of Sales, CEO, COO, Growth Lead
|
||||
3. أرسل Connection Request + Note (النص في SAUDI_60_TARGETS.md)
|
||||
4. بعد القبول → رسالة تفصيلية + Calendly
|
||||
5. Follow-up +2/+5/+10 أيام
|
||||
|
||||
---
|
||||
|
||||
### القناة 2: Cold Email (أعلى volume)
|
||||
**الأداة:** Instantly.ai ($30/شهر) أو Smartlead ($39/شهر)
|
||||
**الحجم:** 30-50 إيميل/يوم × 6 أيام = 180-300/أسبوع
|
||||
**التحويل المتوقع:** 3% reply → 1% demo → 0.3% paid
|
||||
**الإيراد المتوقع:** 300 × 0.3% = ~1 عميل/أسبوع = 990 ريال
|
||||
|
||||
**التنفيذ:**
|
||||
1. اجمع إيميلات من:
|
||||
- LinkedIn profiles (اسأل في الرسالة)
|
||||
- Company websites (contact/about pages)
|
||||
- Apollo.io free tier (50 credits/شهر)
|
||||
- Hunter.io (50 searches مجاناً)
|
||||
2. حمّل في Instantly.ai
|
||||
3. Sequence: 3 إيميلات × 3 أيام بينهم
|
||||
4. Subject lines مُختبرة:
|
||||
- "سؤال سريع عن متابعة الـ leads في [الشركة]"
|
||||
- "73% من العملاء السعوديين يتوقعون رد خلال 30 دقيقة"
|
||||
- "[الاسم] — فكرة لـ [الشركة]"
|
||||
5. **مهم:** كل إيميل فيه سطر opt-out: "إذا ما كان مناسبًا، أرسل 'إيقاف'"
|
||||
|
||||
---
|
||||
|
||||
### القناة 3: WhatsApp Warm (أسرع إقفال)
|
||||
**الأداة:** WhatsApp Business (مجاني)
|
||||
**الحجم:** 5-10 رسائل/يوم (warm contacts فقط)
|
||||
**التحويل المتوقع:** 30% reply → 15% demo → 8% paid
|
||||
**الإيراد المتوقع:** 50 × 8% = ~4 عملاء/أسبوع = 3,960 ريال
|
||||
|
||||
**التنفيذ:**
|
||||
- أرسل لـ: أصدقاء founders، زملاء سابقين، معارف من مؤتمرات
|
||||
- الرسالة: Tier 1 من FIRST_REVENUE_GUARANTEE_PLAYBOOK
|
||||
- **هذي أسرع قناة للإيراد — ابدأ فيها أول**
|
||||
|
||||
---
|
||||
|
||||
### القناة 4: Agency Partners (أعلى LTV)
|
||||
**الأداة:** LinkedIn + WhatsApp
|
||||
**الحجم:** 5 وكالات/أسبوع
|
||||
**التحويل المتوقع:** 20% partner → 2 عملاء لكل وكالة
|
||||
**الإيراد المتوقع:** 1 وكالة = 5,000 setup + 990×2 = 6,980 ريال
|
||||
|
||||
**التنفيذ:**
|
||||
1. ابحث: "digital marketing agency Saudi" أو "وكالة تسويق رقمي الرياض"
|
||||
2. أرسل رسالة وكالة من SAUDI_60_TARGETS.md
|
||||
3. العرض: Setup 3,000-5,000 + 20-30% MRR recurring
|
||||
4. أول عميل مجاني كـ proof
|
||||
|
||||
---
|
||||
|
||||
### القناة 5: Content Inbound (مجاني + مركّب)
|
||||
**الأداة:** LinkedIn + X (مجاني)
|
||||
**الحجم:** 1-2 post/يوم
|
||||
**التحويل المتوقع:** 5 inbound leads/أسبوع بعد أسبوعين
|
||||
**الإيراد المتوقع:** 5 × 10% = 0.5 عميل/أسبوع بعد شهر
|
||||
|
||||
**التنفيذ:**
|
||||
- **يوم 1:** "أطلقنا Dealix — مندوب مبيعات AI بالعربي"
|
||||
- **يوم 2:** "73% من العملاء السعوديين يتوقعون رد خلال 30 دقيقة"
|
||||
- **يوم 3:** "كيف تحوّل وكالتك إلى revenue machine"
|
||||
- **يوم 4:** "3 أيام من الإطلاق — وش تعلمنا"
|
||||
- **يوم 5:** "عرض pilot بريال واحد — أول 5 شركات"
|
||||
- **يوم 6:** "ليش الوكالات هي مستقبل SaaS في السعودية"
|
||||
- **يوم 7:** "تقرير: Saudi AI GTM 2026"
|
||||
|
||||
---
|
||||
|
||||
### القناة 6: Community/Group Posts (warm leads)
|
||||
**الأداة:** WhatsApp/Telegram groups + Reddit r/saudiarabia + LinkedIn groups
|
||||
**الحجم:** 3-5 posts/أسبوع في مجموعات مختلفة
|
||||
**التحويل المتوقع:** 2-3 leads/أسبوع
|
||||
|
||||
**التنفيذ:**
|
||||
- ابحث عن مجموعات: "رواد أعمال سعوديين"، "Saudi Startups"، "SaaS MENA"
|
||||
- انشر قيمة أولاً (مشكلة + حل) — لا تبيع مباشرة
|
||||
- مثال: "سؤال للمؤسسين: كيف تتعاملون مع leads تجي بالليل؟ نشتغل على حل AI بالعربي — أبي أسمع تجاربكم"
|
||||
|
||||
---
|
||||
|
||||
### القناة 7: Referral Program (أقل جهد)
|
||||
**الأداة:** رسالة واتساب + tracking يدوي
|
||||
**الحجم:** 5 referral asks/أسبوع
|
||||
**التحويل المتوقع:** 20% يعطون intro
|
||||
|
||||
**التنفيذ:**
|
||||
- بعد كل محادثة إيجابية (حتى لو ما شرى): "تعرف أحد ممكن يستفيد من ديلكس؟ 10% من أول سنة لك"
|
||||
- رسالة Referral:
|
||||
"هل تعرف founder سعودي عنده leads كثيرة ومتابعة ضعيفة؟ أعطني اسمه وأنا أتواصل معه. لو اشترك = 10% من MRR لك لمدة سنة."
|
||||
|
||||
---
|
||||
|
||||
### القناة 8: Paid Ads (بعد إثبات المنتج)
|
||||
**متى تبدأ:** بعد أول 5 عملاء مدفوعين
|
||||
**الأداة:** LinkedIn Ads + Google Ads
|
||||
**الميزانية:** 2,000-5,000 ريال/شهر
|
||||
**المتوقع:** 10-20 lead/أسبوع × 5% = 1 عميل/أسبوع
|
||||
|
||||
---
|
||||
|
||||
## الجدول اليومي للإيراد
|
||||
|
||||
```
|
||||
07:00 قراءة الردود + تصنيف (reply classifier)
|
||||
08:00 LinkedIn: 10 DMs (Sales Navigator)
|
||||
09:00 Email: 30 cold emails (Instantly.ai)
|
||||
10:00 WhatsApp: 5 warm messages
|
||||
10:30 Demo #1
|
||||
11:30 Demo #2
|
||||
12:00 Follow-ups: كل الردود
|
||||
13:00 استراحة
|
||||
14:00 Partner outreach: 3 وكالات
|
||||
15:00 Demo #3
|
||||
16:00 Content: كتابة + نشر 1-2 posts
|
||||
17:00 Community + Referral asks
|
||||
18:00 Scorecard update + تحضير الغد
|
||||
20:00 ردود متأخرة
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conversion Funnel المستهدف
|
||||
|
||||
```
|
||||
Week 1: 254 touch → 25 reply → 3 demo → 1 pilot → 0 paid
|
||||
Week 2: 490 touch → 50 reply → 8 demo → 3 pilot → 1 paid (990 SAR)
|
||||
Week 3: 490 touch → 70 reply → 10 demo → 5 pilot → 3 paid (2,970 SAR)
|
||||
Week 4: 490 touch → 100 reply → 10 demo → 5 pilot → 6 paid (5,940 SAR)
|
||||
Month 2: target 30 paid = 29,700 SAR MRR
|
||||
Month 3: target 60 paid + 3 agencies = 59,400 + 15,000 = 74,400 SAR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## الأدوات المطلوبة (إجمالي: 149 ريال/شهر)
|
||||
|
||||
| الأداة | الغرض | التكلفة |
|
||||
|--------|--------|---------|
|
||||
| LinkedIn Sales Navigator | بحث + InMail | $80 = 300 ريال |
|
||||
| Instantly.ai | cold email automation | $30 = 112 ريال |
|
||||
| Calendly | حجز مواعيد | مجاني |
|
||||
| WhatsApp Business | warm outreach | مجاني |
|
||||
| Google Sheets | CRM يدوي | مجاني |
|
||||
| Loom | تسجيل demo | مجاني |
|
||||
| PostHog | analytics | مجاني |
|
||||
| Hunter.io | email finder | مجاني (50/شهر) |
|
||||
|
||||
---
|
||||
|
||||
## Pricing Matrix
|
||||
|
||||
| الخطة | السعر | المناسب لـ |
|
||||
|-------|-------|-----------|
|
||||
| Pilot | 1 ريال / 7 أيام | إثبات القيمة |
|
||||
| Starter | 990 ريال/شهر | شركات صغيرة (500 lead/شهر) |
|
||||
| Growth | 2,490 ريال/شهر | شركات متوسطة (2000 lead/شهر) |
|
||||
| Enterprise | مخصص | شركات كبرى |
|
||||
| Agency Setup | 3,000-15,000 ريال | وكالات (لكل عميل) |
|
||||
| Agency MRR | 20-30% من MRR | وكالات (متكرر) |
|
||||
|
||||
---
|
||||
|
||||
## Payment Methods (كلها جاهزة)
|
||||
|
||||
| الطريقة | الاستخدام | الحالة |
|
||||
|---------|----------|--------|
|
||||
| تحويل بنكي | أي مبلغ | جاهز |
|
||||
| STC Pay | سريع | جاهز |
|
||||
| فاتورة يدوية | رسمي | جاهز |
|
||||
| Moyasar | تلقائي | ينتظر KYC |
|
||||
|
||||
---
|
||||
|
||||
## Red Flags (توقف عن الاستهداف لو)
|
||||
|
||||
- الشخص قال "إيقاف" أو "لا" صريحة → أوقف فوراً
|
||||
- أرسلت 3 follow-ups بدون رد → أوقف
|
||||
- القطاع ما فيه leads (مثال: حكومي صرف) → تجاوز
|
||||
- الشخص طلب NDA قبل demo → red flag (يبي يسرق الفكرة)
|
||||
|
||||
## Green Flags (ضاعف الجهد لو)
|
||||
|
||||
- "كم السعر؟" → جاهز يشتري
|
||||
- "عندنا 500+ lead/شهر" → high volume = high value
|
||||
- "ممكن تجربة؟" → pilot ready
|
||||
- "عندي وكالة" → partner ready
|
||||
- "أعرف ناس ممكن يستفيدون" → referral goldmine
|
||||
77
salesflow-saas/docs/ops/DEMO_BOOKING_RUNBOOK.md
Normal file
77
salesflow-saas/docs/ops/DEMO_BOOKING_RUNBOOK.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Dealix — Demo Booking Runbook
|
||||
|
||||
## How to Book First Demo
|
||||
1. Prospect responds positively to outreach message
|
||||
2. Send Calendly link: `calendly.com/sami-assiri11/dealix-demo`
|
||||
3. Or propose 2 specific times: "يناسبك بكرة 11 ص أو 3 م؟"
|
||||
4. Calendly sends confirmation to both parties
|
||||
|
||||
## Demo Preparation (30 min before)
|
||||
- [ ] Open prospect's website in a tab
|
||||
- [ ] Run `POST /api/v1/prospect/enrich-tech` with their domain — note what tools they use
|
||||
- [ ] Run `POST /api/v1/prospect/route` with their company/sector — note classification
|
||||
- [ ] Prepare 1 sector-specific pain point to mention
|
||||
- [ ] Have Calendly open for follow-up booking if needed
|
||||
- [ ] Have pilot offer ready (499 SAR / 7 days)
|
||||
|
||||
## Demo Flow (20 minutes)
|
||||
|
||||
**0:00-2:00 — Context Check**
|
||||
- "شكراً على وقتك. قبل ما نبدأ — وش أكبر تحدي عندكم في متابعة الـ leads حالياً؟"
|
||||
- Listen. Take note.
|
||||
|
||||
**2:00-5:00 — Discovery (3 questions)**
|
||||
1. "كم lead تقريباً تستقبلون شهرياً؟"
|
||||
2. "كم منهم يُتابع خلال أول ساعة؟"
|
||||
3. "إيش يصير لـ leads اللي ما يُرد عليهم بسرعة؟"
|
||||
|
||||
**5:00-12:00 — Show Dealix**
|
||||
- Show pricing/plans API response (real, live)
|
||||
- Show enrich-tech on their domain (real detection)
|
||||
- Show prospect/route classification
|
||||
- Show prospect/message with their company name → generated Arabic message
|
||||
- Explain: "هذا يصير تلقائي لكل lead يجيكم"
|
||||
|
||||
**12:00-16:00 — ROI Discussion**
|
||||
- "لو عندكم [X] lead/شهر، و30% تضيع، يعني [Y] فرصة ضائعة"
|
||||
- "Dealix يرد خلال 45 ثانية — كم من هذول ممكن تنحفظ؟"
|
||||
- Do not guarantee numbers. Say "نقيسها معكم خلال 7 أيام"
|
||||
|
||||
**16:00-18:00 — Handle Objections**
|
||||
- Price: "نبدأ بـ pilot 7 أيام بـ 499 ريال — لو ما شفت قيمة، استرداد كامل"
|
||||
- CRM: "Dealix يشتغل فوق CRM الحالي — ما يستبدله"
|
||||
- Arabic: "نبدأ manual + AI-assisted — نراجع أول الردود معك"
|
||||
- Privacy: "متوافق PDPL — بيانات كل عميل منفصلة"
|
||||
|
||||
**18:00-20:00 — Close**
|
||||
- Strong: "نبدأ الـ pilot الأسبوع الجاي؟"
|
||||
- Medium: "أرسل لك تفاصيل الـ pilot وتقرر؟"
|
||||
- Soft: "فكّر فيها — أتابع معك بكرة"
|
||||
|
||||
## After Demo (same day)
|
||||
- [ ] Update tracker: demo_done = yes
|
||||
- [ ] Send follow-up message:
|
||||
```
|
||||
شكراً على وقتك اليوم [الاسم].
|
||||
|
||||
ملخص: نقدر نجرب Dealix على [X] leads عندكم لمدة 7 أيام.
|
||||
الهدف: نقيس سرعة الرد وعدد المؤهلين والمواعيد المحجوزة.
|
||||
|
||||
التكلفة: 499 ريال — مع ضمان استرداد.
|
||||
|
||||
جاهز نبدأ؟
|
||||
```
|
||||
- [ ] If yes → send payment request (bank/STC Pay)
|
||||
- [ ] If "need time" → schedule follow-up in 48h
|
||||
- [ ] If no → ask "هل تعرف أحد ممكن يستفيد؟"
|
||||
|
||||
## What Qualifies a Successful Demo
|
||||
- Prospect asks about pricing or pilot → high intent
|
||||
- Prospect says "أبي أجرب" → convert immediately
|
||||
- Prospect asks technical questions → engaged (answer, don't oversell)
|
||||
- Prospect goes quiet → follow up in 24h, not immediately
|
||||
|
||||
## Recording
|
||||
- Use Loom or OBS to record screen during demo (ask permission first)
|
||||
- Save recording for internal review
|
||||
- Do not share recording externally without consent
|
||||
133
salesflow-saas/docs/ops/FIRST_5_OUTREACH.md
Normal file
133
salesflow-saas/docs/ops/FIRST_5_OUTREACH.md
Normal file
@ -0,0 +1,133 @@
|
||||
# Dealix — First 5 Outreach Messages
|
||||
|
||||
**Channel:** WhatsApp warm (people you know) + LinkedIn (professional contacts)
|
||||
**Rules:** No spam. No cold WhatsApp blast. Personalized. Professional. Opt-out included.
|
||||
|
||||
---
|
||||
|
||||
## Message 1 — SaaS Founder (warm contact)
|
||||
|
||||
**Target:** صديق/معرفة يملك شركة SaaS سعودية
|
||||
**Pain:** leads تجي من الموقع وتبرد بسبب تأخر الرد
|
||||
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
أطلقت Dealix — نظام يرد على leads شركتك بالعربي
|
||||
خلال 45 ثانية، يؤهلهم، ويحجز مواعيد تلقائياً.
|
||||
|
||||
أبحث عن 3 أصدقاء لتجربة pilot 7 أيام بـ 499 ريال.
|
||||
فكرت فيك لأن [الشركة] عندها leads من الموقع.
|
||||
|
||||
مهتم نتكلم 10 دقائق؟
|
||||
```
|
||||
|
||||
**Follow-up 24h:**
|
||||
```
|
||||
سامي هنا — تابع على رسالة أمس عن Dealix.
|
||||
باختصار: pilot 7 أيام على leads عندكم. يناسبك؟
|
||||
```
|
||||
|
||||
**Follow-up 72h:**
|
||||
```
|
||||
آخر متابعة — لو مو الوقت المناسب أفهم تماماً.
|
||||
لو بالمستقبل يناسب، أنا موجود. شكراً [الاسم].
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message 2 — Agency Owner
|
||||
|
||||
**Target:** صاحب وكالة تسويق رقمي
|
||||
**Pain:** عملاءه يجيبون leads بالإعلانات لكن المتابعة ضعيفة
|
||||
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
أشتغل على Dealix — طبقة AI بالعربي ترد على leads عملاء
|
||||
الوكالات وتأهلهم وتحجز مواعيد.
|
||||
|
||||
الفكرة لكم: تبيعونها كـ setup + retainer. العميل يشوف
|
||||
ROI أسرع — أنتم تزيدون MRR.
|
||||
|
||||
نبدأ بـ pilot مجاني لأول عميل عندكم.
|
||||
20 دقيقة أشرح الآلية؟
|
||||
calendly.com/sami-assiri11/dealix-demo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message 3 — Real Estate Company
|
||||
|
||||
**Target:** شركة عقارية أو مطوّر
|
||||
**Pain:** استفسارات كثيرة عن الأسعار والمواقع تضيع
|
||||
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
لاحظت إن شركات العقار تستقبل عشرات الاستفسارات يومياً
|
||||
عن الأسعار والمواقع — وكثير منها تضيع بسبب تأخر الرد.
|
||||
|
||||
Dealix يرد خلال 45 ثانية بالعربي، يسأل عن الميزانية
|
||||
والموقع المفضل، ويحجز معاينة تلقائياً.
|
||||
|
||||
عندنا تجربة 7 أيام بـ 499 ريال. يناسبك أوريك مثال؟
|
||||
|
||||
إذا ما يناسبكم، قولوا "إيقاف" ولن أتواصل مرة ثانية.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message 4 — B2B Services (Logistics/Construction)
|
||||
|
||||
**Target:** شركة خدمات B2B (شحن، مقاولات)
|
||||
**Pain:** طلبات عروض أسعار متكررة تحتاج فرز
|
||||
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
في قطاع [المقاولات/الشحن]، طلبات عروض الأسعار تجي
|
||||
كثيرة — لكن فرز الجاد من غيره ياخذ وقت.
|
||||
|
||||
Dealix يستقبل الطلب بالعربي، يسأل عن نوع المشروع
|
||||
والميزانية والجدول الزمني، ويصنّف الجدية قبل ما يوصل لفريقكم.
|
||||
|
||||
تجربة 7 أيام بـ 499 ريال. مهتم بمثال مبني على نشاطكم؟
|
||||
|
||||
لو ما يناسبكم: "إيقاف".
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message 5 — Warm Referral Ask
|
||||
|
||||
**Target:** أي معرفة لها شبكة في الأعمال السعودية
|
||||
**Pain:** N/A — you're asking for an intro
|
||||
|
||||
```
|
||||
السلام عليكم [الاسم]،
|
||||
|
||||
أطلقت مشروع Dealix — نظام يساعد الشركات السعودية
|
||||
ترد على استفسارات العملاء بسرعة وتأهلهم بالعربي.
|
||||
|
||||
سؤال سريع: تعرف أحد عنده شركة (عقار، وكالة تسويق،
|
||||
خدمات B2B) وعنده leads كثيرة ومتابعة ضعيفة؟
|
||||
|
||||
لو تعرّفني عليه — 10% من أول سنة اشتراكه لك.
|
||||
|
||||
شكراً مقدماً!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lead Tracker (first 5)
|
||||
|
||||
| # | الاسم | الشركة | القطاع | المصدر | الرسالة | مرسلة؟ | الرد | Demo؟ | التالي |
|
||||
|---|-------|--------|--------|--------|---------|--------|------|-------|--------|
|
||||
| 1 | | | SaaS | warm | Msg 1 | | | | |
|
||||
| 2 | | | Agency | warm | Msg 2 | | | | |
|
||||
| 3 | | | Real Estate | cold/warm | Msg 3 | | | | |
|
||||
| 4 | | | B2B Services | cold/warm | Msg 4 | | | | |
|
||||
| 5 | | | Referral | warm | Msg 5 | | | | |
|
||||
|
||||
**Sami fills in names + sends → marks "مرسلة" → updates "الرد" when reply arrives.**
|
||||
168
salesflow-saas/docs/ops/FULL_OPS_LAUNCH_RUNBOOK.md
Normal file
168
salesflow-saas/docs/ops/FULL_OPS_LAUNCH_RUNBOOK.md
Normal file
@ -0,0 +1,168 @@
|
||||
# Dealix — Full Ops Launch Runbook
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Date:** 2026-04-25
|
||||
|
||||
---
|
||||
|
||||
## Launch Status
|
||||
|
||||
| System | Status | Evidence |
|
||||
|--------|--------|---------|
|
||||
| Backend (Railway) | LIVE | healthz=200, pricing=200, route/message/enrich=200 |
|
||||
| Landing (dealix.me) | LIVE | Returns 200 |
|
||||
| Frontend (Next.js) | DEPLOYED | Marketers page rewritten as sales page |
|
||||
| Manual Payment | READY | Bank transfer + STC Pay documented |
|
||||
| Automated Payment | BLOCKED | Moyasar returns 502 |
|
||||
| Outreach Templates | READY | 5 messages + 60 targets + automation endpoints |
|
||||
| Customer Onboarding | READY | Pilot template + success playbook |
|
||||
| Monitoring | PARTIAL | GitHub Actions healthcheck; Sentry not configured |
|
||||
|
||||
## What Is Live
|
||||
- `/healthz` → 200
|
||||
- `/api/v1/pricing/plans` → 3 plans in SAR
|
||||
- `/api/v1/prospect/route` → rules-based lead classification
|
||||
- `/api/v1/prospect/message` → Arabic message generation
|
||||
- `/api/v1/prospect/enrich-tech` → real tech stack detection
|
||||
- `/api/v1/automation/email/generate` → personalized email + follow-ups
|
||||
- `/api/v1/automation/compliance/check` → compliance gate
|
||||
- `/api/v1/automation/reply/classify` → 12-category reply handler
|
||||
- `dealix.me` → landing page
|
||||
- `trial-signup.html` → lead capture form
|
||||
|
||||
## What Is Not Live
|
||||
- LLM personalization (needs GROQ_API_KEY)
|
||||
- Web discovery (needs GOOGLE_SEARCH_API_KEY)
|
||||
- PostHog funnel tracking (needs POSTHOG_API_KEY)
|
||||
- Sentry error alerting (needs SENTRY_DSN)
|
||||
- Moyasar automated checkout (returns 502)
|
||||
- HubSpot CRM sync (needs HUBSPOT_API_KEY)
|
||||
- Gmail automated send (needs OAuth setup)
|
||||
|
||||
## Manual Blockers (Sami-Only)
|
||||
|
||||
| Blocker | Action | Time | Priority |
|
||||
|---------|--------|------|----------|
|
||||
| GROQ_API_KEY | Add in Railway env Dealix/web | 3 min | P0 |
|
||||
| GOOGLE_SEARCH_API_KEY + CX | Add in Railway | 5 min | P0 |
|
||||
| SENTRY_DSN | Add in Railway | 3 min | P0 |
|
||||
| POSTHOG_API_KEY + HOST | Add in Railway | 3 min | P0 |
|
||||
| Send first 5 messages | WhatsApp + LinkedIn | 30 min | P0 |
|
||||
| Book first demo | Calendly | Depends on replies | P0 |
|
||||
| Test Moyasar key | curl from terminal | 5 min | P1 |
|
||||
| Rollback drill | SSH to Hetzner Console | 15 min | P1 |
|
||||
| DB restore drill | SSH to Hetzner Console | 15 min | P1 |
|
||||
|
||||
---
|
||||
|
||||
## Daily Checklist (until first 3 paid clients)
|
||||
|
||||
### Morning (08:00)
|
||||
- [ ] Check Railway healthz → 200?
|
||||
- [ ] Check Sentry for overnight errors (if configured)
|
||||
- [ ] Review replies from yesterday
|
||||
- [ ] Classify replies (use /api/v1/automation/reply/classify)
|
||||
- [ ] Select today's 5-10 outreach targets
|
||||
|
||||
### Midday (12:00)
|
||||
- [ ] Send 5 outreach messages (WhatsApp warm + LinkedIn)
|
||||
- [ ] Follow up on Day +2 / +5 leads
|
||||
- [ ] Respond to all positive replies within 2 hours
|
||||
- [ ] Book demos for interested prospects
|
||||
|
||||
### Afternoon (16:00)
|
||||
- [ ] Run demos (if scheduled)
|
||||
- [ ] Send pilot offers to demo'd prospects
|
||||
- [ ] Publish 1 LinkedIn/X post
|
||||
|
||||
### Evening (20:00)
|
||||
- [ ] Update lead tracker
|
||||
- [ ] Count: sent / replied / demo'd / offered / paid
|
||||
- [ ] Plan tomorrow's targets
|
||||
- [ ] Handle late replies
|
||||
|
||||
---
|
||||
|
||||
## Payment Test Checklist (for Sami)
|
||||
|
||||
### Manual Payment Test
|
||||
1. Ask a friend to transfer 1 SAR via bank or STC Pay
|
||||
2. Verify receipt in bank app
|
||||
3. Mark test as passed
|
||||
4. This is your production payment path until Moyasar works
|
||||
|
||||
### Moyasar Diagnostic
|
||||
1. Open [dashboard.moyasar.com](https://dashboard.moyasar.com)
|
||||
2. Check Account Status (Active? Pending? Suspended?)
|
||||
3. Check API Keys → is `sk_live_` the current key?
|
||||
4. Test from terminal:
|
||||
```
|
||||
curl -u "sk_live_YOUR_KEY:" https://api.moyasar.com/v1/invoices \
|
||||
-d "amount=100" -d "currency=SAR"
|
||||
```
|
||||
5. If "authentication_error" → key mismatch, regenerate
|
||||
6. If "account_inactive" → complete KYC
|
||||
7. If success → Railway env has whitespace, re-paste carefully
|
||||
|
||||
---
|
||||
|
||||
## Outreach Checklist
|
||||
- [ ] 5 warm WhatsApp messages sent (from FIRST_5_OUTREACH.md)
|
||||
- [ ] 1 LinkedIn founder post published
|
||||
- [ ] 1 LinkedIn DM to agency partner
|
||||
- [ ] All follow-ups current (no lead older than 3 days without follow-up)
|
||||
- [ ] Tracker updated with today's activity
|
||||
|
||||
## Demo Checklist
|
||||
- [ ] Prospect's website reviewed
|
||||
- [ ] enrich-tech run on their domain
|
||||
- [ ] 20-minute demo completed per DEMO_BOOKING_RUNBOOK.md
|
||||
- [ ] Follow-up sent same day
|
||||
- [ ] Pilot offer sent if positive
|
||||
|
||||
## Rollback Checklist (if something breaks)
|
||||
1. Railway → Deployments → click previous green deployment → Redeploy
|
||||
2. If Railway is down: Hetzner Console → `systemctl restart dealix-api`
|
||||
3. If DB corrupted: restore from Railway Postgres auto-backup
|
||||
4. If code breaks: `git revert HEAD && git push origin main`
|
||||
|
||||
## DB Restore Checklist
|
||||
1. Railway Postgres has automatic daily backups
|
||||
2. Railway → Postgres service → Backups → Restore
|
||||
3. After restore: verify `/api/v1/dashboard/metrics` returns data
|
||||
|
||||
## Incident Checklist
|
||||
1. Check Railway logs → identify error
|
||||
2. If 502/503: restart deployment
|
||||
3. If code bug: revert last commit
|
||||
4. If env issue: check Variables tab
|
||||
5. If DNS: check GoDaddy/Cloudflare
|
||||
6. Notify any active pilot clients if downtime > 30 minutes
|
||||
|
||||
---
|
||||
|
||||
## Do Not Touch List
|
||||
- Do NOT add new LLM providers
|
||||
- Do NOT rebuild dashboard
|
||||
- Do NOT start v3.1
|
||||
- Do NOT implement Temporal/LangGraph/Qdrant
|
||||
- Do NOT build voice/webchat/mobile
|
||||
- Do NOT expand to UAE/Egypt
|
||||
- Do NOT hire before 3 paid clients
|
||||
- Do NOT spend on ads before manual outreach proves conversion
|
||||
|
||||
---
|
||||
|
||||
## Definition of Launch-Ready
|
||||
All of these must be true:
|
||||
- [ ] Railway healthz=200
|
||||
- [ ] 4 env keys added (GROQ, GOOGLE, SENTRY, POSTHOG)
|
||||
- [ ] 5+ outreach messages sent
|
||||
- [ ] 1+ demo booked
|
||||
- [ ] Manual payment path tested (1 SAR friend test)
|
||||
|
||||
## Definition of Revenue-Live
|
||||
All of the above, plus:
|
||||
- [ ] 1+ pilot payment received (499 SAR)
|
||||
- [ ] 1+ customer onboarded
|
||||
- [ ] 1+ daily report sent to customer
|
||||
140
salesflow-saas/docs/ops/MARKETERS_PAGE_PLAN.md
Normal file
140
salesflow-saas/docs/ops/MARKETERS_PAGE_PLAN.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Dealix — Marketers Page: Service Sales Specification
|
||||
|
||||
**Purpose:** Convert `/marketers` from a link hub (current: 131 lines, 8 links) into a page that sells Dealix as a service marketers resell to their clients.
|
||||
|
||||
---
|
||||
|
||||
## Positioning
|
||||
|
||||
**Core message:** Dealix gives marketers a new revenue line — AI-powered lead response they sell as a service to their clients.
|
||||
|
||||
**Not:** "Here are resources for marketers"
|
||||
**Instead:** "Here is a service you sell to your clients and earn recurring revenue"
|
||||
|
||||
**One-liner:** "أضف خدمة رد ذكي + تأهيل leads لعملائك — setup fee + MRR شهري متكرر"
|
||||
|
||||
---
|
||||
|
||||
## Target Segments
|
||||
|
||||
| Segment | Size | Pain | What They Buy |
|
||||
|---------|------|------|---------------|
|
||||
| **Performance agencies** (5-30 people) | ~200 in Saudi | Run ads, generate leads, but leads go cold — client blames the agency | A service they add on top of ads |
|
||||
| **Full-service digital agencies** (10-50) | ~100 | Deliver websites + ads but no post-lead follow-up — churn risk | Retention tool for their clients |
|
||||
| **Freelance marketers** | ~500+ | Want recurring income beyond project fees | Low-effort MRR add-on |
|
||||
| **CRM/automation consultants** | ~50 | Implement HubSpot/Zoho but client doesn't use it well | Lead response layer they manage |
|
||||
| **WhatsApp marketing specialists** | ~100 | Know WhatsApp works for Saudi but can't scale responses | AI response they configure |
|
||||
|
||||
---
|
||||
|
||||
## Page Sections (top to bottom)
|
||||
|
||||
### 1. Hero
|
||||
- **Headline AR:** "حوّل وكالتك إلى ماكينة إيرادات متكررة"
|
||||
- **Subheadline:** "أضف خدمة الرد الذكي على leads عملائك — setup fee لك + MRR شهري متكرر"
|
||||
- **Primary CTA:** "احجز مكالمة شراكة" → calendly.com/sami-assiri11/dealix-demo
|
||||
- **Secondary CTA:** "شوف الباقات" → scroll to packages
|
||||
|
||||
### 2. Problem Statement
|
||||
"عملاءك يصرفون 10,000+ ريال/شهر على إعلانات ويجيبون leads.
|
||||
لكن 70% من الـ leads تضيع لأن:
|
||||
- ما أحد يرد خلال أول ساعة
|
||||
- المتابعة يدوية وعشوائية
|
||||
- فريق العميل مشغول
|
||||
|
||||
العميل يلوم الوكالة. الحقيقة: المشكلة مو في الإعلان — المشكلة في الرد."
|
||||
|
||||
### 3. Solution
|
||||
"Dealix يعطيك الحل تبيعه لعملائك:
|
||||
- AI يرد بالعربي خلال 45 ثانية على كل lead
|
||||
- يسأل أسئلة التأهيل (ميزانية؟ جدية؟ موعد؟)
|
||||
- يحجز اجتماع أو يحوّل للمبيعات
|
||||
- يرسل تقرير يومي
|
||||
|
||||
أنت تبيع الخدمة. نحن نشغّل النظام. تحصّل setup fee + نسبة شهرية."
|
||||
|
||||
### 4. Packages Marketers Can Sell
|
||||
|
||||
| الباقة | Setup Fee (لك) | شهري (العميل يدفع) | نصيبك الشهري | ما يحصل العميل |
|
||||
|--------|---------------|-------------------|-------------|---------------|
|
||||
| **الأساسية** | 3,000 SAR | 990 SAR | 297 SAR (30%) | رد AI على واتساب + تأهيل + تقرير أسبوعي |
|
||||
| **المتقدمة** | 7,000 SAR | 2,490 SAR | 622 SAR (25%) | + حجز مواعيد + CRM sync + follow-up |
|
||||
| **المؤسسية** | 15,000 SAR | مخصص | 20-30% | + تقارير executive + تكامل كامل |
|
||||
|
||||
### 5. Workflows Marketers Can Run
|
||||
|
||||
**Workflow 1 — Performance Agency:**
|
||||
1. تشغّل حملة إعلانية لعميل
|
||||
2. Leads تنزل → Dealix يرد فوراً
|
||||
3. Leads المؤهلة تتحول لمواعيد
|
||||
4. العميل يشوف ROI أعلى → يجدد → أنت تحصّل MRR
|
||||
|
||||
**Workflow 2 — Freelance Marketer:**
|
||||
1. تقدم عرض "إدارة حملات + رد ذكي"
|
||||
2. تسعّر الخدمة كاملة (حملة + Dealix)
|
||||
3. Dealix يشتغل بالخلفية
|
||||
4. أنت تركز على الاستراتيجية والمحتوى
|
||||
|
||||
**Workflow 3 — CRM Consultant:**
|
||||
1. تنفذ HubSpot/Zoho لعميل
|
||||
2. تضيف Dealix كطبقة response
|
||||
3. CRM يمتلئ بـ leads مؤهلة تلقائياً
|
||||
4. العميل يشوف قيمة CRM أسرع → retention
|
||||
|
||||
### 6. Agency Use Cases
|
||||
|
||||
| القطاع | المشكلة | Dealix يحل |
|
||||
|--------|---------|-----------|
|
||||
| عقارات | 500+ استفسار/شهر، رد بعد ساعات | رد فوري + تأهيل الميزانية + حجز معاينة |
|
||||
| مقاولات | طلبات عروض أسعار تحتاج فرز | فرز + تصنيف الجدية قبل تحويل للمهندسين |
|
||||
| فنادق/قاعات | استفسارات حجز يومية | رد + تأهيل التاريخ والعدد + حجز مبدئي |
|
||||
| عيادات | مرضى يسألون عن مواعيد وأسعار | رد تلقائي + حجز موعد |
|
||||
| تجارة إلكترونية | رسائل واتساب عن المنتجات | رد + توجيه للشراء |
|
||||
|
||||
### 7. Freelancer Use Cases
|
||||
|
||||
| السيناريو | كيف تربح |
|
||||
|-----------|---------|
|
||||
| "أنا أسوي سوشيال ميديا لـ 5 عملاء" | أضف Dealix لكل عميل = 5 × 990 = 4,950 SAR/شهر إضافي |
|
||||
| "أنا مستشار CRM" | أضف Dealix كخدمة after-implementation = retainer شهري |
|
||||
| "أنا أدير إعلانات" | أضف "الرد الذكي" كـ add-on = يميّزك عن المنافسين |
|
||||
|
||||
### 8. FAQs
|
||||
|
||||
| السؤال | الجواب |
|
||||
|--------|--------|
|
||||
| كم ياخذ الإعداد لكل عميل؟ | يوم واحد للأساسية، 3 أيام للمتقدمة |
|
||||
| هل أحتاج خبرة تقنية؟ | لا — نحن نسوي الإعداد والتشغيل |
|
||||
| كم أقدر أربح؟ | عميل واحد = 3,000 setup + ~990/شهر = 14,880 SAR/سنة |
|
||||
| هل فيه عقد طويل؟ | لا — شهري لك ولعميلك |
|
||||
| ماذا لو العميل ما عجبه؟ | 30 يوم ضمان استرداد |
|
||||
| هل يشتغل مع HubSpot/Zoho؟ | نعم — CRM sync متاح |
|
||||
| هل يفهم العربي السعودي؟ | نعم — مبني Arabic-first |
|
||||
| كم عميل أقدر أضيف؟ | بلا حدود — كل عميل بباقته |
|
||||
| هل بياناتي وبيانات عملائي آمنة؟ | نعم — PDPL compliant + بيانات منفصلة لكل عميل |
|
||||
| كيف أبدأ؟ | احجز مكالمة شراكة 20 دقيقة → نسوي pilot مجاني لأول عميل |
|
||||
|
||||
### 9. Proof/Trust Blocks
|
||||
- "مبني للسوق السعودي — عربي أولاً"
|
||||
- "متوافق مع نظام حماية البيانات الشخصية (PDPL)"
|
||||
- "بيانات كل عميل منفصلة تماماً"
|
||||
- "ضمان استرداد 30 يوم"
|
||||
- "بدون عقد سنوي — شهري"
|
||||
- Dealix logo + dealix.me
|
||||
|
||||
### 10. Final CTA
|
||||
"ابدأ بأول عميل — مجاناً"
|
||||
"نسوي pilot 7 أيام لأول عميل عندك بدون تكلفة. لو اشتغل → تبدأ تحصّل."
|
||||
→ calendly.com/sami-assiri11/dealix-demo
|
||||
|
||||
---
|
||||
|
||||
## Conversion Goals
|
||||
- Primary: Partner books Calendly call
|
||||
- Secondary: Partner submits contact form
|
||||
- Tertiary: Partner shares page with colleague
|
||||
|
||||
## Implementation
|
||||
- File: `frontend/src/app/marketers/page.tsx`
|
||||
- No new dependencies — uses existing Tailwind + lucide-react
|
||||
- Arabic RTL primary, English secondary where needed
|
||||
81
salesflow-saas/docs/ops/NEXT_24H_7D_30D.md
Normal file
81
salesflow-saas/docs/ops/NEXT_24H_7D_30D.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Dealix — Execution Plan: 24h / 7d / 30d
|
||||
|
||||
**Starting state:** 28/39 gates closed. Product live. 0 revenue. 0 messages sent.
|
||||
|
||||
---
|
||||
|
||||
## Next 24 Hours
|
||||
|
||||
| # | Deliverable | Owner | Dependency | Definition of Done | Metric | Risk |
|
||||
|---|-------------|-------|------------|-------------------|--------|------|
|
||||
| 1 | Add GROQ_API_KEY in Railway env Dealix/web | Sami | console.groq.com (free) | `/api/v1/prospect/search-diag` shows GROQ set | LLM features activate | Key in wrong env (use Dealix, not Agent branch) |
|
||||
| 2 | Add GOOGLE_SEARCH_API_KEY + CX in Railway | Sami | Google Cloud Console | `/api/v1/prospect/search-diag` shows Google set | Web discovery active | Needs billing account for Maps |
|
||||
| 3 | Add SENTRY_DSN in Railway | Sami | sentry.io free plan | Sentry receives test error | Error alerting active | None |
|
||||
| 4 | Send 5 WhatsApp warm messages | Sami | Personal contacts who own businesses | 5 conversations started | pipeline > 0 | Low reply rate from cold — use warm only |
|
||||
| 5 | Publish Founder Launch Post on LinkedIn | Sami | LinkedIn account | Post visible + URL captured | Inbound starts | None |
|
||||
| 6 | Send 1 LinkedIn DM to agency partner | Sami | LinkedIn | Connection request sent | Partner pipeline > 0 | May take days for acceptance |
|
||||
|
||||
**24h success = 3 keys added + 5 messages sent + 1 post published.**
|
||||
|
||||
---
|
||||
|
||||
## Next 7 Days
|
||||
|
||||
| # | Deliverable | Owner | Dependency | Definition of Done | Metric | Risk |
|
||||
|---|-------------|-------|------------|-------------------|--------|------|
|
||||
| 1 | 30+ outreach messages across channels | Sami | Templates in COMMAND_CENTER | Messages logged in tracker | 30 touches | Fatigue — batch 5/day |
|
||||
| 2 | First demo booked via Calendly | Sami | At least 1 positive reply | Calendly notification received | 1 demo | Low conversion — try warm contacts first |
|
||||
| 3 | First pilot offered (499 SAR) | Sami | Completed demo | Payment request sent | 1 offer | Prospect may need time |
|
||||
| 4 | First partner contacted | Sami | AGENCY_PARTNER_OFFER.md | Agency responds | 1 partner conversation | Agency may want proof first |
|
||||
| 5 | Moyasar diagnostic completed | Sami | Moyasar dashboard access | Know if live key works or needs fix | Unblocks automated payment | KYC may take days |
|
||||
| 6 | PostHog receives 1+ event | Sami | POSTHOG_API_KEY added | Event visible in PostHog dashboard | Funnel measurement starts | Free plan sufficient |
|
||||
| 7 | 3 LinkedIn posts published | Sami | Content from COMMAND_CENTER | Posts visible | Inbound pipeline | Consistency matters more than perfection |
|
||||
|
||||
**Week 1 success = 1 demo + 1 pilot offer + 3 posts + partner motion started.**
|
||||
|
||||
---
|
||||
|
||||
## Next 30 Days
|
||||
|
||||
| # | Deliverable | Owner | Dependency | Definition of Done | Metric | Risk |
|
||||
|---|-------------|-------|------------|-------------------|--------|------|
|
||||
| 1 | 200+ outreach touches total | Sami | Sustained daily activity | Tracker shows 200+ rows | Volume | Burnout — keep to 10/day |
|
||||
| 2 | 5 demos completed | Sami | Positive replies | Calendly shows 5 past events | Demo rate | Need warm + cold mix |
|
||||
| 3 | 3 pilot payments received | Sami | Completed demos | Bank/STC Pay proof | 1,497 SAR MRR | Conversion uncertainty |
|
||||
| 4 | 1 agency partner active | Sami | Partner signs + brings client | Partner's client in pilot | Partner channel open | Agencies want proof |
|
||||
| 5 | Marketers page rewritten | Claude | MARKETERS_PAGE_PLAN.md | Page sells services, not just links | Page conversion | TSX rewrite needed |
|
||||
| 6 | Moyasar live checkout working | Sami | KYC + key fix | 1 SAR test transaction succeeds | Automated payment | May need new key |
|
||||
| 7 | Case study draft from first client | Sami | First pilot completed | 1-page results document | Social proof | Client may not agree to name |
|
||||
| 8 | Rollback drill completed | Claude/Sami | SSH access to server | Documented rollback in < 5 min | Recovery confidence | SSH currently blocked |
|
||||
| 9 | 10 LinkedIn posts total | Sami | Weekly posting cadence | Posts visible | Brand building | Consistency > perfection |
|
||||
| 10 | PostHog funnel visible | Sami/Claude | POSTHOG key + landing events | landing → signup → demo → paid visible | Data-driven decisions | Free plan limits |
|
||||
|
||||
**Month 1 success = 3 paid pilots (1,497 SAR) + 1 partner + funnel visible + case study draft.**
|
||||
|
||||
---
|
||||
|
||||
## Revenue Trajectory (conservative)
|
||||
|
||||
| Week | Touches | Replies | Demos | Pilots | Paid | MRR (SAR) |
|
||||
|------|---------|---------|-------|--------|------|-----------|
|
||||
| 1 | 30 | 3 | 1 | 0 | 0 | 0 |
|
||||
| 2 | 60 | 8 | 2 | 1 | 0 | 0 |
|
||||
| 3 | 90 | 12 | 3 | 2 | 1 | 499 |
|
||||
| 4 | 120 | 15 | 5 | 3 | 3 | 1,497 |
|
||||
| **Total** | **300** | **38** | **11** | **6** | **3** | **1,497** |
|
||||
|
||||
Assumes: 13% reply rate, 29% demo-from-reply rate, 55% pilot-from-demo, 50% paid-from-pilot.
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Do in 30 Days
|
||||
|
||||
- Do NOT rebuild the dashboard
|
||||
- Do NOT add new LLM providers
|
||||
- Do NOT implement Temporal/LangGraph/Qdrant
|
||||
- Do NOT build voice receptionist or webchat
|
||||
- Do NOT start v3.1
|
||||
- Do NOT expand to UAE/Egypt
|
||||
- Do NOT build mobile app
|
||||
- Do NOT hire before first 3 paid clients
|
||||
- Do NOT spend on ads before proving manual outreach converts
|
||||
55
salesflow-saas/docs/ops/RAILWAY_ENV_KEYS.md
Normal file
55
salesflow-saas/docs/ops/RAILWAY_ENV_KEYS.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Dealix — Railway Environment Keys
|
||||
|
||||
**Where to add:** Railway → Project → Service "web" → Variables tab → Review → Deploy
|
||||
|
||||
---
|
||||
|
||||
## P0 — Required for Launch
|
||||
|
||||
| Variable | Source | Cost | Effect When Missing | Verification |
|
||||
|----------|--------|------|-------------------|-------------|
|
||||
| `GROQ_API_KEY` | [console.groq.com/keys](https://console.groq.com/keys) | Free tier available | LLM features degraded (rules-only) | `curl /api/v1/prospect/search-diag` shows `GROQ_API_KEY.set: true` |
|
||||
| `GOOGLE_SEARCH_API_KEY` | [Google Cloud Console → Credentials](https://console.cloud.google.com/apis/credentials) | 100 queries/day free | `/prospect/search` returns 503 | search-diag shows key set |
|
||||
| `GOOGLE_SEARCH_CX` | Fixed value: `75ae2277dfd754a1a` | Free | Same as above | search-diag shows CX set |
|
||||
| `SENTRY_DSN` | [sentry.io](https://sentry.io) → Create Python project → copy DSN | Free plan | No error alerting | Sentry receives test error |
|
||||
| `POSTHOG_API_KEY` | [posthog.com](https://posthog.com) → Project Settings → API Key | Free plan | No funnel tracking | PostHog dashboard shows events |
|
||||
| `POSTHOG_HOST` | `https://us.i.posthog.com` or `https://eu.i.posthog.com` | — | PostHog events go nowhere | Same as above |
|
||||
|
||||
## P1 — Required for Automated Payment
|
||||
|
||||
| Variable | Source | Cost | Effect When Missing |
|
||||
|----------|--------|------|-------------------|
|
||||
| `MOYASAR_SECRET_KEY` | [dashboard.moyasar.com](https://dashboard.moyasar.com) → API Keys | Per-transaction fee | Checkout returns 502 |
|
||||
| `MOYASAR_PUBLISHABLE_KEY` | Same dashboard | — | Frontend checkout broken |
|
||||
| `MOYASAR_WEBHOOK_SECRET` | Moyasar → Webhooks → Secret | — | Webhook signature validation fails |
|
||||
|
||||
**Note:** Moyasar keys appear to be set but checkout returns 502. See REVENUE_READINESS_CHECKLIST.md → Moyasar Diagnostic Checklist.
|
||||
|
||||
## P2 — Optional Enrichment (after first customer)
|
||||
|
||||
| Variable | Source | Effect When Missing |
|
||||
|----------|--------|-------------------|
|
||||
| `GOOGLE_MAPS_API_KEY` | Google Cloud → Maps → Places API | Local discovery disabled |
|
||||
| `SENDGRID_API_KEY` | sendgrid.com | No automated email |
|
||||
| `WHATSAPP_ACCESS_TOKEN` | Meta Business | No WhatsApp API |
|
||||
| `HUBSPOT_API_KEY` | HubSpot → Private Apps | No CRM sync |
|
||||
| `TAVILY_API_KEY` | tavily.com | No web crawl enrichment |
|
||||
|
||||
## Already Set (verified via search-diag)
|
||||
|
||||
| Variable | Status |
|
||||
|----------|--------|
|
||||
| `MOYASAR_SECRET_KEY` | Set (48 chars, sk_liv...) |
|
||||
| `MOYASAR_WEBHOOK_SECRET` | Set (64 chars) |
|
||||
| `MOYASAR_PUBLIC_KEY` | Set |
|
||||
| `APP_URL` | Set |
|
||||
| `DATABASE_URL` | Set (via Railway Postgres) |
|
||||
|
||||
---
|
||||
|
||||
## Critical Reminders
|
||||
|
||||
1. **Environment dropdown:** Make sure you're in environment "Dealix" (not "adventurous-tenderness" or other Agent branches)
|
||||
2. **Service:** Variables go in service "web" (not Postgres or Redis)
|
||||
3. **Deploy:** After adding variables, you MUST click Review → Deploy. Variables are staged until deployed.
|
||||
4. **No secrets in chat:** Never paste API keys in any chat window. Add them directly in Railway.
|
||||
133
salesflow-saas/docs/ops/REVENUE_READINESS_CHECKLIST.md
Normal file
133
salesflow-saas/docs/ops/REVENUE_READINESS_CHECKLIST.md
Normal file
@ -0,0 +1,133 @@
|
||||
# Dealix — Revenue Readiness Checklist
|
||||
|
||||
**Last verified:** 2026-04-25
|
||||
**Railway healthz:** 200 (verified)
|
||||
**Pricing API:** 200 (verified: Starter 999 / Growth 2999 / Scale 7999 SAR)
|
||||
|
||||
---
|
||||
|
||||
## 1. Pricing Path
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|---------|
|
||||
| Plans in API | DONE | `/api/v1/pricing/plans` returns 3 plans, SAR currency |
|
||||
| Pilot offer defined | DONE | 499 SAR / 7 days / documented in COMMAND_CENTER.md |
|
||||
| Agency pricing defined | DONE | AGENCY_PARTNER_OFFER.md: setup 3K-15K + 20-30% MRR |
|
||||
| Public pricing page | NOT DONE | API exists but no public-facing pricing page on dealix.me |
|
||||
|
||||
**Blocker:** None for manual sales. Pricing page is P1 for inbound.
|
||||
|
||||
## 2. Invoice Path
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|---------|
|
||||
| Manual invoice template | DONE | FIRST_REVENUE_ATTEMPT in revenue-activation/ |
|
||||
| Bank transfer details | DONE | Documented in COMMAND_CENTER.md |
|
||||
| STC Pay | DONE | Ready |
|
||||
| Moyasar live invoice | BLOCKED | `/api/v1/checkout` returns 502 — Moyasar-side issue |
|
||||
| Moyasar sandbox | NOT TESTED | No `MOYASAR_TEST_SECRET_KEY` in Railway env |
|
||||
|
||||
**Workaround:** Manual invoice via bank transfer or STC Pay. Works today.
|
||||
|
||||
## 3. Payment Path
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|---------|
|
||||
| Bank transfer acceptance | READY | Account info available |
|
||||
| STC Pay acceptance | READY | Number available |
|
||||
| Manual proof workflow | READY | Customer sends screenshot → Sami confirms → onboarding starts |
|
||||
| Moyasar automated payment | BLOCKED | 502 on checkout — Moyasar dashboard issue (KYC or key) |
|
||||
| Moyasar test payment (1 SAR) | NOT TESTED | Needs `MOYASAR_TEST_SECRET_KEY` or Sami to test `sk_live_` via curl |
|
||||
| Payment → onboarding trigger | MANUAL | On payment proof → start onboarding checklist within 4h |
|
||||
|
||||
**Manual payment path: FULLY OPERATIONAL.**
|
||||
|
||||
## 4. Booking Path
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|---------|
|
||||
| Calendly link | ACTIVE | calendly.com/sami-assiri11/dealix-demo |
|
||||
| Landing → Calendly | DONE | trial-signup.html redirects on form submit |
|
||||
| Email templates → Calendly | DONE | All email templates include Calendly link |
|
||||
| Outreach messages → Calendly | DONE | All DM templates include Calendly link |
|
||||
| Calendly → webhook | NOT TESTED | Code exists but no E2E booking verified |
|
||||
| Calendly → CRM | NOT TESTED | HubSpot key missing in Railway |
|
||||
|
||||
**Booking path: OPERATIONAL for manual flow (link → booking → Sami sees in Calendly).**
|
||||
|
||||
## 5. CRM Sync Path
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|---------|
|
||||
| HubSpot connector code | EXISTS | `services/crm_sync_service.py` |
|
||||
| HubSpot API key in Railway | MISSING | Not set in Railway env Dealix/web |
|
||||
| E2E: lead → HubSpot contact | NOT TESTED | Cannot test without key |
|
||||
| Manual CRM (Google Sheet) | READY | Tracker template in ops docs |
|
||||
| Pipeline stages defined | DONE | new → sent → replied → demo → pilot → paid → churned |
|
||||
|
||||
**Manual CRM: READY. HubSpot automated: BLOCKED on API key.**
|
||||
|
||||
## 6. Follow-up Path
|
||||
|
||||
| Item | Status | Evidence |
|
||||
|------|--------|---------|
|
||||
| Reply classifier | DONE | `POST /api/v1/automation/reply/classify` — 12 categories |
|
||||
| Follow-up templates (Day +2/+5/+10) | DONE | Generated per lead in email/generate endpoint |
|
||||
| Arabic response templates | DONE | Pre-written Khaliji response per category |
|
||||
| Gmail OAuth adapter | NOT BUILT | Needs Google Cloud OAuth setup |
|
||||
| Manual follow-up | READY | Copy-paste from templates |
|
||||
|
||||
**Manual follow-up: READY. Gmail auto-send: BLOCKED on OAuth setup.**
|
||||
|
||||
## 7. Test Payment Path
|
||||
|
||||
| Test | Status | How to Test |
|
||||
|------|--------|-------------|
|
||||
| Moyasar sandbox | NOT TESTED | Add `MOYASAR_TEST_SECRET_KEY` (sk_test_...) in Railway → hit `/api/v1/checkout` |
|
||||
| Moyasar live | BLOCKED | sk_live_ exists in Railway but returns 502 |
|
||||
| Manual bank transfer | READY | Ask friend to transfer 1 SAR → verify receipt |
|
||||
| Manual STC Pay | READY | Ask friend to STC Pay 1 SAR → verify receipt |
|
||||
|
||||
**Recommended first test:** Manual bank transfer from a friend. Zero technical dependency.
|
||||
|
||||
## 8. Manual Fallback Summary
|
||||
|
||||
| Function | Automated | Manual Fallback | Status |
|
||||
|----------|-----------|----------------|--------|
|
||||
| Lead response | AI endpoint exists | Sami responds manually | READY |
|
||||
| Qualification | Automation endpoint | Sami asks questions manually | READY |
|
||||
| Booking | Calendly auto | Sami proposes 2 times | READY |
|
||||
| Payment | Moyasar | Bank/STC Pay + proof | READY |
|
||||
| CRM | HubSpot | Google Sheet | READY |
|
||||
| Follow-up | Gmail OAuth | Copy-paste templates | READY |
|
||||
| Reporting | Dashboard | Manual WhatsApp/email to client | READY |
|
||||
|
||||
**Every function has a working manual fallback. No function is truly blocked.**
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done for "Revenue Live"
|
||||
|
||||
| Gate | Status | What Proves It |
|
||||
|------|--------|---------------|
|
||||
| First 5 outreach messages sent | NOT DONE | Sami confirms SENT |
|
||||
| First demo booked via Calendly | NOT DONE | Calendly notification |
|
||||
| First pilot payment received (499 SAR) | NOT DONE | Bank/STC Pay screenshot |
|
||||
| First customer onboarded (Day 0 checklist) | NOT DONE | Checklist filled |
|
||||
| First daily report sent to customer | NOT DONE | WhatsApp/email sent |
|
||||
| PostHog receives at least 1 event | NOT DONE | PostHog dashboard check |
|
||||
| Manual payment log has 1 entry | NOT DONE | File updated |
|
||||
|
||||
**0/7 done. Revenue is NOT live. Product is live. The gap is sales activity, not engineering.**
|
||||
|
||||
---
|
||||
|
||||
## Moyasar Diagnostic Checklist (for Sami)
|
||||
|
||||
If 502 persists, check these in order:
|
||||
1. Moyasar dashboard → Account Status: Active? Pending KYC? Suspended?
|
||||
2. Moyasar dashboard → API Keys: Is `sk_live_` the latest generated key?
|
||||
3. Test from terminal: `curl -u "sk_live_...:" https://api.moyasar.com/v1/invoices -d "amount=100" -d "currency=SAR"`
|
||||
4. If "authentication_error" → key wrong or regenerated. Get new key.
|
||||
5. If "account_inactive" → KYC not complete. Finish KYC in Moyasar.
|
||||
6. If success → Railway env has whitespace or wrong value. Re-paste key carefully.
|
||||
81
salesflow-saas/docs/ops/lead_machine/SAUDI_60_TARGETS.md
Normal file
81
salesflow-saas/docs/ops/lead_machine/SAUDI_60_TARGETS.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Dealix — 60 Saudi B2B Targets (Week 1)
|
||||
|
||||
## Direct Customers (20)
|
||||
|
||||
| # | Company | Sector | Website | Why Target | Message Angle |
|
||||
|---|---------|--------|---------|-----------|---------------|
|
||||
| 1 | Foodics | SaaS/F&B | foodics.com | HubSpot+WhatsApp detected | "شفت إنكم تستخدمون HubSpot — كم lead يضيع بسبب بطء الرد؟" |
|
||||
| 2 | Lucidya | SaaS/Social | lucidya.com | 4 tools detected | "شفت حجم الأدوات عندكم — Dealix يكمّلها بالرد الفوري" |
|
||||
| 3 | Lean Technologies | SaaS/Fintech | leantech.me | Saudi fintech scaling | "فريق مبيعات صغير + leads كثيرة = فرصة" |
|
||||
| 4 | Tamara | BNPL | tamara.co | 3 tools detected | "عندكم merchants كثيرة تحتاج متابعة" |
|
||||
| 5 | Rewaa | POS/Retail | rewaa.com | WhatsApp+GTM | "تجار التجزئة يسألون كثير — Dealix يرد بدالكم" |
|
||||
| 6 | Qoyod | Accounting | qoyod.com | HubSpot+WhatsApp | "عملاء المحاسبة يسألون نفس الأسئلة — AI يرد" |
|
||||
| 7 | Maqsam | Cloud Phone | maqsam.com | HubSpot+Framer | "أنتم تبيعون communications — جربوا AI sales" |
|
||||
| 8 | Hakbah | Savings | hakbah.com | WooCommerce+GTM+GA4 | "لاحظت forms كثيرة — كم يُتابع فعلاً؟" |
|
||||
| 9 | Sary | B2B Supply | sary.com | 3 tools | "توزيع B2B = leads كثيرة = Dealix يأهّل" |
|
||||
| 10 | BRKZ | Construction | brkz.com | 2 tools | "مقاولين يسألون عن الأسعار — AI يرد ويأهّل" |
|
||||
| 11 | Aqar | Real Estate | aqar.fm | High volume leads | "عقار = 500+ lead/شهر — كم يُتابع؟" |
|
||||
| 12 | Jarir Online | Retail | jarir.com | ecommerce B2B | "استفسارات الشركات على الموقع — AI يحوّلها لصفقات" |
|
||||
| 13 | Floward | Flowers/Corp | floward.com | Corporate orders | "طلبات B2B (تموين، هدايا) تحتاج متابعة سريعة" |
|
||||
| 14 | Nana | Grocery | nana.sa | Delivery+B2B | "طلبات المطاعم والشركات = pipeline ثابت" |
|
||||
| 15 | Morni | Roadside | morni.com | Service leads | "طلبات الشركات للعقود السنوية" |
|
||||
| 16 | Lendo | Lending | lendo.sa | Fintech B2B | "طلبات التمويل تحتاج تأهيل سريع" |
|
||||
| 17 | Unifonic | Communications | unifonic.com | 6 tools! Chili Piper+HubSpot+Zendesk | "أنتم في مجال التواصل — جربوا AI SDR" |
|
||||
| 18 | HyperPay | Payments | hyperpay.com | Payment gateway | "merchants تسأل عن التفعيل — AI يرد" |
|
||||
| 19 | Moyasar | Payments | moyasar.com | Payment gateway | "developers يسألون عن الـ API — AI يوجّه" |
|
||||
| 20 | Tap Payments | Payments | tap.company | Mixpanel detected | "عملاء جدد يسألون — AI يأهّل ويحوّل" |
|
||||
|
||||
## Agency Partners (20)
|
||||
|
||||
| # | Company | Type | Why Target | Offer |
|
||||
|---|---------|------|-----------|-------|
|
||||
| 1 | Peak Content | Content Marketing | Saudi top agency | Setup 5K + 25% MRR |
|
||||
| 2 | Digital8 | Digital Agency | Riyadh | Setup 3K + 20% MRR |
|
||||
| 3 | Brand Lounge | Branding+Digital | Dubai/Riyadh | Setup 5K + 25% MRR |
|
||||
| 4 | Wavy | Social Media | Saudi focused | Setup 3K + 20% MRR |
|
||||
| 5 | Bold | Performance | Ads focused | Setup 3K + 20% MRR |
|
||||
| 6 | Netaq Digital | Full service | Jeddah | Setup 3K + 20% MRR |
|
||||
| 7 | Jarwal | Marketing | Riyadh | Setup 3K + 20% MRR |
|
||||
| 8 | Zoomly | Growth Agency | Saudi | Setup 3K + 20% MRR |
|
||||
| 9 | UTURN | Content | Saudi media | Content collab |
|
||||
| 10 | Telfaz11 | Video Content | Saudi media | Content collab |
|
||||
| 11 | Techieons | Tech Consulting | IT services | Implementation partner |
|
||||
| 12 | Lean Business | Consulting | Strategy | Referral 10% |
|
||||
| 13 | 2P Agency | Creative | Saudi creative | Setup 3K + 20% MRR |
|
||||
| 14 | Shawaa | Performance | PPC/Social | Setup 3K + 20% MRR |
|
||||
| 15 | Crowd | PR + Digital | Communications | Setup 5K + 25% MRR |
|
||||
| 16 | Rab3i | Marketing | Saudi digital | Setup 3K + 20% MRR |
|
||||
| 17 | Pioneers | Digital Marketing | MENA | Setup 5K + 25% MRR |
|
||||
| 18 | Hayan | HubSpot Partner | CRM implementation | Implementation partner |
|
||||
| 19 | Digiata | Automation | Marketing automation | Implementation partner |
|
||||
| 20 | SocialEyes | Social Media | Riyadh/Dubai | Setup 3K + 20% MRR |
|
||||
|
||||
## Strategic Partners (10)
|
||||
|
||||
| # | Company | Type | Partnership Model |
|
||||
|---|---------|------|------------------|
|
||||
| 1 | Salla | E-commerce Platform | Integration + co-sell to Salla merchants |
|
||||
| 2 | Zid | E-commerce Platform | Integration + co-sell to Zid merchants |
|
||||
| 3 | Wafeq | Accounting SaaS | Cross-sell to Wafeq customers |
|
||||
| 4 | Unifonic | Communications | Unifonic customers → Dealix |
|
||||
| 5 | Tap Payments | Payment Gateway | Tap merchants → Dealix |
|
||||
| 6 | Misk Foundation | Startup Community | Misk ecosystem companies |
|
||||
| 7 | KAUST | Innovation Hub | KAUST startups |
|
||||
| 8 | STC Solutions | Enterprise IT | Enterprise referral |
|
||||
| 9 | Wamda Capital | VC | Portfolio companies |
|
||||
| 10 | Sanabil Investments | PIF VC | Portfolio companies |
|
||||
|
||||
## Warm Network (10)
|
||||
|
||||
| # | Relationship | Why | Ask |
|
||||
|---|-------------|-----|-----|
|
||||
| 1 | صديق يملك شركة SaaS | يفهم المنتج | Pilot مباشر |
|
||||
| 2 | صديق في وكالة تسويق | يبيع لعملائه | Partner |
|
||||
| 3 | زميل سابق مدير مبيعات | يعرف المشكلة | Referral |
|
||||
| 4 | قريب يملك متجر إلكتروني | واتساب كقناة | Pilot |
|
||||
| 5 | معرفة من مؤتمر تقني | شبكة واسعة | Intro |
|
||||
| 6 | صديق في عقارات | leads كثيرة | Pilot |
|
||||
| 7 | زميل دراسة في fintech | يعرف founders | Intro |
|
||||
| 8 | معرفة في F&B | سلسلة مطاعم | B2B pilot |
|
||||
| 9 | مستشار أعمال | يعرف SMEs | Referral |
|
||||
| 10 | Mentor من accelerator | شبكة founders | Intro |
|
||||
@ -1,131 +1,426 @@
|
||||
import Link from "next/link";
|
||||
import {
|
||||
MessageCircle,
|
||||
Download,
|
||||
FileText,
|
||||
CheckSquare,
|
||||
Presentation,
|
||||
Users,
|
||||
TrendingUp,
|
||||
Zap,
|
||||
Shield,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
ArrowLeft,
|
||||
ExternalLink,
|
||||
Compass,
|
||||
Calendar,
|
||||
DollarSign,
|
||||
Briefcase,
|
||||
HelpCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
export const metadata = {
|
||||
title: "Dealix — بوابة المسوّقين",
|
||||
description: "دخول مباشر، تحميلات، قوالب واتساب، وروابط العروض القطاعية.",
|
||||
title: "Dealix — برنامج شراكة الوكالات والمسوّقين",
|
||||
description:
|
||||
"حوّل وكالتك إلى ماكينة إيرادات متكررة. أضف خدمة الرد الذكي + تأهيل leads لعملائك — setup fee + MRR شهري.",
|
||||
};
|
||||
|
||||
const links = [
|
||||
const packages = [
|
||||
{
|
||||
title: "الخطة الاستراتيجية والمنافسة",
|
||||
href: "/strategy",
|
||||
desc: "لماذا Dealix، مراحل التنفيذ، وتحميل الوثيقة الكاملة.",
|
||||
icon: Compass,
|
||||
name: "الأساسية",
|
||||
setupFee: "3,000",
|
||||
monthly: "990",
|
||||
partnerShare: "30%",
|
||||
features: [
|
||||
"رد AI بالعربي على واتساب أو فورم",
|
||||
"5 أسئلة تأهيل مخصصة",
|
||||
"تصنيف leads: حار / دافئ / بارد",
|
||||
"تقرير أسبوعي",
|
||||
],
|
||||
cta: "ابدأ بأول عميل",
|
||||
highlight: false,
|
||||
},
|
||||
{
|
||||
title: "مركز الموارد (كل الروابط)",
|
||||
href: "/resources",
|
||||
desc: "ZIP، العروض، والملفات التسويقية.",
|
||||
icon: Download,
|
||||
name: "المتقدمة",
|
||||
setupFee: "7,000",
|
||||
monthly: "2,490",
|
||||
partnerShare: "25%",
|
||||
features: [
|
||||
"كل مميزات الأساسية",
|
||||
"حجز مواعيد تلقائي (Calendly)",
|
||||
"ربط CRM ثنائي (HubSpot)",
|
||||
"متابعة تلقائية Day +2 / +5 / +10",
|
||||
"تقرير يومي",
|
||||
],
|
||||
cta: "الأنسب للوكالات",
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
title: "فهرس الملفات الثابتة",
|
||||
href: "/dealix-marketing/index.html",
|
||||
desc: "نسخة HTML كاملة من بوابة الأصول.",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
title: "قوالب واتساب (نسخ ولصق)",
|
||||
href: "/dealix-marketing/marketers/whatsapp-playbook-ar.txt",
|
||||
desc: "رسائل جاهزة — عدّل الاسم والرابط فقط.",
|
||||
icon: MessageCircle,
|
||||
},
|
||||
{
|
||||
title: "قائمة تحقق الدخول",
|
||||
href: "/dealix-marketing/marketers/entry-checklist-ar.txt",
|
||||
desc: "تأكد أنك غطيت الخطوات قبل التواصل مع العملاء.",
|
||||
icon: CheckSquare,
|
||||
},
|
||||
{
|
||||
title: "العروض القطاعية (10 قطاعات)",
|
||||
href: "/dealix-presentations/00-dealix-company-master-ar.html",
|
||||
desc: "ابدأ من ملف الشركة ثم اختر رقم القطاع.",
|
||||
icon: Presentation,
|
||||
external: false,
|
||||
},
|
||||
{
|
||||
title: "هيكل العمولة (Markdown)",
|
||||
href: "/dealix-marketing/Dealix_Marketing_Arsenal.md",
|
||||
desc: "Silver / Gold / Platinum — راجع العقد الرسمي للأرقام النهائية.",
|
||||
icon: FileText,
|
||||
name: "المؤسسية",
|
||||
setupFee: "15,000+",
|
||||
monthly: "مخصص",
|
||||
partnerShare: "20-30%",
|
||||
features: [
|
||||
"كل مميزات المتقدمة",
|
||||
"قنوات متعددة (واتساب + إيميل + صوت)",
|
||||
"لوحة تحكم تنفيذية",
|
||||
"سير عمل موافقات",
|
||||
"مدير حساب مخصص",
|
||||
],
|
||||
cta: "تواصل معنا",
|
||||
highlight: false,
|
||||
},
|
||||
];
|
||||
|
||||
const workflows = [
|
||||
{
|
||||
icon: TrendingUp,
|
||||
title: "وكالة أداء (Performance)",
|
||||
desc: "تشغّل حملة → Leads تنزل → Dealix يرد فوراً → العميل يشوف ROI أعلى → يجدد → أنت تحصّل MRR",
|
||||
},
|
||||
{
|
||||
icon: Briefcase,
|
||||
title: "مسوّق مستقل (Freelancer)",
|
||||
desc: 'تقدم عرض "إدارة حملات + رد ذكي" → Dealix يشتغل بالخلفية → أنت تركز على الاستراتيجية → دخل شهري متكرر',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "مستشار CRM / RevOps",
|
||||
desc: "تنفذ HubSpot لعميل → تضيف Dealix كطبقة response → CRM يمتلئ بـ leads مؤهلة → العميل يشوف قيمة CRM أسرع",
|
||||
},
|
||||
];
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
q: "كم ياخذ الإعداد لكل عميل؟",
|
||||
a: "يوم واحد للأساسية، 3 أيام للمتقدمة.",
|
||||
},
|
||||
{
|
||||
q: "هل أحتاج خبرة تقنية؟",
|
||||
a: "لا — نحن نسوي الإعداد والتشغيل بالكامل.",
|
||||
},
|
||||
{
|
||||
q: "كم أقدر أربح من عميل واحد؟",
|
||||
a: "عميل واحد = 3,000 setup + ~990/شهر = 14,880 ريال/سنة.",
|
||||
},
|
||||
{
|
||||
q: "هل فيه عقد طويل؟",
|
||||
a: "لا — شهري لك ولعميلك. بدون التزام سنوي.",
|
||||
},
|
||||
{
|
||||
q: "ماذا لو العميل ما عجبه؟",
|
||||
a: "30 يوم ضمان استرداد كامل.",
|
||||
},
|
||||
{
|
||||
q: "هل يشتغل مع HubSpot / Zoho؟",
|
||||
a: "نعم — CRM sync متاح في الباقة المتقدمة.",
|
||||
},
|
||||
{
|
||||
q: "هل يفهم العربي السعودي؟",
|
||||
a: "نعم — مبني Arabic-first بلهجة خليجية.",
|
||||
},
|
||||
{
|
||||
q: "كيف أبدأ؟",
|
||||
a: "احجز مكالمة شراكة 20 دقيقة → نسوي pilot مجاني لأول عميل عندك.",
|
||||
},
|
||||
];
|
||||
|
||||
const CALENDLY = "https://calendly.com/sami-assiri11/dealix-demo";
|
||||
|
||||
export default function MarketersPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-950 via-slate-900 to-teal-950 text-slate-100">
|
||||
<div className="mx-auto max-w-2xl px-6 py-14">
|
||||
<p className="text-sm font-semibold text-teal-400">Dealix Partner GTM</p>
|
||||
<h1 className="mt-2 text-3xl font-bold tracking-tight">بوابة المسوّقين</h1>
|
||||
<p className="mt-3 text-slate-400 leading-relaxed">
|
||||
مسار واحد للدخول: افتح الروابط أدناه من نفس الموقع (لا حاجة لخادم 8000). انسخ قوالب
|
||||
الواتساب من الملف النصي وعدّل{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-teal-200">
|
||||
{`{الاسم}`}
|
||||
</code>{" "}
|
||||
و
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-teal-200">رابط موقعك</code>.
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-950 via-slate-900 to-slate-950 text-slate-100">
|
||||
{/* ── Hero ── */}
|
||||
<section className="mx-auto max-w-4xl px-6 pb-16 pt-20 text-center">
|
||||
<span className="inline-block rounded-full bg-amber-500/10 px-4 py-1.5 text-sm font-medium text-amber-400 ring-1 ring-amber-500/20">
|
||||
برنامج شراكة الوكالات والمسوّقين
|
||||
</span>
|
||||
<h1 className="mt-6 text-4xl font-extrabold leading-tight tracking-tight lg:text-5xl">
|
||||
حوّل وكالتك إلى{" "}
|
||||
<span className="bg-gradient-to-l from-amber-400 to-amber-600 bg-clip-text text-transparent">
|
||||
ماكينة إيرادات متكررة
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mx-auto mt-6 max-w-2xl text-lg leading-relaxed text-slate-300">
|
||||
أضف خدمة الرد الذكي + تأهيل leads لعملائك. أنت تحصّل setup fee كامل + نسبة
|
||||
شهرية متكررة. نحن نشغّل النظام.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 space-y-3">
|
||||
{links.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex gap-4 rounded-2xl border border-white/10 bg-white/5 p-5 transition hover:border-teal-500/40 hover:bg-white/10"
|
||||
>
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-teal-500/20 text-teal-300">
|
||||
<item.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 font-semibold text-white">
|
||||
{item.title}
|
||||
<ExternalLink className="h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-slate-400">{item.desc}</p>
|
||||
<p className="mt-2 truncate font-mono text-xs text-teal-500/80">{item.href}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-4">
|
||||
<a
|
||||
href={CALENDLY}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-amber-500 px-8 py-3.5 text-base font-bold text-slate-900 transition hover:bg-amber-400"
|
||||
>
|
||||
<Calendar className="h-5 w-5" />
|
||||
احجز مكالمة شراكة
|
||||
</a>
|
||||
<a
|
||||
href="#packages"
|
||||
className="inline-flex items-center gap-2 rounded-xl border border-white/20 px-8 py-3.5 text-base font-medium transition hover:bg-white/10"
|
||||
>
|
||||
شوف الباقات
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mt-12 rounded-2xl border border-teal-500/30 bg-teal-950/40 p-6">
|
||||
<h2 className="flex items-center gap-2 text-lg font-semibold text-teal-200">
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
تلميح واتساب سريع
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-relaxed text-slate-300">
|
||||
احفظ رسالة واحدة كقالب في واتساب (الأجهزة المدعومة) أو استخدم ملاحظات سريعة. لا ترسل
|
||||
لعملاء نهائيين دون تنسيق مع فريق Dealix وحسب سياسة الاستخدام.
|
||||
{/* ── Problem ── */}
|
||||
<section className="border-y border-white/10 bg-slate-900/50 py-16">
|
||||
<div className="mx-auto max-w-3xl px-6 text-center">
|
||||
<h2 className="text-2xl font-bold">المشكلة اللي تعرفها</h2>
|
||||
<p className="mt-4 text-lg leading-relaxed text-slate-300">
|
||||
عملاءك يصرفون <strong className="text-white">10,000+ ريال/شهر</strong> على
|
||||
إعلانات ويجيبون leads. لكن{" "}
|
||||
<strong className="text-amber-400">70% تضيع</strong> لأن:
|
||||
</p>
|
||||
<div className="mt-6 grid gap-4 text-start sm:grid-cols-3">
|
||||
{[
|
||||
{ icon: Clock, text: "ما أحد يرد خلال أول ساعة" },
|
||||
{ icon: MessageCircle, text: "المتابعة يدوية وعشوائية" },
|
||||
{ icon: Users, text: "فريق العميل مشغول" },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.text}
|
||||
className="flex items-start gap-3 rounded-xl border border-white/10 bg-white/5 p-4"
|
||||
>
|
||||
<item.icon className="mt-0.5 h-5 w-5 shrink-0 text-red-400" />
|
||||
<span className="text-sm text-slate-300">{item.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-6 text-base text-slate-400">
|
||||
العميل يلوم الوكالة. الحقيقة: المشكلة مو في الإعلان — المشكلة في الرد.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mt-10 flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-sm text-teal-400 hover:text-teal-300"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 rotate-180" />
|
||||
الصفحة الرئيسية
|
||||
{/* ── Solution ── */}
|
||||
<section className="py-16">
|
||||
<div className="mx-auto max-w-3xl px-6 text-center">
|
||||
<h2 className="text-2xl font-bold">
|
||||
Dealix يعطيك الحل{" "}
|
||||
<span className="text-amber-400">تبيعه لعملائك</span>
|
||||
</h2>
|
||||
<div className="mt-8 grid gap-6 text-start sm:grid-cols-2">
|
||||
{[
|
||||
"AI يرد بالعربي خلال 45 ثانية على كل lead",
|
||||
"يسأل أسئلة التأهيل (ميزانية؟ جدية؟ موعد؟)",
|
||||
"يحجز اجتماع أو يحوّل للمبيعات",
|
||||
"يرسل تقرير يومي عن كل lead",
|
||||
].map((text) => (
|
||||
<div key={text} className="flex items-start gap-3">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 shrink-0 text-emerald-400" />
|
||||
<span className="text-slate-300">{text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-8 rounded-xl border border-amber-500/20 bg-amber-500/5 p-4 text-base text-amber-200">
|
||||
أنت تبيع الخدمة. نحن نشغّل النظام. تحصّل setup fee + نسبة شهرية.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Workflows ── */}
|
||||
<section className="border-y border-white/10 bg-slate-900/50 py-16">
|
||||
<div className="mx-auto max-w-4xl px-6">
|
||||
<h2 className="text-center text-2xl font-bold">كيف تشتغل حسب نوعك</h2>
|
||||
<div className="mt-10 grid gap-6 sm:grid-cols-3">
|
||||
{workflows.map((w) => (
|
||||
<div
|
||||
key={w.title}
|
||||
className="rounded-2xl border border-white/10 bg-white/5 p-6"
|
||||
>
|
||||
<w.icon className="h-8 w-8 text-amber-400" />
|
||||
<h3 className="mt-4 text-lg font-semibold">{w.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-slate-400">{w.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Packages ── */}
|
||||
<section id="packages" className="py-16">
|
||||
<div className="mx-auto max-w-5xl px-6">
|
||||
<h2 className="text-center text-2xl font-bold">
|
||||
باقات تبيعها لعملائك
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-slate-400">
|
||||
Setup fee كامل لك + نسبة شهرية متكررة
|
||||
</p>
|
||||
<div className="mt-10 grid gap-6 lg:grid-cols-3">
|
||||
{packages.map((pkg) => (
|
||||
<div
|
||||
key={pkg.name}
|
||||
className={`relative flex flex-col rounded-2xl border p-6 ${
|
||||
pkg.highlight
|
||||
? "border-amber-500/50 bg-amber-500/5 ring-1 ring-amber-500/20"
|
||||
: "border-white/10 bg-white/5"
|
||||
}`}
|
||||
>
|
||||
{pkg.highlight && (
|
||||
<span className="absolute -top-3 start-4 rounded-full bg-amber-500 px-3 py-0.5 text-xs font-bold text-slate-900">
|
||||
الأكثر طلباً
|
||||
</span>
|
||||
)}
|
||||
<h3 className="text-xl font-bold">{pkg.name}</h3>
|
||||
<div className="mt-3">
|
||||
<span className="text-sm text-slate-400">Setup fee لك:</span>
|
||||
<span className="ms-2 text-2xl font-extrabold text-amber-400">
|
||||
{pkg.setupFee}
|
||||
</span>
|
||||
<span className="text-sm text-slate-400"> ريال</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<span className="text-sm text-slate-400">العميل يدفع:</span>
|
||||
<span className="ms-2 font-bold text-white">{pkg.monthly}</span>
|
||||
<span className="text-sm text-slate-400"> ريال/شهر</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<span className="text-sm text-slate-400">نصيبك الشهري:</span>
|
||||
<span className="ms-2 font-bold text-emerald-400">
|
||||
{pkg.partnerShare}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-4 flex-1 space-y-2">
|
||||
{pkg.features.map((f) => (
|
||||
<li key={f} className="flex items-start gap-2 text-sm text-slate-300">
|
||||
<CheckCircle className="mt-0.5 h-4 w-4 shrink-0 text-emerald-500" />
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a
|
||||
href={CALENDLY}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`mt-6 block rounded-xl py-3 text-center font-bold transition ${
|
||||
pkg.highlight
|
||||
? "bg-amber-500 text-slate-900 hover:bg-amber-400"
|
||||
: "border border-white/20 text-white hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
{pkg.cta}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Revenue Calculator ── */}
|
||||
<section className="border-y border-white/10 bg-slate-900/50 py-16">
|
||||
<div className="mx-auto max-w-3xl px-6 text-center">
|
||||
<DollarSign className="mx-auto h-10 w-10 text-amber-400" />
|
||||
<h2 className="mt-4 text-2xl font-bold">كم تقدر تربح؟</h2>
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div className="text-3xl font-extrabold text-amber-400">14,880</div>
|
||||
<div className="mt-1 text-sm text-slate-400">ريال/سنة من عميل واحد</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div className="text-3xl font-extrabold text-amber-400">74,400</div>
|
||||
<div className="mt-1 text-sm text-slate-400">ريال/سنة من 5 عملاء</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div className="text-3xl font-extrabold text-amber-400">223,800</div>
|
||||
<div className="mt-1 text-sm text-slate-400">ريال/سنة كشريك نشط</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-slate-500">
|
||||
بناءً على: setup 3,000 ريال + 990 ريال/شهر × 30% نصيبك
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Trust / Proof ── */}
|
||||
<section className="py-16">
|
||||
<div className="mx-auto max-w-3xl px-6">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{[
|
||||
{ icon: Shield, text: "متوافق مع نظام حماية البيانات (PDPL)" },
|
||||
{ icon: Zap, text: "مبني للسوق السعودي — عربي أولاً" },
|
||||
{ icon: Clock, text: "بدون عقد سنوي — شهري" },
|
||||
{ icon: CheckCircle, text: "ضمان استرداد 30 يوم" },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.text}
|
||||
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/5 p-4"
|
||||
>
|
||||
<item.icon className="h-5 w-5 shrink-0 text-emerald-400" />
|
||||
<span className="text-sm text-slate-300">{item.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── FAQ ── */}
|
||||
<section className="border-y border-white/10 bg-slate-900/50 py-16">
|
||||
<div className="mx-auto max-w-3xl px-6">
|
||||
<h2 className="flex items-center justify-center gap-2 text-2xl font-bold">
|
||||
<HelpCircle className="h-6 w-6 text-amber-400" />
|
||||
أسئلة شائعة
|
||||
</h2>
|
||||
<div className="mt-8 space-y-4">
|
||||
{faqs.map((faq) => (
|
||||
<div
|
||||
key={faq.q}
|
||||
className="rounded-xl border border-white/10 bg-white/5 p-5"
|
||||
>
|
||||
<h3 className="font-semibold text-white">{faq.q}</h3>
|
||||
<p className="mt-2 text-sm text-slate-400">{faq.a}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Final CTA ── */}
|
||||
<section className="py-20">
|
||||
<div className="mx-auto max-w-2xl px-6 text-center">
|
||||
<h2 className="text-3xl font-extrabold">
|
||||
ابدأ بأول عميل —{" "}
|
||||
<span className="text-amber-400">مجاناً</span>
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-slate-300">
|
||||
نسوي pilot 7 أيام لأول عميل عندك بدون تكلفة.
|
||||
<br />
|
||||
لو اشتغل — تبدأ تحصّل. لو ما اشتغل — لا التزام.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-4">
|
||||
<a
|
||||
href={CALENDLY}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-amber-500 px-8 py-4 text-lg font-bold text-slate-900 transition hover:bg-amber-400"
|
||||
>
|
||||
<Calendar className="h-5 w-5" />
|
||||
احجز مكالمة شراكة
|
||||
</a>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-slate-500">
|
||||
20 دقيقة • بدون التزام • نسوي pilot لأول عميل مجاناً
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Footer nav ── */}
|
||||
<footer className="border-t border-white/10 py-8">
|
||||
<div className="mx-auto flex max-w-4xl flex-wrap items-center justify-center gap-6 px-6 text-sm text-slate-500">
|
||||
<Link href="/" className="hover:text-teal-400">
|
||||
الرئيسية
|
||||
</Link>
|
||||
<Link href="/resources" className="text-sm text-teal-400 hover:text-teal-300">
|
||||
<Link href="/resources" className="hover:text-teal-400">
|
||||
الموارد
|
||||
</Link>
|
||||
<Link href="/dashboard" className="text-sm text-teal-400 hover:text-teal-300">
|
||||
<Link href="/dashboard" className="hover:text-teal-400">
|
||||
المنصة
|
||||
</Link>
|
||||
<Link href="/privacy" className="hover:text-teal-400">
|
||||
الخصوصية
|
||||
</Link>
|
||||
<Link href="/terms" className="hover:text-teal-400">
|
||||
الشروط
|
||||
</Link>
|
||||
<span>© {new Date().getFullYear()} Dealix</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
176
salesflow-saas/landing/trial-signup.html
Normal file
176
salesflow-saas/landing/trial-signup.html
Normal file
@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dealix — جرّب مندوب المبيعات الذكي مجاناً</title>
|
||||
<meta name="description" content="ديلكس يرد على عملائك بالعربي 24/7، يأهلهم، ويحجز المواعيد. جرّب 7 أيام مجاناً.">
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:'Segoe UI',Tahoma,sans-serif;background:#0f172a;color:#fff;min-height:100vh}
|
||||
.hero{max-width:700px;margin:0 auto;padding:40px 20px;text-align:center}
|
||||
.badge{display:inline-block;background:rgba(245,158,11,.15);color:#f59e0b;padding:6px 16px;border-radius:20px;font-size:14px;margin-bottom:24px;border:1px solid rgba(245,158,11,.3)}
|
||||
h1{font-size:clamp(28px,5vw,48px);line-height:1.3;margin-bottom:16px;font-weight:800}
|
||||
h1 span{background:linear-gradient(to left,#f59e0b,#f97316);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.sub{font-size:18px;color:#94a3b8;margin-bottom:32px;line-height:1.6}
|
||||
.form-box{background:#1e293b;border-radius:16px;padding:32px;text-align:right;max-width:500px;margin:0 auto}
|
||||
.form-box h2{font-size:22px;margin-bottom:8px}
|
||||
.form-box p{color:#94a3b8;font-size:14px;margin-bottom:24px}
|
||||
label{display:block;font-size:14px;color:#cbd5e1;margin-bottom:6px;margin-top:16px}
|
||||
input,select,textarea{width:100%;padding:12px;border-radius:8px;border:1px solid #334155;background:#0f172a;color:#fff;font-size:16px}
|
||||
input:focus,select:focus,textarea:focus{outline:none;border-color:#f59e0b}
|
||||
textarea{height:80px;resize:vertical}
|
||||
.btn{display:block;width:100%;padding:14px;background:#f59e0b;color:#0f172a;border:none;border-radius:10px;font-size:18px;font-weight:700;cursor:pointer;margin-top:24px;transition:.2s}
|
||||
.btn:hover{background:#fbbf24}
|
||||
.btn:disabled{opacity:.5;cursor:not-allowed}
|
||||
.trust{display:flex;gap:16px;justify-content:center;margin-top:24px;font-size:13px;color:#64748b;flex-wrap:wrap}
|
||||
.success{display:none;text-align:center;padding:40px}
|
||||
.success h2{color:#10b981;font-size:28px;margin-bottom:16px}
|
||||
.success p{color:#94a3b8;font-size:16px;line-height:1.8}
|
||||
.success a{color:#f59e0b;text-decoration:none;font-weight:700;font-size:20px;display:inline-block;margin-top:20px;padding:14px 32px;background:rgba(245,158,11,.15);border-radius:12px;border:1px solid rgba(245,158,11,.3)}
|
||||
.stats{display:flex;gap:24px;justify-content:center;margin:32px 0;flex-wrap:wrap}
|
||||
.stat{text-align:center}
|
||||
.stat .num{font-size:32px;font-weight:800;color:#f59e0b}
|
||||
.stat .lbl{font-size:13px;color:#64748b;margin-top:4px}
|
||||
.features{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin:32px 0;text-align:right}
|
||||
.feat{background:#1e293b;padding:16px;border-radius:12px}
|
||||
.feat h3{font-size:15px;margin-bottom:6px;color:#f59e0b}
|
||||
.feat p{font-size:13px;color:#94a3b8;line-height:1.5}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hero">
|
||||
<div class="badge">🇸🇦 صُنع للسوق السعودي • 7 أيام مجاناً</div>
|
||||
<h1>موظف مبيعات ذكي<br><span>يقفل لك الصفقات وأنت نايم</span></h1>
|
||||
<p class="sub">يرد على عملائك بالعربي خلال 45 ثانية، يأهلهم، يحجز المواعيد، ويجهّز فريقك للإقفال.</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat"><div class="num">45</div><div class="lbl">ثانية متوسط الرد</div></div>
|
||||
<div class="stat"><div class="num">24/7</div><div class="lbl">بدون توقف</div></div>
|
||||
<div class="stat"><div class="num">73%</div><div class="lbl">عملاء يتوقعون رد سريع</div></div>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<div class="feat"><h3>رد فوري بالعربي</h3><p>يرد على واتساب وإنستقرام والموقع بلهجة سعودية طبيعية</p></div>
|
||||
<div class="feat"><h3>تأهيل ذكي</h3><p>يسأل أسئلة التأهيل الصحيحة ويفرز الجادين</p></div>
|
||||
<div class="feat"><h3>حجز تلقائي</h3><p>يحجز المواعيد مباشرة في تقويمك</p></div>
|
||||
<div class="feat"><h3>تقارير يومية</h3><p>يعطيك تقرير مختصر عن كل فرصة وتقدّم</p></div>
|
||||
</div>
|
||||
|
||||
<div id="formSection" class="form-box">
|
||||
<h2>ابدأ تجربة مجانية 7 أيام</h2>
|
||||
<p>بدون بطاقة ائتمان • بدون التزام • إلغاء أي وقت</p>
|
||||
<form id="leadForm">
|
||||
<label>الاسم الكامل *</label>
|
||||
<input type="text" name="name" required placeholder="مثال: سامي العسيري">
|
||||
|
||||
<label>اسم الشركة *</label>
|
||||
<input type="text" name="company" required placeholder="مثال: شركة النمو للتقنية">
|
||||
|
||||
<label>البريد الإلكتروني *</label>
|
||||
<input type="email" name="email" required placeholder="sami@company.com">
|
||||
|
||||
<label>رقم الجوال (واتساب) *</label>
|
||||
<input type="tel" name="phone" required placeholder="+966 5XX XXX XXXX" dir="ltr">
|
||||
|
||||
<label>القطاع</label>
|
||||
<select name="sector">
|
||||
<option value="">اختر القطاع</option>
|
||||
<option value="saas">SaaS / تقنية</option>
|
||||
<option value="ecommerce">تجارة إلكترونية</option>
|
||||
<option value="realestate">عقارات</option>
|
||||
<option value="fnb">مطاعم / أغذية</option>
|
||||
<option value="services">خدمات B2B</option>
|
||||
<option value="agency">وكالة تسويق / إعلان</option>
|
||||
<option value="logistics">لوجستيك / توصيل</option>
|
||||
<option value="education">تعليم / تدريب</option>
|
||||
<option value="healthcare">صحة / طب</option>
|
||||
<option value="fintech">تقنية مالية</option>
|
||||
<option value="other">قطاع آخر</option>
|
||||
</select>
|
||||
|
||||
<label>كم lead تستقبلون شهرياً (تقريبياً)؟</label>
|
||||
<select name="leads_monthly">
|
||||
<option value="">اختر</option>
|
||||
<option value="0-50">أقل من 50</option>
|
||||
<option value="50-200">50 - 200</option>
|
||||
<option value="200-500">200 - 500</option>
|
||||
<option value="500+">أكثر من 500</option>
|
||||
</select>
|
||||
|
||||
<label>أكبر تحدي في المبيعات حالياً</label>
|
||||
<textarea name="challenge" placeholder="مثال: الرد بطيء على العملاء، متابعة ضعيفة..."></textarea>
|
||||
|
||||
<button type="submit" class="btn" id="submitBtn">ابدأ التجربة المجانية →</button>
|
||||
</form>
|
||||
<div class="trust">
|
||||
<span>✓ بدون بطاقة ائتمان</span>
|
||||
<span>✓ إعداد 10 دقائق</span>
|
||||
<span>✓ إلغاء أي وقت</span>
|
||||
<span>✓ متوافق PDPL</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="successSection" class="success">
|
||||
<h2>تم التسجيل بنجاح! 🎉</h2>
|
||||
<p>شكراً <strong id="userName"></strong>!<br>
|
||||
فريقنا بيتواصل معك خلال ساعتين على واتساب لبدء الإعداد.<br><br>
|
||||
أو احجز موعد Demo مباشرة:</p>
|
||||
<a href="https://calendly.com/sami-assiri11/dealix-demo" target="_blank">📅 احجز Demo الآن</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('leadForm');
|
||||
const formSection = document.getElementById('formSection');
|
||||
const successSection = document.getElementById('successSection');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'جاري التسجيل...';
|
||||
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
data.source = 'landing_page';
|
||||
data.timestamp = new Date().toISOString();
|
||||
data.page_url = window.location.href;
|
||||
|
||||
// Try Railway backend
|
||||
try {
|
||||
await fetch('https://web-dealix.up.railway.app/api/v1/prospect/demo', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} catch(e) { /* silent — manual follow-up */ }
|
||||
|
||||
// Log to PostHog if available
|
||||
try {
|
||||
if (window.posthog) {
|
||||
window.posthog.capture('lead_captured', data);
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Store locally as backup
|
||||
const leads = JSON.parse(localStorage.getItem('dealix_leads') || '[]');
|
||||
leads.push(data);
|
||||
localStorage.setItem('dealix_leads', JSON.stringify(leads));
|
||||
|
||||
// Send notification email via mailto fallback
|
||||
const subject = encodeURIComponent(`Dealix Lead: ${data.company} - ${data.name}`);
|
||||
const body = encodeURIComponent(
|
||||
`New Lead!\n\nName: ${data.name}\nCompany: ${data.company}\nEmail: ${data.email}\nPhone: ${data.phone}\nSector: ${data.sector}\nLeads/mo: ${data.leads_monthly}\nChallenge: ${data.challenge}\nTime: ${data.timestamp}`
|
||||
);
|
||||
|
||||
// Show success
|
||||
document.getElementById('userName').textContent = data.name.split(' ')[0];
|
||||
formSection.style.display = 'none';
|
||||
successSection.style.display = 'block';
|
||||
|
||||
// Open mailto as notification (optional)
|
||||
// window.open(`mailto:sami.assiri11@gmail.com?subject=${subject}&body=${body}`);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user