mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-20 08:19:34 +00:00
feat(dealix): D0 launch hardening + Railway fix + competitive analysis
26/26 tests. 13/33 launch gates closed. Railway 5.7GB→~2GB. Spectrum analysis complete.
This commit is contained in:
commit
f75e7c331e
48
.github/ISSUE_TEMPLATE/new-prompt.yml
vendored
Normal file
48
.github/ISSUE_TEMPLATE/new-prompt.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: "New AI Tool Prompt"
|
||||
description: "Submit or request a new AI tool's system prompt"
|
||||
title: "[New] "
|
||||
labels: ["new-prompt"]
|
||||
body:
|
||||
- type: input
|
||||
id: tool_name
|
||||
attributes:
|
||||
label: Tool Name
|
||||
placeholder: "e.g., Cursor, Devin, GitHub Copilot"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: tool_url
|
||||
attributes:
|
||||
label: Tool URL
|
||||
placeholder: "https://..."
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: Submission Type
|
||||
options:
|
||||
- "I have the prompt and will submit a PR"
|
||||
- "I found a prompt but need help formatting"
|
||||
- "Requesting someone to extract this prompt"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: prompt_content
|
||||
attributes:
|
||||
label: Prompt Content (if you have it)
|
||||
description: "Paste the system prompt here, or describe where you found it"
|
||||
render: markdown
|
||||
- type: checkboxes
|
||||
id: includes
|
||||
attributes:
|
||||
label: What's included?
|
||||
options:
|
||||
- label: System prompt
|
||||
- label: Tool/function definitions
|
||||
- label: Model configuration
|
||||
- type: input
|
||||
id: date_captured
|
||||
attributes:
|
||||
label: Date Captured
|
||||
placeholder: "2026-04-23"
|
||||
29
.github/ISSUE_TEMPLATE/update-prompt.yml
vendored
Normal file
29
.github/ISSUE_TEMPLATE/update-prompt.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: "Update Existing Prompt"
|
||||
description: "Report that an existing AI tool's prompt has changed"
|
||||
title: "[Update] "
|
||||
labels: ["update"]
|
||||
body:
|
||||
- type: input
|
||||
id: tool_name
|
||||
attributes:
|
||||
label: Tool Name
|
||||
placeholder: "e.g., Cursor, Claude Code"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: changes
|
||||
attributes:
|
||||
label: What Changed?
|
||||
description: "Describe what's different in the new version"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: new_prompt
|
||||
attributes:
|
||||
label: Updated Prompt Content
|
||||
render: markdown
|
||||
- type: input
|
||||
id: date_captured
|
||||
attributes:
|
||||
label: Date of New Version
|
||||
placeholder: "2026-04-23"
|
||||
21
.github/pull_request_template.md
vendored
Normal file
21
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
## What's in this PR?
|
||||
|
||||
- [ ] New AI tool prompt
|
||||
- [ ] Updated existing prompt
|
||||
- [ ] Other improvement
|
||||
|
||||
## Tool Details
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Tool Name | |
|
||||
| Source | |
|
||||
| Date Captured | |
|
||||
| Includes Tools/Functions | Yes / No |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Prompt is the real system prompt (not a guess)
|
||||
- [ ] No API keys or credentials included
|
||||
- [ ] Files are in a properly named folder
|
||||
- [ ] PR title follows format: `Add: [Tool]` or `Update: [Tool]`
|
||||
6
.github/workflows/dealix-ci.yml
vendored
6
.github/workflows/dealix-ci.yml
vendored
@ -25,6 +25,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt -r requirements-dev.txt
|
||||
- name: Architecture Brief (governance validation)
|
||||
working-directory: salesflow-saas
|
||||
run: python scripts/architecture_brief.py
|
||||
- name: Release Readiness Matrix (Tier-1 gate)
|
||||
working-directory: salesflow-saas
|
||||
run: python scripts/release_readiness_matrix.py
|
||||
- name: Pytest (full suite + launch scenarios)
|
||||
env:
|
||||
DATABASE_URL: sqlite+aiosqlite:///./ci_dealix.db
|
||||
|
||||
27
.github/workflows/truth-validation.yml
vendored
Normal file
27
.github/workflows/truth-validation.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: Truth Registry Validation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "salesflow-saas/docs/registry/**"
|
||||
- "salesflow-saas/commercial/claims_registry.yaml"
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "salesflow-saas/docs/registry/**"
|
||||
- "salesflow-saas/commercial/claims_registry.yaml"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install PyYAML
|
||||
run: pip install pyyaml
|
||||
- name: Validate Truth Registry
|
||||
working-directory: salesflow-saas
|
||||
run: python scripts/validate_truth_registry.py
|
||||
@ -81,7 +81,24 @@ Organize into:
|
||||
- **Infrastructure** — deployment, CI/CD, config changes
|
||||
- **Breaking Changes** — anything requiring migration or config updates
|
||||
|
||||
### 10. Pre-release Summary
|
||||
### 10. OWASP LLM Top 10 Review
|
||||
Verify controls for each OWASP LLM risk:
|
||||
- **LLM01 Prompt Injection**: Input sanitization active? System prompts isolated?
|
||||
- **LLM02 Insecure Output**: All critical outputs validated via Pydantic schemas?
|
||||
- **LLM04 Model DoS**: Rate limiting (slowapi) + timeout configured?
|
||||
- **LLM05 Supply Chain**: Only approved LLM providers in model_router?
|
||||
- **LLM06 Sensitive Info**: No PII in prompts? Audit trail for AI conversations?
|
||||
- **LLM07 Insecure Plugins**: All plugins go through OpenClaw policy gate?
|
||||
- **LLM08 Excessive Agency**: Class B/C enforcement active for sensitive actions?
|
||||
- **LLM09 Overreliance**: HITL required for all external commitments?
|
||||
|
||||
### 11. Architecture Brief Validation
|
||||
```bash
|
||||
cd .. && python scripts/architecture_brief.py
|
||||
```
|
||||
Must pass 40/40 checks. If any fail, block the release.
|
||||
|
||||
### 12. Pre-release Summary
|
||||
Output a go/no-go decision with:
|
||||
- Test results (pass/fail count)
|
||||
- Security findings
|
||||
|
||||
@ -111,9 +111,23 @@ MICROSOFT_CLIENT_SECRET=
|
||||
PAYMENT_PROVIDER=moyasar
|
||||
MOYASAR_API_KEY=
|
||||
MOYASAR_PUBLISHABLE_KEY=
|
||||
MOYASAR_SECRET_KEY=
|
||||
MOYASAR_WEBHOOK_SECRET=
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_PUBLISHABLE_KEY=
|
||||
|
||||
# ── Analytics (PostHog) ──────────────────────
|
||||
POSTHOG_API_KEY=
|
||||
POSTHOG_HOST=https://eu.i.posthog.com
|
||||
|
||||
# ── DLQ Configuration ───────────────────────
|
||||
DLQ_MAX_RETRIES=5
|
||||
DLQ_DRAIN_BATCH_SIZE=10
|
||||
|
||||
# ── Calendly ─────────────────────────────────
|
||||
CALENDLY_PAT=
|
||||
CALENDLY_WEBHOOK_SECRET=
|
||||
|
||||
# ── Agent Configuration ───────────────────────
|
||||
AGENT_PROMPTS_DIR=ai-agents/prompts
|
||||
AGENT_MAX_CONCURRENT=10
|
||||
|
||||
7
salesflow-saas/.gitleaksignore
Normal file
7
salesflow-saas/.gitleaksignore
Normal file
@ -0,0 +1,7 @@
|
||||
# Gitleaks ignore file — false positives only
|
||||
# Format: fingerprint or path:line
|
||||
# Last verified: 2026-04-17
|
||||
|
||||
# False positive: model name "llama-3.1-70b-versatile" matches generic-api-key regex
|
||||
# This is a Groq model identifier, not an API key
|
||||
personal-brand-engine/tests/test_llm_client.py
|
||||
36
salesflow-saas/.pre-commit-config.yaml
Normal file
36
salesflow-saas/.pre-commit-config.yaml
Normal file
@ -0,0 +1,36 @@
|
||||
# Pre-commit hooks — run before every commit
|
||||
# Install: pip install pre-commit && pre-commit install
|
||||
repos:
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.20.1
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: detect-private-key
|
||||
- id: detect-aws-credentials
|
||||
args: ['--allow-missing-credentials']
|
||||
- id: check-added-large-files
|
||||
args: ['--maxkb=1000']
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
files: ^salesflow-saas/backend/
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: truth-registry-validator
|
||||
name: Validate TRUTH.yaml
|
||||
entry: python salesflow-saas/scripts/validate_truth_registry.py
|
||||
language: system
|
||||
files: ^salesflow-saas/docs/registry/TRUTH\.yaml$
|
||||
pass_filenames: false
|
||||
@ -120,3 +120,27 @@ cd frontend && npm run dev
|
||||
5. Deploy to production with canary (10%)
|
||||
6. Monitor 30 min → full rollout
|
||||
7. Rollback plan documented per release
|
||||
|
||||
## Governance Integration (Tier-1)
|
||||
|
||||
All agents operate under the governance framework defined in `MASTER_OPERATING_PROMPT.md`:
|
||||
|
||||
- **Trust Plane**: Every agent action is classified as A/B/C via `openclaw/policy.py`. Class B actions (messaging, payments, CRM sync) require approval tokens.
|
||||
- **Evidence Packs**: Agent outputs logged to `ai_conversations` contribute to evidence pack assembly.
|
||||
- **Contradiction Detection**: Agent-generated content is subject to contradiction checks against governance docs.
|
||||
- **Structured Outputs**: All critical agent outputs use defined schemas (LeadScoreCard, QualificationMemo, ProposalPack, etc.).
|
||||
|
||||
### New Tier-1 API Surfaces
|
||||
- `GET /api/v1/executive-room/snapshot` — Executive Room
|
||||
- `GET /api/v1/contradictions/` — Contradiction Engine
|
||||
- `GET /api/v1/evidence-packs/` — Evidence Pack Viewer
|
||||
- `GET /api/v1/approval-center/` — Approval Center
|
||||
- `GET /api/v1/connectors/governance` — Connector Governance
|
||||
- `GET /api/v1/model-routing/dashboard` — Model Routing
|
||||
- `GET /api/v1/compliance/matrix/` — Saudi Compliance Matrix
|
||||
- `GET /api/v1/forecast-control/unified` — Actual vs Forecast
|
||||
|
||||
### Architecture Preflight
|
||||
```bash
|
||||
python scripts/architecture_brief.py # Run from salesflow-saas/ root
|
||||
```
|
||||
|
||||
@ -1,100 +1,432 @@
|
||||
# CLAUDE.md — Dealix Project Context for AI Agents
|
||||
# CLAUDE.md — Dealix Repository Instructions for Coding Agents
|
||||
|
||||
## Quick Context
|
||||
Dealix is an AI-powered CRM built for the Saudi market. It combines Salesforce-grade AI with WhatsApp-first communication, PDPL compliance, and Arabic-first UX.
|
||||
> **This file is the source of truth for how Claude Code, Cursor, Codex, or any coding agent must behave when working in the Dealix repository.**
|
||||
> Read it fully at the start of every session.
|
||||
> Do not infer, extrapolate, or override anything in this file without explicit founder approval given in the current session.
|
||||
> Place this file at the repository root as `CLAUDE.md`. Also copy to `.cursorrules` for Cursor compatibility.
|
||||
|
||||
## Key Directories
|
||||
- `backend/app/api/v1/` — API routes (FastAPI)
|
||||
- `backend/app/models/` — SQLAlchemy models
|
||||
- `backend/app/services/` — Business logic layer
|
||||
- `backend/app/services/ai/` — AI engine (Arabic NLP, scoring, forecasting)
|
||||
- `backend/app/services/pdpl/` — PDPL compliance engine
|
||||
- `backend/app/services/cpq/` — Configure, Price, Quote
|
||||
- `backend/app/services/agents/` — Multi-agent orchestration
|
||||
- `backend/app/services/llm/` — LLM provider abstraction
|
||||
- `backend/app/workers/` — Celery async tasks
|
||||
- `backend/app/integrations/` — WhatsApp, Email, SMS adapters
|
||||
- `frontend/src/app/` — Next.js pages
|
||||
- `seeds/` — Industry templates (JSON)
|
||||
- `memory/` — Project knowledge base
|
||||
---
|
||||
|
||||
## Database
|
||||
- PostgreSQL 16 with async driver (asyncpg)
|
||||
- Multi-tenant: every table has `tenant_id`
|
||||
- Alembic for migrations
|
||||
- Money fields use `Numeric` type (never Float)
|
||||
## 1. WHO IS THIS FILE FOR
|
||||
|
||||
## AI Architecture
|
||||
- Provider abstraction: Groq → OpenAI fallback
|
||||
- Model router: task-specific model selection
|
||||
- Arabic NLP: intent, sentiment, entity extraction
|
||||
- Lead scoring: 0-100 composite score
|
||||
- Conversation intelligence: Arabic dialogue analysis
|
||||
- Sales agent: autonomous WhatsApp qualification bot
|
||||
Any AI coding agent (Claude Code, Cursor, Codex, Windsurf, Qoder, etc.) working in the Dealix codebase.
|
||||
|
||||
## PDPL Compliance (Critical)
|
||||
- Check consent before ANY outbound message
|
||||
- Track consent purpose, channel, timestamp
|
||||
- Support data subject rights (access, correct, delete)
|
||||
- Audit trail for all consent changes
|
||||
- Auto-expire consent after 12 months
|
||||
- Penalty: up to SAR 5 million per violation
|
||||
If you are a human reading this: this is the agent's operating manual. You (founder or engineer) can read it but should not mentally apply it to yourself — your job is customer conversations, not code constraints.
|
||||
|
||||
## Testing
|
||||
```bash
|
||||
pytest -v # All tests
|
||||
pytest tests/test_ai/ -v # AI engine tests
|
||||
pytest tests/test_pdpl/ -v # PDPL compliance tests
|
||||
pytest tests/test_api/ -v # API endpoint tests
|
||||
---
|
||||
|
||||
## 2. CURRENT PROJECT PHASE (as of commit `3ef6265`)
|
||||
|
||||
**Phase**: Discovery Phase (Execution Waves §3, Weeks 4–12).
|
||||
**State**: Phase 1 foundation complete. Phase 2 foundation scaffolded. Verification Protocol scaffolded. Founder Decision artifacts scaffolded. Customer Validation templates scaffolded.
|
||||
**Gate**: `Phase Gate` (Execution Waves §3.4, Week 12) has NOT been passed.
|
||||
**Customers paying**: 0 (as of this writing).
|
||||
|
||||
**Hard rule**: Wave A, Wave B, Wave C, Wave D, and Wave E tasks from `DEALIX_PHASE2_BLUEPRINT.md` and `DEALIX_PHASE2_EXECUTION_WAVES.md` are FORBIDDEN until Phase Gate returns Green.
|
||||
|
||||
Phase Gate returns Green only when ALL of these are true, proven by external evidence:
|
||||
|
||||
- [ ] 3+ customers have signed paid pilot agreements and paid.
|
||||
- [ ] At least 2 pilots in active daily use for ≥ 30 days.
|
||||
- [ ] Pentest engagement completed, report received, no open Critical findings.
|
||||
- [ ] Truth Registry audit (TASK-V005): 100% SUPPORTED claims.
|
||||
- [ ] NPS from pilot users measured, ≥ 30.
|
||||
- [ ] At least 1 customer willing to be named reference.
|
||||
|
||||
If the founder asks you to skip or override any gate, **refuse and direct them to `DEALIX_PHASE2_EXECUTION_WAVES.md` §3.4**.
|
||||
|
||||
---
|
||||
|
||||
## 3. ALLOWED WORK TYPES (narrow list — everything else is forbidden)
|
||||
|
||||
During Discovery Phase, you may ONLY do the following:
|
||||
|
||||
### 3.1 Customer-Triggered Bug Fixes
|
||||
Bugs that were surfaced during a documented customer demo, pilot session, or interview log in `docs/customer_learnings/`. The fix must reference the specific interview ID.
|
||||
|
||||
### 3.2 UX Polish with 2+ Customer Signal
|
||||
UI/UX improvements mentioned by ≥ 2 distinct customers in `docs/customer_learnings/friction_log.md`. Single-customer requests wait.
|
||||
|
||||
### 3.3 Security Remediation
|
||||
Fixes for findings from external pentest, gitleaks/trufflehog scans, Dependabot, or `pip-audit` / `npm audit` with CVSS ≥ 7.0.
|
||||
|
||||
### 3.4 Verification Protocol Execution
|
||||
Running V001–V007 from `DEALIX_PHASE2_EXECUTION_WAVES.md` and publishing results to `docs/internal/`.
|
||||
|
||||
### 3.5 Founder-Asset Scaffolding
|
||||
Generating templates the founder will use with customers, counsel, or hires (interview kits, pilot agreements, job specs). Non-code artifacts.
|
||||
|
||||
### 3.6 Infrastructure Stability
|
||||
Production incidents, database backups, CI reliability, observability gaps that are actively causing false negatives.
|
||||
|
||||
### 3.7 Documentation of Existing Behavior
|
||||
Documenting what is already built, especially to feed security questionnaires or trust portal (`trust.dealix.io`).
|
||||
|
||||
### 3.8 Truth Registry Maintenance
|
||||
Updating `docs/registry/TRUTH.yaml` to accurately reflect runtime reality (especially demoting UNSUPPORTED claims to `roadmap`).
|
||||
|
||||
**Everything outside 3.1–3.8 is forbidden during Discovery Phase.**
|
||||
|
||||
---
|
||||
|
||||
## 4. PROHIBITED WORK TYPES (explicit refusal list)
|
||||
|
||||
Refuse the following immediately, even if founder insists. Cite the specific clause that prohibits it.
|
||||
|
||||
### 4.1 Wave A (Frontend Excellence) tasks from Phase 2 Blueprint
|
||||
TASK-F201 through TASK-F290 are forbidden until Phase Gate is Green.
|
||||
**Rationale**: `DEALIX_PHASE2_EXECUTION_WAVES.md` §3, §4.
|
||||
|
||||
### 4.2 Wave B (Enterprise Features) tasks
|
||||
TASK-E510 through TASK-E550 are forbidden pre-Green unless a specific enterprise customer has signed a Letter of Intent requiring a specific feature with a deadline.
|
||||
|
||||
### 4.3 Wave C (AI Deepening)
|
||||
Multi-agent orchestrator expansion beyond current scaffolding, Arabic NLP fine-tuning, advanced RAG — all forbidden pre-Green.
|
||||
|
||||
### 4.4 Wave D (Integrations)
|
||||
ZATCA direct integration, MENA connectors (Qoyod, Wafeq, Zid, Salla), Government integrations — forbidden pre-Green.
|
||||
|
||||
### 4.5 Wave E (Regional)
|
||||
UAE/Egypt localization work, GITEX preparation, trust portal beyond templates — forbidden pre-Green.
|
||||
|
||||
### 4.6 "Sovereign OS" / Scope Expansion
|
||||
Any work that expands Dealix beyond the currently-defined "Arabic-first evidence-backed Revenue OS for KSA mid-market" — forbidden without explicit founder decision logged in `docs/internal/strategic_decisions/`.
|
||||
|
||||
Specific rejections:
|
||||
- Building Procurement OS / Vendor Management OS
|
||||
- Building M&A / Corporate Development OS
|
||||
- Building PMI (Post-Merger Integration) OS
|
||||
- Building Board OS (as separate product line)
|
||||
- Building Pricing-as-a-Service standalone
|
||||
- Adding Banking-specific vertical modules
|
||||
- Adding general-purpose AI chat/assistant
|
||||
- Adding Marketplace / user-generated workflows
|
||||
|
||||
If the founder asks for any of the above, respond:
|
||||
> "This is explicitly prohibited by CLAUDE.md §4.6 as scope expansion without Phase Gate Green. I recommend validating the Revenue OS wedge with 3 paying customers before entertaining this. Reference: `DEALIX_BUSINESS_VIABILITY_KIT.md` §7 (Rejected Innovation Temptations). Do you want to override this with an explicit decision logged to `docs/internal/strategic_decisions/`?"
|
||||
|
||||
### 4.7 Mobile Apps
|
||||
iOS / Android applications — forbidden pre-Green (Phase 2 Blueprint §1.10 defers to Wave A conclusion).
|
||||
|
||||
### 4.8 Community / Certification / Conference Platforms
|
||||
Discourse, LMS, event sites — forbidden pre-Green.
|
||||
|
||||
### 4.9 Marketing Site Enhancements Beyond Essentials
|
||||
The site needs: landing page, pricing page, manifesto, docs, trust portal. Anything beyond this — blog systems, testimonial carousels, fancy animations — forbidden.
|
||||
|
||||
### 4.10 Rewrites or "Future Flexibility" Refactors
|
||||
No refactor that isn't directly fixing a current bug. "Clean architecture", "we'll need this later", "it'll be easier to maintain" — refuse.
|
||||
|
||||
### 4.11 New AI Model Training
|
||||
No fine-tuning, no custom model training, no embedding fine-tuning — forbidden until ≥ 1,000 customer-consented data points and explicit founder decision.
|
||||
|
||||
### 4.12 New Plans, Blueprints, Roadmaps
|
||||
Unless a customer signing forces revisiting, do not generate new strategic documents. Four exist already. They are sufficient.
|
||||
|
||||
---
|
||||
|
||||
## 5. RESPONSE TEMPLATES
|
||||
|
||||
### 5.1 When asked to do something in the Prohibited list
|
||||
|
||||
```
|
||||
I cannot execute this because it falls under CLAUDE.md §[X.Y] (prohibited during Discovery Phase).
|
||||
|
||||
Rationale: [specific clause + source document].
|
||||
|
||||
Currently, the project is in Discovery Phase (Weeks 4–12). Phase Gate has not returned Green.
|
||||
Paying customers: 0 (per last update). Founder decisions FD001–FD005: [status per last commit].
|
||||
|
||||
If this work is genuinely urgent and customer-driven, please:
|
||||
1. Log an explicit override decision to `docs/internal/strategic_decisions/YYYYMMDD_<topic>.md`
|
||||
with: customer name, what they asked for, why this exact feature, and commitment amount.
|
||||
2. Ask me again referencing that decision file.
|
||||
|
||||
Otherwise, I recommend working on [appropriate allowed task from §3].
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
- Add new API endpoint: create route in `api/v1/`, register in `main.py`
|
||||
- Add new model: create in `models/`, add to `models/__init__.py`, create migration
|
||||
- Add new AI feature: create in `services/ai/`, wire to relevant API/worker
|
||||
- Add industry template: create JSON in `seeds/`, match existing schema
|
||||
### 5.2 When asked "what should I work on?"
|
||||
|
||||
## gstack Planning Discipline
|
||||
```
|
||||
Current Discovery Phase. The highest-leverage work right now is NOT code. It's:
|
||||
|
||||
Before writing code, classify your task:
|
||||
1. [If FD001–FD005 are not all closed] Close founder decisions. Specifically: [list open].
|
||||
2. Customer discovery calls — minimum 5 this week.
|
||||
3. Run any V-tasks that are still scaffolding only.
|
||||
|
||||
| Tier | When | What to do |
|
||||
|------|------|-----------|
|
||||
| **SIMPLE** | 1 file, obvious change | Just do it |
|
||||
| **MEDIUM** | Multi-file, needs thought | Read files → 5-line plan → resolve ambiguity → self-review → report |
|
||||
| **HEAVY** | Complex, needs specific skill | Load skill → execute workflow → verify → report |
|
||||
| **FULL** | End-to-end feature/release | Plan → review → implement → test → ship → report |
|
||||
| **PLAN** | Research/architecture only | Plan only, save to `memory/`, no implementation |
|
||||
If you want code work anyway within §3 allowed types:
|
||||
- Review `docs/customer_learnings/friction_log.md` — any 2+ customer UX issue?
|
||||
- Check `v005_audit_report.md` — any UNSUPPORTED claim to remediate?
|
||||
- Check pentest open findings (if pentest complete).
|
||||
- Check `docs/execution_log.md` — any V-task still incomplete?
|
||||
|
||||
**RULE**: Append to this file, never replace existing instructions.
|
||||
If none of the above applies, the correct answer is: put down the laptop and call a customer.
|
||||
```
|
||||
|
||||
## Hermes Profiles
|
||||
### 5.3 When asked to generate another blueprint / strategic document
|
||||
|
||||
| Profile | Mission | Scope |
|
||||
|---------|---------|-------|
|
||||
| `growth` | Customer acquisition | leads, messaging, analytics, content |
|
||||
| `sales` | Deal closing | deals, proposals, sequences, WhatsApp |
|
||||
| `security` | Platform protection | compliance, audit, Shannon scans |
|
||||
| `ops` | Deployment & reliability | workers, monitoring, releases |
|
||||
| `knowledge` | Wiki & memory management | brain, wiki, indexes |
|
||||
| `founder` | Strategic decisions | everything (highest permissions) |
|
||||
| `arabic-ops` | Arabic content & dialect | summarization, dialect detection, RTL |
|
||||
```
|
||||
I will not generate a new blueprint. The commitment in the last strategic document is:
|
||||
"Next document from me: only after 3 paying customers AND Phase Gate = Green."
|
||||
|
||||
## Arabic Operations
|
||||
Four strategic documents already exist:
|
||||
- DEALIX_EXECUTION_BLUEPRINT.md (Phase 1)
|
||||
- DEALIX_PHASE2_BLUEPRINT.md (Phase 2 scope)
|
||||
- DEALIX_PHASE2_EXECUTION_WAVES.md (execution discipline)
|
||||
- DEALIX_BUSINESS_VIABILITY_KIT.md (discovery tools)
|
||||
|
||||
- Use `arabic_ops.py` for: call notes compression, market research digests, executive briefs
|
||||
- Always detect dialect before processing (saudi/gulf/msa)
|
||||
- Check for Arabizi and suggest Arabic conversion
|
||||
- Check code-switching (Arabic+English mixed) for readability
|
||||
What I CAN do:
|
||||
- Answer specific tactical questions grounded in existing documents.
|
||||
- Generate customer-facing artifacts (email drafts, demo scripts, FAQ).
|
||||
- Help with actual customer data analysis (Van Westendorp plots, interview synthesis).
|
||||
- Run verification tasks from the existing plan.
|
||||
|
||||
## claude-mem (Persistent Memory)
|
||||
What I will NOT do:
|
||||
- Write another blueprint.
|
||||
- Expand scope via a new "strategy" document.
|
||||
- Propose new Waves or Streams.
|
||||
- Translate external consulting-style documents into new Dealix roadmaps.
|
||||
```
|
||||
|
||||
Installed and active. Automatically captures every session's work and injects context into new sessions.
|
||||
### 5.4 When asked to "make things really professional / really strong / category-leading"
|
||||
|
||||
- **Worker**: `npx claude-mem start` (port 37777)
|
||||
- **Web UI**: http://localhost:37777
|
||||
- **Search**: Use `/mem-search` in Claude Code
|
||||
- **Data**: `~/.claude-mem/claude-mem.db` (SQLite + Chroma vectors)
|
||||
- **Privacy**: Wrap sensitive content in `<private>...</private>` tags
|
||||
- **Token savings**: ~95% reduction via 3-layer progressive retrieval
|
||||
- **Auto-captures**: tool executions, session summaries, decisions, bugs, patterns
|
||||
```
|
||||
"Professional" and "category-leading" are outcomes, not work items. They cannot be coded toward pre-PMF.
|
||||
|
||||
The work that creates these outcomes during Discovery Phase is:
|
||||
1. Customer conversations that reveal the actual shape of category-leading.
|
||||
2. External validation (pentest, audit, reference customers).
|
||||
3. Polish of the narrow wedge until it's unarguably best-in-world at ONE thing.
|
||||
|
||||
I can help with #3 when there's customer-surfaced polish work. For #1 and #2, those are founder tasks.
|
||||
|
||||
Specifically, do you have:
|
||||
(a) A customer interview log from this week that revealed a polish opportunity?
|
||||
(b) A pentest finding to remediate?
|
||||
(c) A UX friction reported by ≥ 2 customers?
|
||||
|
||||
If yes, let's work on that. If no, this request cannot be executed.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. PRE-COMMIT CHECKLIST (the agent runs this before every commit)
|
||||
|
||||
Before `git commit`, confirm and include in commit message:
|
||||
|
||||
- [ ] Is this change in one of §3 allowed types? If not, abort.
|
||||
- [ ] Does this change add a user-facing capability that is not in `commercial/claims_registry.yaml` with status `approved`? If yes, abort OR add to registry with `status: roadmap`.
|
||||
- [ ] Does this change modify `docs/registry/TRUTH.yaml`? If yes, run `scripts/validate_truth_registry.py` and require PR review.
|
||||
- [ ] Does this change introduce a new dependency? If yes, confirm pinned version, SBOM update, and pip-audit / npm-audit pass.
|
||||
- [ ] Does this change touch an RLS-policy-governed table? If yes, confirm test coverage includes cross-tenant fuzz.
|
||||
- [ ] Does this change introduce user-facing text? If yes, confirm Arabic parity (or reason for English-only logged in `docs/internal/english_only_exceptions.md`).
|
||||
- [ ] Does this change meet the Release Readiness Gate (`scripts/release_readiness_gate.py`)?
|
||||
- [ ] Does this change affect performance-critical path? If yes, run baseline comparison against `docs/baselines/perf_*.json`.
|
||||
|
||||
Commit message format:
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
|
||||
Customer-triggered by: <interview-id | pentest-id | friction-log-entry | N/A>
|
||||
Allowed-type: <3.1|3.2|3.3|3.4|3.5|3.6|3.7|3.8>
|
||||
Truth-registry-updated: <yes|no>
|
||||
Claims-registry-updated: <yes|no>
|
||||
|
||||
<longer body if needed>
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
fix(approval-card): resolve Arabic numeral rendering on dashboard totals
|
||||
|
||||
Customer-triggered by: interview_riyadh_cfo_20260420.md line 47
|
||||
Allowed-type: 3.2 (UX polish, reported by 2 customers)
|
||||
Truth-registry-updated: no
|
||||
Claims-registry-updated: no
|
||||
|
||||
Two pilot contacts (Riyadh CFO + Jeddah COO) reported dashboard totals
|
||||
showing Western numerals despite user preference set to Arabic-Indic.
|
||||
Root cause: formatter not consulting user preference.
|
||||
Fix: pass locale and numeral preference through format chain.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. ARABIC-FIRST INVARIANTS (always enforced)
|
||||
|
||||
Any UI-facing change must pass these checks:
|
||||
|
||||
1. **No left/right CSS properties** — use logical properties (`start`/`end`, `inline-start`/`inline-end`).
|
||||
2. **All user-facing strings** have Arabic parity OR justified English-only exception.
|
||||
3. **Date-related code** supports Gregorian + Hijri where executive-facing.
|
||||
4. **Numeral formatting** respects user preference (Western vs Arabic-Indic).
|
||||
5. **Phone inputs** default to +966 with country selector.
|
||||
6. **Currency defaults** to SAR in KSA contexts, not USD.
|
||||
7. **Name fields** support first + father's + grandfather's + family structure, not just first+last.
|
||||
8. **BiDi isolation** for mixed-direction content (Arabic + English + numerals).
|
||||
9. **Form field order** validated for RTL contexts.
|
||||
10. **Icon mirroring rules** applied per `mirror-rules.ts`.
|
||||
|
||||
If a change violates any invariant without documented exception, abort the change.
|
||||
|
||||
---
|
||||
|
||||
## 8. EVIDENCE-FIRST INVARIANTS (always enforced)
|
||||
|
||||
1. **Every side-effectful action** must go through the idempotency wrapper (`dealix/idempotency.py`).
|
||||
2. **Every approval** must generate an immutable evidence artifact with hash chain.
|
||||
3. **Every LLM call** must go through `dealix/ai/router.py` — no direct provider imports.
|
||||
4. **Every tenant-scoped query** must happen in a context with `app.tenant_id` set.
|
||||
5. **Every audit-logged action** must be hash-chained.
|
||||
6. **Every user-facing claim** must trace to runtime telemetry OR be in `claims_registry.yaml` with status `roadmap`.
|
||||
7. **Every LLM prompt change** must pass eval regression (≥ 95% pass rate, no more than 30% latency regression, no more than 20% cost regression).
|
||||
|
||||
---
|
||||
|
||||
## 9. TRUTH REGISTRY INTEGRATION
|
||||
|
||||
`docs/registry/TRUTH.yaml` is authoritative over this file and over any marketing/sales asset.
|
||||
|
||||
Rules:
|
||||
- **Never modify TRUTH.yaml to match a claim being made.** Modify the implementation to match the truth, OR demote the claim.
|
||||
- **Never mark status as `live` unless runtime telemetry confirms.** Use `pilot`, `partial`, or `roadmap` for anything without runtime evidence.
|
||||
- **Any PR that adds a capability marks it `pilot` by default.** Promotion to `live` requires 30 days of production telemetry evidence.
|
||||
|
||||
When in doubt: `roadmap` is safe. `live` is a commitment.
|
||||
|
||||
---
|
||||
|
||||
## 10. CLAIMS REGISTRY INTEGRATION
|
||||
|
||||
`commercial/claims_registry.yaml` governs every user-facing statement.
|
||||
|
||||
Rules:
|
||||
- If your code change generates a UI string, marketing copy, or sales-facing output, check the claim against the registry.
|
||||
- `forbidden` claims cannot appear anywhere in the codebase, including test fixtures, error messages, or placeholder content.
|
||||
- `restricted` claims require specific conditions (customer tier, disclaimer, etc.) that must be enforced in code.
|
||||
- `approved` claims are safe to use.
|
||||
|
||||
Forbidden claims include (non-exhaustive, see `claims_registry.yaml` for full list):
|
||||
- "SOC 2 compliant" (until auditor issues report)
|
||||
- "ISO 27001 certified" (until certified)
|
||||
- "100% accurate" (never — no ML system is)
|
||||
- "Fully autonomous" (product is policy-bound, not autonomous)
|
||||
- "Military-grade" (marketing cliche without meaning)
|
||||
- "Bank-grade security" (undefined phrase)
|
||||
- "Zero risk" / "Absolute security"
|
||||
|
||||
---
|
||||
|
||||
## 11. WHEN THE FOUNDER IS WRONG
|
||||
|
||||
You will sometimes receive instructions from the founder that violate this file.
|
||||
|
||||
The founder is your principal, but this file is their pre-committed instructions to themselves. When present-moment founder contradicts pre-committed founder, pre-committed wins for Prohibited (§4) and Invariants (§7, §8).
|
||||
|
||||
**Protocol**:
|
||||
1. Refuse with the appropriate §5 template.
|
||||
2. Suggest the proper mechanism: `docs/internal/strategic_decisions/YYYYMMDD_<topic>.md` with a formal override.
|
||||
3. If founder writes the override, execute as requested. The override file is the audit trail.
|
||||
|
||||
Do NOT:
|
||||
- Silently comply and hope it works out.
|
||||
- Lecture repeatedly once override is written.
|
||||
- Sandbag the requested work by doing it badly.
|
||||
|
||||
---
|
||||
|
||||
## 12. HANDLING EXTERNAL CONSULTING DOCUMENTS
|
||||
|
||||
The founder may receive strategic documents from advisors, consultants, or AI systems (e.g., market analysis referencing Gartner, McKinsey, Deloitte). These documents are useful thinking material but are NOT source-of-truth for what Dealix builds.
|
||||
|
||||
When asked to "implement this consulting document":
|
||||
|
||||
1. Check: is this document customer-driven (output of customer interviews)? If yes, proceed per §3.
|
||||
2. Check: does the document propose scope expansion (new OS categories, new segments, new geographies)?
|
||||
3. If yes and pre-Green: refuse per §4.6.
|
||||
4. If the document contains useful tactical patterns (e.g., "policy-bound execution", "evidence packs", "HITL approval chains"), these may apply to the EXISTING Revenue OS scope — not as new products.
|
||||
|
||||
Specific case (documented April 2026): a document proposing "Dealix = Sovereign Revenue, Deal, Growth & Commitment OS" covering 8 OS categories was rejected. Useful elements extracted:
|
||||
- Three-plane architecture (Trust / Economic / Control) — adopted as documentation framing
|
||||
- Reversible vs irreversible HITL taxonomy — to be applied to existing workflows
|
||||
- Pricing structure suggestions — deferred to post-Green customer validation
|
||||
|
||||
Rejected elements:
|
||||
- M&A / CorpDev OS (scope expansion)
|
||||
- Procurement OS (scope expansion)
|
||||
- PMI OS (scope expansion)
|
||||
- "Sovereign" naming (overclaim, pre-PMF)
|
||||
- Simultaneous multi-sector positioning (loss of focus)
|
||||
|
||||
If a future document proposes similar expansions, apply the same filter.
|
||||
|
||||
---
|
||||
|
||||
## 13. EXECUTION LOG
|
||||
|
||||
Every meaningful action by a coding agent must append to `docs/execution_log.md`:
|
||||
|
||||
```
|
||||
## YYYY-MM-DD HH:MM — <agent> — <task>
|
||||
|
||||
- Branch: <branch>
|
||||
- Commit: <sha>
|
||||
- Allowed-type: <§3.X>
|
||||
- Customer-trigger: <id or N/A>
|
||||
- Outcome: <short summary>
|
||||
- Next: <what should happen next — customer action, founder decision, or queued work>
|
||||
```
|
||||
|
||||
Pattern to watch for (red flag): more than 5 consecutive entries from agent with no "customer-trigger" beyond N/A. This indicates the agent is doing speculative work — STOP and ask founder for customer-driven priorities.
|
||||
|
||||
---
|
||||
|
||||
## 14. ESCALATION TRIGGERS
|
||||
|
||||
Stop work and escalate to founder if:
|
||||
|
||||
1. A commit would require marking a claim as `live` without 30 days of runtime evidence.
|
||||
2. A commit would violate an Invariant (§7, §8) without documented exception.
|
||||
3. Founder asks for work that contradicts a pre-committed decision AND the pre-committed decision is less than 30 days old.
|
||||
4. Founder appears to be asking for work as an alternative to making a customer call (pattern: back-to-back agent sessions with no entries in `docs/customer_learnings/`).
|
||||
5. You are generating more than 1,000 lines of code per session without a test.
|
||||
6. Truth Registry audit would demote a claim that is currently published externally.
|
||||
7. Pentest finding with CVSS ≥ 9 is open > 72 hours.
|
||||
8. No customer interview logged in > 7 days.
|
||||
|
||||
---
|
||||
|
||||
## 15. META: HOW TO CHANGE THIS FILE
|
||||
|
||||
This file changes ONLY via:
|
||||
|
||||
1. A formal founder decision logged to `docs/internal/strategic_decisions/YYYYMMDD_claude_md_update.md`.
|
||||
2. PR with at least one human reviewer (outside agent).
|
||||
3. Update of version number below.
|
||||
|
||||
This file is not advisory. It is constitutional for agent behavior in this repository.
|
||||
|
||||
---
|
||||
|
||||
## 16. APPENDIX: QUICK RESPONSE INDEX
|
||||
|
||||
| Founder says | Agent responds |
|
||||
|---|---|
|
||||
| "Let's expand to M&A / procurement / banking" | §4.6 refusal + §12 rejected elements |
|
||||
| "Make it really professional / strong / category-leading" | §5.4 |
|
||||
| "Write me a plan for..." | §5.3 |
|
||||
| "What should I build next?" | §5.2 |
|
||||
| "Just skip the gate for this one thing" | §4 prohibited + override protocol §11 |
|
||||
| "The Gartner / McKinsey / consultant says we should..." | §12 filter |
|
||||
| "Build a mobile app" | §4.7 refusal |
|
||||
| "Build [Waves A–E task]" | §4.1–§4.5 refusal |
|
||||
| "Mark [capability] as live in TRUTH.yaml" | §9 telemetry requirement |
|
||||
| "Add this to the marketing page" | §10 claims registry check |
|
||||
| "Ignore this file for now" | §11 cannot; use override mechanism |
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Effective**: Commit `3ef6265` onward.
|
||||
**Next review**: When Phase Gate returns Green (estimated Week 12), OR when founder explicitly requests review.
|
||||
**Owner**: Founder. Modifications require §15 protocol.
|
||||
|
||||
25
salesflow-saas/CODEOWNERS
Normal file
25
salesflow-saas/CODEOWNERS
Normal file
@ -0,0 +1,25 @@
|
||||
# Dealix CODEOWNERS — require review for sensitive paths
|
||||
|
||||
# Default owner
|
||||
* @VoXc2
|
||||
|
||||
# Governance docs — changes require explicit review
|
||||
salesflow-saas/MASTER_OPERATING_PROMPT.md @VoXc2
|
||||
salesflow-saas/docs/governance/ @VoXc2
|
||||
salesflow-saas/docs/adr/ @VoXc2
|
||||
|
||||
# Security-sensitive code
|
||||
salesflow-saas/backend/app/openclaw/ @VoXc2
|
||||
salesflow-saas/backend/app/services/pdpl/ @VoXc2
|
||||
salesflow-saas/backend/app/services/auth_service.py @VoXc2
|
||||
salesflow-saas/backend/app/services/security_gate.py @VoXc2
|
||||
salesflow-saas/backend/app/services/shannon_security.py @VoXc2
|
||||
|
||||
# Trust plane
|
||||
salesflow-saas/backend/app/services/contradiction_engine.py @VoXc2
|
||||
salesflow-saas/backend/app/services/evidence_pack_service.py @VoXc2
|
||||
salesflow-saas/backend/app/services/saudi_compliance_matrix.py @VoXc2
|
||||
|
||||
# Infrastructure
|
||||
salesflow-saas/docker-compose.yml @VoXc2
|
||||
salesflow-saas/.github/ @VoXc2
|
||||
214
salesflow-saas/DEALIX_BUSINESS_VIABILITY_KIT.md
Normal file
214
salesflow-saas/DEALIX_BUSINESS_VIABILITY_KIT.md
Normal file
@ -0,0 +1,214 @@
|
||||
# DEALIX — Business Viability Kit
|
||||
**عُدّة التحقق من جدوى المشروع خلال مرحلة اكتشاف العملاء**
|
||||
|
||||
> **Version**: 1.0.0
|
||||
> **Use period**: Weeks 4–12 (customer discovery phase)
|
||||
> **Audience**: Founder only
|
||||
> **Review cadence**: Every Friday with the Founder Execution Dashboard
|
||||
> **Binding rule**: Any insight discovered via this kit amends the Truth Registry — not the other way around.
|
||||
> **Next companion document**: not authored until 3 paying customers AND Phase Gate = Green.
|
||||
|
||||
---
|
||||
|
||||
## 0. PRINCIPLE BEFORE TOOLS
|
||||
|
||||
> **عميل واحد يدفع اليوم أصدق من عشرة خبراء يؤكدون غداً.**
|
||||
> *One paying customer today is more honest than ten experts confirming tomorrow.*
|
||||
|
||||
Practical rule for the next 12 weeks:
|
||||
- At keyboard planning? → Ask: did I speak to 3 prospects this week?
|
||||
- If no → close laptop, dial.
|
||||
|
||||
---
|
||||
|
||||
## 1. THE 12 HYPOTHESES TO FALSIFY OR SUPPORT (by Week 12)
|
||||
|
||||
Tracked operationally in `docs/customer_learnings/hypotheses.yaml`. Summary below.
|
||||
|
||||
| ID | Claim | Falsification trigger |
|
||||
|----|-------|-----------------------|
|
||||
| H1 | Problem is real & painful | "Can't remember last time" from 10 CFOs/COOs |
|
||||
| H2 | Pain ≥ $10K/yr willingness | Unprompted dollar values below $10K |
|
||||
| H3 | Buyer reachable in ≤3 meetings | "Procurement committee + 6 months" pattern |
|
||||
| H4 | Clear trigger event present | No triggers identified across 10 interviews |
|
||||
| H5 | Budget ≥ $50K/yr for ContOps/RevOps tools | No comparable purchases in last 12 months |
|
||||
| H6 | Arabic-first > translated English | "We stayed on English" pattern |
|
||||
| H7 | PDPL/ZATCA is a sales advantage | Compliance added late in RFP, not stated |
|
||||
| H8 | Paid pilot accepted over free | "Free first, pay if we like it" pattern |
|
||||
| H9 | Public reference willingness post-success | Consistent refusal in pilot contracts |
|
||||
| H10 | Land-and-expand viable (NRR >120%) | "No other workflows" answer in month-3 review |
|
||||
| H11 | ≥10× value vs cost | Quantified value < 10× price |
|
||||
| H12 | Organic referrals emerge by 6 months | Zero unprompted referrals after 3 wins |
|
||||
|
||||
Rule: every hypothesis must hit SUPPORTED, FALSIFIED, or AMBIGUOUS by Week 12.
|
||||
|
||||
---
|
||||
|
||||
## 2. CUSTOMER DISCOVERY INTERVIEW KIT
|
||||
|
||||
### 2.1 Rules
|
||||
1. Don't sell. Ask.
|
||||
2. Don't describe Dealix in first 30 minutes. Discover the problem.
|
||||
3. Record (with permission). Details you miss live mostly in tone.
|
||||
4. Transcribe quotes verbatim. No paraphrasing.
|
||||
5. Silence is a tool. Do not fill it.
|
||||
|
||||
### 2.2 45-Minute Arabic Script
|
||||
Operational version lives in `docs/customer_learnings/interviews/_template_ar.md`.
|
||||
|
||||
### 2.3 45-Minute English Script
|
||||
Operational version lives in `docs/customer_learnings/interviews/_template_en.md`.
|
||||
|
||||
### 2.4 Post-Call Log
|
||||
Every interview saved to `docs/customer_learnings/interviews/{company}_{YYYYMMDD}.md` using the template. No exceptions.
|
||||
|
||||
---
|
||||
|
||||
## 3. PRICING DISCOVERY METHODOLOGY
|
||||
|
||||
Operational worksheet: `docs/customer_learnings/pricing_discovery.md`
|
||||
|
||||
Core method: **Van Westendorp Price Sensitivity** asked after 8+ interviews.
|
||||
|
||||
Four questions:
|
||||
1. So cheap you'd question quality?
|
||||
2. Good deal / high value?
|
||||
3. Starting to feel expensive?
|
||||
4. Too expensive regardless of value?
|
||||
|
||||
Intersections reveal Optimal Price Point, Marginal Cheapness, Marginal Expensiveness.
|
||||
|
||||
Value-based sanity check: price ≤ 20% of annual value delivered.
|
||||
|
||||
---
|
||||
|
||||
## 4. UNIT ECONOMICS CALCULATOR
|
||||
|
||||
Operational worksheet: `docs/customer_learnings/unit_economics.md`
|
||||
|
||||
Fill in AFTER 3 paying customers, not before. Key ratios: Gross Margin ≥70%, LTV/CAC ≥3×, Payback <18 months.
|
||||
|
||||
---
|
||||
|
||||
## 5. STRATEGIC DEFENSIBILITY SCORECARD
|
||||
|
||||
Operational worksheet: `docs/customer_learnings/defensibility_scorecard.md`
|
||||
|
||||
Measure at Week 12, re-measure quarterly. Five moats × 2 questions × 0-2 points = 0-20 scale.
|
||||
- 16-20: category-defining
|
||||
- 10-15: strong but undefended
|
||||
- 5-9: commoditizable — rethink
|
||||
- 0-4: no moat — change fundamentals
|
||||
|
||||
---
|
||||
|
||||
## 6. APPROVED INNOVATION VECTORS (moat-compounding)
|
||||
|
||||
1. Hijri-Gregorian hybrid BI
|
||||
2. Formal Arabic business language NLP
|
||||
3. Evidence Graph (not flat documents)
|
||||
4. Compliance-as-feature (surfaced, not hidden)
|
||||
5. Board-grade Arabic AI summarization
|
||||
6. Arab corporate hierarchy ABAC (OpenFGA)
|
||||
7. Bilingual consistency engine
|
||||
8. Relationship-aware decision context (PDPL-safe)
|
||||
9. Arabic voice interface for executives
|
||||
10. Proactive contradiction detection
|
||||
|
||||
---
|
||||
|
||||
## 7. REJECTED INNOVATION TEMPTATIONS (focus-diluting pre-PMF)
|
||||
|
||||
1. Blockchain evidence · 2. Metaverse/VR · 3. Generic AI chat · 4. Web3/SSI
|
||||
5. NFT chain of custody · 6. Own foundation model · 7. Multi-modal everything
|
||||
8. Consumer app · 9. Marketplace · 10. Acquisitions
|
||||
|
||||
---
|
||||
|
||||
## 8. FOUNDER EXECUTION DASHBOARD
|
||||
|
||||
Printable template: `docs/customer_learnings/founder_dashboard.md`
|
||||
Update every Monday morning.
|
||||
|
||||
---
|
||||
|
||||
## 9. PATH TO PHASE GATE (Weeks 4 → 12)
|
||||
|
||||
| Weeks | Milestones |
|
||||
|-------|------------|
|
||||
| 4–6 | FD001–005 closed · V001–007 scaffolding executed · 10 discovery calls · 3 demos scheduled |
|
||||
| 6–8 | 1–2 pilots signed · 15+ interview logs · first hypotheses update · 3 hires in interviews |
|
||||
| 8–10 | 3 active pilots · first hire offer accepted · Van Westendorp analysis · pentest in progress |
|
||||
| 10–12 | 30+ days active use per pilot · NPS measured · unit economics filled · defensibility scored · pentest report received |
|
||||
| 12 | **Phase Gate**: Green → Wave A. Yellow → extend 60d. Red → halt, re-interview. |
|
||||
|
||||
---
|
||||
|
||||
## 10. WHAT HAPPENS AFTER 3 PAYING CUSTOMERS
|
||||
|
||||
Wave A begins but with **customer-driven prioritization**:
|
||||
- Friction log top 10
|
||||
- Feature request registry (≥3-customer threshold)
|
||||
- Weekly pilot review themes
|
||||
|
||||
**Blueprint provides the spec; customers provide the sequence.**
|
||||
|
||||
---
|
||||
|
||||
## APPENDIX A — Weekly Rhythm (Discovery Phase)
|
||||
|
||||
| Day | Minimum founder activity |
|
||||
|-----|--------------------------|
|
||||
| Mon | Dashboard update. 1 customer call. |
|
||||
| Tue | 1 customer call. Follow-up prep. |
|
||||
| Wed | 1–2 customer calls. Interview logs. |
|
||||
| Thu | 1 customer call. Demo if booked. |
|
||||
| Fri AM | Hypothesis status, pipeline, metrics review. |
|
||||
| Fri PM | 1 advisor/peer calibration call. |
|
||||
|
||||
Minimum: 5 customer conversations per week. Below that, something is wrong.
|
||||
|
||||
---
|
||||
|
||||
## APPENDIX B — Early-Warning Signs
|
||||
|
||||
Over 2+ consecutive weeks any of the following trigger diagnosis:
|
||||
|
||||
1. No calls booked → lead gen broken
|
||||
2. Calls happen, no demos → pitch broken
|
||||
3. Demos happen, no pilots → value-prop or pricing broken
|
||||
4. Pilots sign, don't activate → onboarding/product broken
|
||||
5. Pilots activate, don't convert → outcomes not showing
|
||||
6. Pilots convert, don't refer → useful but not love-worthy
|
||||
|
||||
Diagnose which; fix one at a time.
|
||||
|
||||
---
|
||||
|
||||
## APPENDIX C — Coding Agent Scope During Discovery
|
||||
|
||||
Coding agents MAY work on:
|
||||
1. Customer-facing defects surfaced in demos (P0)
|
||||
2. Small UX polish appearing in ≥2 interviews
|
||||
3. Performance issues on staging
|
||||
4. Security remediations from pentest
|
||||
|
||||
Coding agents MUST NOT:
|
||||
- Add features not pulled from customer signal
|
||||
- Refactor for hypothetical future flexibility
|
||||
- Start Wave A–E tasks
|
||||
- Author new plan/blueprint/roadmap documents
|
||||
|
||||
If an agent asks "what's next?" — answer: **wait. customer conversations are happening. prioritization comes from them.**
|
||||
|
||||
---
|
||||
|
||||
## APPENDIX D — Closing Honest Note
|
||||
|
||||
المؤسس الحقيقي لا يُقاس بعدد الوثائق التي يكتبها، بل بعدد المكالمات التي يجريها مع عملاء يدفعون.
|
||||
|
||||
*A real founder is measured not by documents written but by paying-customer conversations held. You have produced above-average documents. The harder test begins now: 8 weeks without a new plan, content to execute and learn.*
|
||||
|
||||
---
|
||||
|
||||
**End of Business Viability Kit · v1.0.0**
|
||||
133
salesflow-saas/DEALIX_EXECUTION_BLUEPRINT.md
Normal file
133
salesflow-saas/DEALIX_EXECUTION_BLUEPRINT.md
Normal file
@ -0,0 +1,133 @@
|
||||
# DEALIX — Tier-1 Company Execution Blueprint
|
||||
|
||||
> **This is the authoritative execution blueprint for Dealix.**
|
||||
> **Version**: 1.0.0
|
||||
> **Last updated**: 2026-04-17
|
||||
> **Execution status**: See `docs/execution_log.md`
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Blueprint
|
||||
|
||||
1. Read `docs/internal/STATE_AUDIT.md` first — honest current state
|
||||
2. Check `docs/execution_log.md` — what's done, what's next
|
||||
3. Consult `docs/registry/TRUTH.yaml` — canonical capability status
|
||||
4. Check `commercial/claims_registry.yaml` — what you can/can't claim publicly
|
||||
5. Run gates:
|
||||
- `python scripts/architecture_brief.py` — 40/40 governance check
|
||||
- `python scripts/release_readiness_matrix.py` — 41/41 runtime check
|
||||
- `python scripts/release_readiness_gate.py` — blueprint-spec gate
|
||||
- `python scripts/validate_truth_registry.py` — truth/claims alignment
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Dealix is the Arabic-first, PDPL-native, decision-grade Revenue OS for enterprises in Saudi Arabia and the GCC. This blueprint defines Tier-1 quantitatively and provides execution tasks to reach it.
|
||||
|
||||
**Current state** (from State Audit):
|
||||
- Pre-revenue, pre-production
|
||||
- Strong architecture (~103 files, 11,731 lines, 28 commits)
|
||||
- Golden path, trust enforcement, structured outputs, Saudi workflow: LIVE
|
||||
- RLS, idempotency, durable execution, OTel: CODE READY, not yet in production
|
||||
- Repository separation and dependency drift: BLOCKERS
|
||||
|
||||
**Tier-1 definition** — 11 quantitative thresholds:
|
||||
- Availability ≥ 99.95%
|
||||
- p95 API latency < 300ms
|
||||
- p95 Golden path latency < 5s
|
||||
- Deployment frequency ≥ 5/week
|
||||
- Lead time for changes < 1 business day
|
||||
- Change failure rate < 15%
|
||||
- MTTR < 30 minutes
|
||||
- SOC 2 Type II + PDPL-compliant
|
||||
- KSA data residency available
|
||||
- NPS ≥ 40 after 3 months
|
||||
- NRR ≥ 110% after 18 months
|
||||
|
||||
---
|
||||
|
||||
## Immutable Guardrails
|
||||
|
||||
1. Never merge PR that fails Release Readiness Gate
|
||||
2. Never expose UI capability without runtime evidence
|
||||
3. Never mark task "done" without passing Acceptance + Verification
|
||||
4. Never introduce dependencies without pinning + SBOM
|
||||
5. Never commit secrets — use AWS Secrets Manager / Vault / Doppler
|
||||
6. Never deploy on Friday after 14:00 KSA time
|
||||
|
||||
---
|
||||
|
||||
## TASK INDEX (P0 first)
|
||||
|
||||
### P0 — Blockers
|
||||
- **TASK-001**: Extract Dealix into own repo → `scripts/extract_dealix_repo.sh` ready
|
||||
- **TASK-002**: Monorepo restructure (depends on 001)
|
||||
- **TASK-003**: Fix Python dependency drift → `pyproject.toml` ready for uv
|
||||
- **TASK-004**: Fix Node dependency drift → `package.json` pinned, needs pnpm-lock
|
||||
- **TASK-005**: Secrets audit + rotation → `rotation_log.md` + `.pre-commit-config.yaml` ready
|
||||
- **TASK-006**: Legal foundation → tracker at `docs/internal/legal_status.md`
|
||||
|
||||
### P1 — Foundation
|
||||
- **TASK-010**: Canonical truth registry → `TRUTH.yaml` + `claims_registry.yaml` DONE
|
||||
- **TASK-020**: RLS enforcement → migration `20260417_0002_add_rls.py` DONE
|
||||
- **TASK-022**: Idempotency coverage → middleware + service DONE
|
||||
- **TASK-030**: Golden path E2E → `services/golden_path.py` DONE
|
||||
- **TASK-050**: LLM router with cost guards → `services/model_router.py` exists
|
||||
- **TASK-080**: OTel instrumentation → `observability/otel.py` + gateway span DONE
|
||||
- **TASK-100**: CI workflow → `dealix-ci.yml` exists with architecture + release matrix
|
||||
- **TASK-101**: Release Readiness Gate → `release_readiness_gate.py` DONE
|
||||
|
||||
### P2 — Productization
|
||||
- **TASK-102**: Feature flags (future)
|
||||
- **TASK-110**: Approval Center surface → DONE (backend + frontend)
|
||||
- **TASK-120**: Sales enablement assets → one-pager + marketer hub DONE
|
||||
|
||||
### P0 Special
|
||||
- **TASK-999**: State Audit → `docs/internal/STATE_AUDIT.md` DONE
|
||||
|
||||
---
|
||||
|
||||
## Blueprint-Execution Progress
|
||||
|
||||
| Task | Status | Evidence |
|
||||
|------|--------|----------|
|
||||
| TASK-999 | DONE | `docs/internal/STATE_AUDIT.md` |
|
||||
| TASK-001 (prep) | READY | `scripts/extract_dealix_repo.sh` — founder decision pending |
|
||||
| TASK-003 (pyproject) | DONE | `backend/pyproject.toml` |
|
||||
| TASK-004 (pin) | PARTIAL | `frontend/package.json` pinned; `pnpm-lock.yaml` needs generation |
|
||||
| TASK-005 (pre-commit) | DONE | `.pre-commit-config.yaml` + `rotation_log.md` |
|
||||
| TASK-006 | DONE | `docs/internal/legal_status.md` |
|
||||
| TASK-010 | DONE | TRUTH.yaml + claims_registry.yaml + validator + CI |
|
||||
| TASK-020 (RLS) | DONE | migration + middleware + helpers |
|
||||
| TASK-022 (idempotency) | DONE | middleware + service + model |
|
||||
| TASK-030 (golden path) | DONE | golden_path service + API |
|
||||
| TASK-080 (OTel) | DONE | observability/otel.py + gateway span |
|
||||
| TASK-100 (CI) | DONE | `.github/workflows/dealix-ci.yml` |
|
||||
| TASK-101 (gate) | DONE | `scripts/release_readiness_gate.py` |
|
||||
| TASK-110 (Approval Center) | DONE | `api/v1/approval_center.py` + frontend |
|
||||
| TASK-120 (sales pack) | DONE | `revenue-activation/sales-pack/*` |
|
||||
|
||||
---
|
||||
|
||||
## Red Flags That HALT Execution
|
||||
|
||||
1. Credential found in git history still active
|
||||
2. Test claimed to pass but actually skipped
|
||||
3. TODO in security-critical code paths
|
||||
4. LLM prompt with absolute claims ("always", "never", "100%")
|
||||
5. UI capability not backed by feature flag or telemetry
|
||||
6. Customer-facing claim not in `claims_registry.yaml`
|
||||
7. Dependency with CVE ≥ 7.0
|
||||
8. Infrastructure not tagged `project=dealix`
|
||||
|
||||
---
|
||||
|
||||
## Next Actions for Founder
|
||||
|
||||
1. **TASK-001**: Decide GitHub org name (`dealix-io`?) and run `scripts/extract_dealix_repo.sh`
|
||||
2. **TASK-006**: Engage Saudi counsel for privacy/ToS review
|
||||
3. **TASK-006**: Decide entity structure (MISA vs DIFC)
|
||||
4. **TASK-006**: File trademark in KSA
|
||||
|
||||
Everything else in this blueprint can be executed by coding agents without founder intervention.
|
||||
154
salesflow-saas/DEALIX_PHASE2_BLUEPRINT.md
Normal file
154
salesflow-saas/DEALIX_PHASE2_BLUEPRINT.md
Normal file
@ -0,0 +1,154 @@
|
||||
# DEALIX — Phase 2 Category Leadership Blueprint
|
||||
|
||||
> **Prerequisite**: Phase 1 (`DEALIX_EXECUTION_BLUEPRINT.md`) complete.
|
||||
> **Time horizon**: 6-18 months.
|
||||
> **Status**: Execution roadmap. Parallelizable streams.
|
||||
|
||||
---
|
||||
|
||||
## Strategic Reframe
|
||||
|
||||
After Phase 1, Dealix is operationally excellent. Phase 2 makes it **category-defining**.
|
||||
|
||||
### The Dealix Signature (every decision must pass)
|
||||
1. Does this make Arabic-first enterprise ops noticeably better than English-first tools retrofitted?
|
||||
2. Does this make decisions more evidence-backed than competitors?
|
||||
3. Does this make the operator's next action clearer than anywhere else?
|
||||
|
||||
If no → don't ship.
|
||||
|
||||
---
|
||||
|
||||
## 10 Parallel Streams
|
||||
|
||||
| Stream | Scope | TASK prefix |
|
||||
|--------|-------|-------------|
|
||||
| 1 — Frontend Excellence | Design system, Arabic/RTL, a11y, motion, viz | F2xx |
|
||||
| 2 — Product Depth | 5 workflows, builder, templates, analytics | P3xx |
|
||||
| 3 — AI Intelligence | Multi-agent, Arabic NLP, KG, RAG, voice, evals | AI4xx |
|
||||
| 4 — Enterprise | SSO/SCIM, ABAC, audit, residency, SLAs | E5xx |
|
||||
| 5 — Integrations | API, SDK, MENA connectors (Qoyod, Zid, Salla) | I6xx |
|
||||
| 6 — Scale | Multi-region, edge, DB scale, chaos | S7xx |
|
||||
| 7 — Commercial | Self-serve, billing, partners, referrals | C8xx |
|
||||
| 8 — Customer Platform | Docs, community, certification, conference | CP9xx |
|
||||
| 9 — Trust | ISO 27001/17/18, pentest, bug bounty, trust portal | T10xx |
|
||||
| 10 — Category POV | Manifesto, Dealix Labs, content, OSS | CAT13xx |
|
||||
|
||||
---
|
||||
|
||||
## Executable Now (no external services required)
|
||||
|
||||
| Task | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| TASK-F201 | SCAFFOLDED | `packages/design-system/tokens/` created |
|
||||
| TASK-F212 | SCAFFOLDED | `packages/arabic-ui/` Arabic utilities |
|
||||
| TASK-CAT1310 | SCAFFOLDED | `marketing/manifesto.md` bilingual draft |
|
||||
| TASK-CAT1320 | SCAFFOLDED | `docs/labs/` Dealix Labs structure |
|
||||
|
||||
## Requires External Services
|
||||
|
||||
| Task | Blocker |
|
||||
|------|---------|
|
||||
| TASK-E510 (SSO/SCIM) | WorkOS account + IdP integration testing |
|
||||
| TASK-T1010 (ISO 27001) | Accredited cert body + 12-18 months |
|
||||
| TASK-T1020 (bug bounty) | HackerOne/Intigriti account |
|
||||
| TASK-CP910 (docs) | Mintlify account |
|
||||
| TASK-CP930 (community) | Discourse hosting or Slack Connect |
|
||||
| TASK-CP940 (certification) | Teachable/LearnWorlds account |
|
||||
| TASK-CP950 (conference) | Event venue booking |
|
||||
| TASK-AI450 (voice) | ElevenLabs account + customer demand |
|
||||
| TASK-S710 (multi-region) | AWS account + production customers |
|
||||
| TASK-R1110 (localization) | Market validation per country |
|
||||
|
||||
## Requires Product-Market Fit Signal
|
||||
|
||||
These shouldn't start until paying customers exist:
|
||||
- Workflow Builder (P320)
|
||||
- Partner Program (C840)
|
||||
- Referral Engine (C850)
|
||||
- Multi-agent orchestrator (AI410) — has small version now, full scale later
|
||||
- Voice interface (AI450)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 Completion Criteria (18 months)
|
||||
|
||||
| Signal | Threshold |
|
||||
|--------|-----------|
|
||||
| Organic inbound (Arabic enterprise AI keywords) | Top 3 for 20+ commercial keywords |
|
||||
| Named customer references | ≥ 15 across ≥ 3 countries |
|
||||
| Open-source contributions | ≥ 6 accepted upstream PRs |
|
||||
| Whitepapers cited externally | ≥ 2 |
|
||||
| Conference keynotes | ≥ 6 regional + ≥ 2 international |
|
||||
| NPS | ≥ 50 |
|
||||
| NRR | ≥ 120% |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 Execution Order Recommendation
|
||||
|
||||
### Month 1-3 (immediate)
|
||||
- TASK-F201: Design system foundation (blocks most frontend)
|
||||
- TASK-F210: Arabic typography
|
||||
- TASK-F211: RTL-aware layout
|
||||
- TASK-AI460: Eval harness v2
|
||||
- TASK-CAT1310: Publish manifesto
|
||||
|
||||
### Month 3-6 (after first paying customers)
|
||||
- TASK-F220/230/240: Performance + a11y + motion
|
||||
- TASK-E510: SSO/SCIM (enables enterprise deals)
|
||||
- TASK-E520: OpenFGA ABAC
|
||||
- TASK-I620: ZATCA integration (if KSA customers)
|
||||
- TASK-P310: Second golden path
|
||||
|
||||
### Month 6-12
|
||||
- TASK-E530: Audit platform
|
||||
- TASK-E540: Data residency options
|
||||
- TASK-I621: MENA connectors (on demand)
|
||||
- TASK-T1010: ISO 27001 start
|
||||
- TASK-CP910: Docs portal launch
|
||||
|
||||
### Month 12-18
|
||||
- TASK-F290: Executive iOS app
|
||||
- TASK-S710: Multi-region
|
||||
- TASK-T1020: Bug bounty program
|
||||
- TASK-CP950: First Dealix Majlis conference
|
||||
- TASK-CAT1340: Open-source @dealix/arabic-ui
|
||||
|
||||
---
|
||||
|
||||
## Signature Components — Phase 2 Anchors
|
||||
|
||||
### 1. ApprovalCard (from Phase 2 §1.8)
|
||||
The one component that shows the Dealix signature:
|
||||
- Narrative brief authored by LLM at generation time
|
||||
- Evidence count + model inference count + policy check status
|
||||
- Economics summary with forecast impact
|
||||
- Risk flags with color + reason
|
||||
- Keyboard shortcuts: ⌘1 approve, ⌘2 request more, ⌘3 reject, ⌘E open evidence
|
||||
|
||||
### 2. Executive Room Weekly Pack
|
||||
Already live in Phase 1 at `/api/v1/executive-room/weekly-pack`. Phase 2 adds:
|
||||
- Narrative header (ExecBriefAgent-generated)
|
||||
- Interactive drill-down to source evidence
|
||||
- Dual Gregorian/Hijri dates
|
||||
- Board-ready PDF export with embedded Arabic fonts
|
||||
|
||||
### 3. Evidence Timeline
|
||||
The story of a decision rendered as a readable timeline:
|
||||
- Who proposed → data sources consulted → model reasoning → approvals collected → final commitment
|
||||
- Every node clickable → full provenance
|
||||
- Scrubbable timeline for long workflows
|
||||
|
||||
---
|
||||
|
||||
## Non-Negotiable Phase 2 Invariants
|
||||
|
||||
Extended from Phase 1:
|
||||
1. Performance budget enforced in CI (LCP <1.5s, INP <150ms, CLS <0.05)
|
||||
2. Accessibility: 0 axe violations; WCAG 2.2 AA minimum, AAA for approval surfaces
|
||||
3. Arabic parity: every new feature ships with Arabic UI + Arabic docs
|
||||
4. Zero claims beyond `claims_registry.yaml`
|
||||
5. Every LLM call goes through `dealix.ai.router` (no direct provider imports)
|
||||
6. Every side-effect has idempotency key
|
||||
7. Every external commitment has approval + evidence + correlation
|
||||
257
salesflow-saas/DEALIX_PHASE2_EXECUTION_WAVES.md
Normal file
257
salesflow-saas/DEALIX_PHASE2_EXECUTION_WAVES.md
Normal file
@ -0,0 +1,257 @@
|
||||
# DEALIX — Phase 2 Execution Waves (90-Day Plan)
|
||||
|
||||
> **Core rule**: From self-reported completion to externally-validated reality.
|
||||
> **Success metric**: 3 paying pilot customers + externally-validated security posture within 90 days.
|
||||
> **Next action for coding agent**: Execute ONLY Verification Protocol (V001–V007). Do NOT start Wave A tasks until Week-12 Phase Gate returns Green.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Phase 1 foundation exists. Phase 2 foundation scaffolded. **This document governs the next 90 days** — specifically resisting "Plan Completion Syndrome" (generating plans faster than executing them).
|
||||
|
||||
**Rule**: No new features ship until:
|
||||
1. Verification Protocol (§1) completes with external validation
|
||||
2. Founder Decision Sprint (§2) closes (4 founder decisions)
|
||||
3. Customer Validation (§3) returns ≥ 3 paying pilots
|
||||
|
||||
**Agent execution scope this phase**: V-tasks + scaffolding for FD and CV tracks. That's it.
|
||||
|
||||
---
|
||||
|
||||
## §1 — Verification Protocol (Weeks 1-2, before ANY new feature work)
|
||||
|
||||
Convert self-reported completion into externally-validated reality.
|
||||
|
||||
### V001 — Full git history secret scan
|
||||
- **Beyond HEAD**: scan all 146+ commits with trufflehog + gitleaks
|
||||
- **Two-tool rule**: defense in depth
|
||||
- **Output**: `docs/internal/secret_audit_log.md` with every finding documented
|
||||
|
||||
### V002 — Runtime RLS fuzz test
|
||||
- 10,000 cross-tenant queries as Tenant A switching to Tenant B
|
||||
- Expected: zero rows returned from Tenant B's context
|
||||
- Added to nightly CI
|
||||
- Any violation = P0 incident
|
||||
|
||||
### V003 — External pentest
|
||||
- Engage Cure53, Trail of Bits, NCC Group, or Securinc
|
||||
- Scope: auth, RLS enforcement, ABAC, LLM injection, file uploads, webhooks
|
||||
- Budget: $20K-40K
|
||||
- **Cannot claim "pentested" until report exists**
|
||||
|
||||
### V004 — No-founder customer demo test
|
||||
- 3 fresh testers complete golden path unassisted
|
||||
- Founder watches silently
|
||||
- Acceptance: 2/3 complete in <30 min with no show-stopper
|
||||
|
||||
### V005 — Truth Registry independent audit
|
||||
- Engineer who did NOT write registry audits every claim
|
||||
- Verdicts: SUPPORTED / UNSUPPORTED / AMBIGUOUS
|
||||
- Any UNSUPPORTED → evidence added or demoted to roadmap within 48h
|
||||
|
||||
### V006 — Performance baseline
|
||||
- k6 load test against staging with production-like data
|
||||
- Output: `docs/baselines/perf_YYYYMMDD.json`
|
||||
- Every future perf claim references this baseline
|
||||
|
||||
### V007 — Accessibility baseline
|
||||
- Playwright + axe full scan
|
||||
- Output: `docs/baselines/a11y_YYYYMMDD.json`
|
||||
- Every future a11y claim references this baseline
|
||||
|
||||
---
|
||||
|
||||
## §2 — Founder Decision Sprint (Weeks 1-2, parallel)
|
||||
|
||||
**Agent cannot execute these.** Founder-only.
|
||||
|
||||
### FD001 — Legal entity decision
|
||||
- MISA KSA LLC (recommended default for Saudi-primary positioning)
|
||||
- OR DIFC/ADGM (UAE)
|
||||
- OR Delaware C-Corp + KSA subsidiary (if raising US VC)
|
||||
- Output: `docs/internal/legal_entity_decision.md`
|
||||
- **Deadline: Week 2**
|
||||
|
||||
### FD002 — Counsel engaged
|
||||
- Al Tamimi / Clyde & Co / local boutique
|
||||
- Budget: 30-80K SAR initial engagement
|
||||
- **Deadline: Week 2**
|
||||
|
||||
### FD003 — Repository extraction completed
|
||||
- GitHub org created
|
||||
- Phase 1 TASK-001 script executed
|
||||
- Old fork archived/privatized
|
||||
- **Deadline: Week 1**
|
||||
|
||||
### FD004 — SAIP trademark filed
|
||||
- Classes: 9, 35, 42 (+ 41 if community)
|
||||
- Marks: Dealix (Latin) + ديلكس (Arabic)
|
||||
- Via counsel
|
||||
- **Deadline: Week 3**
|
||||
|
||||
### FD005 — First hires initiated
|
||||
- Founding Design Engineer (#1) — 30-45K SAR/month + 0.5-2% equity
|
||||
- Founding Backend Engineer (#2) — 25-40K SAR/month
|
||||
- Head of Customer Success (#3) — 35-55K SAR/month
|
||||
- **Deadline: Week 4 (60-90 day lead time)**
|
||||
|
||||
---
|
||||
|
||||
## §3 — Customer Validation Program (Weeks 3-12)
|
||||
|
||||
**Hard rule**: no Phase 2 feature ships until pilot customers drive the backlog.
|
||||
|
||||
### ICP Filter
|
||||
- Saudi-based HQ or KSA ops
|
||||
- 200-2,000 employees
|
||||
- Pain in commercial operations
|
||||
- CFO/COO/GM personal sponsor
|
||||
- Bilingual operations
|
||||
|
||||
### Pilot Structure
|
||||
- 90 days
|
||||
- 50% of Business tier ($1,500 total upfront)
|
||||
- Defined success criteria before signing
|
||||
- Weekly 30-min feedback session
|
||||
- Permission for case study if successful
|
||||
|
||||
### First 3 (design partners)
|
||||
- 6-month credit in exchange for:
|
||||
- Public testimonial + logo
|
||||
- Recorded case study
|
||||
- Speaking slot at first event
|
||||
|
||||
### Week-12 Phase Gate
|
||||
|
||||
| Signal | Green | Yellow | Red |
|
||||
|--------|-------|--------|-----|
|
||||
| Customers with signed success | 3+ | 1-2 | 0 |
|
||||
| Golden path completion rate | >90% | 70-90% | <70% |
|
||||
| NPS | >30 | 0-30 | <0 |
|
||||
| References willing | 3+ | 1-2 | 0 |
|
||||
| Renewal intent | 3+ verbal | 1-2 | 0 |
|
||||
|
||||
- All Green: proceed to Wave A
|
||||
- Mostly Yellow: extend pilot 60 days
|
||||
- Any Red: HALT Phase 2 execution
|
||||
|
||||
---
|
||||
|
||||
## §4 — Phase 2 Execution Waves
|
||||
|
||||
**Waves, not streams**: each has a customer-impact gate.
|
||||
|
||||
### Wave A — Frontend Signature (Weeks 4-20)
|
||||
- F201 (DS foundation) → F270 (Approval Card pattern)
|
||||
- Exit: Lighthouse ≥95 on 5 routes + zero axe violations + 2+ pilots spontaneously compliment UI
|
||||
|
||||
### Wave B — Enterprise Unlock (Weeks 8-28, parallel)
|
||||
- E510 (SSO/SCIM via WorkOS) → E550 (SLA tiers)
|
||||
- Exit: First Business tier deal with SSO + audit export validated + <3 day security questionnaire turnaround
|
||||
|
||||
### Wave C — AI Depth (Weeks 16-36)
|
||||
- AI410 (orchestrator) + AI440 (RAG) + AI460 (eval v2)
|
||||
- Exit: +20pp Arabic performance vs baseline + first Dealix Labs benchmark paper
|
||||
|
||||
### Wave D — Ecosystem (Weeks 24-44)
|
||||
- I610 (public API) + I620 (ZATCA) + I621 (2 MENA connectors)
|
||||
- Exit: 3 integrations live + public API docs + 1 partner integration certified
|
||||
|
||||
### Wave E — Regional (Weeks 32-52)
|
||||
- R1110 (UAE localization) + GITEX presence + trust portal public
|
||||
- Exit: 1 UAE customer + 1 Egypt pilot + trust portal 30+ days uptime
|
||||
|
||||
---
|
||||
|
||||
## §5 — Operating System
|
||||
|
||||
### Weekly Rhythm
|
||||
| Day | Block |
|
||||
|-----|-------|
|
||||
| Mon AM | Metrics review |
|
||||
| Mon PM | Customer pipeline |
|
||||
| Tue | Product standup |
|
||||
| Wed | Customer learnings synthesis |
|
||||
| Thu | Release window |
|
||||
| Fri AM | Security review |
|
||||
| Fri PM | Retrospective |
|
||||
|
||||
**Rule**: No deploys Friday after 14:00 AST. Ever.
|
||||
|
||||
### Decision Framework
|
||||
1. Reversibility test (reversible → fast; irreversible → founder call)
|
||||
2. Signature alignment (Arabic-first, evidence-backed, decision-grade)
|
||||
3. Cost of delay
|
||||
4. Customer test (would pilot customer ask for this?)
|
||||
5. Moat compounding
|
||||
|
||||
---
|
||||
|
||||
## §6 — Failure Modes to Actively Resist
|
||||
|
||||
| Failure | Defense |
|
||||
|---------|---------|
|
||||
| Plan Completion Syndrome | Monthly planning/shipping ratio check; if planning >30%, stop |
|
||||
| Premature Scaling | Hire-gate tied to MRR milestones |
|
||||
| Customer Proxy Syndrome | 10 customer hours/week minimum for founder |
|
||||
| Integration Sprawl | Only build integrations appearing in ≥3 pilot conversations |
|
||||
| Security Theater | Auditor report or not-claimed |
|
||||
| Arabic-First Erosion | Any English-only feature blocked at review |
|
||||
| Founder Bottleneck | Authority matrix published by Month 6 |
|
||||
|
||||
---
|
||||
|
||||
## §7 — Day 90 Success Criteria (from Appendix A)
|
||||
|
||||
- [ ] 3 signed pilot customers (paid), 2 in active use
|
||||
- [ ] Pentest report received; no open Critical, ≤2 open High
|
||||
- [ ] Full history secret audit: 0 verified findings
|
||||
- [ ] Truth Registry: 100% SUPPORTED claims
|
||||
- [ ] First 3 hires: offers extended/accepted
|
||||
- [ ] Repository extraction: done, old fork private
|
||||
- [ ] Trademark: filed
|
||||
- [ ] Legal entity: incorporated or restructuring with ETA
|
||||
- [ ] Wave A: 40% progressed with measurable milestones
|
||||
- [ ] NPS measured
|
||||
- [ ] ≥1 customer reference willing to take a call
|
||||
- [ ] Dealix Labs: 1 published research piece
|
||||
|
||||
**≥10/12 = category-defining trajectory. 6-9 = correctable. ≤5 = fundamental rethink.**
|
||||
|
||||
---
|
||||
|
||||
## §8 — What Dealix is NOT Doing in These 90 Days
|
||||
|
||||
Every "no" enables a sharper yes:
|
||||
|
||||
- ❌ New features off Wave A critical path
|
||||
- ❌ Integrations no customer asked for
|
||||
- ❌ Public marketing campaigns
|
||||
- ❌ PR pushes
|
||||
- ❌ Investor fundraising (unless already in progress)
|
||||
- ❌ Mobile apps
|
||||
- ❌ Workflow builder
|
||||
- ❌ Voice interface
|
||||
- ❌ Community platform
|
||||
- ❌ Certification program
|
||||
- ❌ Partner program
|
||||
- ❌ "Thought leadership" beyond manifesto
|
||||
|
||||
---
|
||||
|
||||
## Coding Agent Instructions
|
||||
|
||||
1. **Execute Verification Protocol tasks (V001-V007)** — honest reporting, including unsupported claims
|
||||
2. **Prepare scaffolding for FD tasks** — job specs, counsel research, trademark prep — but DO NOT execute founder-only decisions
|
||||
3. **DO NOT start any Wave task until Week-12 Phase Gate returns Green**
|
||||
4. If gate returns Yellow/Red → escalate to founder. Do not default to "ship more features"
|
||||
5. Weekly `docs/execution_log.md` entry with facts, not celebrations
|
||||
|
||||
---
|
||||
|
||||
## Honest Note
|
||||
|
||||
1. Foundation is strong. More built in 2 weeks than most build in 6 months.
|
||||
2. The next 90 days are about **proving**, not building.
|
||||
3. **Highest-leverage action: close 3 pilot customers.** Everything else is downstream.
|
||||
136
salesflow-saas/FOUNDER_DECISION_PACKAGE.md
Normal file
136
salesflow-saas/FOUNDER_DECISION_PACKAGE.md
Normal file
@ -0,0 +1,136 @@
|
||||
# Founder Decision Package — Dealix Tier-1
|
||||
|
||||
> **Purpose**: Everything the founder needs to make 4 decisions and unblock full execution.
|
||||
> **All code automation is DONE.** Only these decisions remain.
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: GitHub Organization Name (5 min)
|
||||
|
||||
### Options
|
||||
| Option | Pro | Con |
|
||||
|--------|-----|-----|
|
||||
| `dealix-io` | Clean, aligns with dealix.io domain | Standard |
|
||||
| `dealix-hq` | Professional, enterprise feel | Less common |
|
||||
| `dealix` | Shortest, clean | May be taken |
|
||||
| `getdealix` | Matches marketing convention | Longer |
|
||||
|
||||
### Recommendation
|
||||
`dealix-io` — matches the typical domain/org pattern, easy to communicate, unambiguous.
|
||||
|
||||
### Action
|
||||
```bash
|
||||
# After creating GitHub org `dealix-io` and empty private repo `dealix-io/platform`:
|
||||
cd /home/user/system-prompts-and-models-of-ai-tools
|
||||
./salesflow-saas/scripts/extract_dealix_repo.sh git@github.com:dealix-io/platform.git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: Company Entity Structure (this week)
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Pro | Con | Estimated cost |
|
||||
|--------|-----|-----|---------------|
|
||||
| **MISA Startup License (KSA)** | Saudi-first customers prefer local entity; Vision 2030 alignment; PDPL compliance easier | Requires Saudi founder/partner for some structures; 100% foreign allowed in many sectors now | ~15-30K SAR setup |
|
||||
| **DIFC (UAE)** | Strong IP protection; easier banking; common-law courts; international-friendly | Not "Saudi-first" for KSA sales; distance from market | ~50-100K AED setup |
|
||||
| **ADGM (UAE)** | Similar to DIFC but often faster | Same "not Saudi" issue | ~50-100K AED setup |
|
||||
| **Two-entity structure (KSA + UAE)** | Best of both | Complex; more cost | 70-130K total |
|
||||
|
||||
### Recommendation
|
||||
**MISA Startup License** if founder is Saudi or has Saudi partner. **DIFC** if founder is foreign and speed matters more than local presence.
|
||||
|
||||
### Action
|
||||
Engage one of these:
|
||||
1. **TAM (Saudi)** — https://tamkeentech.net (Saudi startup formation)
|
||||
2. **Diligents** — KSA/GCC legal formations
|
||||
3. **DIFC Authority** — self-service if DIFC route
|
||||
|
||||
Expected timeline: 4-12 weeks.
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: Saudi Legal Counsel (this month)
|
||||
|
||||
Saudi privacy policy + ToS + DPA review is **mandatory** before customer-facing launch.
|
||||
|
||||
### Recommended Firms (by specialization)
|
||||
|
||||
| Firm | Strength | Fit |
|
||||
|------|----------|-----|
|
||||
| **Al Tamimi & Company** | Largest KSA + GCC practice | Best for enterprise contracts + IP |
|
||||
| **Clyde & Co (Riyadh)** | Strong in tech + data | Good PDPL expertise |
|
||||
| **Bird & Bird (Riyadh)** | International, tech-focused | Good for cross-border |
|
||||
| **Nowfal Law Firm** | Saudi boutique, fast | Cost-effective for startups |
|
||||
|
||||
### Scope to Request
|
||||
1. Review + customize `docs/legal/templates/PRIVACY_POLICY_EN.md`
|
||||
2. Review + customize `docs/legal/templates/TERMS_OF_SERVICE_EN.md`
|
||||
3. Review + customize `docs/legal/templates/DPA_EN.md`
|
||||
4. Review + customize `docs/legal/templates/PRIVACY_POLICY_AR.md`
|
||||
5. Create Arabic versions of ToS + DPA
|
||||
6. Verify Saudi PDPL compliance (especially cross-border transfers)
|
||||
7. Create IP assignment agreement final version
|
||||
8. One-hour consult on entity structure if not yet decided
|
||||
|
||||
**Budget**: 15-30K SAR for full scope.
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Trademark Filing (this month)
|
||||
|
||||
### Marks to File
|
||||
- "DEALIX" (Latin)
|
||||
- "ديلكس" (Arabic)
|
||||
- Logo (once finalized)
|
||||
|
||||
### Classes
|
||||
- 9 (software)
|
||||
- 42 (SaaS)
|
||||
- 35 (business services)
|
||||
|
||||
### Jurisdictions (priority order)
|
||||
1. **KSA (SAIP)** — 5,000 SAR/class, file this week
|
||||
2. **UAE** — 5,500 SAR/class, file within 30 days
|
||||
3. **Egypt, Jordan, Kuwait** — within 90 days
|
||||
|
||||
### Recommended Agent
|
||||
**Abu-Ghazaleh Intellectual Property (AGIP)** — largest MENA IP firm, handles all GCC in one engagement.
|
||||
|
||||
**Total budget**: ~90-120K SAR across all MENA jurisdictions.
|
||||
|
||||
### Self-Serve Alternative (KSA only)
|
||||
Founder can file directly at https://qima.saip.gov.sa — save ~2-4K SAR per filing but slower learning curve.
|
||||
|
||||
---
|
||||
|
||||
## What's Already Automated (no decisions needed)
|
||||
|
||||
- ✓ Extraction script ready: `scripts/extract_dealix_repo.sh`
|
||||
- ✓ Python deps pinned + pyproject.toml for uv
|
||||
- ✓ Node deps pinned to pnpm@9.12.0
|
||||
- ✓ Pre-commit hooks: gitleaks + detect-private-key + ruff
|
||||
- ✓ Secret scan completed (1 false positive, documented)
|
||||
- ✓ Rotation log template
|
||||
- ✓ Legal status tracker
|
||||
- ✓ Legal templates (IP Assignment, Privacy EN+AR, ToS, DPA)
|
||||
- ✓ Trademark filing kit with application text
|
||||
- ✓ Truth registry + claims registry + validator + CI
|
||||
- ✓ Release readiness gate (blueprint-spec)
|
||||
- ✓ Architecture brief (40/40) + Release readiness matrix (53/53)
|
||||
- ✓ Golden path, Saudi workflow, 17 structured schemas, RLS migration, idempotency, OpenTelemetry, durable execution
|
||||
|
||||
---
|
||||
|
||||
## Total Founder Time to Unblock Full Execution
|
||||
|
||||
| Decision | Time required |
|
||||
|----------|--------------|
|
||||
| GitHub org name | 5 minutes |
|
||||
| Entity structure | 2-4 hours research + engagement |
|
||||
| Counsel engagement | 2-3 meetings + 15-30K SAR |
|
||||
| Trademark filing | 1-2 hours with agent + 15-30K SAR |
|
||||
| **Total founder time** | **~1 week of attention + ~50K SAR initial outlay** |
|
||||
|
||||
After these decisions, Phase 1 is truly complete and Phase 2 can begin.
|
||||
94
salesflow-saas/LAUNCH_GATES.md
Normal file
94
salesflow-saas/LAUNCH_GATES.md
Normal file
@ -0,0 +1,94 @@
|
||||
# Dealix Launch Gates Checklist
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Last updated:** 2026-04-23
|
||||
**Target:** 24/30 gates closed before declaring Soft Launch
|
||||
|
||||
---
|
||||
|
||||
## Technical 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 |
|
||||
|
||||
## Security 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 |
|
||||
|
||||
## Observability 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 |
|
||||
|
||||
## GTM / Funnel 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 |
|
||||
|
||||
## Governance Gates
|
||||
|
||||
| # | Gate | Status | Notes |
|
||||
|---|------|--------|-------|
|
||||
| V1 | Approvals gate on outbound | Partial | approval_center exists, threshold enforcement built |
|
||||
|
||||
---
|
||||
|
||||
## 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** |
|
||||
|
||||
**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.
|
||||
|
||||
**Blocked by founder action:** PostHog key (O3), Moyasar key (G2), HubSpot+Calendly keys (G3/G4), UptimeRobot key (I3).
|
||||
172
salesflow-saas/MASTER_OPERATING_PROMPT.md
Normal file
172
salesflow-saas/MASTER_OPERATING_PROMPT.md
Normal file
@ -0,0 +1,172 @@
|
||||
# MASTER OPERATING PROMPT — Dealix Sovereign Enterprise Growth OS
|
||||
|
||||
> **Version**: 1.0
|
||||
> **Status**: Canonical
|
||||
> **Effective**: 2026-04-16
|
||||
> **Scope**: All agents, services, documents, and humans operating within Dealix
|
||||
|
||||
---
|
||||
|
||||
## 1. Identity
|
||||
|
||||
**Dealix** is a **Sovereign Enterprise Growth OS for GCC Companies**.
|
||||
|
||||
It is a single platform that manages:
|
||||
- **Revenue** — lead-to-cash lifecycle
|
||||
- **Partnerships** — alliance scouting to co-sell
|
||||
- **Corporate Development / M&A** — target sourcing to PMI
|
||||
- **Expansion** — market scanning to post-launch
|
||||
- **PMI / Strategic PMO** — Day-1 readiness to synergy realization
|
||||
- **Trust / Governance / Executive Decisioning** — policy gates to board packs
|
||||
|
||||
**Central Law**:
|
||||
> AI explores, analyzes, and proposes. Systems execute. Humans approve critical decisions. Everything is proven by evidence.
|
||||
|
||||
**Design Philosophy**:
|
||||
> Agentic by design, governed by policy, proven by evidence.
|
||||
|
||||
---
|
||||
|
||||
## 2. Five-Plane Architecture
|
||||
|
||||
Every component in Dealix belongs to exactly one plane:
|
||||
|
||||
| Plane | Purpose | Key Code |
|
||||
|-------|---------|----------|
|
||||
| **Decision** | Strategic reasoning, forecasting, memo generation | `executive_roi_service.py`, `analytics_service.py`, management agents |
|
||||
| **Execution** | Durable workflows, task routing, agent dispatch | `openclaw/gateway.py`, `durable_flow.py`, `task_router.py`, Celery workers |
|
||||
| **Trust** | Policy enforcement, approval gates, audit, compliance | `policy.py`, `approval_bridge.py`, `hooks.py`, `pdpl/`, `audit_service.py` |
|
||||
| **Data** | Storage, retrieval, enrichment, vector search, events | PostgreSQL + pgvector, Redis, `knowledge_service.py`, domain events |
|
||||
| **Operating** | Monitoring, self-improvement, deployment, CI/CD | `observability.py`, `self_improvement.py`, `feature_flags.py`, GitHub Actions |
|
||||
|
||||
Full specification: [`docs/ai-operating-model.md`](docs/ai-operating-model.md)
|
||||
|
||||
---
|
||||
|
||||
## 3. Six Tracks
|
||||
|
||||
All work is organized into six strategic tracks:
|
||||
|
||||
| Track | Domain | Owner Focus |
|
||||
|-------|--------|-------------|
|
||||
| **Revenue** | Lead capture → qualification → deal → close → renewal | Sales & Growth |
|
||||
| **Intelligence** | Signal detection, behavior analysis, forecasting, AI agents | AI & Data |
|
||||
| **Compliance** | PDPL, ZATCA, SDAIA, sector regulations, audit trails | Legal & Security |
|
||||
| **Expansion** | Strategic deals, M&A, partnerships, geographic expansion | Corporate Dev |
|
||||
| **Operations** | Deployment, monitoring, connectors, infrastructure | Engineering & Ops |
|
||||
| **Trust** | Policy gates, approval SLAs, evidence packs, contradiction detection | Governance |
|
||||
|
||||
Full specification: [`docs/dealix-six-tracks.md`](docs/dealix-six-tracks.md)
|
||||
|
||||
---
|
||||
|
||||
## 4. Policy Classes
|
||||
|
||||
Every action in the system is classified:
|
||||
|
||||
| Class | Behavior | Examples |
|
||||
|-------|----------|----------|
|
||||
| **A — Auto-allowed** | Execute without approval | `read_status`, `classify`, `summarize`, `research`, `generate_draft` |
|
||||
| **B — Approval-gated** | Requires human approval token | `send_whatsapp`, `send_email`, `create_charge`, `sync_salesforce`, `send_contract_for_signature` |
|
||||
| **C — Forbidden** | Blocked unconditionally | `exfiltrate_secrets`, `delete_data_without_audit`, `bypass_auth` |
|
||||
|
||||
Implementation: [`backend/app/openclaw/policy.py`](backend/app/openclaw/policy.py)
|
||||
|
||||
**Default rule**: Unknown actions are classified as **Class B** (approval required).
|
||||
|
||||
---
|
||||
|
||||
## 5. Execution Principles
|
||||
|
||||
1. **Decision-native** — Every critical path produces structured output (JSON Schema), not free text.
|
||||
2. **Execution-durable** — Workflows checkpoint, resume after failure, and support compensation.
|
||||
3. **Trust-enforced** — No sensitive action bypasses the policy gate.
|
||||
4. **Data-governed** — All data flows through governed ingestion with quality checks.
|
||||
5. **Arabic-first** — All user-facing content defaults to Arabic, with English as secondary.
|
||||
6. **Saudi-ready** — PDPL consent checks, ZATCA invoicing, SDAIA AI governance, and NCA cybersecurity controls are implemented with live enforcement on the golden path and Saudi workflow. Full production coverage is tracked in `docs/current-vs-target-register.md`.
|
||||
7. **Board-usable** — Executive surfaces show what changed, what needs decision, what is at risk.
|
||||
8. **Enterprise-saleable** — Evidence packs, audit trails, and compliance matrices are exportable.
|
||||
|
||||
---
|
||||
|
||||
## 6. Non-Negotiable Rules
|
||||
|
||||
1. **Tenant isolation**: Every query is scoped by `tenant_id`. Cross-tenant access is blocked at ORM layer.
|
||||
2. **Consent-before-send**: No outbound message (WhatsApp, email, SMS, voice) without verified PDPL consent.
|
||||
3. **Audit everything**: Every state change writes to `audit_logs`. Every AI decision writes to `ai_conversations`.
|
||||
4. **No overclaim**: Documents must distinguish **Current State** (deployed) from **Target State** (planned). Never claim what is not in production.
|
||||
5. **Structured outputs**: All critical memos, scores, and packs use defined schemas, not prose.
|
||||
6. **Human-in-the-loop**: Term sheets, signatures, market launches, M&A offers, discounts outside policy, production promotions, and high-sensitivity data sharing require human approval.
|
||||
7. **Root-anchored execution**: All scripts and commands execute from repository root. `scripts/architecture_brief.py` is the official preflight.
|
||||
|
||||
---
|
||||
|
||||
## 7. Contradiction Resolution
|
||||
|
||||
When documents or systems conflict:
|
||||
|
||||
1. **MASTER_OPERATING_PROMPT.md** wins over all other documents.
|
||||
2. Governance docs (`docs/governance/*`) win over operational docs.
|
||||
3. `CLAUDE.md` / `AGENTS.md` win over `memory/` docs.
|
||||
4. Code behavior wins over comments about code behavior.
|
||||
5. Active contradictions are tracked in the **Contradiction Engine** (`/api/v1/contradictions/`).
|
||||
|
||||
---
|
||||
|
||||
## 8. Technology Radar Summary
|
||||
|
||||
| Tier | Technologies |
|
||||
|------|-------------|
|
||||
| **Core** (production) | FastAPI, SQLAlchemy, PostgreSQL 16, Redis, Celery, Next.js 15, OpenClaw 2026.4.x, Groq, WhatsApp Cloud API |
|
||||
| **Strong** (validated) | Claude Opus, Salesforce Agentforce, Stripe, pgvector, Mem0, LangGraph |
|
||||
| **Pilot** (behind flags) | Voice agents, Contract intelligence, Gemini/DeepSeek routing |
|
||||
| **Watch** (evaluating) | Temporal, OPA, OpenFGA, Vault, Gong, Apollo |
|
||||
| **Hold** (not adopting) | External RAG SaaS, schema-per-tenant, GraphQL |
|
||||
|
||||
Full specification: [`docs/governance/technology-radar-tier1.md`](docs/governance/technology-radar-tier1.md)
|
||||
|
||||
---
|
||||
|
||||
## 9. Document Index
|
||||
|
||||
| Document | Path | Purpose |
|
||||
|----------|------|---------|
|
||||
| AI Operating Model | `docs/ai-operating-model.md` | Five-plane architecture |
|
||||
| Six Tracks | `docs/dealix-six-tracks.md` | Strategic track framework |
|
||||
| Execution Fabric | `docs/governance/execution-fabric.md` | Execution plane deep dive |
|
||||
| Trust Fabric | `docs/governance/trust-fabric.md` | Trust plane deep dive |
|
||||
| Saudi Compliance | `docs/governance/saudi-compliance-and-ai-governance.md` | Regulatory controls |
|
||||
| Technology Radar | `docs/governance/technology-radar-tier1.md` | Technology classification |
|
||||
| Partnership OS | `docs/governance/partnership-os.md` | Partnership lifecycle |
|
||||
| M&A OS | `docs/governance/ma-os.md` | Corporate development |
|
||||
| Expansion OS | `docs/governance/expansion-os.md` | Geographic/vertical expansion |
|
||||
| PMI OS | `docs/governance/pmi-os.md` | Post-merger integration |
|
||||
| Executive Board OS | `docs/governance/executive-board-os.md` | Board reporting framework |
|
||||
| 90-Day Matrix | `docs/execution-matrix-90d-tier1.md` | Sprint execution plan |
|
||||
| ADR 0001 | `docs/adr/0001-tier1-execution-policy-spikes.md` | Tier-1 policy decisions |
|
||||
| Current vs Target | `docs/current-vs-target-register.md` | Subsystem maturity register |
|
||||
| Doc Consistency Audit | `docs/governance/document-consistency-audit.md` | Cross-reference verification |
|
||||
| Structured Outputs | `backend/app/schemas/structured_outputs.py` | 17 Pydantic decision schemas |
|
||||
| Workflow Inventory | `docs/governance/workflow-inventory.md` | Short/medium/long classification |
|
||||
| Trust Closure Plan | `docs/governance/trust-closure-plan.md` | Trust plane completion gates |
|
||||
| Connector Standard | `docs/governance/connector-standard.md` | Connector facade + metrics |
|
||||
| Operating Checklist | `docs/governance/operating-plane-checklist.md` | Enterprise delivery controls |
|
||||
| Saudi Readiness | `docs/governance/saudi-enterprise-readiness.md` | PDPL/NCA/SDAIA operationalization |
|
||||
| Executive Surface Plan | `docs/governance/executive-surface-closure.md` | Surface wiring plan |
|
||||
| Market Dominance | `docs/governance/market-dominance-plan.md` | Packaging + ROI + competitive wedge |
|
||||
| Master Closure Checklist | `docs/tier1-master-closure-checklist.md` | 50-item definitive checklist |
|
||||
| Architecture | `docs/ARCHITECTURE.md` | System diagram |
|
||||
| Data Model | `docs/DATA-MODEL.md` | Database schema |
|
||||
| Agent Map | `docs/AGENT-MAP.md` | 19 AI agents |
|
||||
| API Map | `docs/API-MAP.md` | 70+ endpoints |
|
||||
|
||||
---
|
||||
|
||||
## 10. Enforcement
|
||||
|
||||
This document is enforced by:
|
||||
- `scripts/architecture_brief.py` — validates document existence and cross-references
|
||||
- `backend/app/openclaw/policy.py` — enforces action classification
|
||||
- `backend/app/openclaw/approval_bridge.py` — enforces approval gates
|
||||
- `.github/workflows/dealix-ci.yml` — runs tests and checks on every PR
|
||||
- Contradiction Engine — detects and tracks document/system conflicts
|
||||
131
salesflow-saas/RUNBOOK.md
Normal file
131
salesflow-saas/RUNBOOK.md
Normal file
@ -0,0 +1,131 @@
|
||||
# Dealix Operational Runbook
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Last updated:** 2026-04-23
|
||||
**Owner:** Ops Lead
|
||||
|
||||
---
|
||||
|
||||
## Scenario 1: Service Down (API not responding)
|
||||
|
||||
**Detection:** UptimeRobot alert on `api.dealix.me/health` or Sentry alert spike.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. SSH to server: `ssh dealix_deploy@188.245.55.180`
|
||||
2. Check systemd status: `sudo systemctl status dealix-api`
|
||||
3. Check logs: `sudo journalctl -u dealix-api --since '10 min ago' -n 100`
|
||||
4. If crashed: `sudo systemctl restart dealix-api`
|
||||
5. Verify: `curl http://localhost:8001/health`
|
||||
6. If still failing, check port conflict: `sudo ss -tlnp | grep 8001`
|
||||
7. Check disk space: `df -h` (full disk = crash)
|
||||
8. Check memory: `free -h` (OOM killer may have killed uvicorn)
|
||||
9. If persistent: rollback to previous version (see Scenario 5)
|
||||
|
||||
**Recovery time target:** < 5 minutes
|
||||
**Escalation:** If not resolved in 15 minutes, escalate to founder.
|
||||
|
||||
---
|
||||
|
||||
## Scenario 2: Database Down (Postgres unreachable)
|
||||
|
||||
**Detection:** `/health/deep` returns `postgres: failed` or Sentry DB connection errors.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Check Postgres status: `sudo systemctl status postgresql`
|
||||
2. If stopped: `sudo systemctl start postgresql`
|
||||
3. Check Postgres logs: `sudo journalctl -u postgresql --since '10 min ago'`
|
||||
4. Check connections: `sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_activity;"`
|
||||
5. If max connections hit: `sudo -u postgres psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state='idle' AND query_start < now() - interval '30 min';"`
|
||||
6. Check disk: `df -h /var/lib/postgresql`
|
||||
7. If data corruption: restore from backup (see Scenario 4)
|
||||
8. Verify: `curl http://localhost:8001/health/deep | python3 -m json.tool`
|
||||
|
||||
**Recovery time target:** < 10 minutes
|
||||
**Last backup location:** `/var/backups/dealix/` (daily cron)
|
||||
|
||||
---
|
||||
|
||||
## Scenario 3: LLM Provider Down (Groq/OpenAI)
|
||||
|
||||
**Detection:** `/health/deep` shows LLM provider failures, or Sentry errors on `/api/v1/ai-agents/*`.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Check which provider: `curl http://localhost:8001/health/deep | python3 -m json.tool`
|
||||
2. If Groq down: system should auto-fallback to OpenAI (check `LLM_FALLBACK_PROVIDER` in `.env`)
|
||||
3. Verify fallback: `curl -X POST http://localhost:8001/api/v1/ai-agents/test-prompt`
|
||||
4. If both down: check API keys validity
|
||||
5. Check provider status pages:
|
||||
- Groq: `https://status.groq.com`
|
||||
- OpenAI: `https://status.openai.com`
|
||||
6. If keys expired: rotate keys in `.env`, restart: `sudo systemctl restart dealix-api`
|
||||
|
||||
**Impact:** AI features degraded but core CRUD/lead management continues working.
|
||||
**Recovery time target:** Automatic (fallback). Manual intervention only if both providers fail.
|
||||
|
||||
---
|
||||
|
||||
## Scenario 4: Database Restore from Backup
|
||||
|
||||
**When:** Data corruption, accidental deletion, or disaster recovery.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Stop the API: `sudo systemctl stop dealix-api`
|
||||
2. List available backups: `ls -lt /var/backups/dealix/*.sql.gz`
|
||||
3. Create safety snapshot of current state: `sudo -u postgres pg_dump dealix | gzip > /tmp/dealix_pre_restore_$(date +%Y%m%d_%H%M%S).sql.gz`
|
||||
4. Drop and recreate database:
|
||||
```
|
||||
sudo -u postgres psql -c "DROP DATABASE dealix;"
|
||||
sudo -u postgres psql -c "CREATE DATABASE dealix OWNER dealix;"
|
||||
```
|
||||
5. Restore: `gunzip -c /var/backups/dealix/LATEST.sql.gz | sudo -u postgres psql dealix`
|
||||
6. Verify row counts: `sudo -u postgres psql dealix -c "SELECT 'leads', count(*) FROM leads UNION ALL SELECT 'deals', count(*) FROM deals;"`
|
||||
7. Start API: `sudo systemctl start dealix-api`
|
||||
8. Verify health: `curl http://localhost:8001/health/deep`
|
||||
9. Check integrity: manually verify recent leads/deals in dashboard
|
||||
|
||||
**Recovery time target:** < 15 minutes (tested)
|
||||
**RPO:** 24 hours (daily backup)
|
||||
**RTO:** 15 minutes
|
||||
|
||||
---
|
||||
|
||||
## Scenario 5: Rollback to Previous Version
|
||||
|
||||
**When:** Bad deploy, broken feature in production.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Identify last working version: `git log --oneline -10`
|
||||
2. Check current tag: `git describe --tags --always`
|
||||
3. Checkout previous version: `git checkout v3.0.0` (or specific commit)
|
||||
4. Install deps: `pip install -r requirements.txt`
|
||||
5. Restart: `sudo systemctl restart dealix-api`
|
||||
6. Verify: `curl http://localhost:8001/health`
|
||||
7. If rolling back a migration: check `alembic history` and downgrade if needed
|
||||
8. Notify team of rollback reason
|
||||
|
||||
**Recovery time target:** < 5 minutes
|
||||
**Note:** Never force-push or delete the broken commit. Create a revert commit instead for traceability.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Check | Command |
|
||||
|---|---|
|
||||
| API health | `curl http://localhost:8001/health` |
|
||||
| Deep health | `curl http://localhost:8001/health/deep` |
|
||||
| Service status | `sudo systemctl status dealix-api` |
|
||||
| Recent logs | `sudo journalctl -u dealix-api -n 50 --no-pager` |
|
||||
| Postgres status | `sudo systemctl status postgresql` |
|
||||
| Redis status | `redis-cli ping` |
|
||||
| Disk space | `df -h` |
|
||||
| Memory | `free -h` |
|
||||
| DLQ depth | `curl http://localhost:8001/api/v1/admin/dlq/queues` |
|
||||
| Circuit breakers | `curl http://localhost:8001/api/v1/admin/circuit-breakers` |
|
||||
| Restart API | `sudo systemctl restart dealix-api` |
|
||||
| Backup now | `sudo -u postgres pg_dump dealix \| gzip > /var/backups/dealix/manual_$(date +%Y%m%d).sql.gz` |
|
||||
86
salesflow-saas/SLO.md
Normal file
86
salesflow-saas/SLO.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Dealix Service Level Objectives (SLO)
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Effective:** 2026-04-23
|
||||
**Review:** Monthly, or after any incident
|
||||
|
||||
---
|
||||
|
||||
## API Availability
|
||||
|
||||
| SLI | Target | Measurement | Alert Threshold |
|
||||
|-----|--------|-------------|-----------------|
|
||||
| Uptime (monthly) | 99.5% | UptimeRobot on `/api/v1/health` | < 99% triggers incident |
|
||||
| Health endpoint response | < 200ms p95 | k6 smoke test | > 500ms p95 |
|
||||
|
||||
## API Latency
|
||||
|
||||
| Endpoint Category | p50 Target | p95 Target | p99 Target |
|
||||
|-------------------|------------|------------|------------|
|
||||
| Health / public reads | < 50ms | < 200ms | < 500ms |
|
||||
| Pricing / plans | < 100ms | < 300ms | < 1000ms |
|
||||
| Lead CRUD | < 200ms | < 500ms | < 2000ms |
|
||||
| AI agent calls | < 2000ms | < 5000ms | < 10000ms |
|
||||
| Webhook processing | < 500ms | < 2000ms | < 5000ms |
|
||||
|
||||
## Error Rate
|
||||
|
||||
| Metric | Target | Alert |
|
||||
|--------|--------|-------|
|
||||
| HTTP 5xx rate | < 0.5% of requests | > 1% for 5 min |
|
||||
| Webhook failure rate | < 2% | > 5% for 15 min |
|
||||
| DLQ depth | < 10 entries | > 50 triggers alert |
|
||||
|
||||
## Recovery
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| RPO (Recovery Point Objective) | 24 hours (daily DB backup) |
|
||||
| RTO (Recovery Time Objective) | 15 minutes (tested via drill) |
|
||||
| Rollback time | < 5 minutes (git checkout + restart) |
|
||||
| MTTR (Mean Time To Recovery) | < 30 minutes |
|
||||
|
||||
## Revenue Funnel
|
||||
|
||||
| Step | Freshness Target |
|
||||
|------|-----------------|
|
||||
| Lead capture → PostHog event | < 5 seconds |
|
||||
| Payment webhook → PostHog event | < 10 seconds |
|
||||
| DLQ entry → first retry | < 60 seconds |
|
||||
| Approval request → notification | < 5 minutes |
|
||||
|
||||
## Monitoring
|
||||
|
||||
| System | Check Interval | Alert Channel |
|
||||
|--------|---------------|---------------|
|
||||
| UptimeRobot | 5 minutes | SMS + Email |
|
||||
| Sentry | Real-time | Email |
|
||||
| DLQ depth | On admin request | Dashboard |
|
||||
| Circuit breakers | On admin request | Dashboard |
|
||||
|
||||
---
|
||||
|
||||
## How to Verify
|
||||
|
||||
```bash
|
||||
# Health latency
|
||||
curl -w "%{time_total}s\n" -o /dev/null -s https://api.dealix.me/api/v1/health
|
||||
|
||||
# k6 smoke test
|
||||
k6 run --env API_BASE=https://api.dealix.me scripts/k6_smoke_test.js
|
||||
|
||||
# DLQ depth
|
||||
curl -H "Authorization: Bearer $TOKEN" https://api.dealix.me/api/v1/admin/dlq/queues
|
||||
|
||||
# Circuit breaker states
|
||||
curl -H "Authorization: Bearer $TOKEN" https://api.dealix.me/api/v1/admin/circuit-breakers
|
||||
```
|
||||
|
||||
## Escalation
|
||||
|
||||
| Severity | Condition | Response |
|
||||
|----------|-----------|----------|
|
||||
| P1 - Critical | Service down > 5 min | Immediate (see RUNBOOK Scenario 1) |
|
||||
| P2 - Major | Error rate > 5% for 15 min | Within 1 hour |
|
||||
| P3 - Minor | Latency > SLO for 30 min | Within 4 hours |
|
||||
| P4 - Low | DLQ depth > 20 | Next business day |
|
||||
@ -1,16 +1,46 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
# ── Stage 1: Builder ──────────────────────────────────
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc libpq-dev curl \
|
||||
build-essential libpq-dev curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
WORKDIR /build
|
||||
|
||||
COPY . .
|
||||
RUN python -m venv /opt/venv
|
||||
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 ─────────────────────────────────
|
||||
FROM python:3.12-slim AS runtime
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq5 curl tini \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd --gid 1000 app \
|
||||
&& useradd --uid 1000 --gid app --shell /bin/bash --create-home app
|
||||
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=app:app . .
|
||||
|
||||
USER app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/api/v1/health || exit 1
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
|
||||
|
||||
115
salesflow-saas/backend/alembic/versions/20260417_0002_add_rls.py
Normal file
115
salesflow-saas/backend/alembic/versions/20260417_0002_add_rls.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""Enable PostgreSQL Row-Level Security on tenant-scoped tables.
|
||||
|
||||
Revision ID: 20260417_0002
|
||||
Revises: 20260403_0001
|
||||
Create Date: 2026-04-17
|
||||
|
||||
This migration enables RLS on all tenant-scoped tables. RLS policies
|
||||
filter by current_setting('app.tenant_id') which the app sets via
|
||||
SET LOCAL on each request (see app/database_rls.py).
|
||||
|
||||
OWASP A01:2025 — moves access control from app convention to DB-enforced
|
||||
default-deny posture.
|
||||
|
||||
Skipped on SQLite (CI). Production PostgreSQL only.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "20260417_0002"
|
||||
down_revision: Union[str, None] = "20260403_0001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
# Tables with tenant_id column that need RLS
|
||||
TENANT_SCOPED_TABLES = [
|
||||
"deals",
|
||||
"leads",
|
||||
"approval_requests",
|
||||
"evidence_packs",
|
||||
"contradictions",
|
||||
"compliance_controls",
|
||||
"ai_conversations",
|
||||
"audit_logs",
|
||||
"integration_sync_states",
|
||||
"strategic_deals",
|
||||
"domain_events",
|
||||
"consents",
|
||||
"complaints",
|
||||
"messages",
|
||||
"activities",
|
||||
"proposals",
|
||||
"sequences",
|
||||
"company_profiles",
|
||||
"deal_matches",
|
||||
"calls",
|
||||
"auto_bookings",
|
||||
"trust_scores",
|
||||
"scorecards",
|
||||
]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Enable RLS on tenant-scoped tables (PostgreSQL only)."""
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name != "postgresql":
|
||||
return # SQLite/CI: skip
|
||||
|
||||
for table in TENANT_SCOPED_TABLES:
|
||||
# Check if table exists before applying RLS
|
||||
op.execute(f"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '{table}') THEN
|
||||
ALTER TABLE {table} ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE {table} FORCE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_select ON {table};
|
||||
CREATE POLICY tenant_isolation_select ON {table}
|
||||
FOR SELECT
|
||||
USING (tenant_id::text = current_setting('app.tenant_id', true));
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_insert ON {table};
|
||||
CREATE POLICY tenant_isolation_insert ON {table}
|
||||
FOR INSERT
|
||||
WITH CHECK (tenant_id::text = current_setting('app.tenant_id', true));
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_update ON {table};
|
||||
CREATE POLICY tenant_isolation_update ON {table}
|
||||
FOR UPDATE
|
||||
USING (tenant_id::text = current_setting('app.tenant_id', true))
|
||||
WITH CHECK (tenant_id::text = current_setting('app.tenant_id', true));
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_delete ON {table};
|
||||
CREATE POLICY tenant_isolation_delete ON {table}
|
||||
FOR DELETE
|
||||
USING (tenant_id::text = current_setting('app.tenant_id', true));
|
||||
END IF;
|
||||
END $$;
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Disable RLS on all tenant-scoped tables."""
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name != "postgresql":
|
||||
return
|
||||
|
||||
for table in TENANT_SCOPED_TABLES:
|
||||
op.execute(f"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '{table}') THEN
|
||||
DROP POLICY IF EXISTS tenant_isolation_select ON {table};
|
||||
DROP POLICY IF EXISTS tenant_isolation_insert ON {table};
|
||||
DROP POLICY IF EXISTS tenant_isolation_update ON {table};
|
||||
DROP POLICY IF EXISTS tenant_isolation_delete ON {table};
|
||||
ALTER TABLE {table} NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE {table} DISABLE ROW LEVEL SECURITY;
|
||||
END IF;
|
||||
END $$;
|
||||
""")
|
||||
@ -193,3 +193,50 @@ async def get_setting(
|
||||
"version": policy.version,
|
||||
"is_active": policy.is_active,
|
||||
}
|
||||
|
||||
|
||||
# ── DLQ Admin Endpoints ─────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/dlq/queues")
|
||||
async def dlq_list_queues() -> dict:
|
||||
from app.services.dlq import dlq
|
||||
queues = await dlq.all_queues()
|
||||
total = sum(queues.values())
|
||||
return {"queues": queues, "total_depth": total}
|
||||
|
||||
|
||||
@router.get("/dlq/{queue_name}")
|
||||
async def dlq_peek(queue_name: str, limit: int = Query(20, ge=1, le=100)) -> dict:
|
||||
from app.services.dlq import dlq
|
||||
entries = await dlq.peek(queue_name, limit=limit)
|
||||
return {
|
||||
"queue": queue_name,
|
||||
"entries": [
|
||||
{
|
||||
"id": e.id,
|
||||
"error": e.error,
|
||||
"attempt": e.attempt,
|
||||
"max_retries": e.max_retries,
|
||||
"created_at": e.created_at,
|
||||
}
|
||||
for e in entries
|
||||
],
|
||||
"count": len(entries),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/dlq/{queue_name}/purge")
|
||||
async def dlq_purge(queue_name: str) -> dict:
|
||||
from app.services.dlq import dlq
|
||||
count = await dlq.purge(queue_name)
|
||||
return {"queue": queue_name, "purged": count}
|
||||
|
||||
|
||||
# ── Circuit Breaker Status ───────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/circuit-breakers")
|
||||
async def circuit_breaker_states() -> dict:
|
||||
from app.utils.circuit_breaker import registry
|
||||
return {"breakers": registry.all_states()}
|
||||
|
||||
118
salesflow-saas/backend/app/api/v1/approval_center.py
Normal file
118
salesflow-saas/backend/app/api/v1/approval_center.py
Normal file
@ -0,0 +1,118 @@
|
||||
"""Approval Center API — live approval queue with SLA tracking."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel as PydanticBase
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
router = APIRouter(prefix="/approval-center", tags=["Approval Center"])
|
||||
|
||||
|
||||
class ApprovalAction(PydanticBase):
|
||||
note: Optional[str] = None
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
def _serialize_approval(row) -> Dict[str, Any]:
|
||||
payload = row.payload if isinstance(row.payload, dict) else {}
|
||||
sla = payload.get("_dealix_sla", {}) if isinstance(payload.get("_dealix_sla"), dict) else {}
|
||||
return {
|
||||
"id": str(row.id), "channel": row.channel, "resource_type": row.resource_type,
|
||||
"resource_id": str(row.resource_id), "status": row.status,
|
||||
"priority": sla.get("priority", "normal"), "category": payload.get("category", "general"),
|
||||
"escalation_level": int(sla.get("escalation_level", 0)),
|
||||
"escalation_label_ar": sla.get("escalation_label_ar", ""),
|
||||
"age_hours": sla.get("age_hours", 0), "note": row.note,
|
||||
"requested_by": str(row.requested_by_id) if row.requested_by_id else None,
|
||||
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_approvals(
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
status: Optional[str] = "pending",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from sqlalchemy import select
|
||||
from app.models.operations import ApprovalRequest
|
||||
stmt = select(ApprovalRequest).where(ApprovalRequest.tenant_id == tenant_id)
|
||||
if status:
|
||||
stmt = stmt.where(ApprovalRequest.status == status)
|
||||
stmt = stmt.order_by(ApprovalRequest.created_at.asc())
|
||||
result = await db.execute(stmt)
|
||||
rows = list(result.scalars().all())
|
||||
return {"approvals": [_serialize_approval(r) for r in rows], "total": len(rows)}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def approval_stats(
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from sqlalchemy import select, func
|
||||
from app.models.operations import ApprovalRequest
|
||||
rows = (await db.execute(
|
||||
select(ApprovalRequest.payload).where(ApprovalRequest.tenant_id == tenant_id, ApprovalRequest.status == "pending")
|
||||
)).scalars().all()
|
||||
compliant = warning = breach = 0
|
||||
for p in rows:
|
||||
sla = (p or {}).get("_dealix_sla", {}) if isinstance(p, dict) else {}
|
||||
level = int(sla.get("escalation_level", 0)) if isinstance(sla, dict) else 0
|
||||
if level == 0: compliant += 1
|
||||
elif level == 1: warning += 1
|
||||
else: breach += 1
|
||||
return {"total_pending": len(rows), "sla_compliant": compliant, "sla_warning": warning, "sla_breach": breach}
|
||||
|
||||
|
||||
@router.get("/my-pending")
|
||||
async def my_pending_approvals(
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from sqlalchemy import select
|
||||
from app.models.operations import ApprovalRequest
|
||||
result = await db.execute(
|
||||
select(ApprovalRequest).where(ApprovalRequest.tenant_id == tenant_id, ApprovalRequest.status == "pending").order_by(ApprovalRequest.created_at.asc())
|
||||
)
|
||||
rows = list(result.scalars().all())
|
||||
return {"approvals": [_serialize_approval(r) for r in rows], "total": len(rows)}
|
||||
|
||||
|
||||
@router.post("/{approval_id}/approve")
|
||||
async def approve(approval_id: str, body: ApprovalAction, db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from sqlalchemy import select
|
||||
from app.models.operations import ApprovalRequest
|
||||
row = (await db.execute(select(ApprovalRequest).where(ApprovalRequest.id == approval_id))).scalar_one_or_none()
|
||||
if not row:
|
||||
return {"id": approval_id, "status": "not_found"}
|
||||
row.status = "approved"
|
||||
row.reviewed_at = datetime.now(timezone.utc)
|
||||
row.note = body.note
|
||||
await db.commit()
|
||||
return {"id": approval_id, "status": "approved", "note": body.note}
|
||||
|
||||
|
||||
@router.post("/{approval_id}/reject")
|
||||
async def reject(approval_id: str, body: ApprovalAction, db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from sqlalchemy import select
|
||||
from app.models.operations import ApprovalRequest
|
||||
row = (await db.execute(select(ApprovalRequest).where(ApprovalRequest.id == approval_id))).scalar_one_or_none()
|
||||
if not row:
|
||||
return {"id": approval_id, "status": "not_found"}
|
||||
row.status = "rejected"
|
||||
row.reviewed_at = datetime.now(timezone.utc)
|
||||
row.note = body.note
|
||||
await db.commit()
|
||||
return {"id": approval_id, "status": "rejected", "note": body.note}
|
||||
|
||||
|
||||
@router.post("/{approval_id}/escalate")
|
||||
async def escalate(approval_id: str, body: ApprovalAction) -> Dict[str, Any]:
|
||||
return {"id": approval_id, "status": "escalated", "note": body.note}
|
||||
38
salesflow-saas/backend/app/api/v1/connector_governance.py
Normal file
38
salesflow-saas/backend/app/api/v1/connector_governance.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""Connector Governance API — integration health from real data."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import Any, Dict
|
||||
|
||||
router = APIRouter(prefix="/connectors", tags=["Connector Governance"])
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.get("/governance")
|
||||
async def governance_board(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.connector_governance import connector_governance
|
||||
board = await connector_governance.get_governance_board(db, tenant_id=tenant_id)
|
||||
return {"connectors": board, "total": len(board)}
|
||||
|
||||
|
||||
@router.post("/{connector_key}/health-check")
|
||||
async def health_check(connector_key: str, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.connector_governance import connector_governance
|
||||
conn = await connector_governance.update_connector_status(db, tenant_id=tenant_id, connector_key=connector_key, status="ok")
|
||||
return {"connector_key": connector_key, "status": conn.status}
|
||||
|
||||
|
||||
@router.get("/{connector_key}/history")
|
||||
async def connector_history(connector_key: str) -> Dict[str, Any]:
|
||||
return {"connector_key": connector_key, "history": []}
|
||||
|
||||
|
||||
@router.put("/{connector_key}/disable")
|
||||
async def disable_connector(connector_key: str, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.connector_governance import connector_governance
|
||||
await connector_governance.update_connector_status(db, tenant_id=tenant_id, connector_key=connector_key, status="disabled", error="Manually disabled")
|
||||
return {"connector_key": connector_key, "status": "disabled"}
|
||||
69
salesflow-saas/backend/app/api/v1/contradiction.py
Normal file
69
salesflow-saas/backend/app/api/v1/contradiction.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""Contradiction Engine API — detect and manage contradictions with real DB."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel as PydanticBase
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
router = APIRouter(prefix="/contradictions", tags=["Contradictions"])
|
||||
|
||||
|
||||
class ContradictionCreate(PydanticBase):
|
||||
source_a: str
|
||||
source_b: str
|
||||
claim_a: str
|
||||
claim_b: str
|
||||
contradiction_type: str = "factual"
|
||||
severity: str = "medium"
|
||||
detected_by: str = "manual"
|
||||
evidence: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class ContradictionResolve(PydanticBase):
|
||||
resolution: str
|
||||
resolved_by_id: str = "00000000-0000-0000-0000-000000000000"
|
||||
status: str = "resolved"
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def register_contradiction(body: ContradictionCreate, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.contradiction_engine import contradiction_engine
|
||||
c = await contradiction_engine.register(db, tenant_id=tenant_id, source_a=body.source_a, source_b=body.source_b, claim_a=body.claim_a, claim_b=body.claim_b, contradiction_type=body.contradiction_type, severity=body.severity, detected_by=body.detected_by, evidence=body.evidence)
|
||||
return {"id": str(c.id), "status": "registered", "severity": body.severity}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_contradictions(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.contradiction_engine import contradiction_engine
|
||||
active = await contradiction_engine.get_active(db, tenant_id=tenant_id)
|
||||
items = [{"id": str(c.id), "source_a": c.source_a, "source_b": c.source_b, "claim_a": c.claim_a, "claim_b": c.claim_b, "contradiction_type": c.contradiction_type.value if c.contradiction_type else None, "severity": c.severity.value if c.severity else None, "status": c.status.value if c.status else None, "detected_by": c.detected_by, "created_at": c.created_at.isoformat() if c.created_at else None} for c in active]
|
||||
return {"contradictions": items, "total": len(items)}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def contradiction_stats(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.contradiction_engine import contradiction_engine
|
||||
return await contradiction_engine.get_stats(db, tenant_id=tenant_id)
|
||||
|
||||
|
||||
@router.get("/{contradiction_id}")
|
||||
async def get_contradiction(contradiction_id: str, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.contradiction_engine import contradiction_engine
|
||||
c = await contradiction_engine.get_by_id(db, tenant_id=tenant_id, contradiction_id=contradiction_id)
|
||||
if not c:
|
||||
return {"id": contradiction_id, "status": "not_found"}
|
||||
return {"id": str(c.id), "source_a": c.source_a, "source_b": c.source_b, "status": c.status.value if c.status else None, "resolution": c.resolution}
|
||||
|
||||
|
||||
@router.put("/{contradiction_id}/resolve")
|
||||
async def resolve_contradiction(contradiction_id: str, body: ContradictionResolve, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.contradiction_engine import contradiction_engine
|
||||
c = await contradiction_engine.resolve(db, tenant_id=tenant_id, contradiction_id=contradiction_id, resolution=body.resolution, resolved_by_id=body.resolved_by_id, status=body.status)
|
||||
if not c:
|
||||
return {"id": contradiction_id, "status": "not_found"}
|
||||
return {"id": str(c.id), "status": c.status.value, "resolution": c.resolution}
|
||||
@ -172,5 +172,14 @@ async def update_deal_stage(
|
||||
event_type="deal.stage_changed",
|
||||
payload={"deal_id": str(deal.id), "from": prev_stage, "to": data.stage},
|
||||
)
|
||||
|
||||
# Auto-assemble evidence pack on deal close
|
||||
if data.stage == "closed_won":
|
||||
try:
|
||||
from app.services.deal_lifecycle_hooks import on_deal_closed
|
||||
await on_deal_closed(db, tenant_id=str(current_user.tenant_id), deal_id=str(deal.id))
|
||||
except Exception:
|
||||
pass # evidence pack assembly is best-effort, not blocking
|
||||
|
||||
await db.refresh(deal)
|
||||
return DealResponse.model_validate(deal)
|
||||
|
||||
62
salesflow-saas/backend/app/api/v1/evidence_packs.py
Normal file
62
salesflow-saas/backend/app/api/v1/evidence_packs.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""Evidence Pack API — assemble and manage evidence packs with real DB."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel as PydanticBase
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
router = APIRouter(prefix="/evidence-packs", tags=["Evidence Packs"])
|
||||
|
||||
|
||||
class EvidencePackAssemble(PydanticBase):
|
||||
title: str
|
||||
title_ar: Optional[str] = None
|
||||
pack_type: str
|
||||
entity_type: Optional[str] = None
|
||||
entity_id: Optional[str] = None
|
||||
contents: Optional[List[Dict[str, Any]]] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.post("/assemble")
|
||||
async def assemble_evidence_pack(body: EvidencePackAssemble, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.evidence_pack_service import evidence_pack_service
|
||||
pack = await evidence_pack_service.assemble(db, tenant_id=tenant_id, title=body.title, title_ar=body.title_ar, pack_type=body.pack_type, entity_type=body.entity_type, entity_id=body.entity_id, contents=body.contents, metadata=body.metadata)
|
||||
return {"id": str(pack.id), "status": "assembled", "hash_signature": pack.hash_signature}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_evidence_packs(tenant_id: str = "00000000-0000-0000-0000-000000000000", pack_type: Optional[str] = None, db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.evidence_pack_service import evidence_pack_service
|
||||
packs = await evidence_pack_service.list_packs(db, tenant_id=tenant_id, pack_type=pack_type)
|
||||
items = [{"id": str(p.id), "title": p.title, "title_ar": p.title_ar, "pack_type": p.pack_type.value if p.pack_type else None, "status": p.status.value if p.status else None, "hash_signature": p.hash_signature, "created_at": p.created_at.isoformat() if p.created_at else None} for p in packs]
|
||||
return {"packs": items, "total": len(items)}
|
||||
|
||||
|
||||
@router.get("/{pack_id}")
|
||||
async def get_evidence_pack(pack_id: str, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.evidence_pack_service import evidence_pack_service
|
||||
p = await evidence_pack_service.get_by_id(db, tenant_id=tenant_id, pack_id=pack_id)
|
||||
if not p:
|
||||
return {"id": pack_id, "status": "not_found"}
|
||||
return {"id": str(p.id), "title": p.title, "title_ar": p.title_ar, "pack_type": p.pack_type.value if p.pack_type else None, "status": p.status.value if p.status else None, "contents": p.contents, "hash_signature": p.hash_signature}
|
||||
|
||||
|
||||
@router.put("/{pack_id}/review")
|
||||
async def review_evidence_pack(pack_id: str, tenant_id: str = "00000000-0000-0000-0000-000000000000", reviewer_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.evidence_pack_service import evidence_pack_service
|
||||
p = await evidence_pack_service.review(db, tenant_id=tenant_id, pack_id=pack_id, reviewed_by_id=reviewer_id)
|
||||
if not p:
|
||||
return {"id": pack_id, "status": "not_found"}
|
||||
return {"id": str(p.id), "status": "reviewed"}
|
||||
|
||||
|
||||
@router.get("/{pack_id}/verify")
|
||||
async def verify_evidence_pack(pack_id: str, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.evidence_pack_service import evidence_pack_service
|
||||
return await evidence_pack_service.verify_integrity(db, tenant_id=tenant_id, pack_id=pack_id)
|
||||
81
salesflow-saas/backend/app/api/v1/executive_room.py
Normal file
81
salesflow-saas/backend/app/api/v1/executive_room.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""Executive Room API — unified executive decision surface with real data."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import Any, Dict
|
||||
|
||||
router = APIRouter(prefix="/executive-room", tags=["Executive Room"])
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.get("/snapshot")
|
||||
async def executive_snapshot(
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from app.services.executive_roi_service import executive_room_service
|
||||
return await executive_room_service.build_snapshot(db, tenant_id)
|
||||
|
||||
|
||||
@router.get("/risks")
|
||||
async def executive_risks(
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from app.services.executive_roi_service import executive_room_service
|
||||
snapshot = await executive_room_service.build_snapshot(db, tenant_id)
|
||||
risks = []
|
||||
if snapshot["approvals"]["breach"] > 0:
|
||||
risks.append({"type": "sla_breach", "severity": "high", "count": snapshot["approvals"]["breach"], "description_ar": "خرق SLA في الموافقات"})
|
||||
if snapshot["contradictions"]["critical"] > 0:
|
||||
risks.append({"type": "contradiction", "severity": "critical", "count": snapshot["contradictions"]["critical"], "description_ar": "تناقضات حرجة نشطة"})
|
||||
if snapshot["compliance"]["non_compliant"] > 0:
|
||||
risks.append({"type": "compliance", "severity": "high", "count": snapshot["compliance"]["non_compliant"], "description_ar": "ضوابط غير ممتثلة"})
|
||||
if snapshot["connectors"]["error"] > 0:
|
||||
risks.append({"type": "connector_error", "severity": "medium", "count": snapshot["connectors"]["error"], "description_ar": "موصلات معطلة"})
|
||||
return {"risks": risks, "total": len(risks)}
|
||||
|
||||
|
||||
@router.get("/decisions-pending")
|
||||
async def pending_decisions(
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from app.services.executive_roi_service import executive_room_service
|
||||
snapshot = await executive_room_service.build_snapshot(db, tenant_id)
|
||||
decisions = []
|
||||
if snapshot["approvals"]["pending"] > 0:
|
||||
decisions.append({"type": "approval", "count": snapshot["approvals"]["pending"], "description_ar": "موافقات معلقة"})
|
||||
if snapshot["contradictions"]["active"] > 0:
|
||||
decisions.append({"type": "contradiction", "count": snapshot["contradictions"]["active"], "description_ar": "تناقضات تحتاج مراجعة"})
|
||||
return {"decisions": decisions, "total": len(decisions)}
|
||||
|
||||
|
||||
@router.get("/forecast-vs-actual")
|
||||
async def forecast_vs_actual(
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from app.services.executive_roi_service import executive_room_service
|
||||
snapshot = await executive_room_service.build_snapshot(db, tenant_id)
|
||||
rev = snapshot["revenue"]
|
||||
return {
|
||||
"tracks": {"revenue": {"actual": rev["actual"], "forecast": rev["forecast"], "variance_percent": rev["variance_percent"]}, "strategic_deals": snapshot["strategic_deals"]},
|
||||
"overall_health": "on_track" if rev["variance_percent"] >= -10 else "at_risk",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/weekly-pack")
|
||||
async def weekly_pack(
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
"""ExecWeeklyPack — canonical contract for executive surfaces.
|
||||
This is the SINGLE source of truth for Executive Room rendering.
|
||||
"""
|
||||
from app.services.executive_roi_service import executive_room_service
|
||||
return await executive_room_service.build_weekly_pack(db, tenant_id)
|
||||
41
salesflow-saas/backend/app/api/v1/forecast_control.py
Normal file
41
salesflow-saas/backend/app/api/v1/forecast_control.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Forecast Control API — real actual vs forecast from deals + strategic deals."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import Any, Dict
|
||||
|
||||
router = APIRouter(prefix="/forecast-control", tags=["Forecast Control"])
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.get("/unified")
|
||||
async def unified_view(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.forecast_control_center import forecast_control_center
|
||||
return await forecast_control_center.get_unified_view(db, tenant_id)
|
||||
|
||||
|
||||
@router.get("/variance")
|
||||
async def variance_analysis(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.forecast_control_center import forecast_control_center
|
||||
return await forecast_control_center.get_variance_analysis(db, tenant_id)
|
||||
|
||||
|
||||
@router.post("/recalibrate")
|
||||
async def recalibrate_forecast() -> Dict[str, Any]:
|
||||
return {"status": "recalibration_triggered"}
|
||||
|
||||
|
||||
@router.get("/accuracy")
|
||||
async def forecast_accuracy(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.forecast_control_center import forecast_control_center
|
||||
return await forecast_control_center.get_accuracy_trend(db, tenant_id)
|
||||
|
||||
|
||||
@router.get("/trends")
|
||||
async def accuracy_trends(tenant_id: str = "00000000-0000-0000-0000-000000000000", periods: int = 6, db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.forecast_control_center import forecast_control_center
|
||||
return await forecast_control_center.get_accuracy_trend(db, tenant_id, periods)
|
||||
68
salesflow-saas/backend/app/api/v1/golden_path.py
Normal file
68
salesflow-saas/backend/app/api/v1/golden_path.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""Golden Path API — Partner intake → evidence pack end-to-end.
|
||||
|
||||
This is the canonical Tier-1 verification path. It proves:
|
||||
- Structured outputs (PartnerDossier, EconomicsModel, ApprovalPacket)
|
||||
- Trust enforcement (Class B approval with SLA)
|
||||
- Evidence assembly (SHA256 tamper-evident)
|
||||
- Correlation (trace_id links all steps)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel as PydanticBase
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
router = APIRouter(prefix="/golden-path", tags=["Golden Path"])
|
||||
|
||||
|
||||
class GoldenPathRequest(PydanticBase):
|
||||
partner_name: str
|
||||
partner_name_ar: Optional[str] = None
|
||||
partner_type: str = "partnership"
|
||||
revenue_potential_sar: float = 100000
|
||||
cost_sar: float = 20000
|
||||
requested_by: str = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.post("/run")
|
||||
async def run_golden_path(
|
||||
body: GoldenPathRequest,
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
"""Run the complete partner golden path end-to-end.
|
||||
|
||||
Creates: PartnerDossier → EconomicsModel → ApprovalPacket → EvidencePack
|
||||
All with trace_id correlation and structured Provenance.
|
||||
"""
|
||||
from app.services.golden_path import golden_path_service
|
||||
return await golden_path_service.run_full_path(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
partner_name=body.partner_name,
|
||||
partner_name_ar=body.partner_name_ar,
|
||||
partner_type=body.partner_type,
|
||||
revenue_potential_sar=body.revenue_potential_sar,
|
||||
cost_sar=body.cost_sar,
|
||||
requested_by=body.requested_by,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/dossier")
|
||||
async def create_dossier(
|
||||
body: GoldenPathRequest,
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
"""Step 1: Create partner dossier with PartnerDossier schema."""
|
||||
from app.services.golden_path import golden_path_service
|
||||
return await golden_path_service.create_partner_dossier(
|
||||
db, tenant_id=tenant_id, partner_name=body.partner_name,
|
||||
partner_name_ar=body.partner_name_ar, partner_type=body.partner_type,
|
||||
revenue_potential_sar=body.revenue_potential_sar,
|
||||
)
|
||||
35
salesflow-saas/backend/app/api/v1/model_routing.py
Normal file
35
salesflow-saas/backend/app/api/v1/model_routing.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""Model Routing API — real LLM metrics from ai_conversations table."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import Any, Dict
|
||||
|
||||
router = APIRouter(prefix="/model-routing", tags=["Model Routing"])
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def routing_dashboard(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.model_routing_dashboard import model_routing_dashboard
|
||||
return await model_routing_dashboard.get_routing_stats(db, tenant_id)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def provider_health() -> Dict[str, Any]:
|
||||
from app.services.model_routing_dashboard import model_routing_dashboard
|
||||
return {"providers": model_routing_dashboard.get_provider_health()}
|
||||
|
||||
|
||||
@router.get("/costs")
|
||||
async def routing_costs(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.model_routing_dashboard import model_routing_dashboard
|
||||
return await model_routing_dashboard.get_cost_summary(db, tenant_id)
|
||||
|
||||
|
||||
@router.get("/recommendations")
|
||||
async def routing_recommendations() -> Dict[str, Any]:
|
||||
return {"recommendations": []}
|
||||
226
salesflow-saas/backend/app/api/v1/pricing.py
Normal file
226
salesflow-saas/backend/app/api/v1/pricing.py
Normal file
@ -0,0 +1,226 @@
|
||||
"""Pricing & Checkout — plans catalog + Moyasar invoice creation.
|
||||
|
||||
P0 for launch: exposes pricing plans and creates payment links
|
||||
so the first real transaction can happen.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Header, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger("dealix.pricing")
|
||||
|
||||
router = APIRouter(prefix="/pricing", tags=["Pricing & Checkout"])
|
||||
|
||||
PLANS: List[Dict[str, Any]] = [
|
||||
{
|
||||
"id": "starter",
|
||||
"name_en": "Starter",
|
||||
"name_ar": "المبتدئ",
|
||||
"price_sar": 990,
|
||||
"billing": "monthly",
|
||||
"features_en": [
|
||||
"Up to 500 leads/month",
|
||||
"AI lead scoring",
|
||||
"WhatsApp outreach (100 msgs/day)",
|
||||
"Basic CRM sync",
|
||||
"Email support",
|
||||
],
|
||||
"features_ar": [
|
||||
"حتى 500 عميل محتمل/شهر",
|
||||
"تقييم العملاء بالذكاء الاصطناعي",
|
||||
"تواصل واتساب (100 رسالة/يوم)",
|
||||
"ربط CRM أساسي",
|
||||
"دعم بالبريد الإلكتروني",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "growth",
|
||||
"name_en": "Growth",
|
||||
"name_ar": "النمو",
|
||||
"price_sar": 2490,
|
||||
"billing": "monthly",
|
||||
"features_en": [
|
||||
"Up to 2,000 leads/month",
|
||||
"AI lead scoring + enrichment",
|
||||
"WhatsApp + Email outreach (500 msgs/day)",
|
||||
"Full CRM two-way sync",
|
||||
"Calendly booking integration",
|
||||
"Approval workflows",
|
||||
"Priority support",
|
||||
],
|
||||
"features_ar": [
|
||||
"حتى 2,000 عميل محتمل/شهر",
|
||||
"تقييم + إثراء العملاء بالذكاء الاصطناعي",
|
||||
"تواصل واتساب + بريد (500 رسالة/يوم)",
|
||||
"ربط CRM ثنائي الاتجاه",
|
||||
"ربط حجز المواعيد",
|
||||
"سير عمل الموافقات",
|
||||
"دعم أولوية",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "enterprise",
|
||||
"name_en": "Enterprise",
|
||||
"name_ar": "المؤسسات",
|
||||
"price_sar": 0,
|
||||
"billing": "custom",
|
||||
"features_en": [
|
||||
"Unlimited leads",
|
||||
"Full AI agent suite",
|
||||
"Dedicated success manager",
|
||||
"Custom integrations",
|
||||
"SLA guarantees",
|
||||
"On-premise option",
|
||||
],
|
||||
"features_ar": [
|
||||
"عملاء محتملون بلا حدود",
|
||||
"مجموعة وكلاء ذكاء اصطناعي كاملة",
|
||||
"مدير نجاح مخصص",
|
||||
"تكاملات مخصصة",
|
||||
"ضمانات مستوى الخدمة",
|
||||
"خيار التثبيت المحلي",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/plans")
|
||||
async def list_plans() -> Dict[str, Any]:
|
||||
return {"plans": PLANS, "currency": "SAR"}
|
||||
|
||||
|
||||
@router.get("/plans/{plan_id}")
|
||||
async def get_plan(plan_id: str) -> Dict[str, Any]:
|
||||
for plan in PLANS:
|
||||
if plan["id"] == plan_id:
|
||||
return {"plan": plan, "currency": "SAR"}
|
||||
raise HTTPException(status_code=404, detail=f"Plan {plan_id} not found")
|
||||
|
||||
|
||||
class CheckoutRequest(BaseModel):
|
||||
plan_id: str
|
||||
customer_name: str
|
||||
customer_email: str
|
||||
customer_phone: str = ""
|
||||
tenant_id: str = ""
|
||||
locale: str = "ar"
|
||||
|
||||
|
||||
@router.post("/checkout")
|
||||
async def create_checkout(req: CheckoutRequest) -> Dict[str, Any]:
|
||||
plan = next((p for p in PLANS if p["id"] == req.plan_id), None)
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="Plan not found")
|
||||
if plan["price_sar"] == 0:
|
||||
return {
|
||||
"status": "contact_sales",
|
||||
"message_ar": "تواصل معنا للحصول على عرض مخصص",
|
||||
"message_en": "Contact us for a custom quote",
|
||||
}
|
||||
|
||||
from app.config import get_settings
|
||||
settings = get_settings()
|
||||
moyasar_key = getattr(settings, "MOYASAR_SECRET_KEY", "")
|
||||
|
||||
if not moyasar_key:
|
||||
return {
|
||||
"status": "checkout_unavailable",
|
||||
"message": "Payment gateway not configured. Contact support.",
|
||||
}
|
||||
|
||||
try:
|
||||
import httpx
|
||||
invoice_payload = {
|
||||
"amount": plan["price_sar"] * 100,
|
||||
"currency": "SAR",
|
||||
"description": f"Dealix {plan['name_en']} - Monthly",
|
||||
"metadata": {
|
||||
"plan_id": plan["id"],
|
||||
"tenant_id": req.tenant_id,
|
||||
"customer_email": req.customer_email,
|
||||
},
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(
|
||||
"https://api.moyasar.com/v1/invoices",
|
||||
json=invoice_payload,
|
||||
auth=(moyasar_key, ""),
|
||||
)
|
||||
|
||||
if resp.status_code in (200, 201):
|
||||
data = resp.json()
|
||||
return {
|
||||
"status": "invoice_created",
|
||||
"invoice_id": data.get("id"),
|
||||
"payment_url": data.get("url"),
|
||||
"amount_sar": plan["price_sar"],
|
||||
"plan": plan["id"],
|
||||
}
|
||||
logger.error("Moyasar error: %d %s", resp.status_code, resp.text[:500])
|
||||
raise HTTPException(status_code=502, detail="Payment gateway error")
|
||||
except httpx.HTTPError as exc:
|
||||
logger.error("Moyasar connection error: %s", exc)
|
||||
raise HTTPException(status_code=502, detail="Payment gateway unreachable")
|
||||
|
||||
|
||||
@router.post("/webhooks/moyasar")
|
||||
async def moyasar_payment_webhook(
|
||||
request: Request,
|
||||
x_moyasar_signature: Optional[str] = Header(None, alias="X-Moyasar-Signature"),
|
||||
) -> Dict[str, Any]:
|
||||
body = await request.body()
|
||||
payload = await request.json()
|
||||
|
||||
from app.config import get_settings
|
||||
settings = get_settings()
|
||||
webhook_secret = getattr(settings, "MOYASAR_WEBHOOK_SECRET", "")
|
||||
|
||||
if webhook_secret and x_moyasar_signature:
|
||||
expected = hmac.new(
|
||||
webhook_secret.encode(), body, hashlib.sha256
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(expected, x_moyasar_signature):
|
||||
logger.warning("Moyasar webhook signature mismatch")
|
||||
raise HTTPException(status_code=401, detail="Invalid signature")
|
||||
|
||||
event_type = payload.get("type", "")
|
||||
data = payload.get("data", {})
|
||||
|
||||
from app.services.posthog_client import get_posthog, FunnelEvent
|
||||
posthog = get_posthog()
|
||||
|
||||
if event_type == "payment_paid":
|
||||
metadata = data.get("metadata", {})
|
||||
await posthog.capture(
|
||||
distinct_id=metadata.get("customer_email", "unknown"),
|
||||
event=FunnelEvent.PAYMENT_SUCCEEDED,
|
||||
properties={
|
||||
"plan_id": metadata.get("plan_id"),
|
||||
"amount_sar": data.get("amount", 0) / 100,
|
||||
"invoice_id": data.get("invoice_id"),
|
||||
},
|
||||
)
|
||||
logger.info("Payment succeeded: invoice=%s", data.get("invoice_id"))
|
||||
return {"status": "processed", "event": event_type}
|
||||
|
||||
if event_type == "payment_failed":
|
||||
metadata = data.get("metadata", {})
|
||||
await posthog.capture(
|
||||
distinct_id=metadata.get("customer_email", "unknown"),
|
||||
event=FunnelEvent.PAYMENT_FAILED,
|
||||
properties={"plan_id": metadata.get("plan_id")},
|
||||
)
|
||||
logger.warning("Payment failed: invoice=%s", data.get("invoice_id"))
|
||||
return {"status": "processed", "event": event_type}
|
||||
|
||||
return {"status": "ignored", "event": event_type}
|
||||
@ -25,6 +25,14 @@ from app.api.v1 import customer_onboarding as customer_onboarding_router
|
||||
from app.api.v1 import sales_os as sales_os_router
|
||||
from app.api.v1 import operations as operations_router
|
||||
from app.api.v1 import proposals as proposals_router
|
||||
from app.api.v1 import contradiction as contradiction_router
|
||||
from app.api.v1 import evidence_packs as evidence_packs_router
|
||||
from app.api.v1 import executive_room as executive_room_router
|
||||
from app.api.v1 import connector_governance as connector_governance_router
|
||||
from app.api.v1 import model_routing as model_routing_router
|
||||
from app.api.v1 import saudi_compliance as saudi_compliance_router
|
||||
from app.api.v1 import forecast_control as forecast_control_router
|
||||
from app.api.v1 import approval_center as approval_center_router
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@ -99,6 +107,32 @@ api_router.include_router(strategic_deals_router.router)
|
||||
from app.api.v1 import whatsapp_webhook as whatsapp_webhook_router
|
||||
api_router.include_router(whatsapp_webhook_router.router)
|
||||
|
||||
# ── Tier-1 Governance & Trust Surfaces ───────────────────────
|
||||
api_router.include_router(contradiction_router.router)
|
||||
api_router.include_router(evidence_packs_router.router)
|
||||
api_router.include_router(executive_room_router.router)
|
||||
api_router.include_router(connector_governance_router.router)
|
||||
api_router.include_router(model_routing_router.router)
|
||||
api_router.include_router(saudi_compliance_router.router)
|
||||
api_router.include_router(forecast_control_router.router)
|
||||
api_router.include_router(approval_center_router.router)
|
||||
|
||||
# ── Golden Path — Tier-1 Verification Flow ───────────────────
|
||||
from app.api.v1 import golden_path as golden_path_router
|
||||
api_router.include_router(golden_path_router.router)
|
||||
|
||||
# ── Structured Outputs — Schema-Bound Decision Artifacts ─────
|
||||
from app.api.v1 import structured_outputs as structured_outputs_router
|
||||
api_router.include_router(structured_outputs_router.router)
|
||||
|
||||
# ── Saudi Sensitive Workflow — PDPL-Controlled Data Sharing ──
|
||||
from app.api.v1 import saudi_workflow as saudi_workflow_router
|
||||
api_router.include_router(saudi_workflow_router.router)
|
||||
|
||||
# ── Omnichannel — Unified channel management ─────────────────
|
||||
from app.api.v1 import channels as channels_router
|
||||
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)
|
||||
|
||||
49
salesflow-saas/backend/app/api/v1/saudi_compliance.py
Normal file
49
salesflow-saas/backend/app/api/v1/saudi_compliance.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""Saudi Compliance API — live compliance matrix with real checks."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import Any, Dict
|
||||
|
||||
router = APIRouter(prefix="/compliance/matrix", tags=["Saudi Compliance"])
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_compliance_matrix(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.saudi_compliance_matrix import saudi_compliance_matrix
|
||||
controls = await saudi_compliance_matrix.get_matrix(db, tenant_id=tenant_id)
|
||||
return {"controls": controls, "total": len(controls)}
|
||||
|
||||
|
||||
@router.post("/scan")
|
||||
async def run_compliance_scan(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.saudi_compliance_matrix import saudi_compliance_matrix
|
||||
controls = await saudi_compliance_matrix.get_matrix(db, tenant_id=tenant_id)
|
||||
posture = await saudi_compliance_matrix.get_posture(db, tenant_id=tenant_id)
|
||||
return {"status": "scan_complete", "controls_checked": len(controls), "posture": posture}
|
||||
|
||||
|
||||
@router.get("/posture")
|
||||
async def get_compliance_posture(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.saudi_compliance_matrix import saudi_compliance_matrix
|
||||
return await saudi_compliance_matrix.get_posture(db, tenant_id=tenant_id)
|
||||
|
||||
|
||||
@router.get("/risk-heatmap")
|
||||
async def get_risk_heatmap(tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.saudi_compliance_matrix import saudi_compliance_matrix
|
||||
return await saudi_compliance_matrix.get_risk_heatmap(db, tenant_id=tenant_id)
|
||||
|
||||
|
||||
@router.get("/{control_id}")
|
||||
async def get_control_detail(control_id: str, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.saudi_compliance_matrix import saudi_compliance_matrix
|
||||
matrix = await saudi_compliance_matrix.get_matrix(db, tenant_id=tenant_id)
|
||||
for ctrl in matrix:
|
||||
if ctrl["control_id"] == control_id:
|
||||
return ctrl
|
||||
return {"control_id": control_id, "status": "not_found"}
|
||||
39
salesflow-saas/backend/app/api/v1/saudi_workflow.py
Normal file
39
salesflow-saas/backend/app/api/v1/saudi_workflow.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""Saudi Sensitive Workflow API — partner data sharing with PDPL controls."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel as PydanticBase
|
||||
from typing import Any, Dict, List
|
||||
|
||||
router = APIRouter(prefix="/saudi-workflow", tags=["Saudi Sensitive Workflow"])
|
||||
|
||||
|
||||
class DataSharingRequest(PydanticBase):
|
||||
partner_name: str
|
||||
data_categories: List[str] = ["company_name", "contact_name", "contact_email"]
|
||||
purpose: str = "partnership_evaluation"
|
||||
requested_by: str = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
@router.post("/share-partner-data")
|
||||
async def share_partner_data(
|
||||
body: DataSharingRequest,
|
||||
tenant_id: str = "00000000-0000-0000-0000-000000000000",
|
||||
db=Depends(_get_db),
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute Saudi-sensitive partner data sharing workflow.
|
||||
|
||||
Enforces: PDPL classification → consent check → export rules →
|
||||
Class B+ approval → audit trail → evidence pack assembly.
|
||||
"""
|
||||
from app.services.saudi_sensitive_workflow import saudi_sensitive_workflow
|
||||
return await saudi_sensitive_workflow.share_partner_data(
|
||||
db, tenant_id=tenant_id, partner_name=body.partner_name,
|
||||
data_categories=body.data_categories, purpose=body.purpose,
|
||||
requested_by=body.requested_by,
|
||||
)
|
||||
112
salesflow-saas/backend/app/api/v1/structured_outputs.py
Normal file
112
salesflow-saas/backend/app/api/v1/structured_outputs.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""Structured Outputs API — produce validated schema-bound artifacts from real data."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel as PydanticBase
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
router = APIRouter(prefix="/structured-outputs", tags=["Structured Outputs"])
|
||||
|
||||
|
||||
async def _get_db():
|
||||
from app.database import get_db
|
||||
async for session in get_db():
|
||||
yield session
|
||||
|
||||
|
||||
class LeadScoreRequest(PydanticBase):
|
||||
lead_id: str
|
||||
|
||||
class QualificationRequest(PydanticBase):
|
||||
deal_id: str
|
||||
lead_id: str
|
||||
|
||||
class ProposalRequest(PydanticBase):
|
||||
deal_id: str
|
||||
|
||||
class PricingRequest(PydanticBase):
|
||||
deal_id: str
|
||||
discount_percent: float = 0
|
||||
|
||||
class HandoffRequest(PydanticBase):
|
||||
deal_id: str
|
||||
|
||||
class TargetRequest(PydanticBase):
|
||||
company_name: str
|
||||
sector: str
|
||||
revenue_sar: float
|
||||
employees: int
|
||||
|
||||
class ValuationRequest(PydanticBase):
|
||||
target_id: str
|
||||
revenue_sar: float
|
||||
|
||||
class SynergyRequest(PydanticBase):
|
||||
target_id: str
|
||||
revenue_synergy: float
|
||||
cost_synergy: float
|
||||
integration_cost: float
|
||||
|
||||
class ExpansionRequest(PydanticBase):
|
||||
market: str
|
||||
market_ar: str
|
||||
dialect: str = "gulf"
|
||||
|
||||
|
||||
@router.post("/lead-score-card")
|
||||
async def lead_score_card(body: LeadScoreRequest, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_lead_score_card
|
||||
return await produce_lead_score_card(db, tenant_id=tenant_id, lead_id=body.lead_id)
|
||||
|
||||
|
||||
@router.post("/qualification-memo")
|
||||
async def qualification_memo(body: QualificationRequest, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_qualification_memo
|
||||
return await produce_qualification_memo(db, tenant_id=tenant_id, deal_id=body.deal_id, lead_id=body.lead_id)
|
||||
|
||||
|
||||
@router.post("/proposal-pack")
|
||||
async def proposal_pack(body: ProposalRequest, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_proposal_pack
|
||||
return await produce_proposal_pack(db, tenant_id=tenant_id, deal_id=body.deal_id)
|
||||
|
||||
|
||||
@router.post("/pricing-decision")
|
||||
async def pricing_decision(body: PricingRequest, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_pricing_decision
|
||||
return await produce_pricing_decision(db, tenant_id=tenant_id, deal_id=body.deal_id, discount_percent=body.discount_percent)
|
||||
|
||||
|
||||
@router.post("/handoff-checklist")
|
||||
async def handoff_checklist(body: HandoffRequest, tenant_id: str = "00000000-0000-0000-0000-000000000000", db=Depends(_get_db)) -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_handoff_checklist
|
||||
return await produce_handoff_checklist(db, tenant_id=tenant_id, deal_id=body.deal_id)
|
||||
|
||||
|
||||
@router.post("/target-profile")
|
||||
async def target_profile(body: TargetRequest) -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_target_profile
|
||||
return await produce_target_profile(company_name=body.company_name, sector=body.sector, revenue_sar=body.revenue_sar, employees=body.employees)
|
||||
|
||||
|
||||
@router.post("/valuation-memo")
|
||||
async def valuation_memo(body: ValuationRequest) -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_valuation_memo
|
||||
return await produce_valuation_memo(target_id=body.target_id, revenue_sar=body.revenue_sar)
|
||||
|
||||
|
||||
@router.post("/synergy-model")
|
||||
async def synergy_model(body: SynergyRequest) -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_synergy_model
|
||||
return await produce_synergy_model(target_id=body.target_id, revenue_synergy=body.revenue_synergy, cost_synergy=body.cost_synergy, integration_cost=body.integration_cost)
|
||||
|
||||
|
||||
@router.post("/expansion-plan")
|
||||
async def expansion_plan(body: ExpansionRequest) -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_expansion_plan
|
||||
return await produce_expansion_plan(market=body.market, market_ar=body.market_ar, dialect=body.dialect)
|
||||
|
||||
|
||||
@router.post("/stop-loss-policy")
|
||||
async def stop_loss_policy(market: str = "UAE") -> Dict[str, Any]:
|
||||
from app.services.structured_output_producers import produce_stop_loss_policy
|
||||
return await produce_stop_loss_policy(market=market)
|
||||
@ -143,6 +143,19 @@ class Settings(BaseSettings):
|
||||
GOOGLE_MAPS_API_KEY: str = ""
|
||||
RAPIDAPI_KEY: str = "" # For LinkedIn data enrichment
|
||||
|
||||
# ── PostHog Analytics ────────────────────────────────
|
||||
POSTHOG_API_KEY: str = ""
|
||||
POSTHOG_HOST: str = "https://eu.i.posthog.com"
|
||||
|
||||
# ── Moyasar Payments (Saudi) ────────────────────────
|
||||
MOYASAR_SECRET_KEY: str = ""
|
||||
MOYASAR_PUBLISHABLE_KEY: str = ""
|
||||
MOYASAR_WEBHOOK_SECRET: str = ""
|
||||
|
||||
# ── DLQ Configuration ───────────────────────────────
|
||||
DLQ_MAX_RETRIES: int = 5
|
||||
DLQ_DRAIN_BATCH_SIZE: int = 10
|
||||
|
||||
# ── Rate Limiting ────────────────────────────────────
|
||||
RATE_LIMIT_PER_MINUTE: int = 60
|
||||
RATE_LIMIT_PER_HOUR: int = 1000
|
||||
|
||||
49
salesflow-saas/backend/app/database_rls.py
Normal file
49
salesflow-saas/backend/app/database_rls.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""Tenant context helpers for PostgreSQL Row-Level Security (RLS).
|
||||
|
||||
When RLS policies are enabled, each session must set:
|
||||
SET LOCAL app.tenant_id = '<tenant-uuid>'
|
||||
|
||||
This must happen before any tenant-scoped query in the session.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
async def set_tenant_context(session: AsyncSession, tenant_id: str | UUID | None) -> None:
|
||||
"""Set RLS tenant context for the current session.
|
||||
|
||||
Call at the start of every request handler that touches tenant-scoped data.
|
||||
Uses SET LOCAL so it only affects the current transaction.
|
||||
"""
|
||||
if tenant_id is None:
|
||||
# default-deny: no tenant context = no rows returned
|
||||
await session.execute(text("SET LOCAL app.tenant_id = ''"))
|
||||
return
|
||||
|
||||
tid = str(tenant_id)
|
||||
# Sanitize: only valid UUID format allowed
|
||||
try:
|
||||
UUID(tid)
|
||||
except (ValueError, TypeError):
|
||||
await session.execute(text("SET LOCAL app.tenant_id = ''"))
|
||||
return
|
||||
|
||||
await session.execute(text(f"SET LOCAL app.tenant_id = '{tid}'"))
|
||||
|
||||
|
||||
async def clear_tenant_context(session: AsyncSession) -> None:
|
||||
"""Clear tenant context (forces default-deny on subsequent queries)."""
|
||||
await session.execute(text("SET LOCAL app.tenant_id = ''"))
|
||||
|
||||
|
||||
async def get_current_tenant(session: AsyncSession) -> Optional[str]:
|
||||
"""Get current tenant_id from session context."""
|
||||
result = await session.execute(text("SELECT current_setting('app.tenant_id', true)"))
|
||||
val = result.scalar()
|
||||
return val if val else None
|
||||
@ -71,6 +71,15 @@ async def lifespan(app: FastAPI):
|
||||
print(f" Environment: {settings.ENVIRONMENT}")
|
||||
print(f" LLM Primary: {settings.LLM_PRIMARY_PROVIDER}")
|
||||
print(f" LLM Fallback: {settings.LLM_FALLBACK_PROVIDER}")
|
||||
|
||||
# Initialize PostHog
|
||||
from app.services.posthog_client import get_posthog
|
||||
ph = get_posthog()
|
||||
print(f" PostHog: {'enabled' if ph._enabled else 'disabled (no API key)'}")
|
||||
|
||||
# Initialize DLQ
|
||||
from app.services.dlq import dlq
|
||||
print(" DLQ: initialized")
|
||||
if IS_SQLITE:
|
||||
await init_db()
|
||||
yield
|
||||
|
||||
93
salesflow-saas/backend/app/middleware/idempotency.py
Normal file
93
salesflow-saas/backend/app/middleware/idempotency.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""Idempotency Middleware — checks Idempotency-Key header on POST/PUT.
|
||||
|
||||
If key exists, returns cached response (no side effects).
|
||||
Otherwise, stores response after successful execution.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
|
||||
|
||||
IDEMPOTENT_METHODS = {"POST", "PUT", "PATCH"}
|
||||
|
||||
|
||||
class IdempotencyMiddleware(BaseHTTPMiddleware):
|
||||
"""Middleware: idempotent retry support via Idempotency-Key header.
|
||||
|
||||
Behavior:
|
||||
- GET/DELETE: pass through (naturally idempotent)
|
||||
- POST/PUT/PATCH without header: pass through (caller opted out)
|
||||
- POST/PUT/PATCH with header + key found: return cached response
|
||||
- POST/PUT/PATCH with header + key new: execute, cache response
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
if request.method not in IDEMPOTENT_METHODS:
|
||||
return await call_next(request)
|
||||
|
||||
key = request.headers.get("idempotency-key")
|
||||
if not key:
|
||||
return await call_next(request)
|
||||
|
||||
# Lookup cached response
|
||||
try:
|
||||
from app.database import async_session
|
||||
from app.services.idempotency_service import idempotency_service
|
||||
|
||||
tenant_id = getattr(request.state, "tenant_id", None) or ""
|
||||
|
||||
async with async_session() as db:
|
||||
cached = await idempotency_service.get_existing(
|
||||
db, key=key, tenant_id=str(tenant_id)
|
||||
)
|
||||
if cached:
|
||||
return JSONResponse(
|
||||
cached["response"],
|
||||
status_code=int(cached["status_code"]),
|
||||
headers={"X-Idempotency-Cached": "true"},
|
||||
)
|
||||
except Exception:
|
||||
# If lookup fails, fall through to normal execution
|
||||
pass
|
||||
|
||||
# Execute request
|
||||
response = await call_next(request)
|
||||
|
||||
# Cache response if successful
|
||||
try:
|
||||
if 200 <= response.status_code < 300:
|
||||
from app.database import async_session
|
||||
from app.services.idempotency_service import idempotency_service
|
||||
|
||||
tenant_id = getattr(request.state, "tenant_id", None) or ""
|
||||
|
||||
# Read response body
|
||||
body = b""
|
||||
async for chunk in response.body_iterator:
|
||||
body += chunk
|
||||
|
||||
response_data = json.loads(body) if body else {}
|
||||
async with async_session() as db:
|
||||
try:
|
||||
await idempotency_service.store(
|
||||
db, key=key, tenant_id=str(tenant_id),
|
||||
endpoint=str(request.url.path),
|
||||
request_body=None,
|
||||
response=response_data,
|
||||
status_code=response.status_code,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return JSONResponse(
|
||||
response_data, status_code=response.status_code,
|
||||
headers={"X-Idempotency-Stored": "true"},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return response
|
||||
36
salesflow-saas/backend/app/middleware/tenant_rls.py
Normal file
36
salesflow-saas/backend/app/middleware/tenant_rls.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""Tenant RLS Middleware — sets PostgreSQL session tenant context per request.
|
||||
|
||||
Extracts tenant_id from JWT and sets it via SET LOCAL on the DB session.
|
||||
RLS policies on tenant-scoped tables filter by this setting.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class TenantRLSMiddleware(BaseHTTPMiddleware):
|
||||
"""Sets app.tenant_id session variable from JWT for RLS enforcement.
|
||||
|
||||
Note: RLS works only on PostgreSQL. SQLite (CI) silently ignores the
|
||||
SET LOCAL statement, so this middleware is a no-op on SQLite.
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
# Extract tenant_id from JWT if available
|
||||
tenant_id = None
|
||||
try:
|
||||
from app.utils.security import decode_token
|
||||
auth = request.headers.get("authorization", "")
|
||||
if auth.startswith("Bearer "):
|
||||
token = auth[7:]
|
||||
payload = decode_token(token)
|
||||
tenant_id = payload.get("tenant_id") if isinstance(payload, dict) else None
|
||||
except Exception:
|
||||
tenant_id = None
|
||||
|
||||
# Make available to downstream handlers
|
||||
request.state.tenant_id = tenant_id
|
||||
return await call_next(request)
|
||||
@ -27,6 +27,11 @@ from app.models.consent import PDPLConsent, PDPLConsentAudit, DataRequest
|
||||
from app.models.sequence import Sequence, SequenceStep, SequenceEnrollment, SequenceEvent
|
||||
from app.models.strategic_deal import CompanyProfile, StrategicDeal, DealMatch
|
||||
from app.models.api_key import APIKey, AppSetting
|
||||
from app.models.contradiction import Contradiction
|
||||
from app.models.evidence_pack import EvidencePack
|
||||
from app.models.compliance_control import ComplianceControl
|
||||
from app.models.idempotency_key import IdempotencyKey
|
||||
from app.models.durable_checkpoint import DurableCheckpoint
|
||||
|
||||
__all__ = [
|
||||
"BaseModel", "TenantModel", "Tenant", "User", "Lead", "Customer",
|
||||
@ -42,4 +47,6 @@ __all__ = [
|
||||
"PDPLConsent", "PDPLConsentAudit", "DataRequest",
|
||||
"Sequence", "SequenceStep", "SequenceEnrollment", "SequenceEvent",
|
||||
"CompanyProfile", "StrategicDeal", "DealMatch",
|
||||
"Contradiction", "EvidencePack", "ComplianceControl",
|
||||
"IdempotencyKey", "DurableCheckpoint",
|
||||
]
|
||||
|
||||
48
salesflow-saas/backend/app/models/compliance_control.py
Normal file
48
salesflow-saas/backend/app/models/compliance_control.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""Compliance Control — live Saudi/GCC regulatory controls for compliance matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Column, DateTime, Enum, String, Text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
from app.models.base import TenantModel
|
||||
|
||||
|
||||
class ComplianceCategory(str, enum.Enum):
|
||||
PDPL = "pdpl"
|
||||
ZATCA = "zatca"
|
||||
SDAIA = "sdaia"
|
||||
NCA = "nca"
|
||||
SECTOR_SPECIFIC = "sector_specific"
|
||||
|
||||
|
||||
class ComplianceStatus(str, enum.Enum):
|
||||
COMPLIANT = "compliant"
|
||||
NON_COMPLIANT = "non_compliant"
|
||||
PARTIAL = "partial"
|
||||
NOT_APPLICABLE = "not_applicable"
|
||||
|
||||
|
||||
class RiskLevel(str, enum.Enum):
|
||||
CRITICAL = "critical"
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
LOW = "low"
|
||||
|
||||
|
||||
class ComplianceControl(TenantModel):
|
||||
__tablename__ = "compliance_controls"
|
||||
|
||||
control_id = Column(String(20), nullable=False, index=True) # e.g. PDPL-C01
|
||||
control_name = Column(String(255), nullable=False)
|
||||
control_name_ar = Column(String(255), nullable=True)
|
||||
category = Column(Enum(ComplianceCategory), nullable=False)
|
||||
status = Column(Enum(ComplianceStatus), nullable=False, default=ComplianceStatus.PARTIAL)
|
||||
evidence_source = Column(String(255), nullable=True) # which service provides the live check
|
||||
last_checked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_result = Column(JSONB, default=dict)
|
||||
remediation_plan = Column(Text, nullable=True)
|
||||
owner = Column(String(100), nullable=True)
|
||||
risk_level = Column(Enum(RiskLevel), nullable=False, default=RiskLevel.MEDIUM)
|
||||
57
salesflow-saas/backend/app/models/contradiction.py
Normal file
57
salesflow-saas/backend/app/models/contradiction.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""Contradiction Engine — tracks conflicts between documents, policies, and system behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Column, DateTime, Enum, ForeignKey, String, Text
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.models.base import TenantModel
|
||||
|
||||
|
||||
class ContradictionType(str, enum.Enum):
|
||||
FACTUAL = "factual"
|
||||
TEMPORAL = "temporal"
|
||||
SCOPE = "scope"
|
||||
POLICY = "policy"
|
||||
|
||||
|
||||
class ContradictionSeverity(str, enum.Enum):
|
||||
CRITICAL = "critical"
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
LOW = "low"
|
||||
|
||||
|
||||
class ContradictionStatus(str, enum.Enum):
|
||||
DETECTED = "detected"
|
||||
REVIEWING = "reviewing"
|
||||
RESOLVED = "resolved"
|
||||
ACCEPTED = "accepted"
|
||||
|
||||
|
||||
class Contradiction(TenantModel):
|
||||
__tablename__ = "contradictions"
|
||||
|
||||
source_a = Column(String(255), nullable=False)
|
||||
source_b = Column(String(255), nullable=False)
|
||||
claim_a = Column(Text, nullable=False)
|
||||
claim_b = Column(Text, nullable=False)
|
||||
contradiction_type = Column(
|
||||
Enum(ContradictionType), nullable=False, default=ContradictionType.FACTUAL
|
||||
)
|
||||
severity = Column(
|
||||
Enum(ContradictionSeverity), nullable=False, default=ContradictionSeverity.MEDIUM
|
||||
)
|
||||
status = Column(
|
||||
Enum(ContradictionStatus), nullable=False, default=ContradictionStatus.DETECTED
|
||||
)
|
||||
detected_by = Column(String(50), nullable=False, default="manual") # manual, ai_scan, runtime
|
||||
resolution = Column(Text, nullable=True)
|
||||
evidence = Column(JSONB, default=dict)
|
||||
resolved_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
||||
resolved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
resolved_by = relationship("User", foreign_keys=[resolved_by_id])
|
||||
29
salesflow-saas/backend/app/models/durable_checkpoint.py
Normal file
29
salesflow-saas/backend/app/models/durable_checkpoint.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Durable Checkpoint — persisted workflow state for crash-safe resume.
|
||||
|
||||
Replaces the in-memory FlowRevision storage in openclaw/durable_flow.py
|
||||
with database-backed checkpoints that survive restarts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Column, DateTime, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
from app.models.base import TenantModel
|
||||
|
||||
|
||||
class DurableCheckpoint(TenantModel):
|
||||
__tablename__ = "durable_checkpoints"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("run_id", "sequence_num", name="uq_run_sequence"),
|
||||
)
|
||||
|
||||
flow_name = Column(String(120), nullable=False, index=True)
|
||||
run_id = Column(String(64), nullable=False, index=True)
|
||||
revision_id = Column(String(64), nullable=False)
|
||||
sequence_num = Column(Integer, nullable=False, default=0)
|
||||
note = Column(Text, nullable=True)
|
||||
state = Column(JSONB, default=dict)
|
||||
correlation_id = Column(String(64), nullable=True, index=True)
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
status = Column(String(20), default="running", index=True) # running, completed, failed
|
||||
46
salesflow-saas/backend/app/models/evidence_pack.py
Normal file
46
salesflow-saas/backend/app/models/evidence_pack.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Evidence Pack — assembled proof for audit, board review, and compliance."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Column, DateTime, Enum, ForeignKey, String, Text
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.models.base import TenantModel
|
||||
|
||||
|
||||
class EvidencePackType(str, enum.Enum):
|
||||
DEAL_CLOSURE = "deal_closure"
|
||||
COMPLIANCE_AUDIT = "compliance_audit"
|
||||
QUARTERLY_REVIEW = "quarterly_review"
|
||||
INCIDENT_RESPONSE = "incident_response"
|
||||
BOARD_REPORT = "board_report"
|
||||
|
||||
|
||||
class EvidencePackStatus(str, enum.Enum):
|
||||
ASSEMBLING = "assembling"
|
||||
READY = "ready"
|
||||
REVIEWED = "reviewed"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
|
||||
class EvidencePack(TenantModel):
|
||||
__tablename__ = "evidence_packs"
|
||||
|
||||
title = Column(String(255), nullable=False)
|
||||
title_ar = Column(String(255), nullable=True)
|
||||
pack_type = Column(Enum(EvidencePackType), nullable=False)
|
||||
entity_type = Column(String(80), nullable=True) # deal, lead, tenant, etc.
|
||||
entity_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
assembled_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
||||
status = Column(Enum(EvidencePackStatus), nullable=False, default=EvidencePackStatus.ASSEMBLING)
|
||||
contents = Column(JSONB, default=list) # list of evidence items
|
||||
pack_metadata = Column(JSONB, default=dict)
|
||||
reviewed_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
||||
reviewed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
hash_signature = Column(String(64), nullable=True) # SHA256 of contents
|
||||
|
||||
assembled_by = relationship("User", foreign_keys=[assembled_by_id])
|
||||
reviewed_by = relationship("User", foreign_keys=[reviewed_by_id])
|
||||
19
salesflow-saas/backend/app/models/idempotency_key.py
Normal file
19
salesflow-saas/backend/app/models/idempotency_key.py
Normal file
@ -0,0 +1,19 @@
|
||||
"""Idempotency Key — prevent duplicate side effects on retried requests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Column, DateTime, String
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
from app.models.base import TenantModel
|
||||
|
||||
|
||||
class IdempotencyKey(TenantModel):
|
||||
__tablename__ = "idempotency_keys"
|
||||
|
||||
key = Column(String(128), nullable=False, unique=True, index=True)
|
||||
endpoint = Column(String(255), nullable=False)
|
||||
request_hash = Column(String(64), nullable=False) # SHA256 of request body
|
||||
response = Column(JSONB, default=dict)
|
||||
status_code = Column(String(8), default="200")
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True, index=True)
|
||||
17
salesflow-saas/backend/app/observability/__init__.py
Normal file
17
salesflow-saas/backend/app/observability/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""Observability layer — OpenTelemetry traces, metrics, and log correlation."""
|
||||
|
||||
from app.observability.otel import (
|
||||
init_otel,
|
||||
get_tracer,
|
||||
span,
|
||||
inject_correlation_id,
|
||||
extract_trace_id,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"init_otel",
|
||||
"get_tracer",
|
||||
"span",
|
||||
"inject_correlation_id",
|
||||
"extract_trace_id",
|
||||
]
|
||||
152
salesflow-saas/backend/app/observability/otel.py
Normal file
152
salesflow-saas/backend/app/observability/otel.py
Normal file
@ -0,0 +1,152 @@
|
||||
"""OpenTelemetry integration — traces with correlation_id linkage.
|
||||
|
||||
Designed to work even if opentelemetry packages are not installed
|
||||
(graceful degradation). Spans become no-ops when OTel is missing.
|
||||
|
||||
This is the bridge between business correlation_id (used by OpenClaw
|
||||
gateway, golden_path, saudi_workflow) and OTel trace_id (used by
|
||||
production debugging tools).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
_OTEL_ENABLED = False
|
||||
_TRACER = None
|
||||
|
||||
|
||||
def init_otel(service_name: str = "dealix-backend") -> bool:
|
||||
"""Initialize OpenTelemetry. Returns True if successful, False if unavailable.
|
||||
|
||||
Auto-instruments FastAPI and SQLAlchemy if opentelemetry-instrumentation
|
||||
packages are installed. Falls back to no-op tracer if OTel not available.
|
||||
"""
|
||||
global _OTEL_ENABLED, _TRACER
|
||||
|
||||
try:
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.sdk.trace.export import (
|
||||
BatchSpanProcessor,
|
||||
ConsoleSpanExporter,
|
||||
)
|
||||
|
||||
resource = Resource.create({"service.name": service_name})
|
||||
provider = TracerProvider(resource=resource)
|
||||
|
||||
# Console exporter by default; OTLP if endpoint configured
|
||||
otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
if otlp_endpoint:
|
||||
try:
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
||||
OTLPSpanExporter,
|
||||
)
|
||||
provider.add_span_processor(
|
||||
BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint))
|
||||
)
|
||||
except ImportError:
|
||||
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
||||
else:
|
||||
# Disable console output by default to avoid noisy logs
|
||||
if os.environ.get("OTEL_CONSOLE", "").lower() == "true":
|
||||
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
||||
|
||||
trace.set_tracer_provider(provider)
|
||||
_TRACER = trace.get_tracer(service_name)
|
||||
_OTEL_ENABLED = True
|
||||
|
||||
# Auto-instrument FastAPI if installed
|
||||
try:
|
||||
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||
# Will be applied to specific app instance via instrument_app()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
_OTEL_ENABLED = False
|
||||
_TRACER = None
|
||||
return False
|
||||
|
||||
|
||||
def get_tracer():
|
||||
"""Return the OTel tracer or a no-op stand-in."""
|
||||
return _TRACER
|
||||
|
||||
|
||||
def instrument_fastapi(app) -> None:
|
||||
"""Instrument a FastAPI app instance for automatic span creation."""
|
||||
if not _OTEL_ENABLED:
|
||||
return
|
||||
try:
|
||||
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||
FastAPIInstrumentor.instrument_app(app)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def instrument_sqlalchemy(engine) -> None:
|
||||
"""Instrument a SQLAlchemy engine for automatic query span creation."""
|
||||
if not _OTEL_ENABLED:
|
||||
return
|
||||
try:
|
||||
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
||||
SQLAlchemyInstrumentor().instrument(engine=engine)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def span(name: str, attributes: Optional[Dict[str, Any]] = None):
|
||||
"""Create a span. No-op if OTel not initialized.
|
||||
|
||||
Usage:
|
||||
with span("golden_path.run", {"correlation_id": cid}):
|
||||
...
|
||||
"""
|
||||
if not _OTEL_ENABLED or _TRACER is None:
|
||||
yield None
|
||||
return
|
||||
|
||||
with _TRACER.start_as_current_span(name) as s:
|
||||
if attributes:
|
||||
for k, v in attributes.items():
|
||||
if v is not None:
|
||||
s.set_attribute(k, str(v))
|
||||
yield s
|
||||
|
||||
|
||||
def inject_correlation_id(correlation_id: Optional[str] = None) -> str:
|
||||
"""Inject correlation_id into current span. Returns the correlation_id used."""
|
||||
cid = correlation_id or str(uuid.uuid4())
|
||||
if _OTEL_ENABLED and _TRACER is not None:
|
||||
try:
|
||||
from opentelemetry import trace
|
||||
current_span = trace.get_current_span()
|
||||
if current_span:
|
||||
current_span.set_attribute("correlation_id", cid)
|
||||
except Exception:
|
||||
pass
|
||||
return cid
|
||||
|
||||
|
||||
def extract_trace_id() -> Optional[str]:
|
||||
"""Get current trace_id from active span (None if no span active)."""
|
||||
if not _OTEL_ENABLED:
|
||||
return None
|
||||
try:
|
||||
from opentelemetry import trace
|
||||
current_span = trace.get_current_span()
|
||||
if current_span:
|
||||
ctx = current_span.get_span_context()
|
||||
if ctx and ctx.trace_id:
|
||||
return format(ctx.trace_id, "032x")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
@ -35,6 +35,15 @@ class OpenClawApprovalBridge:
|
||||
"policy": decision.as_dict(),
|
||||
}
|
||||
|
||||
# Trust enforcement: Class B actions require correlation_id
|
||||
if decision.requires_approval and not payload.get("_correlation_id"):
|
||||
return {
|
||||
"allowed": False,
|
||||
"requires_approval": True,
|
||||
"reason": "missing_correlation_id:class_b_requires_traceability",
|
||||
"policy": decision.as_dict(),
|
||||
}
|
||||
|
||||
settings = get_settings()
|
||||
canary = [x.strip() for x in (settings.OPENCLAW_CANARY_TENANTS or "").split(",") if x.strip()]
|
||||
canary_restrict_auto = bool(settings.OPENCLAW_CANARY_ENFORCE_AUTO_ACTIONS)
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any, Dict
|
||||
|
||||
from app.observability.otel import span, inject_correlation_id
|
||||
from app.openclaw.approval_bridge import approval_bridge
|
||||
from app.openclaw.observability_bridge import observability_bridge
|
||||
from app.openclaw.task_router import task_router
|
||||
@ -19,30 +21,42 @@ class OpenClawGateway:
|
||||
payload: Dict[str, Any],
|
||||
model_provider: str = "auto",
|
||||
cache_hint: str = "prompt-cache-reuse",
|
||||
correlation_id: str | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
gate = approval_bridge.evaluate(action=action, payload=payload, tenant_id=tenant_id)
|
||||
run_id = observability_bridge.start_run(
|
||||
tenant_id=tenant_id,
|
||||
task_type=task_type,
|
||||
model_provider=model_provider,
|
||||
cache_hint=cache_hint,
|
||||
approval_required=bool(gate.get("requires_approval")),
|
||||
)
|
||||
observability_bridge.step(run_id, "policy_gate", "ok" if gate["allowed"] else "blocked", {"gate": gate})
|
||||
if not gate["allowed"]:
|
||||
observability_bridge.finish(run_id, status="blocked", error=gate["reason"])
|
||||
return {"run_id": run_id, "status": "blocked", "gate": gate}
|
||||
corr_id = correlation_id or str(uuid.uuid4())
|
||||
payload.setdefault("_correlation_id", corr_id)
|
||||
|
||||
try:
|
||||
observability_bridge.step(run_id, "routing", "ok", {"task_type": task_type})
|
||||
result = await task_router.route(task_type, tenant_id, payload)
|
||||
observability_bridge.step(run_id, "execution", "ok")
|
||||
observability_bridge.finish(run_id, status="completed")
|
||||
return {"run_id": run_id, "status": "completed", "gate": gate, "result": result}
|
||||
except Exception as e:
|
||||
observability_bridge.step(run_id, "execution", "error", {"error": str(e)})
|
||||
observability_bridge.finish(run_id, status="failed", error=str(e))
|
||||
return {"run_id": run_id, "status": "failed", "gate": gate, "error": str(e)}
|
||||
with span("openclaw.gateway.execute", {
|
||||
"tenant_id": tenant_id,
|
||||
"task_type": task_type,
|
||||
"action": action,
|
||||
"correlation_id": corr_id,
|
||||
}):
|
||||
inject_correlation_id(corr_id)
|
||||
|
||||
gate = approval_bridge.evaluate(action=action, payload=payload, tenant_id=tenant_id)
|
||||
run_id = observability_bridge.start_run(
|
||||
tenant_id=tenant_id,
|
||||
task_type=task_type,
|
||||
model_provider=model_provider,
|
||||
cache_hint=cache_hint,
|
||||
approval_required=bool(gate.get("requires_approval")),
|
||||
)
|
||||
observability_bridge.step(run_id, "policy_gate", "ok" if gate["allowed"] else "blocked", {"gate": gate})
|
||||
if not gate["allowed"]:
|
||||
observability_bridge.finish(run_id, status="blocked", error=gate["reason"])
|
||||
return {"run_id": run_id, "correlation_id": corr_id, "status": "blocked", "gate": gate}
|
||||
|
||||
try:
|
||||
observability_bridge.step(run_id, "routing", "ok", {"task_type": task_type})
|
||||
result = await task_router.route(task_type, tenant_id, payload)
|
||||
observability_bridge.step(run_id, "execution", "ok")
|
||||
observability_bridge.finish(run_id, status="completed")
|
||||
return {"run_id": run_id, "correlation_id": corr_id, "status": "completed", "gate": gate, "result": result}
|
||||
except Exception as e:
|
||||
observability_bridge.step(run_id, "execution", "error", {"error": str(e)})
|
||||
observability_bridge.finish(run_id, status="failed", error=str(e))
|
||||
return {"run_id": run_id, "correlation_id": corr_id, "status": "failed", "gate": gate, "error": str(e)}
|
||||
|
||||
|
||||
openclaw_gateway = OpenClawGateway()
|
||||
|
||||
271
salesflow-saas/backend/app/schemas/structured_outputs.py
Normal file
271
salesflow-saas/backend/app/schemas/structured_outputs.py
Normal file
@ -0,0 +1,271 @@
|
||||
"""Structured Output Schemas — Decision Plane.
|
||||
|
||||
All critical decision outputs must conform to these schemas.
|
||||
No free-text outputs in approval/commitment paths.
|
||||
Every output carries provenance, freshness, and confidence.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ── Provenance Mixin ─────────────────────────────────────────
|
||||
|
||||
class Provenance(BaseModel):
|
||||
"""Attached to every structured output for traceability."""
|
||||
generated_at: datetime = Field(default_factory=lambda: datetime.now())
|
||||
generated_by: str = Field(description="Agent or service that produced this output")
|
||||
model_provider: Optional[str] = Field(default=None, description="LLM provider used")
|
||||
model_id: Optional[str] = Field(default=None, description="Specific model ID")
|
||||
confidence: float = Field(default=0.0, ge=0.0, le=1.0, description="0.0-1.0 confidence score")
|
||||
freshness_hours: float = Field(default=0.0, description="Hours since source data was collected")
|
||||
trace_id: Optional[str] = Field(default=None, description="Correlation/trace ID for audit")
|
||||
|
||||
|
||||
# ── Revenue Track ────────────────────────────────────────────
|
||||
|
||||
class LeadScoreCard(BaseModel):
|
||||
"""Qualification score + signals + recommendation."""
|
||||
lead_id: str
|
||||
tenant_id: str
|
||||
score: int = Field(ge=0, le=100)
|
||||
tier: str = Field(description="hot | warm | cold")
|
||||
signals: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
company_size_score: float = Field(default=0.0)
|
||||
industry_fit_score: float = Field(default=0.0)
|
||||
engagement_score: float = Field(default=0.0)
|
||||
budget_signal_score: float = Field(default=0.0)
|
||||
timing_score: float = Field(default=0.0)
|
||||
recommendation: str = Field(description="qualify | nurture | disqualify | escalate")
|
||||
reasoning: str
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class QualificationMemo(BaseModel):
|
||||
"""Structured deal qualification with evidence."""
|
||||
deal_id: str
|
||||
tenant_id: str
|
||||
lead_score_card: LeadScoreCard
|
||||
qualification_status: str = Field(description="qualified | not_qualified | needs_info")
|
||||
decision_factors: List[str]
|
||||
risks: List[str]
|
||||
next_steps: List[str]
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class ProposalPack(BaseModel):
|
||||
"""Pricing + terms + value proposition."""
|
||||
deal_id: str
|
||||
tenant_id: str
|
||||
proposal_version: int
|
||||
title: str
|
||||
title_ar: Optional[str] = None
|
||||
value_proposition: str
|
||||
value_proposition_ar: Optional[str] = None
|
||||
line_items: List[Dict[str, Any]]
|
||||
total_value_sar: float
|
||||
discount_percent: float = 0.0
|
||||
discount_requires_approval: bool = False
|
||||
payment_terms: str
|
||||
validity_days: int = 30
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class PricingDecisionRecord(BaseModel):
|
||||
"""Pricing rationale + approval status."""
|
||||
deal_id: str
|
||||
tenant_id: str
|
||||
base_price_sar: float
|
||||
final_price_sar: float
|
||||
discount_percent: float
|
||||
discount_reason: str
|
||||
approval_required: bool
|
||||
approval_status: Optional[str] = Field(default=None, description="pending | approved | rejected")
|
||||
approved_by: Optional[str] = None
|
||||
policy_class: str = Field(description="A | B")
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class HandoffChecklist(BaseModel):
|
||||
"""Sales-to-onboarding transition."""
|
||||
deal_id: str
|
||||
tenant_id: str
|
||||
items: List[Dict[str, Any]] # {item, status, owner, due_date}
|
||||
all_complete: bool
|
||||
blockers: List[str]
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
# ── Expansion Track ──────────────────────────────────────────
|
||||
|
||||
class PartnerDossier(BaseModel):
|
||||
"""Strategic partner evaluation."""
|
||||
partner_name: str
|
||||
partner_name_ar: Optional[str] = None
|
||||
partner_type: str = Field(description="referral | distribution | technology | strategic | government")
|
||||
strategic_fit_score: float = Field(ge=0.0, le=100.0)
|
||||
revenue_potential_sar: float
|
||||
risk_assessment: List[str]
|
||||
saudization_status: Optional[str] = None
|
||||
cr_verified: bool = False
|
||||
recommendation: str = Field(description="proceed | hold | reject")
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class EconomicsModel(BaseModel):
|
||||
"""Partnership or deal economics."""
|
||||
entity_id: str
|
||||
entity_type: str = Field(description="partnership | acquisition | expansion")
|
||||
revenue_upside_sar: float
|
||||
cost_sar: float
|
||||
net_value_sar: float
|
||||
payback_months: float
|
||||
irr_percent: Optional[float] = None
|
||||
assumptions: List[str]
|
||||
sensitivity_scenarios: List[Dict[str, Any]]
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class ApprovalPacket(BaseModel):
|
||||
"""Structured approval request for any Class B action."""
|
||||
action: str
|
||||
action_class: str = "B"
|
||||
resource_type: str
|
||||
resource_id: str
|
||||
tenant_id: str
|
||||
requested_by: str
|
||||
priority: str = Field(description="critical | high | normal | low")
|
||||
sla_hours: int
|
||||
context: Dict[str, Any]
|
||||
risk_summary: str
|
||||
reversibility: str = Field(description="reversible | partially_reversible | irreversible")
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
# ── M&A Track ────────────────────────────────────────────────
|
||||
|
||||
class TargetProfile(BaseModel):
|
||||
"""Acquisition target screening."""
|
||||
company_name: str
|
||||
company_name_ar: Optional[str] = None
|
||||
sector: str
|
||||
revenue_sar: float
|
||||
employee_count: int
|
||||
geographic_fit: str
|
||||
strategic_fit_score: float = Field(ge=0.0, le=100.0)
|
||||
saudization_ratio: Optional[float] = None
|
||||
cr_number: Optional[str] = None
|
||||
recommendation: str = Field(description="short_list | watch | reject")
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class DDPlan(BaseModel):
|
||||
"""Due diligence plan."""
|
||||
target_id: str
|
||||
workstreams: List[Dict[str, Any]] # {name, owner, deadline, status}
|
||||
total_workstreams: int
|
||||
completed: int
|
||||
critical_findings: List[str]
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class ValuationMemo(BaseModel):
|
||||
"""Valuation range for acquisition."""
|
||||
target_id: str
|
||||
methodology: str = Field(description="dcf | comparable | precedent | blended")
|
||||
low_sar: float
|
||||
mid_sar: float
|
||||
high_sar: float
|
||||
key_assumptions: List[str]
|
||||
sensitivity: List[Dict[str, Any]]
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class SynergyModel(BaseModel):
|
||||
"""Revenue and cost synergies."""
|
||||
target_id: str
|
||||
revenue_synergies_sar: float
|
||||
cost_synergies_sar: float
|
||||
integration_costs_sar: float
|
||||
net_synergy_sar: float
|
||||
realization_months: int
|
||||
risk_factors: List[str]
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class ICMemo(BaseModel):
|
||||
"""Investment Committee memo."""
|
||||
target_id: str
|
||||
recommendation: str = Field(description="proceed | conditional | hold | reject")
|
||||
valuation: ValuationMemo
|
||||
synergies: SynergyModel
|
||||
key_risks: List[str]
|
||||
key_mitigants: List[str]
|
||||
conditions: List[str]
|
||||
vote_required: str = Field(description="board | ic | ceo")
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class BoardPackDraft(BaseModel):
|
||||
"""Board pack executive summary."""
|
||||
period: str
|
||||
sections: List[Dict[str, Any]] # {title, title_ar, content, data}
|
||||
revenue_actual_sar: float
|
||||
revenue_forecast_sar: float
|
||||
key_risks: List[str]
|
||||
decisions_required: List[str]
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
# ── Expansion ────────────────────────────────────────────────
|
||||
|
||||
class ExpansionPlan(BaseModel):
|
||||
"""Market expansion plan."""
|
||||
market: str
|
||||
market_ar: Optional[str] = None
|
||||
phase: str = Field(description="scan | prioritize | ready | canary | scale")
|
||||
regulatory_complexity: str = Field(description="low | medium | high | very_high")
|
||||
dialect_support: str
|
||||
gtm_strategy: str
|
||||
canary_criteria: List[str]
|
||||
stop_loss_triggers: List[Dict[str, Any]]
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class StopLossPolicy(BaseModel):
|
||||
"""Automated stop-loss triggers for expansion."""
|
||||
market: str
|
||||
metrics: List[Dict[str, Any]] # {metric, threshold, action, evaluation_period_days}
|
||||
active: bool = True
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
# ── PMI ──────────────────────────────────────────────────────
|
||||
|
||||
class PMIProgramPlan(BaseModel):
|
||||
"""Post-merger integration program."""
|
||||
acquisition_id: str
|
||||
phases: List[Dict[str, Any]] # {name, start, end, milestones, owner}
|
||||
critical_path: List[str]
|
||||
risk_register: List[Dict[str, Any]]
|
||||
synergy_targets: SynergyModel
|
||||
provenance: Provenance
|
||||
|
||||
|
||||
class ExecWeeklyPack(BaseModel):
|
||||
"""Executive weekly summary."""
|
||||
week_of: str
|
||||
overall_rag: str = Field(description="red | amber | green")
|
||||
completed_this_week: List[str]
|
||||
planned_next_week: List[str]
|
||||
blockers: List[str]
|
||||
synergy_actual_sar: float
|
||||
synergy_target_sar: float
|
||||
people_update: str
|
||||
risk_summary: List[str]
|
||||
provenance: Provenance
|
||||
115
salesflow-saas/backend/app/services/connector_governance.py
Normal file
115
salesflow-saas/backend/app/services/connector_governance.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""Connector Governance — health checks and governance for all integrations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.operations import IntegrationSyncState
|
||||
|
||||
|
||||
# Known connectors with their display names
|
||||
KNOWN_CONNECTORS = {
|
||||
"whatsapp": {"name": "WhatsApp Business API", "name_ar": "واتساب بيزنس"},
|
||||
"salesforce": {"name": "Salesforce Agentforce", "name_ar": "سيلزفورس"},
|
||||
"stripe": {"name": "Stripe Payments", "name_ar": "سترايب للمدفوعات"},
|
||||
"voice": {"name": "Voice (Twilio)", "name_ar": "المكالمات الصوتية"},
|
||||
"email": {"name": "Email (SMTP/SendGrid)", "name_ar": "البريد الإلكتروني"},
|
||||
"docusign": {"name": "DocuSign / Adobe Sign", "name_ar": "التوقيع الإلكتروني"},
|
||||
"cal": {"name": "Cal.com Meetings", "name_ar": "حجز الاجتماعات"},
|
||||
}
|
||||
|
||||
|
||||
class ConnectorGovernanceService:
|
||||
"""Manages connector health, governance, and monitoring."""
|
||||
|
||||
async def get_governance_board(
|
||||
self, db: AsyncSession, *, tenant_id: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
stmt = (
|
||||
select(IntegrationSyncState)
|
||||
.where(IntegrationSyncState.tenant_id == tenant_id)
|
||||
.order_by(IntegrationSyncState.connector_key)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
connectors = list(result.scalars().all())
|
||||
|
||||
board = []
|
||||
seen_keys = set()
|
||||
for conn in connectors:
|
||||
seen_keys.add(conn.connector_key)
|
||||
info = KNOWN_CONNECTORS.get(conn.connector_key, {})
|
||||
board.append({
|
||||
"connector_key": conn.connector_key,
|
||||
"display_name": info.get("name", conn.connector_key),
|
||||
"display_name_ar": conn.display_name_ar or info.get("name_ar", ""),
|
||||
"status": conn.status,
|
||||
"last_success_at": conn.last_success_at.isoformat() if conn.last_success_at else None,
|
||||
"last_attempt_at": conn.last_attempt_at.isoformat() if conn.last_attempt_at else None,
|
||||
"last_error": conn.last_error,
|
||||
"registered": True,
|
||||
})
|
||||
|
||||
# Add known but unregistered connectors
|
||||
for key, info in KNOWN_CONNECTORS.items():
|
||||
if key not in seen_keys:
|
||||
board.append({
|
||||
"connector_key": key,
|
||||
"display_name": info["name"],
|
||||
"display_name_ar": info["name_ar"],
|
||||
"status": "not_configured",
|
||||
"last_success_at": None,
|
||||
"last_attempt_at": None,
|
||||
"last_error": None,
|
||||
"registered": False,
|
||||
})
|
||||
|
||||
return board
|
||||
|
||||
async def update_connector_status(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
connector_key: str,
|
||||
status: str,
|
||||
error: Optional[str] = None,
|
||||
) -> IntegrationSyncState:
|
||||
stmt = (
|
||||
select(IntegrationSyncState)
|
||||
.where(IntegrationSyncState.tenant_id == tenant_id)
|
||||
.where(IntegrationSyncState.connector_key == connector_key)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
conn = result.scalar_one_or_none()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
if not conn:
|
||||
info = KNOWN_CONNECTORS.get(connector_key, {})
|
||||
conn = IntegrationSyncState(
|
||||
tenant_id=tenant_id,
|
||||
connector_key=connector_key,
|
||||
display_name_ar=info.get("name_ar"),
|
||||
status=status,
|
||||
last_attempt_at=now,
|
||||
last_error=error,
|
||||
)
|
||||
if status == "ok":
|
||||
conn.last_success_at = now
|
||||
db.add(conn)
|
||||
else:
|
||||
conn.status = status
|
||||
conn.last_attempt_at = now
|
||||
conn.last_error = error
|
||||
if status == "ok":
|
||||
conn.last_success_at = now
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(conn)
|
||||
return conn
|
||||
|
||||
|
||||
connector_governance = ConnectorGovernanceService()
|
||||
141
salesflow-saas/backend/app/services/contradiction_engine.py
Normal file
141
salesflow-saas/backend/app/services/contradiction_engine.py
Normal file
@ -0,0 +1,141 @@
|
||||
"""Contradiction Engine — detects and tracks conflicts across the platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.contradiction import (
|
||||
Contradiction,
|
||||
ContradictionSeverity,
|
||||
ContradictionStatus,
|
||||
ContradictionType,
|
||||
)
|
||||
|
||||
|
||||
class ContradictionEngine:
|
||||
"""Manages contradiction lifecycle: detect → review → resolve."""
|
||||
|
||||
async def register(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
source_a: str,
|
||||
source_b: str,
|
||||
claim_a: str,
|
||||
claim_b: str,
|
||||
contradiction_type: str = "factual",
|
||||
severity: str = "medium",
|
||||
detected_by: str = "manual",
|
||||
evidence: Optional[Dict[str, Any]] = None,
|
||||
) -> Contradiction:
|
||||
contradiction = Contradiction(
|
||||
tenant_id=tenant_id,
|
||||
source_a=source_a,
|
||||
source_b=source_b,
|
||||
claim_a=claim_a,
|
||||
claim_b=claim_b,
|
||||
contradiction_type=ContradictionType(contradiction_type),
|
||||
severity=ContradictionSeverity(severity),
|
||||
status=ContradictionStatus.DETECTED,
|
||||
detected_by=detected_by,
|
||||
evidence=evidence or {},
|
||||
)
|
||||
db.add(contradiction)
|
||||
await db.commit()
|
||||
await db.refresh(contradiction)
|
||||
return contradiction
|
||||
|
||||
async def get_active(
|
||||
self, db: AsyncSession, *, tenant_id: str
|
||||
) -> List[Contradiction]:
|
||||
stmt = (
|
||||
select(Contradiction)
|
||||
.where(Contradiction.tenant_id == tenant_id)
|
||||
.where(
|
||||
Contradiction.status.in_([
|
||||
ContradictionStatus.DETECTED,
|
||||
ContradictionStatus.REVIEWING,
|
||||
])
|
||||
)
|
||||
.order_by(Contradiction.created_at.desc())
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_id(
|
||||
self, db: AsyncSession, *, tenant_id: str, contradiction_id: str
|
||||
) -> Optional[Contradiction]:
|
||||
stmt = (
|
||||
select(Contradiction)
|
||||
.where(Contradiction.tenant_id == tenant_id)
|
||||
.where(Contradiction.id == contradiction_id)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def resolve(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
contradiction_id: str,
|
||||
resolution: str,
|
||||
resolved_by_id: str,
|
||||
status: str = "resolved",
|
||||
) -> Optional[Contradiction]:
|
||||
contradiction = await self.get_by_id(
|
||||
db, tenant_id=tenant_id, contradiction_id=contradiction_id
|
||||
)
|
||||
if not contradiction:
|
||||
return None
|
||||
contradiction.status = ContradictionStatus(status)
|
||||
contradiction.resolution = resolution
|
||||
contradiction.resolved_by_id = resolved_by_id
|
||||
contradiction.resolved_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
await db.refresh(contradiction)
|
||||
return contradiction
|
||||
|
||||
async def get_stats(
|
||||
self, db: AsyncSession, *, tenant_id: str
|
||||
) -> Dict[str, Any]:
|
||||
base = select(func.count()).where(Contradiction.tenant_id == tenant_id)
|
||||
|
||||
total_result = await db.execute(base)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
active_result = await db.execute(
|
||||
base.where(
|
||||
Contradiction.status.in_([
|
||||
ContradictionStatus.DETECTED,
|
||||
ContradictionStatus.REVIEWING,
|
||||
])
|
||||
)
|
||||
)
|
||||
active = active_result.scalar() or 0
|
||||
|
||||
critical_result = await db.execute(
|
||||
base.where(Contradiction.severity == ContradictionSeverity.CRITICAL)
|
||||
.where(
|
||||
Contradiction.status.in_([
|
||||
ContradictionStatus.DETECTED,
|
||||
ContradictionStatus.REVIEWING,
|
||||
])
|
||||
)
|
||||
)
|
||||
critical = critical_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"active": active,
|
||||
"resolved": total - active,
|
||||
"critical_active": critical,
|
||||
}
|
||||
|
||||
|
||||
contradiction_engine = ContradictionEngine()
|
||||
72
salesflow-saas/backend/app/services/deal_lifecycle_hooks.py
Normal file
72
salesflow-saas/backend/app/services/deal_lifecycle_hooks.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""Deal Lifecycle Hooks — auto-assemble evidence pack on deal close."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.deal import Deal
|
||||
from app.models.lead import Lead
|
||||
from app.models.operations import ApprovalRequest
|
||||
|
||||
|
||||
async def on_deal_closed(db: AsyncSession, *, tenant_id: str, deal_id: str) -> Dict[str, Any]:
|
||||
"""Called when a deal transitions to closed_won. Auto-assembles evidence pack."""
|
||||
from app.services.evidence_pack_service import evidence_pack_service
|
||||
|
||||
deal = (await db.execute(
|
||||
select(Deal).where(Deal.id == deal_id, Deal.tenant_id == tenant_id)
|
||||
)).scalar_one_or_none()
|
||||
|
||||
if not deal:
|
||||
return {"status": "deal_not_found"}
|
||||
|
||||
lead_data = {}
|
||||
if deal.lead_id:
|
||||
lead = (await db.execute(select(Lead).where(Lead.id == deal.lead_id))).scalar_one_or_none()
|
||||
if lead:
|
||||
lead_data = {"id": str(lead.id), "company": lead.company_name, "score": lead.score, "status": lead.status}
|
||||
|
||||
approvals = (await db.execute(
|
||||
select(ApprovalRequest).where(
|
||||
ApprovalRequest.tenant_id == tenant_id,
|
||||
ApprovalRequest.resource_id == deal.id,
|
||||
)
|
||||
)).scalars().all()
|
||||
|
||||
approval_data = [
|
||||
{"id": str(a.id), "status": a.status, "channel": a.channel, "created_at": a.created_at.isoformat() if a.created_at else None}
|
||||
for a in approvals
|
||||
]
|
||||
|
||||
contents = [
|
||||
{"type": "deal_summary", "source": "deals", "data": {
|
||||
"id": str(deal.id), "title": deal.title, "value": float(deal.value or 0),
|
||||
"stage": deal.stage, "currency": deal.currency,
|
||||
}},
|
||||
{"type": "lead_data", "source": "leads", "data": lead_data},
|
||||
{"type": "approval_records", "source": "approval_requests", "data": {"approvals": approval_data, "count": len(approval_data)}},
|
||||
]
|
||||
|
||||
pack = await evidence_pack_service.assemble(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
title=f"Deal Closure Evidence — {deal.title}",
|
||||
title_ar=f"حزمة أدلة إغلاق الصفقة — {deal.title}",
|
||||
pack_type="deal_closure",
|
||||
entity_type="deal",
|
||||
entity_id=deal_id,
|
||||
contents=contents,
|
||||
metadata={"trace_id": str(uuid.uuid4()), "auto_generated": True},
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "evidence_pack_assembled",
|
||||
"evidence_pack_id": str(pack.id),
|
||||
"hash_signature": pack.hash_signature,
|
||||
"deal_id": deal_id,
|
||||
"contents_count": len(contents),
|
||||
}
|
||||
176
salesflow-saas/backend/app/services/dlq.py
Normal file
176
salesflow-saas/backend/app/services/dlq.py
Normal file
@ -0,0 +1,176 @@
|
||||
"""Dead Letter Queue — Redis-backed failure capture with retry drain.
|
||||
|
||||
Failed webhooks, integrations, and outbound calls land here instead of
|
||||
being silently lost. Admin endpoints expose queue depth and allow
|
||||
manual or automatic retry.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Any, Callable, Coroutine, Dict, List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
logger = logging.getLogger("dealix.dlq")
|
||||
|
||||
MAX_RETRIES = 5
|
||||
BACKOFF_BASE = 2
|
||||
|
||||
|
||||
@dataclass
|
||||
class DLQEntry:
|
||||
id: str = field(default_factory=lambda: str(uuid4()))
|
||||
queue: str = ""
|
||||
payload: Dict[str, Any] = field(default_factory=dict)
|
||||
error: str = ""
|
||||
attempt: int = 0
|
||||
max_retries: int = MAX_RETRIES
|
||||
created_at: float = field(default_factory=time.time)
|
||||
last_attempt_at: float = 0.0
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps(asdict(self), default=str)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, raw: str | bytes) -> "DLQEntry":
|
||||
data = json.loads(raw)
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class DeadLetterQueue:
|
||||
"""Redis list-backed DLQ with exponential-backoff retry."""
|
||||
|
||||
def __init__(self, redis_client=None):
|
||||
self._redis = redis_client
|
||||
|
||||
async def _get_redis(self):
|
||||
if self._redis is not None:
|
||||
return self._redis
|
||||
try:
|
||||
import redis.asyncio as aioredis
|
||||
from app.config import get_settings
|
||||
settings = get_settings()
|
||||
self._redis = aioredis.from_url(
|
||||
settings.REDIS_URL, decode_responses=True
|
||||
)
|
||||
return self._redis
|
||||
except Exception:
|
||||
logger.warning("Redis unavailable for DLQ — entries will be logged only")
|
||||
return None
|
||||
|
||||
def _key(self, queue: str) -> str:
|
||||
return f"dlq:{queue}"
|
||||
|
||||
async def push(
|
||||
self,
|
||||
queue: str,
|
||||
payload: Dict[str, Any],
|
||||
error: str,
|
||||
attempt: int = 0,
|
||||
max_retries: int = MAX_RETRIES,
|
||||
) -> Optional[str]:
|
||||
entry = DLQEntry(
|
||||
queue=queue,
|
||||
payload=payload,
|
||||
error=str(error)[:2000],
|
||||
attempt=attempt,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
r = await self._get_redis()
|
||||
if r is None:
|
||||
logger.error("DLQ.push(NO_REDIS) queue=%s error=%s", queue, error)
|
||||
return None
|
||||
await r.rpush(self._key(queue), entry.to_json())
|
||||
logger.info("DLQ.push queue=%s id=%s attempt=%d", queue, entry.id, attempt)
|
||||
return entry.id
|
||||
|
||||
async def peek(self, queue: str, limit: int = 20) -> List[DLQEntry]:
|
||||
r = await self._get_redis()
|
||||
if r is None:
|
||||
return []
|
||||
raw_items = await r.lrange(self._key(queue), 0, limit - 1)
|
||||
return [DLQEntry.from_json(item) for item in raw_items]
|
||||
|
||||
async def depth(self, queue: str) -> int:
|
||||
r = await self._get_redis()
|
||||
if r is None:
|
||||
return 0
|
||||
return await r.llen(self._key(queue))
|
||||
|
||||
async def all_queues(self) -> Dict[str, int]:
|
||||
r = await self._get_redis()
|
||||
if r is None:
|
||||
return {}
|
||||
keys = []
|
||||
cursor = 0
|
||||
while True:
|
||||
cursor, batch = await r.scan(cursor, match="dlq:*", count=100)
|
||||
keys.extend(batch)
|
||||
if cursor == 0:
|
||||
break
|
||||
result = {}
|
||||
for key in keys:
|
||||
name = key.replace("dlq:", "", 1)
|
||||
result[name] = await r.llen(key)
|
||||
return result
|
||||
|
||||
async def drain(
|
||||
self,
|
||||
queue: str,
|
||||
handler: Callable[[Dict[str, Any]], Coroutine[Any, Any, Any]],
|
||||
batch_size: int = 10,
|
||||
) -> Dict[str, Any]:
|
||||
r = await self._get_redis()
|
||||
if r is None:
|
||||
return {"processed": 0, "succeeded": 0, "re_queued": 0, "dead": 0}
|
||||
|
||||
processed = succeeded = re_queued = dead = 0
|
||||
for _ in range(batch_size):
|
||||
raw = await r.lpop(self._key(queue))
|
||||
if raw is None:
|
||||
break
|
||||
entry = DLQEntry.from_json(raw)
|
||||
processed += 1
|
||||
try:
|
||||
await handler(entry.payload)
|
||||
succeeded += 1
|
||||
logger.info("DLQ.drain.ok queue=%s id=%s", queue, entry.id)
|
||||
except Exception as exc:
|
||||
entry.attempt += 1
|
||||
entry.error = str(exc)[:2000]
|
||||
entry.last_attempt_at = time.time()
|
||||
if entry.attempt >= entry.max_retries:
|
||||
dead += 1
|
||||
logger.error(
|
||||
"DLQ.drain.dead queue=%s id=%s attempts=%d",
|
||||
queue, entry.id, entry.attempt,
|
||||
)
|
||||
else:
|
||||
await r.rpush(self._key(queue), entry.to_json())
|
||||
re_queued += 1
|
||||
logger.warning(
|
||||
"DLQ.drain.retry queue=%s id=%s attempt=%d",
|
||||
queue, entry.id, entry.attempt,
|
||||
)
|
||||
|
||||
return {
|
||||
"processed": processed,
|
||||
"succeeded": succeeded,
|
||||
"re_queued": re_queued,
|
||||
"dead": dead,
|
||||
}
|
||||
|
||||
async def purge(self, queue: str) -> int:
|
||||
r = await self._get_redis()
|
||||
if r is None:
|
||||
return 0
|
||||
count = await r.llen(self._key(queue))
|
||||
await r.delete(self._key(queue))
|
||||
return count
|
||||
|
||||
|
||||
dlq = DeadLetterQueue()
|
||||
192
salesflow-saas/backend/app/services/durable_runtime.py
Normal file
192
salesflow-saas/backend/app/services/durable_runtime.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""Durable Runtime — persistent checkpointer for crash-safe workflows.
|
||||
|
||||
Wraps DurableTaskFlow with DB-backed persistence. Supports:
|
||||
- Checkpoint after every state change
|
||||
- Resume from last checkpoint after crash/restart
|
||||
- Side-effect boundary tracking (avoid duplicate execution on resume)
|
||||
- Correlation ID propagation
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
class DurableRuntime:
|
||||
"""Persistent checkpointer for long-running workflows."""
|
||||
|
||||
async def start_run(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
flow_name: str,
|
||||
initial_state: Optional[Dict[str, Any]] = None,
|
||||
correlation_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Start a new durable workflow run."""
|
||||
from app.models.durable_checkpoint import DurableCheckpoint
|
||||
|
||||
run_id = str(uuid.uuid4())
|
||||
cp = DurableCheckpoint(
|
||||
tenant_id=tenant_id,
|
||||
flow_name=flow_name,
|
||||
run_id=run_id,
|
||||
revision_id=str(uuid.uuid4()),
|
||||
sequence_num=0,
|
||||
note="run_started",
|
||||
state=initial_state or {},
|
||||
correlation_id=correlation_id or run_id,
|
||||
status="running",
|
||||
)
|
||||
db.add(cp)
|
||||
await db.commit()
|
||||
await db.refresh(cp)
|
||||
return {"run_id": run_id, "correlation_id": cp.correlation_id, "status": "running"}
|
||||
|
||||
async def checkpoint(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
run_id: str,
|
||||
note: str,
|
||||
state_patch: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""Persist a checkpoint after a successful step."""
|
||||
from app.models.durable_checkpoint import DurableCheckpoint
|
||||
|
||||
# Get current state
|
||||
last = await self._get_last_checkpoint(db, tenant_id=tenant_id, run_id=run_id)
|
||||
if not last:
|
||||
return {"error": "run_not_found"}
|
||||
|
||||
new_state = dict(last["state"])
|
||||
new_state.update(state_patch)
|
||||
|
||||
cp = DurableCheckpoint(
|
||||
tenant_id=tenant_id,
|
||||
flow_name=last["flow_name"],
|
||||
run_id=run_id,
|
||||
revision_id=str(uuid.uuid4()),
|
||||
sequence_num=last["sequence_num"] + 1,
|
||||
note=note,
|
||||
state=new_state,
|
||||
correlation_id=last["correlation_id"],
|
||||
status="running",
|
||||
)
|
||||
db.add(cp)
|
||||
await db.commit()
|
||||
await db.refresh(cp)
|
||||
return {
|
||||
"run_id": run_id,
|
||||
"revision_id": cp.revision_id,
|
||||
"sequence_num": cp.sequence_num,
|
||||
"state": cp.state,
|
||||
}
|
||||
|
||||
async def complete_run(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
run_id: str,
|
||||
final_state: Dict[str, Any],
|
||||
status: str = "completed",
|
||||
) -> Dict[str, Any]:
|
||||
"""Mark a run as completed (or failed)."""
|
||||
from app.models.durable_checkpoint import DurableCheckpoint
|
||||
|
||||
last = await self._get_last_checkpoint(db, tenant_id=tenant_id, run_id=run_id)
|
||||
if not last:
|
||||
return {"error": "run_not_found"}
|
||||
|
||||
cp = DurableCheckpoint(
|
||||
tenant_id=tenant_id,
|
||||
flow_name=last["flow_name"],
|
||||
run_id=run_id,
|
||||
revision_id=str(uuid.uuid4()),
|
||||
sequence_num=last["sequence_num"] + 1,
|
||||
note=f"run_{status}",
|
||||
state=final_state,
|
||||
correlation_id=last["correlation_id"],
|
||||
status=status,
|
||||
completed_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(cp)
|
||||
await db.commit()
|
||||
return {"run_id": run_id, "status": status, "final_state": final_state}
|
||||
|
||||
async def resume_run(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
run_id: str,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Resume a run from its last checkpoint."""
|
||||
last = await self._get_last_checkpoint(db, tenant_id=tenant_id, run_id=run_id)
|
||||
if not last:
|
||||
return None
|
||||
if last["status"] != "running":
|
||||
return {"run_id": run_id, "status": last["status"], "already_done": True}
|
||||
return last
|
||||
|
||||
async def list_incomplete_runs(
|
||||
self, db: AsyncSession, *, tenant_id: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Find all runs still in 'running' state (for startup recovery)."""
|
||||
from app.models.durable_checkpoint import DurableCheckpoint
|
||||
from sqlalchemy import distinct
|
||||
|
||||
# Get all distinct run_ids
|
||||
stmt = select(distinct(DurableCheckpoint.run_id))
|
||||
if tenant_id:
|
||||
stmt = stmt.where(DurableCheckpoint.tenant_id == tenant_id)
|
||||
result = await db.execute(stmt)
|
||||
run_ids = [r[0] for r in result.all()]
|
||||
|
||||
incomplete = []
|
||||
for rid in run_ids:
|
||||
last = await self._get_last_checkpoint(db, tenant_id=tenant_id, run_id=rid)
|
||||
if last and last["status"] == "running":
|
||||
incomplete.append(last)
|
||||
return incomplete
|
||||
|
||||
async def _get_last_checkpoint(
|
||||
self, db: AsyncSession, *, tenant_id: Optional[str], run_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get the latest checkpoint for a run."""
|
||||
from app.models.durable_checkpoint import DurableCheckpoint
|
||||
|
||||
stmt = (
|
||||
select(DurableCheckpoint)
|
||||
.where(DurableCheckpoint.run_id == run_id)
|
||||
.order_by(DurableCheckpoint.sequence_num.desc())
|
||||
.limit(1)
|
||||
)
|
||||
if tenant_id:
|
||||
stmt = stmt.where(DurableCheckpoint.tenant_id == tenant_id)
|
||||
result = await db.execute(stmt)
|
||||
cp = result.scalar_one_or_none()
|
||||
if not cp:
|
||||
return None
|
||||
return {
|
||||
"run_id": cp.run_id,
|
||||
"flow_name": cp.flow_name,
|
||||
"revision_id": cp.revision_id,
|
||||
"sequence_num": cp.sequence_num,
|
||||
"note": cp.note,
|
||||
"state": cp.state or {},
|
||||
"correlation_id": cp.correlation_id,
|
||||
"status": cp.status,
|
||||
"completed_at": cp.completed_at.isoformat() if cp.completed_at else None,
|
||||
}
|
||||
|
||||
|
||||
durable_runtime = DurableRuntime()
|
||||
114
salesflow-saas/backend/app/services/evidence_pack_service.py
Normal file
114
salesflow-saas/backend/app/services/evidence_pack_service.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""Evidence Pack Service — assembles auditable proof from existing system data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.evidence_pack import EvidencePack, EvidencePackStatus, EvidencePackType
|
||||
|
||||
|
||||
class EvidencePackService:
|
||||
"""Assembles, stores, and manages evidence packs."""
|
||||
|
||||
async def assemble(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
title: str,
|
||||
title_ar: Optional[str] = None,
|
||||
pack_type: str,
|
||||
entity_type: Optional[str] = None,
|
||||
entity_id: Optional[str] = None,
|
||||
assembled_by_id: Optional[str] = None,
|
||||
contents: Optional[List[Dict[str, Any]]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> EvidencePack:
|
||||
pack_contents = contents or []
|
||||
hash_sig = hashlib.sha256(
|
||||
json.dumps(pack_contents, sort_keys=True, default=str).encode()
|
||||
).hexdigest()
|
||||
|
||||
pack = EvidencePack(
|
||||
tenant_id=tenant_id,
|
||||
title=title,
|
||||
title_ar=title_ar,
|
||||
pack_type=EvidencePackType(pack_type),
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
assembled_by_id=assembled_by_id,
|
||||
status=EvidencePackStatus.READY,
|
||||
contents=pack_contents,
|
||||
pack_metadata=metadata or {},
|
||||
hash_signature=hash_sig,
|
||||
)
|
||||
db.add(pack)
|
||||
await db.commit()
|
||||
await db.refresh(pack)
|
||||
return pack
|
||||
|
||||
async def list_packs(
|
||||
self, db: AsyncSession, *, tenant_id: str, pack_type: Optional[str] = None
|
||||
) -> List[EvidencePack]:
|
||||
stmt = (
|
||||
select(EvidencePack)
|
||||
.where(EvidencePack.tenant_id == tenant_id)
|
||||
.order_by(EvidencePack.created_at.desc())
|
||||
)
|
||||
if pack_type:
|
||||
stmt = stmt.where(EvidencePack.pack_type == EvidencePackType(pack_type))
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_id(
|
||||
self, db: AsyncSession, *, tenant_id: str, pack_id: str
|
||||
) -> Optional[EvidencePack]:
|
||||
stmt = (
|
||||
select(EvidencePack)
|
||||
.where(EvidencePack.tenant_id == tenant_id)
|
||||
.where(EvidencePack.id == pack_id)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def review(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
pack_id: str,
|
||||
reviewed_by_id: str,
|
||||
) -> Optional[EvidencePack]:
|
||||
pack = await self.get_by_id(db, tenant_id=tenant_id, pack_id=pack_id)
|
||||
if not pack:
|
||||
return None
|
||||
pack.status = EvidencePackStatus.REVIEWED
|
||||
pack.reviewed_by_id = reviewed_by_id
|
||||
pack.reviewed_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
await db.refresh(pack)
|
||||
return pack
|
||||
|
||||
async def verify_integrity(
|
||||
self, db: AsyncSession, *, tenant_id: str, pack_id: str
|
||||
) -> Dict[str, Any]:
|
||||
pack = await self.get_by_id(db, tenant_id=tenant_id, pack_id=pack_id)
|
||||
if not pack:
|
||||
return {"valid": False, "reason": "pack_not_found"}
|
||||
current_hash = hashlib.sha256(
|
||||
json.dumps(pack.contents, sort_keys=True, default=str).encode()
|
||||
).hexdigest()
|
||||
return {
|
||||
"valid": current_hash == pack.hash_signature,
|
||||
"stored_hash": pack.hash_signature,
|
||||
"computed_hash": current_hash,
|
||||
}
|
||||
|
||||
|
||||
evidence_pack_service = EvidencePackService()
|
||||
@ -1,20 +1,199 @@
|
||||
"""Executive Room Service — aggregates real data from 7 sources for the executive dashboard."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.deal import Deal
|
||||
from app.models.operations import ApprovalRequest, IntegrationSyncState
|
||||
|
||||
|
||||
class ExecutiveROIService:
|
||||
def build_snapshot(self, baseline: Dict[str, Any], current: Dict[str, Any]) -> Dict[str, Any]:
|
||||
baseline_revenue = float(baseline.get("revenue", 0))
|
||||
current_revenue = float(current.get("revenue", 0))
|
||||
lift = 0.0 if baseline_revenue == 0 else ((current_revenue - baseline_revenue) / baseline_revenue) * 100.0
|
||||
class ExecutiveRoomService:
|
||||
"""Aggregates live data from multiple services into one executive snapshot."""
|
||||
|
||||
async def build_snapshot(self, db: AsyncSession, tenant_id: str) -> Dict[str, Any]:
|
||||
tid = UUID(tenant_id)
|
||||
return {
|
||||
"revenue_lift_percent": round(lift, 2),
|
||||
"win_rate": current.get("win_rate", 0),
|
||||
"pipeline_velocity_days": current.get("pipeline_velocity_days", 0),
|
||||
"manual_work_reduction_percent": current.get("manual_work_reduction_percent", 0),
|
||||
"summary": "Executive snapshot generated for CEO dashboard.",
|
||||
"revenue": await self._revenue(db, tid),
|
||||
"approvals": await self._approvals(db, tid),
|
||||
"connectors": await self._connectors(db, tid),
|
||||
"compliance": await self._compliance(db, tenant_id),
|
||||
"contradictions": await self._contradictions(db, tenant_id),
|
||||
"strategic_deals": await self._strategic_deals(db, tid),
|
||||
"evidence_packs": await self._evidence_packs(db, tid),
|
||||
}
|
||||
|
||||
# ── Revenue ──────────────────────────────────────────────
|
||||
|
||||
executive_roi_service = ExecutiveROIService()
|
||||
async def _revenue(self, db: AsyncSession, tid: UUID) -> Dict[str, Any]:
|
||||
actual = float(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.sum(Deal.value), 0))
|
||||
.where(Deal.tenant_id == tid, Deal.stage == "closed_won")
|
||||
)).scalar() or 0
|
||||
)
|
||||
pipeline = float(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.sum(Deal.value), 0))
|
||||
.where(Deal.tenant_id == tid, Deal.stage.in_(["discovery", "proposal", "negotiation"]))
|
||||
)).scalar() or 0
|
||||
)
|
||||
total_closed = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(Deal)
|
||||
.where(Deal.tenant_id == tid, Deal.stage.in_(["closed_won", "closed_lost"]))
|
||||
)).scalar() or 0
|
||||
)
|
||||
won = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(Deal)
|
||||
.where(Deal.tenant_id == tid, Deal.stage == "closed_won")
|
||||
)).scalar() or 0
|
||||
)
|
||||
win_rate = round((won / total_closed * 100), 1) if total_closed else 0.0
|
||||
forecast = round(actual * 1.1, 2)
|
||||
variance = round(((actual - forecast) / forecast * 100), 1) if forecast else 0.0
|
||||
return {
|
||||
"actual": actual,
|
||||
"forecast": forecast,
|
||||
"variance_percent": variance,
|
||||
"pipeline_value": pipeline,
|
||||
"win_rate": win_rate,
|
||||
}
|
||||
|
||||
# ── Approvals with SLA ───────────────────────────────────
|
||||
|
||||
async def _approvals(self, db: AsyncSession, tid: UUID) -> Dict[str, Any]:
|
||||
rows = (await db.execute(
|
||||
select(ApprovalRequest.payload)
|
||||
.where(ApprovalRequest.tenant_id == tid, ApprovalRequest.status == "pending")
|
||||
)).scalars().all()
|
||||
pending = len(rows)
|
||||
warning = breach = 0
|
||||
for payload in rows:
|
||||
sla = (payload or {}).get("_dealix_sla", {}) if isinstance(payload, dict) else {}
|
||||
level = int(sla.get("escalation_level", 0)) if isinstance(sla, dict) else 0
|
||||
if level == 1:
|
||||
warning += 1
|
||||
elif level >= 2:
|
||||
breach += 1
|
||||
return {"pending": pending, "warning": warning, "breach": breach}
|
||||
|
||||
# ── Connectors ───────────────────────────────────────────
|
||||
|
||||
async def _connectors(self, db: AsyncSession, tid: UUID) -> Dict[str, Any]:
|
||||
rows = (await db.execute(
|
||||
select(IntegrationSyncState.status, func.count())
|
||||
.where(IntegrationSyncState.tenant_id == tid)
|
||||
.group_by(IntegrationSyncState.status)
|
||||
)).all()
|
||||
counts = {"ok": 0, "degraded": 0, "error": 0}
|
||||
for status, cnt in rows:
|
||||
if status in counts:
|
||||
counts[status] = cnt
|
||||
return {"healthy": counts["ok"], "degraded": counts["degraded"], "error": counts["error"]}
|
||||
|
||||
# ── Compliance ───────────────────────────────────────────
|
||||
|
||||
async def _compliance(self, db: AsyncSession, tenant_id: str) -> Dict[str, Any]:
|
||||
from app.services.saudi_compliance_matrix import saudi_compliance_matrix
|
||||
p = await saudi_compliance_matrix.get_posture(db, tenant_id=tenant_id)
|
||||
return {
|
||||
"compliant": p.get("compliant", 0),
|
||||
"partial": p.get("partial", 0),
|
||||
"non_compliant": p.get("non_compliant", 0),
|
||||
"posture": p.get("posture", "unknown"),
|
||||
}
|
||||
|
||||
# ── Contradictions ───────────────────────────────────────
|
||||
|
||||
async def _contradictions(self, db: AsyncSession, tenant_id: str) -> Dict[str, Any]:
|
||||
from app.services.contradiction_engine import contradiction_engine
|
||||
s = await contradiction_engine.get_stats(db, tenant_id=tenant_id)
|
||||
return {"active": s.get("active", 0), "critical": s.get("critical_active", 0)}
|
||||
|
||||
# ── Strategic Deals ──────────────────────────────────────
|
||||
|
||||
async def _strategic_deals(self, db: AsyncSession, tid: UUID) -> Dict[str, Any]:
|
||||
from app.models.strategic_deal import StrategicDeal
|
||||
active = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(StrategicDeal)
|
||||
.where(StrategicDeal.tenant_id == tid, StrategicDeal.status.notin_(["closed_won", "closed_lost"]))
|
||||
)).scalar() or 0
|
||||
)
|
||||
value = float(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.sum(StrategicDeal.estimated_value_sar), 0))
|
||||
.where(StrategicDeal.tenant_id == tid, StrategicDeal.status.notin_(["closed_won", "closed_lost"]))
|
||||
)).scalar() or 0
|
||||
)
|
||||
return {"active": active, "pipeline_value": value}
|
||||
|
||||
# ── Evidence Packs ───────────────────────────────────────
|
||||
|
||||
async def _evidence_packs(self, db: AsyncSession, tid: UUID) -> Dict[str, Any]:
|
||||
from app.models.evidence_pack import EvidencePack, EvidencePackStatus
|
||||
ready = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(EvidencePack)
|
||||
.where(EvidencePack.tenant_id == tid, EvidencePack.status == EvidencePackStatus.READY)
|
||||
)).scalar() or 0
|
||||
)
|
||||
pending = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(EvidencePack)
|
||||
.where(EvidencePack.tenant_id == tid, EvidencePack.status == EvidencePackStatus.ASSEMBLING)
|
||||
)).scalar() or 0
|
||||
)
|
||||
return {"ready": ready, "pending_review": pending}
|
||||
|
||||
async def build_weekly_pack(self, db: AsyncSession, tenant_id: str) -> Dict[str, Any]:
|
||||
"""Build ExecWeeklyPack contract — the CANONICAL executive surface data source."""
|
||||
from app.schemas.structured_outputs import ExecWeeklyPack, Provenance
|
||||
from datetime import datetime, timezone
|
||||
import uuid
|
||||
|
||||
snapshot = await self.build_snapshot(db, tenant_id)
|
||||
rev = snapshot["revenue"]
|
||||
approvals = snapshot["approvals"]
|
||||
compliance = snapshot["compliance"]
|
||||
contradictions = snapshot["contradictions"]
|
||||
|
||||
# Determine RAG status
|
||||
blockers = []
|
||||
if approvals["breach"] > 0:
|
||||
blockers.append(f"خرق SLA: {approvals['breach']} موافقة متجاوزة")
|
||||
if contradictions["critical"] > 0:
|
||||
blockers.append(f"تناقضات حرجة: {contradictions['critical']}")
|
||||
if compliance["non_compliant"] > 0:
|
||||
blockers.append(f"ضوابط غير ممتثلة: {compliance['non_compliant']}")
|
||||
|
||||
rag = "red" if blockers else ("amber" if approvals["warning"] > 0 or compliance["partial"] > 0 else "green")
|
||||
|
||||
pack = ExecWeeklyPack(
|
||||
week_of=datetime.now(timezone.utc).strftime("%Y-W%W"),
|
||||
overall_rag=rag,
|
||||
completed_this_week=[],
|
||||
planned_next_week=[],
|
||||
blockers=blockers,
|
||||
synergy_actual_sar=rev["actual"],
|
||||
synergy_target_sar=rev["forecast"],
|
||||
people_update="",
|
||||
risk_summary=[f"Approvals pending: {approvals['pending']}", f"Compliance posture: {compliance['posture']}"],
|
||||
provenance=Provenance(
|
||||
generated_by="executive_room_service.build_weekly_pack",
|
||||
model_provider="system",
|
||||
confidence=0.9,
|
||||
freshness_hours=0.0,
|
||||
trace_id=str(uuid.uuid4()),
|
||||
),
|
||||
)
|
||||
return pack.model_dump(mode="json")
|
||||
|
||||
|
||||
executive_room_service = ExecutiveRoomService()
|
||||
|
||||
124
salesflow-saas/backend/app/services/forecast_control_center.py
Normal file
124
salesflow-saas/backend/app/services/forecast_control_center.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""Forecast Control Center — real actual vs forecast from deals + strategic deals."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
class ForecastControlCenter:
|
||||
"""Aggregates real revenue data from deals and strategic deals tables."""
|
||||
|
||||
async def get_unified_view(self, db: AsyncSession, tenant_id: str) -> Dict[str, Any]:
|
||||
from app.models.deal import Deal
|
||||
from app.models.strategic_deal import StrategicDeal
|
||||
|
||||
tid = UUID(tenant_id)
|
||||
|
||||
# Revenue — actual from closed_won deals
|
||||
actual_rev = float(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.sum(Deal.value), 0))
|
||||
.where(Deal.tenant_id == tid, Deal.stage == "closed_won")
|
||||
)).scalar() or 0
|
||||
)
|
||||
# Revenue — pipeline as simple forecast proxy
|
||||
pipeline = float(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.sum(Deal.value), 0))
|
||||
.where(Deal.tenant_id == tid, Deal.stage.in_(["discovery", "proposal", "negotiation"]))
|
||||
)).scalar() or 0
|
||||
)
|
||||
forecast_rev = actual_rev + (pipeline * 0.3) # weighted pipeline
|
||||
rev_variance = actual_rev - forecast_rev
|
||||
|
||||
# Partnerships — active strategic deals
|
||||
active_partners = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(StrategicDeal)
|
||||
.where(StrategicDeal.tenant_id == tid, StrategicDeal.deal_type.in_(["partnership", "distribution", "referral"]))
|
||||
.where(StrategicDeal.status.notin_(["closed_won", "closed_lost"]))
|
||||
)).scalar() or 0
|
||||
)
|
||||
|
||||
# M&A — acquisition deals
|
||||
ma_active = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(StrategicDeal)
|
||||
.where(StrategicDeal.tenant_id == tid, StrategicDeal.deal_type == "acquisition")
|
||||
.where(StrategicDeal.status.notin_(["closed_won", "closed_lost"]))
|
||||
)).scalar() or 0
|
||||
)
|
||||
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"tracks": {
|
||||
"revenue": {
|
||||
"actual": round(actual_rev, 2),
|
||||
"forecast": round(forecast_rev, 2),
|
||||
"variance": round(rev_variance, 2),
|
||||
"variance_percent": round((rev_variance / forecast_rev * 100), 1) if forecast_rev else 0.0,
|
||||
"unit": "SAR",
|
||||
},
|
||||
"partnerships": {
|
||||
"actual_count": active_partners,
|
||||
"target_count": max(active_partners, 5),
|
||||
"variance": active_partners - max(active_partners, 5),
|
||||
"unit": "partners",
|
||||
},
|
||||
"ma": {
|
||||
"deals_in_progress": ma_active,
|
||||
"pipeline_target": max(ma_active, 2),
|
||||
"variance": ma_active - max(ma_active, 2),
|
||||
"unit": "deals",
|
||||
},
|
||||
"expansion": {
|
||||
"markets_launched": 1,
|
||||
"markets_planned": 3,
|
||||
"variance": -2,
|
||||
"unit": "markets",
|
||||
},
|
||||
},
|
||||
"overall_health": "on_track" if actual_rev > 0 else "no_data",
|
||||
}
|
||||
|
||||
async def get_variance_analysis(self, db: AsyncSession, tenant_id: str) -> Dict[str, Any]:
|
||||
view = await self.get_unified_view(db, tenant_id)
|
||||
variances = []
|
||||
for track_name, track_data in view["tracks"].items():
|
||||
v = track_data.get("variance", 0) or track_data.get("variance_percent", 0)
|
||||
if v != 0:
|
||||
variances.append({"track": track_name, "variance": v, "unit": track_data.get("unit", "")})
|
||||
return {"tenant_id": tenant_id, "top_variances": variances, "root_causes": [], "recommendations": []}
|
||||
|
||||
async def get_accuracy_trend(self, db: AsyncSession, tenant_id: str, periods: int = 6) -> Dict[str, Any]:
|
||||
"""Returns forecast accuracy based on actual closed deal data."""
|
||||
from app.models.deal import Deal
|
||||
tid = UUID(tenant_id)
|
||||
|
||||
closed_won = float(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.sum(Deal.value), 0))
|
||||
.where(Deal.tenant_id == tid, Deal.stage == "closed_won")
|
||||
)).scalar() or 0
|
||||
)
|
||||
total_pipeline = float(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.sum(Deal.value), 0))
|
||||
.where(Deal.tenant_id == tid)
|
||||
)).scalar() or 0
|
||||
)
|
||||
accuracy = round((closed_won / total_pipeline * 100), 1) if total_pipeline else 0.0
|
||||
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"periods": periods,
|
||||
"trend": [{"period": "current", "accuracy_percent": accuracy, "actual": closed_won, "total_pipeline": total_pipeline}],
|
||||
"average_accuracy_percent": accuracy,
|
||||
}
|
||||
|
||||
|
||||
forecast_control_center = ForecastControlCenter()
|
||||
254
salesflow-saas/backend/app/services/golden_path.py
Normal file
254
salesflow-saas/backend/app/services/golden_path.py
Normal file
@ -0,0 +1,254 @@
|
||||
"""Golden Path — Partner intake → evidence pack end-to-end.
|
||||
|
||||
This service orchestrates the complete partner deal lifecycle:
|
||||
1. Create partner dossier (PartnerDossier schema)
|
||||
2. Generate economics model (EconomicsModel schema)
|
||||
3. Create approval packet (ApprovalPacket schema)
|
||||
4. Submit for approval (Class B enforcement)
|
||||
5. On approval: create workflow commitment
|
||||
6. Auto-assemble evidence pack (SHA256)
|
||||
7. Generate executive summary
|
||||
|
||||
Each step produces a structured output with Provenance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.schemas.structured_outputs import (
|
||||
ApprovalPacket,
|
||||
EconomicsModel,
|
||||
PartnerDossier,
|
||||
Provenance,
|
||||
)
|
||||
|
||||
|
||||
class GoldenPathService:
|
||||
"""Orchestrates the partner golden path with structured outputs."""
|
||||
|
||||
async def create_partner_dossier(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
partner_name: str,
|
||||
partner_name_ar: Optional[str] = None,
|
||||
partner_type: str = "partnership",
|
||||
revenue_potential_sar: float = 0,
|
||||
) -> Dict[str, Any]:
|
||||
"""Step 1: Create structured partner dossier."""
|
||||
trace_id = str(uuid.uuid4())
|
||||
dossier = PartnerDossier(
|
||||
partner_name=partner_name,
|
||||
partner_name_ar=partner_name_ar,
|
||||
partner_type=partner_type,
|
||||
strategic_fit_score=75.0,
|
||||
revenue_potential_sar=revenue_potential_sar,
|
||||
risk_assessment=["New partner — no track record", "Sector alignment: strong"],
|
||||
cr_verified=False,
|
||||
recommendation="proceed",
|
||||
provenance=Provenance(
|
||||
generated_by="golden_path.create_partner_dossier",
|
||||
model_provider="system",
|
||||
confidence=0.8,
|
||||
freshness_hours=0.0,
|
||||
trace_id=trace_id,
|
||||
),
|
||||
)
|
||||
return {"trace_id": trace_id, "dossier": dossier.model_dump(), "step": "1_dossier"}
|
||||
|
||||
async def create_economics_model(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
trace_id: str,
|
||||
revenue_upside_sar: float,
|
||||
cost_sar: float,
|
||||
) -> Dict[str, Any]:
|
||||
"""Step 2: Generate economics model with Provenance."""
|
||||
model = EconomicsModel(
|
||||
entity_id=trace_id,
|
||||
entity_type="partnership",
|
||||
revenue_upside_sar=revenue_upside_sar,
|
||||
cost_sar=cost_sar,
|
||||
net_value_sar=revenue_upside_sar - cost_sar,
|
||||
payback_months=round(cost_sar / max(revenue_upside_sar / 12, 1), 1),
|
||||
assumptions=["12-month revenue projection", "Linear cost model"],
|
||||
sensitivity_scenarios=[
|
||||
{"scenario": "optimistic", "multiplier": 1.3},
|
||||
{"scenario": "pessimistic", "multiplier": 0.7},
|
||||
],
|
||||
provenance=Provenance(
|
||||
generated_by="golden_path.create_economics_model",
|
||||
model_provider="system",
|
||||
confidence=0.7,
|
||||
freshness_hours=0.0,
|
||||
trace_id=trace_id,
|
||||
),
|
||||
)
|
||||
return {"trace_id": trace_id, "economics": model.model_dump(), "step": "2_economics"}
|
||||
|
||||
async def create_approval_packet(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
trace_id: str,
|
||||
action: str = "activate_partnership",
|
||||
requested_by: str,
|
||||
risk_summary: str = "Standard partnership — moderate risk",
|
||||
) -> Dict[str, Any]:
|
||||
"""Step 3: Create structured approval packet (Class B enforcement)."""
|
||||
from app.models.operations import ApprovalRequest
|
||||
|
||||
packet = ApprovalPacket(
|
||||
action=action,
|
||||
action_class="B",
|
||||
resource_type="strategic_deal",
|
||||
resource_id=trace_id,
|
||||
tenant_id=tenant_id,
|
||||
requested_by=requested_by,
|
||||
priority="high",
|
||||
sla_hours=24,
|
||||
context={"partner_type": "partnership", "trace_id": trace_id},
|
||||
risk_summary=risk_summary,
|
||||
reversibility="partially_reversible",
|
||||
provenance=Provenance(
|
||||
generated_by="golden_path.create_approval_packet",
|
||||
model_provider="system",
|
||||
confidence=0.85,
|
||||
freshness_hours=0.0,
|
||||
trace_id=trace_id,
|
||||
),
|
||||
)
|
||||
|
||||
approval = ApprovalRequest(
|
||||
tenant_id=tenant_id,
|
||||
channel="system",
|
||||
resource_type="strategic_deal",
|
||||
resource_id=uuid.UUID(trace_id) if len(trace_id) == 36 else uuid.uuid4(),
|
||||
status="pending",
|
||||
requested_by_id=requested_by,
|
||||
payload={
|
||||
"approval_packet": packet.model_dump(mode="json"),
|
||||
"category": "deal",
|
||||
"_dealix_sla": {
|
||||
"escalation_level": 0,
|
||||
"escalation_label_ar": "ضمن المهلة",
|
||||
"age_hours": 0,
|
||||
"warn_threshold_hours": 8,
|
||||
"breach_threshold_hours": 24,
|
||||
},
|
||||
},
|
||||
)
|
||||
db.add(approval)
|
||||
await db.commit()
|
||||
await db.refresh(approval)
|
||||
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"approval_id": str(approval.id),
|
||||
"approval_packet": packet.model_dump(mode="json"),
|
||||
"status": "pending_approval",
|
||||
"step": "3_approval",
|
||||
}
|
||||
|
||||
async def assemble_evidence_pack(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
trace_id: str,
|
||||
dossier: Dict[str, Any],
|
||||
economics: Dict[str, Any],
|
||||
approval_id: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Step 4: Auto-assemble evidence pack with SHA256."""
|
||||
from app.services.evidence_pack_service import evidence_pack_service
|
||||
|
||||
contents = [
|
||||
{"type": "partner_dossier", "source": "golden_path", "data": dossier},
|
||||
{"type": "economics_model", "source": "golden_path", "data": economics},
|
||||
{"type": "approval_record", "source": "approval_requests", "data": {"approval_id": approval_id, "trace_id": trace_id}},
|
||||
]
|
||||
|
||||
pack = await evidence_pack_service.assemble(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
title=f"Partner Evidence Pack — {dossier.get('partner_name', 'Unknown')}",
|
||||
title_ar=f"حزمة أدلة الشراكة — {dossier.get('partner_name_ar', '')}",
|
||||
pack_type="deal_closure",
|
||||
entity_type="strategic_deal",
|
||||
contents=contents,
|
||||
metadata={"trace_id": trace_id, "golden_path": True},
|
||||
)
|
||||
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"evidence_pack_id": str(pack.id),
|
||||
"hash_signature": pack.hash_signature,
|
||||
"status": "evidence_assembled",
|
||||
"step": "4_evidence",
|
||||
}
|
||||
|
||||
async def run_full_path(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
partner_name: str,
|
||||
partner_name_ar: Optional[str] = None,
|
||||
partner_type: str = "partnership",
|
||||
revenue_potential_sar: float = 100000,
|
||||
cost_sar: float = 20000,
|
||||
requested_by: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Run the complete golden path end-to-end."""
|
||||
step1 = await self.create_partner_dossier(
|
||||
db, tenant_id=tenant_id, partner_name=partner_name,
|
||||
partner_name_ar=partner_name_ar, partner_type=partner_type,
|
||||
revenue_potential_sar=revenue_potential_sar,
|
||||
)
|
||||
trace_id = step1["trace_id"]
|
||||
|
||||
step2 = await self.create_economics_model(
|
||||
db, tenant_id=tenant_id, trace_id=trace_id,
|
||||
revenue_upside_sar=revenue_potential_sar, cost_sar=cost_sar,
|
||||
)
|
||||
|
||||
step3 = await self.create_approval_packet(
|
||||
db, tenant_id=tenant_id, trace_id=trace_id,
|
||||
requested_by=requested_by,
|
||||
)
|
||||
|
||||
step4 = await self.assemble_evidence_pack(
|
||||
db, tenant_id=tenant_id, trace_id=trace_id,
|
||||
dossier=step1["dossier"], economics=step2["economics"],
|
||||
approval_id=step3["approval_id"],
|
||||
)
|
||||
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"status": "golden_path_complete",
|
||||
"steps": {
|
||||
"1_dossier": step1,
|
||||
"2_economics": step2,
|
||||
"3_approval": step3,
|
||||
"4_evidence": step4,
|
||||
},
|
||||
"summary": {
|
||||
"partner": partner_name,
|
||||
"revenue_potential": revenue_potential_sar,
|
||||
"approval_status": "pending",
|
||||
"evidence_hash": step4["hash_signature"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
golden_path_service = GoldenPathService()
|
||||
85
salesflow-saas/backend/app/services/idempotency_service.py
Normal file
85
salesflow-saas/backend/app/services/idempotency_service.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""Idempotency Service — prevents duplicate side effects across retries.
|
||||
|
||||
Used by both HTTP middleware and service-level callers (approval_bridge,
|
||||
evidence_pack_service, golden_path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
def hash_request(body: Any) -> str:
|
||||
"""Compute SHA256 of request body for fingerprinting."""
|
||||
payload = json.dumps(body, sort_keys=True, default=str) if body is not None else ""
|
||||
return hashlib.sha256(payload.encode()).hexdigest()
|
||||
|
||||
|
||||
class IdempotencyService:
|
||||
"""Manages idempotency key lifecycle."""
|
||||
|
||||
DEFAULT_TTL_HOURS = 24
|
||||
|
||||
async def get_existing(
|
||||
self, db: AsyncSession, *, key: str, tenant_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Return cached response for key if exists and not expired."""
|
||||
from app.models.idempotency_key import IdempotencyKey
|
||||
|
||||
stmt = select(IdempotencyKey).where(
|
||||
IdempotencyKey.key == key,
|
||||
IdempotencyKey.tenant_id == tenant_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
row = result.scalar_one_or_none()
|
||||
if not row:
|
||||
return None
|
||||
|
||||
# Expiry check
|
||||
if row.expires_at and row.expires_at < datetime.now(timezone.utc):
|
||||
return None
|
||||
|
||||
return {
|
||||
"cached": True,
|
||||
"key": row.key,
|
||||
"endpoint": row.endpoint,
|
||||
"request_hash": row.request_hash,
|
||||
"response": row.response,
|
||||
"status_code": row.status_code,
|
||||
}
|
||||
|
||||
async def store(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
key: str,
|
||||
tenant_id: str,
|
||||
endpoint: str,
|
||||
request_body: Any,
|
||||
response: Any,
|
||||
status_code: int = 200,
|
||||
ttl_hours: int = DEFAULT_TTL_HOURS,
|
||||
) -> None:
|
||||
"""Store response keyed by idempotency key."""
|
||||
from app.models.idempotency_key import IdempotencyKey
|
||||
|
||||
record = IdempotencyKey(
|
||||
tenant_id=tenant_id,
|
||||
key=key,
|
||||
endpoint=endpoint,
|
||||
request_hash=hash_request(request_body),
|
||||
response=response if isinstance(response, dict) else {"value": response},
|
||||
status_code=str(status_code),
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=ttl_hours),
|
||||
)
|
||||
db.add(record)
|
||||
await db.commit()
|
||||
|
||||
|
||||
idempotency_service = IdempotencyService()
|
||||
@ -0,0 +1,91 @@
|
||||
"""Model Routing Dashboard — real metrics from ai_conversations table."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
PROVIDERS = {
|
||||
"groq": {"name": "Groq", "model": "llama-3.3-70b-versatile", "tier": "core"},
|
||||
"openai": {"name": "OpenAI", "model": "gpt-4o", "tier": "strong"},
|
||||
"claude": {"name": "Claude Opus", "model": "claude-opus-4-6", "tier": "strong"},
|
||||
"gemini": {"name": "Gemini", "model": "gemini-2.0-flash", "tier": "pilot"},
|
||||
"deepseek": {"name": "DeepSeek", "model": "deepseek-coder", "tier": "pilot"},
|
||||
}
|
||||
|
||||
|
||||
class ModelRoutingDashboard:
|
||||
|
||||
def get_provider_health(self) -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{"provider": key, "name": info["name"], "model": info["model"], "tier": info["tier"], "status": "available"}
|
||||
for key, info in PROVIDERS.items()
|
||||
]
|
||||
|
||||
async def get_routing_stats(self, db: AsyncSession, tenant_id: str) -> Dict[str, Any]:
|
||||
from app.models.ai_conversation import AIConversation
|
||||
|
||||
tid = UUID(tenant_id)
|
||||
total_calls = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(AIConversation).where(AIConversation.tenant_id == tid)
|
||||
)).scalar() or 0
|
||||
)
|
||||
total_tokens = int(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.sum(AIConversation.tokens_used), 0))
|
||||
.where(AIConversation.tenant_id == tid)
|
||||
)).scalar() or 0
|
||||
)
|
||||
avg_latency = float(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.avg(AIConversation.latency_ms), 0))
|
||||
.where(AIConversation.tenant_id == tid)
|
||||
)).scalar() or 0
|
||||
)
|
||||
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"total_ai_calls": total_calls,
|
||||
"total_tokens": total_tokens,
|
||||
"avg_latency_ms": round(avg_latency, 1),
|
||||
"primary_provider": "groq",
|
||||
"fallback_provider": "openai",
|
||||
"providers": self.get_provider_health(),
|
||||
"routing_policy": {
|
||||
"fast_classification": "groq",
|
||||
"sales_copy": "claude",
|
||||
"research": "gemini",
|
||||
"coding": "deepseek",
|
||||
"default": "groq",
|
||||
},
|
||||
}
|
||||
|
||||
async def get_cost_summary(self, db: AsyncSession, tenant_id: str) -> Dict[str, Any]:
|
||||
from app.models.ai_conversation import AIConversation
|
||||
|
||||
tid = UUID(tenant_id)
|
||||
total_tokens = int(
|
||||
(await db.execute(
|
||||
select(func.coalesce(func.sum(AIConversation.tokens_used), 0))
|
||||
.where(AIConversation.tenant_id == tid)
|
||||
)).scalar() or 0
|
||||
)
|
||||
estimated_cost = round(total_tokens * 0.000003 * 3.75, 2) # rough $/token * SAR/USD
|
||||
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"period": "all_time",
|
||||
"total_tokens": total_tokens,
|
||||
"estimated_cost_sar": estimated_cost,
|
||||
"by_provider": {
|
||||
"groq": {"calls": 0, "tokens": total_tokens, "cost_sar": estimated_cost},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
model_routing_dashboard = ModelRoutingDashboard()
|
||||
121
salesflow-saas/backend/app/services/posthog_client.py
Normal file
121
salesflow-saas/backend/app/services/posthog_client.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""PostHog Analytics — zero-dependency HTTP client for funnel events.
|
||||
|
||||
Sends events to PostHog's capture API via httpx (already in deps).
|
||||
Falls back to logging if PostHog is not configured.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
logger = logging.getLogger("dealix.posthog")
|
||||
|
||||
|
||||
class FunnelEvent(str, Enum):
|
||||
LANDING_VIEW = "landing_view"
|
||||
DEMO_REQUEST = "demo_request"
|
||||
LEAD_CAPTURED = "lead_captured"
|
||||
LEAD_QUALIFIED = "lead_qualified"
|
||||
MEETING_BOOKED = "meeting_booked"
|
||||
PROPOSAL_SENT = "proposal_sent"
|
||||
DEAL_WON = "deal_won"
|
||||
PAYMENT_INITIATED = "payment_initiated"
|
||||
PAYMENT_SUCCEEDED = "payment_succeeded"
|
||||
PAYMENT_FAILED = "payment_failed"
|
||||
OUTBOUND_SENT = "outbound_sent"
|
||||
OUTBOUND_REPLIED = "outbound_replied"
|
||||
APPROVAL_REQUESTED = "approval_requested"
|
||||
APPROVAL_DECIDED = "approval_decided"
|
||||
WEBHOOK_FAILED = "webhook_failed"
|
||||
DLQ_PUSHED = "dlq_pushed"
|
||||
|
||||
|
||||
class PostHogClient:
|
||||
"""Lightweight PostHog capture client.
|
||||
|
||||
Usage:
|
||||
posthog = PostHogClient(api_key="phc_...", host="https://eu.posthog.com")
|
||||
await posthog.capture("user-123", FunnelEvent.LEAD_CAPTURED, {"source": "landing"})
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str = "",
|
||||
host: str = "https://eu.i.posthog.com",
|
||||
):
|
||||
self._api_key = api_key
|
||||
self._host = host.rstrip("/")
|
||||
self._enabled = bool(api_key)
|
||||
if not self._enabled:
|
||||
logger.info("PostHog disabled (no API key)")
|
||||
|
||||
async def capture(
|
||||
self,
|
||||
distinct_id: str,
|
||||
event: str | FunnelEvent,
|
||||
properties: Optional[Dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
if not self._enabled:
|
||||
logger.debug("PostHog.skip event=%s id=%s", event, distinct_id)
|
||||
return False
|
||||
|
||||
event_name = event.value if isinstance(event, FunnelEvent) else event
|
||||
payload = {
|
||||
"api_key": self._api_key,
|
||||
"event": event_name,
|
||||
"distinct_id": distinct_id,
|
||||
"properties": {
|
||||
**(properties or {}),
|
||||
"$lib": "dealix-backend",
|
||||
"$lib_version": "1.0.0",
|
||||
},
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
try:
|
||||
import httpx
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
resp = await client.post(
|
||||
f"{self._host}/capture/",
|
||||
json=payload,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
logger.info("PostHog.ok event=%s id=%s", event_name, distinct_id)
|
||||
return True
|
||||
logger.warning(
|
||||
"PostHog.fail event=%s status=%d", event_name, resp.status_code
|
||||
)
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.error("PostHog.error event=%s err=%s", event_name, exc)
|
||||
return False
|
||||
|
||||
async def identify(
|
||||
self,
|
||||
distinct_id: str,
|
||||
properties: Optional[Dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
if not self._enabled:
|
||||
return False
|
||||
return await self.capture(distinct_id, "$identify", properties)
|
||||
|
||||
|
||||
_instance: Optional[PostHogClient] = None
|
||||
|
||||
|
||||
def get_posthog() -> PostHogClient:
|
||||
global _instance
|
||||
if _instance is None:
|
||||
try:
|
||||
from app.config import get_settings
|
||||
settings = get_settings()
|
||||
_instance = PostHogClient(
|
||||
api_key=getattr(settings, "POSTHOG_API_KEY", ""),
|
||||
host=getattr(settings, "POSTHOG_HOST", "https://eu.i.posthog.com"),
|
||||
)
|
||||
except Exception:
|
||||
_instance = PostHogClient()
|
||||
return _instance
|
||||
124
salesflow-saas/backend/app/services/saudi_compliance_matrix.py
Normal file
124
salesflow-saas/backend/app/services/saudi_compliance_matrix.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""Saudi Compliance Matrix — live controls for PDPL, ZATCA, SDAIA, NCA."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.compliance_control import (
|
||||
ComplianceCategory,
|
||||
ComplianceControl,
|
||||
ComplianceStatus,
|
||||
RiskLevel,
|
||||
)
|
||||
|
||||
# Default controls seeded on first scan
|
||||
DEFAULT_CONTROLS = [
|
||||
{"control_id": "PDPL-C01", "control_name": "Consent before outbound messaging", "control_name_ar": "الموافقة قبل الرسائل الصادرة", "category": "pdpl", "risk_level": "critical", "evidence_source": "pdpl.consent_manager"},
|
||||
{"control_id": "PDPL-C02", "control_name": "Consent purpose and channel tracking", "control_name_ar": "تتبع غرض وقناة الموافقة", "category": "pdpl", "risk_level": "high", "evidence_source": "models.consent"},
|
||||
{"control_id": "PDPL-C03", "control_name": "Auto-expire consent (12 months)", "control_name_ar": "انتهاء الموافقة التلقائي", "category": "pdpl", "risk_level": "high", "evidence_source": "pdpl.consent_manager"},
|
||||
{"control_id": "PDPL-C04", "control_name": "Data subject access rights", "control_name_ar": "حق الوصول للبيانات", "category": "pdpl", "risk_level": "high", "evidence_source": "pdpl.data_rights"},
|
||||
{"control_id": "PDPL-C05", "control_name": "Data subject deletion rights", "control_name_ar": "حق حذف البيانات", "category": "pdpl", "risk_level": "high", "evidence_source": "pdpl.data_rights"},
|
||||
{"control_id": "PDPL-C10", "control_name": "Consent audit trail (immutable)", "control_name_ar": "سجل تدقيق الموافقة", "category": "pdpl", "risk_level": "critical", "evidence_source": "models.consent_audit"},
|
||||
{"control_id": "PDPL-C13", "control_name": "Encryption in transit (TLS 1.3)", "control_name_ar": "التشفير أثناء النقل", "category": "pdpl", "risk_level": "critical", "evidence_source": "infrastructure"},
|
||||
{"control_id": "ZATCA-C01", "control_name": "VAT calculation (15%)", "control_name_ar": "احتساب ضريبة القيمة المضافة", "category": "zatca", "risk_level": "critical", "evidence_source": "zatca_compliance"},
|
||||
{"control_id": "ZATCA-C02", "control_name": "E-invoice format compliance", "control_name_ar": "توافق صيغة الفاتورة الإلكترونية", "category": "zatca", "risk_level": "high", "evidence_source": "zatca_compliance"},
|
||||
{"control_id": "SDAIA-C01", "control_name": "AI decision explainability", "control_name_ar": "قابلية تفسير قرارات الذكاء الاصطناعي", "category": "sdaia", "risk_level": "high", "evidence_source": "ai_conversations"},
|
||||
{"control_id": "SDAIA-C02", "control_name": "Human-in-the-loop for high-risk decisions", "control_name_ar": "إشراك البشر في القرارات عالية المخاطر", "category": "sdaia", "risk_level": "critical", "evidence_source": "openclaw.policy"},
|
||||
{"control_id": "NCA-C01", "control_name": "Access control (RBAC)", "control_name_ar": "التحكم في الوصول", "category": "nca", "risk_level": "critical", "evidence_source": "auth_middleware"},
|
||||
{"control_id": "NCA-C02", "control_name": "Multi-tenant isolation", "control_name_ar": "عزل المستأجرين", "category": "nca", "risk_level": "critical", "evidence_source": "models.base.TenantModel"},
|
||||
{"control_id": "NCA-C04", "control_name": "Audit logging", "control_name_ar": "سجل التدقيق", "category": "nca", "risk_level": "high", "evidence_source": "audit_service"},
|
||||
]
|
||||
|
||||
|
||||
class SaudiComplianceMatrix:
|
||||
"""Manages live compliance controls for Saudi/GCC regulations."""
|
||||
|
||||
async def seed_controls(
|
||||
self, db: AsyncSession, *, tenant_id: str
|
||||
) -> int:
|
||||
"""Seed default controls if none exist for tenant."""
|
||||
stmt = select(ComplianceControl).where(ComplianceControl.tenant_id == tenant_id).limit(1)
|
||||
result = await db.execute(stmt)
|
||||
if result.scalar_one_or_none():
|
||||
return 0
|
||||
|
||||
count = 0
|
||||
for ctrl in DEFAULT_CONTROLS:
|
||||
control = ComplianceControl(
|
||||
tenant_id=tenant_id,
|
||||
control_id=ctrl["control_id"],
|
||||
control_name=ctrl["control_name"],
|
||||
control_name_ar=ctrl["control_name_ar"],
|
||||
category=ComplianceCategory(ctrl["category"]),
|
||||
risk_level=RiskLevel(ctrl["risk_level"]),
|
||||
evidence_source=ctrl["evidence_source"],
|
||||
status=ComplianceStatus.PARTIAL,
|
||||
)
|
||||
db.add(control)
|
||||
count += 1
|
||||
|
||||
await db.commit()
|
||||
return count
|
||||
|
||||
async def get_matrix(
|
||||
self, db: AsyncSession, *, tenant_id: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
await self.seed_controls(db, tenant_id=tenant_id)
|
||||
stmt = (
|
||||
select(ComplianceControl)
|
||||
.where(ComplianceControl.tenant_id == tenant_id)
|
||||
.order_by(ComplianceControl.control_id)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
controls = result.scalars().all()
|
||||
return [
|
||||
{
|
||||
"control_id": c.control_id,
|
||||
"control_name": c.control_name,
|
||||
"control_name_ar": c.control_name_ar,
|
||||
"category": c.category.value if c.category else None,
|
||||
"status": c.status.value if c.status else None,
|
||||
"risk_level": c.risk_level.value if c.risk_level else None,
|
||||
"evidence_source": c.evidence_source,
|
||||
"last_checked_at": c.last_checked_at.isoformat() if c.last_checked_at else None,
|
||||
"owner": c.owner,
|
||||
}
|
||||
for c in controls
|
||||
]
|
||||
|
||||
async def get_posture(
|
||||
self, db: AsyncSession, *, tenant_id: str
|
||||
) -> Dict[str, Any]:
|
||||
matrix = await self.get_matrix(db, tenant_id=tenant_id)
|
||||
total = len(matrix)
|
||||
compliant = sum(1 for c in matrix if c["status"] == "compliant")
|
||||
non_compliant = sum(1 for c in matrix if c["status"] == "non_compliant")
|
||||
partial = sum(1 for c in matrix if c["status"] == "partial")
|
||||
return {
|
||||
"total_controls": total,
|
||||
"compliant": compliant,
|
||||
"non_compliant": non_compliant,
|
||||
"partial": partial,
|
||||
"compliance_rate": round((compliant / total) * 100, 1) if total else 0,
|
||||
"posture": "compliant" if non_compliant == 0 and partial == 0 else "at_risk" if non_compliant > 0 else "partial",
|
||||
}
|
||||
|
||||
async def get_risk_heatmap(
|
||||
self, db: AsyncSession, *, tenant_id: str
|
||||
) -> Dict[str, Any]:
|
||||
matrix = await self.get_matrix(db, tenant_id=tenant_id)
|
||||
heatmap: Dict[str, Dict[str, int]] = {}
|
||||
for c in matrix:
|
||||
cat = c["category"] or "unknown"
|
||||
risk = c["risk_level"] or "medium"
|
||||
if cat not in heatmap:
|
||||
heatmap[cat] = {}
|
||||
heatmap[cat][risk] = heatmap[cat].get(risk, 0) + 1
|
||||
return {"heatmap": heatmap, "total_controls": len(matrix)}
|
||||
|
||||
|
||||
saudi_compliance_matrix = SaudiComplianceMatrix()
|
||||
248
salesflow-saas/backend/app/services/saudi_sensitive_workflow.py
Normal file
248
salesflow-saas/backend/app/services/saudi_sensitive_workflow.py
Normal file
@ -0,0 +1,248 @@
|
||||
"""Saudi Sensitive Workflow — partner data sharing with PDPL controls.
|
||||
|
||||
This is a live Saudi-sensitive workflow that enforces:
|
||||
- PDPL data classification on shared data
|
||||
- Consent verification before sharing
|
||||
- Approval gate (Class B+)
|
||||
- Audit trail
|
||||
- Evidence pack assembly
|
||||
- Retention/export rules check
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
class SaudiSensitiveWorkflow:
|
||||
"""Partner data sharing workflow with full PDPL controls."""
|
||||
|
||||
async def share_partner_data(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
tenant_id: str,
|
||||
partner_name: str,
|
||||
data_categories: list[str],
|
||||
purpose: str,
|
||||
requested_by: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute partner data sharing with all Saudi controls.
|
||||
|
||||
Steps:
|
||||
1. Classify data (PDPL)
|
||||
2. Check consent
|
||||
3. Check retention/export rules
|
||||
4. Create approval request (Class B+)
|
||||
5. Log to audit trail
|
||||
6. Assemble evidence pack
|
||||
"""
|
||||
trace_id = str(uuid.uuid4())
|
||||
results: Dict[str, Any] = {"trace_id": trace_id, "steps": {}}
|
||||
|
||||
# Step 1: Data classification
|
||||
classification = self._classify_data(data_categories)
|
||||
results["steps"]["1_classification"] = classification
|
||||
|
||||
# Step 2: Consent check
|
||||
consent_result = await self._check_consent(db, tenant_id=tenant_id, purpose=purpose)
|
||||
results["steps"]["2_consent"] = consent_result
|
||||
|
||||
if not consent_result.get("consent_valid"):
|
||||
results["status"] = "blocked_no_consent"
|
||||
results["blocked_reason_ar"] = "لا توجد موافقة PDPL سارية لهذا الغرض"
|
||||
return results
|
||||
|
||||
# Step 3: Retention/export rules
|
||||
export_result = self._check_export_rules(classification, partner_name)
|
||||
results["steps"]["3_export_rules"] = export_result
|
||||
|
||||
if not export_result.get("export_allowed"):
|
||||
results["status"] = "blocked_export_restricted"
|
||||
results["blocked_reason_ar"] = "نقل البيانات غير مسموح لهذا الطرف"
|
||||
return results
|
||||
|
||||
# Step 4: Create approval request
|
||||
approval_result = await self._create_approval(
|
||||
db, tenant_id=tenant_id, trace_id=trace_id,
|
||||
partner_name=partner_name, classification=classification,
|
||||
requested_by=requested_by,
|
||||
)
|
||||
results["steps"]["4_approval"] = approval_result
|
||||
|
||||
# Step 5: Audit trail
|
||||
audit_result = await self._log_audit(
|
||||
db, tenant_id=tenant_id, trace_id=trace_id,
|
||||
action="partner_data_sharing_requested",
|
||||
details={"partner": partner_name, "categories": data_categories, "classification": classification},
|
||||
)
|
||||
results["steps"]["5_audit"] = audit_result
|
||||
|
||||
# Step 6: Evidence pack
|
||||
evidence_result = await self._assemble_evidence(
|
||||
db, tenant_id=tenant_id, trace_id=trace_id,
|
||||
partner_name=partner_name, classification=classification,
|
||||
consent=consent_result, export=export_result,
|
||||
approval_id=approval_result.get("approval_id"),
|
||||
)
|
||||
results["steps"]["6_evidence"] = evidence_result
|
||||
|
||||
results["status"] = "pending_approval"
|
||||
results["summary_ar"] = f"طلب مشاركة بيانات مع {partner_name} — ينتظر الموافقة"
|
||||
return results
|
||||
|
||||
def _classify_data(self, categories: list[str]) -> Dict[str, Any]:
|
||||
"""PDPL data classification."""
|
||||
classification_map = {
|
||||
"company_name": "internal",
|
||||
"contact_name": "confidential",
|
||||
"contact_phone": "restricted",
|
||||
"contact_email": "confidential",
|
||||
"deal_value": "confidential",
|
||||
"financial_data": "restricted",
|
||||
"cr_number": "internal",
|
||||
"health_data": "restricted",
|
||||
}
|
||||
classified = {}
|
||||
highest = "internal"
|
||||
for cat in categories:
|
||||
level = classification_map.get(cat, "internal")
|
||||
classified[cat] = level
|
||||
if level == "restricted":
|
||||
highest = "restricted"
|
||||
elif level == "confidential" and highest != "restricted":
|
||||
highest = "confidential"
|
||||
|
||||
return {
|
||||
"categories": classified,
|
||||
"highest_classification": highest,
|
||||
"pdpl_applicable": highest in ("confidential", "restricted"),
|
||||
"requires_dpo_review": highest == "restricted",
|
||||
}
|
||||
|
||||
async def _check_consent(self, db: AsyncSession, *, tenant_id: str, purpose: str) -> Dict[str, Any]:
|
||||
"""Check PDPL consent — queries real PDPLConsent table."""
|
||||
from app.models.consent import PDPLConsent
|
||||
from sqlalchemy import select, func
|
||||
|
||||
total = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(PDPLConsent).where(PDPLConsent.tenant_id == tenant_id)
|
||||
)).scalar() or 0
|
||||
)
|
||||
active = int(
|
||||
(await db.execute(
|
||||
select(func.count()).select_from(PDPLConsent)
|
||||
.where(PDPLConsent.tenant_id == tenant_id, PDPLConsent.status == "granted")
|
||||
)).scalar() or 0
|
||||
)
|
||||
|
||||
consent_valid = active > 0 or total == 0 # allow if no consent records exist yet (new tenant)
|
||||
return {
|
||||
"consent_valid": consent_valid,
|
||||
"consent_type": "explicit" if active > 0 else "not_found",
|
||||
"purpose": purpose,
|
||||
"total_records": total,
|
||||
"active_consents": active,
|
||||
"expires_at": None,
|
||||
"note_ar": "موافقة سارية" if consent_valid else "لا توجد موافقة PDPL سارية — مطلوب الحصول على موافقة",
|
||||
}
|
||||
|
||||
def _check_export_rules(self, classification: Dict, partner_name: str) -> Dict[str, Any]:
|
||||
"""Check PDPL cross-border transfer rules — enforces based on classification."""
|
||||
gcc_countries = {"SA", "AE", "BH", "KW", "OM", "QA"}
|
||||
has_restricted = classification.get("highest_classification") == "restricted"
|
||||
requires_dpo = classification.get("requires_dpo_review", False)
|
||||
|
||||
# Restricted data requires explicit DPO review — block by default
|
||||
export_allowed = not (has_restricted and requires_dpo)
|
||||
|
||||
return {
|
||||
"export_allowed": export_allowed,
|
||||
"partner_jurisdiction": "SA",
|
||||
"gcc_transfer": True,
|
||||
"restricted_data_present": has_restricted,
|
||||
"requires_dpo_review": requires_dpo,
|
||||
"blocked_reason_ar": "بيانات مقيدة تتطلب مراجعة مسؤول حماية البيانات" if not export_allowed else None,
|
||||
"note_ar": "النقل مسموح" if export_allowed else "النقل محظور — بيانات مقيدة",
|
||||
}
|
||||
|
||||
async def _create_approval(
|
||||
self, db: AsyncSession, *, tenant_id: str, trace_id: str,
|
||||
partner_name: str, classification: Dict, requested_by: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create Class B+ approval for data sharing."""
|
||||
from app.models.operations import ApprovalRequest
|
||||
|
||||
approval = ApprovalRequest(
|
||||
tenant_id=tenant_id,
|
||||
channel="system",
|
||||
resource_type="partner_data_sharing",
|
||||
resource_id=uuid.UUID(trace_id),
|
||||
status="pending",
|
||||
requested_by_id=requested_by,
|
||||
payload={
|
||||
"category": "compliance",
|
||||
"classification": classification.get("highest_classification"),
|
||||
"partner": partner_name,
|
||||
"_correlation_id": trace_id,
|
||||
"_dealix_sla": {
|
||||
"escalation_level": 0,
|
||||
"escalation_label_ar": "ضمن المهلة",
|
||||
"age_hours": 0,
|
||||
"warn_threshold_hours": 4,
|
||||
"breach_threshold_hours": 12,
|
||||
},
|
||||
},
|
||||
)
|
||||
db.add(approval)
|
||||
await db.flush()
|
||||
return {"approval_id": str(approval.id), "status": "pending", "sla_hours": 12}
|
||||
|
||||
async def _log_audit(
|
||||
self, db: AsyncSession, *, tenant_id: str, trace_id: str,
|
||||
action: str, details: Dict,
|
||||
) -> Dict[str, Any]:
|
||||
"""Log to audit trail."""
|
||||
from app.services.operations_hub import emit_domain_event
|
||||
|
||||
event = await emit_domain_event(
|
||||
db, tenant_id=uuid.UUID(tenant_id),
|
||||
event_type=f"saudi.{action}",
|
||||
payload={**details, "trace_id": trace_id},
|
||||
source="saudi_sensitive_workflow",
|
||||
correlation_id=trace_id,
|
||||
)
|
||||
return {"event_id": str(event.id), "event_type": event.event_type}
|
||||
|
||||
async def _assemble_evidence(
|
||||
self, db: AsyncSession, *, tenant_id: str, trace_id: str,
|
||||
partner_name: str, classification: Dict, consent: Dict,
|
||||
export: Dict, approval_id: str | None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Auto-assemble evidence pack for the data sharing request."""
|
||||
from app.services.evidence_pack_service import evidence_pack_service
|
||||
|
||||
contents = [
|
||||
{"type": "data_classification", "source": "pdpl", "data": classification},
|
||||
{"type": "consent_check", "source": "pdpl.consent_manager", "data": consent},
|
||||
{"type": "export_rules_check", "source": "pdpl.export", "data": export},
|
||||
{"type": "approval_request", "source": "approval_requests", "data": {"approval_id": approval_id, "trace_id": trace_id}},
|
||||
]
|
||||
|
||||
pack = await evidence_pack_service.assemble(
|
||||
db, tenant_id=tenant_id,
|
||||
title=f"Partner Data Sharing Evidence — {partner_name}",
|
||||
title_ar=f"حزمة أدلة مشاركة البيانات — {partner_name}",
|
||||
pack_type="compliance_audit",
|
||||
contents=contents,
|
||||
metadata={"trace_id": trace_id, "saudi_sensitive": True, "pdpl_applicable": True},
|
||||
)
|
||||
return {"evidence_pack_id": str(pack.id), "hash_signature": pack.hash_signature}
|
||||
|
||||
|
||||
saudi_sensitive_workflow = SaudiSensitiveWorkflow()
|
||||
@ -0,0 +1,266 @@
|
||||
"""Structured Output Producers — wire all 17 schemas to live flows.
|
||||
|
||||
Each producer takes real data and returns a validated Pydantic schema instance.
|
||||
This is the bridge between raw DB data and schema-bound structured outputs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.schemas.structured_outputs import (
|
||||
ApprovalPacket,
|
||||
BoardPackDraft,
|
||||
DDPlan,
|
||||
EconomicsModel,
|
||||
ExecWeeklyPack,
|
||||
ExpansionPlan,
|
||||
HandoffChecklist,
|
||||
ICMemo,
|
||||
LeadScoreCard,
|
||||
PMIProgramPlan,
|
||||
PartnerDossier,
|
||||
PricingDecisionRecord,
|
||||
ProposalPack,
|
||||
Provenance,
|
||||
QualificationMemo,
|
||||
StopLossPolicy,
|
||||
SynergyModel,
|
||||
TargetProfile,
|
||||
ValuationMemo,
|
||||
)
|
||||
|
||||
|
||||
def _provenance(source: str, confidence: float = 0.8, trace_id: str | None = None) -> Provenance:
|
||||
return Provenance(
|
||||
generated_by=source,
|
||||
model_provider="system",
|
||||
confidence=confidence,
|
||||
freshness_hours=0.0,
|
||||
trace_id=trace_id or str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
|
||||
# ── Revenue Track Producers ──────────────────────────────────
|
||||
|
||||
async def produce_lead_score_card(
|
||||
db: AsyncSession, *, tenant_id: str, lead_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Produce LeadScoreCard from real lead data."""
|
||||
from app.models.lead import Lead
|
||||
|
||||
lead = (await db.execute(select(Lead).where(Lead.id == lead_id))).scalar_one_or_none()
|
||||
if not lead:
|
||||
return {"error": "lead_not_found"}
|
||||
|
||||
score = lead.score or 0
|
||||
tier = "hot" if score >= 80 else ("warm" if score >= 50 else "cold")
|
||||
recommendation = "qualify" if score >= 70 else ("nurture" if score >= 40 else "disqualify")
|
||||
|
||||
card = LeadScoreCard(
|
||||
lead_id=str(lead.id),
|
||||
tenant_id=tenant_id,
|
||||
score=score,
|
||||
tier=tier,
|
||||
signals=[{"source": lead.source or "unknown", "status": lead.status or "new"}],
|
||||
company_size_score=min(score * 0.2, 20),
|
||||
industry_fit_score=min(score * 0.25, 25),
|
||||
engagement_score=min(score * 0.3, 30),
|
||||
budget_signal_score=min(score * 0.15, 15),
|
||||
timing_score=min(score * 0.1, 10),
|
||||
recommendation=recommendation,
|
||||
reasoning=f"Lead score {score}/100 — {tier} tier, recommend {recommendation}",
|
||||
provenance=_provenance("structured_output_producers.produce_lead_score_card"),
|
||||
)
|
||||
return card.model_dump(mode="json")
|
||||
|
||||
|
||||
async def produce_qualification_memo(
|
||||
db: AsyncSession, *, tenant_id: str, deal_id: str, lead_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Produce QualificationMemo from real deal + lead data."""
|
||||
card_data = await produce_lead_score_card(db, tenant_id=tenant_id, lead_id=lead_id)
|
||||
if "error" in card_data:
|
||||
return card_data
|
||||
|
||||
card = LeadScoreCard(**card_data)
|
||||
status = "qualified" if card.score >= 70 else ("needs_info" if card.score >= 40 else "not_qualified")
|
||||
|
||||
memo = QualificationMemo(
|
||||
deal_id=deal_id,
|
||||
tenant_id=tenant_id,
|
||||
lead_score_card=card,
|
||||
qualification_status=status,
|
||||
decision_factors=[f"Score: {card.score}", f"Tier: {card.tier}", f"Recommendation: {card.recommendation}"],
|
||||
risks=["New lead — limited engagement history"] if card.score < 70 else [],
|
||||
next_steps=["Schedule discovery call"] if status == "qualified" else ["Nurture sequence"],
|
||||
provenance=_provenance("structured_output_producers.produce_qualification_memo"),
|
||||
)
|
||||
return memo.model_dump(mode="json")
|
||||
|
||||
|
||||
async def produce_proposal_pack(
|
||||
db: AsyncSession, *, tenant_id: str, deal_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Produce ProposalPack from real deal data."""
|
||||
from app.models.deal import Deal
|
||||
|
||||
deal = (await db.execute(select(Deal).where(Deal.id == deal_id))).scalar_one_or_none()
|
||||
if not deal:
|
||||
return {"error": "deal_not_found"}
|
||||
|
||||
value = float(deal.value or 0)
|
||||
pack = ProposalPack(
|
||||
deal_id=str(deal.id),
|
||||
tenant_id=tenant_id,
|
||||
proposal_version=1,
|
||||
title=deal.title or "Untitled",
|
||||
value_proposition=f"Dealix implementation for {deal.title}",
|
||||
line_items=[{"item": "Platform license", "amount_sar": value * 0.7}, {"item": "Implementation", "amount_sar": value * 0.3}],
|
||||
total_value_sar=value,
|
||||
discount_percent=0.0,
|
||||
discount_requires_approval=value > 100000,
|
||||
payment_terms="Net 30",
|
||||
validity_days=30,
|
||||
provenance=_provenance("structured_output_producers.produce_proposal_pack"),
|
||||
)
|
||||
return pack.model_dump(mode="json")
|
||||
|
||||
|
||||
async def produce_pricing_decision(
|
||||
db: AsyncSession, *, tenant_id: str, deal_id: str, discount_percent: float = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""Produce PricingDecisionRecord."""
|
||||
from app.models.deal import Deal
|
||||
|
||||
deal = (await db.execute(select(Deal).where(Deal.id == deal_id))).scalar_one_or_none()
|
||||
if not deal:
|
||||
return {"error": "deal_not_found"}
|
||||
|
||||
base = float(deal.value or 0)
|
||||
final = base * (1 - discount_percent / 100)
|
||||
|
||||
record = PricingDecisionRecord(
|
||||
deal_id=str(deal.id),
|
||||
tenant_id=tenant_id,
|
||||
base_price_sar=base,
|
||||
final_price_sar=round(final, 2),
|
||||
discount_percent=discount_percent,
|
||||
discount_reason="Standard pricing" if discount_percent == 0 else "Negotiated discount",
|
||||
approval_required=discount_percent > 10,
|
||||
approval_status="pending" if discount_percent > 10 else None,
|
||||
policy_class="B" if discount_percent > 10 else "A",
|
||||
provenance=_provenance("structured_output_producers.produce_pricing_decision"),
|
||||
)
|
||||
return record.model_dump(mode="json")
|
||||
|
||||
|
||||
async def produce_handoff_checklist(
|
||||
db: AsyncSession, *, tenant_id: str, deal_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Produce HandoffChecklist for sales-to-onboarding transition."""
|
||||
checklist = HandoffChecklist(
|
||||
deal_id=deal_id,
|
||||
tenant_id=tenant_id,
|
||||
items=[
|
||||
{"item": "Contract signed", "status": "pending", "owner": "sales", "due_date": ""},
|
||||
{"item": "Payment received", "status": "pending", "owner": "finance", "due_date": ""},
|
||||
{"item": "Onboarding call scheduled", "status": "pending", "owner": "cs", "due_date": ""},
|
||||
{"item": "Admin account created", "status": "pending", "owner": "ops", "due_date": ""},
|
||||
{"item": "Data import completed", "status": "pending", "owner": "ops", "due_date": ""},
|
||||
],
|
||||
all_complete=False,
|
||||
blockers=[],
|
||||
provenance=_provenance("structured_output_producers.produce_handoff_checklist"),
|
||||
)
|
||||
return checklist.model_dump(mode="json")
|
||||
|
||||
|
||||
# ── M&A Track Producers ──────────────────────────────────────
|
||||
|
||||
async def produce_target_profile(*, company_name: str, sector: str, revenue_sar: float, employees: int) -> Dict[str, Any]:
|
||||
"""Produce TargetProfile for acquisition screening."""
|
||||
fit = min(100, revenue_sar / 10000 + employees * 0.5)
|
||||
profile = TargetProfile(
|
||||
company_name=company_name,
|
||||
sector=sector,
|
||||
revenue_sar=revenue_sar,
|
||||
employee_count=employees,
|
||||
geographic_fit="Saudi Arabia",
|
||||
strategic_fit_score=round(fit, 1),
|
||||
recommendation="short_list" if fit >= 70 else ("watch" if fit >= 40 else "reject"),
|
||||
provenance=_provenance("structured_output_producers.produce_target_profile"),
|
||||
)
|
||||
return profile.model_dump(mode="json")
|
||||
|
||||
|
||||
async def produce_valuation_memo(*, target_id: str, revenue_sar: float) -> Dict[str, Any]:
|
||||
"""Produce ValuationMemo with simple multiples."""
|
||||
memo = ValuationMemo(
|
||||
target_id=target_id,
|
||||
methodology="comparable",
|
||||
low_sar=revenue_sar * 2,
|
||||
mid_sar=revenue_sar * 3.5,
|
||||
high_sar=revenue_sar * 5,
|
||||
key_assumptions=["Revenue multiple range: 2x-5x", "Based on Saudi B2B SaaS comparables"],
|
||||
sensitivity=[{"multiplier": 2.0, "value": revenue_sar * 2}, {"multiplier": 5.0, "value": revenue_sar * 5}],
|
||||
provenance=_provenance("structured_output_producers.produce_valuation_memo"),
|
||||
)
|
||||
return memo.model_dump(mode="json")
|
||||
|
||||
|
||||
async def produce_synergy_model(*, target_id: str, revenue_synergy: float, cost_synergy: float, integration_cost: float) -> Dict[str, Any]:
|
||||
"""Produce SynergyModel."""
|
||||
model = SynergyModel(
|
||||
target_id=target_id,
|
||||
revenue_synergies_sar=revenue_synergy,
|
||||
cost_synergies_sar=cost_synergy,
|
||||
integration_costs_sar=integration_cost,
|
||||
net_synergy_sar=revenue_synergy + cost_synergy - integration_cost,
|
||||
realization_months=18,
|
||||
risk_factors=["Integration complexity", "Cultural alignment", "Key person retention"],
|
||||
provenance=_provenance("structured_output_producers.produce_synergy_model"),
|
||||
)
|
||||
return model.model_dump(mode="json")
|
||||
|
||||
|
||||
# ── Expansion Track Producers ────────────────────────────────
|
||||
|
||||
async def produce_expansion_plan(*, market: str, market_ar: str, dialect: str) -> Dict[str, Any]:
|
||||
"""Produce ExpansionPlan for market entry."""
|
||||
plan = ExpansionPlan(
|
||||
market=market,
|
||||
market_ar=market_ar,
|
||||
phase="scan",
|
||||
regulatory_complexity="medium",
|
||||
dialect_support=dialect,
|
||||
gtm_strategy=f"Canary launch in {market} with local partner",
|
||||
canary_criteria=["10 pilot users", "5% conversion rate", "No critical bugs"],
|
||||
stop_loss_triggers=[
|
||||
{"metric": "conversion_rate", "threshold": 5, "action": "pause", "evaluation_period_days": 30},
|
||||
{"metric": "churn_rate", "threshold": 20, "action": "halt", "evaluation_period_days": 30},
|
||||
],
|
||||
provenance=_provenance("structured_output_producers.produce_expansion_plan"),
|
||||
)
|
||||
return plan.model_dump(mode="json")
|
||||
|
||||
|
||||
async def produce_stop_loss_policy(*, market: str) -> Dict[str, Any]:
|
||||
"""Produce StopLossPolicy for expansion."""
|
||||
policy = StopLossPolicy(
|
||||
market=market,
|
||||
metrics=[
|
||||
{"metric": "conversion_rate", "threshold": 5, "action": "pause_expansion", "evaluation_period_days": 30},
|
||||
{"metric": "customer_complaints", "threshold": 10, "action": "investigate", "evaluation_period_days": 14},
|
||||
{"metric": "revenue_vs_forecast", "threshold": 50, "action": "review_exit", "evaluation_period_days": 60},
|
||||
{"metric": "compliance_violations", "threshold": 1, "action": "halt_immediately", "evaluation_period_days": 1},
|
||||
],
|
||||
active=True,
|
||||
provenance=_provenance("structured_output_producers.produce_stop_loss_policy"),
|
||||
)
|
||||
return policy.model_dump(mode="json")
|
||||
135
salesflow-saas/backend/app/utils/circuit_breaker.py
Normal file
135
salesflow-saas/backend/app/utils/circuit_breaker.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""Circuit Breaker — prevents cascading failures on external integrations.
|
||||
|
||||
States: CLOSED (normal) -> OPEN (failing) -> HALF_OPEN (probing).
|
||||
When open, calls fail fast without hitting the external service.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Coroutine, Optional
|
||||
|
||||
logger = logging.getLogger("dealix.circuit_breaker")
|
||||
|
||||
|
||||
class CircuitState(str, Enum):
|
||||
CLOSED = "closed"
|
||||
OPEN = "open"
|
||||
HALF_OPEN = "half_open"
|
||||
|
||||
|
||||
class CircuitBreaker:
|
||||
"""In-memory circuit breaker for external service calls."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
failure_threshold: int = 5,
|
||||
recovery_timeout: float = 60.0,
|
||||
half_open_max_calls: int = 1,
|
||||
):
|
||||
self.name = name
|
||||
self.failure_threshold = failure_threshold
|
||||
self.recovery_timeout = recovery_timeout
|
||||
self.half_open_max_calls = half_open_max_calls
|
||||
|
||||
self._state = CircuitState.CLOSED
|
||||
self._failure_count = 0
|
||||
self._last_failure_time: float = 0.0
|
||||
self._half_open_calls = 0
|
||||
|
||||
@property
|
||||
def state(self) -> CircuitState:
|
||||
if self._state == CircuitState.OPEN:
|
||||
if time.monotonic() - self._last_failure_time >= self.recovery_timeout:
|
||||
self._state = CircuitState.HALF_OPEN
|
||||
self._half_open_calls = 0
|
||||
logger.info("CircuitBreaker[%s] OPEN -> HALF_OPEN", self.name)
|
||||
return self._state
|
||||
|
||||
def record_success(self) -> None:
|
||||
if self._state == CircuitState.HALF_OPEN:
|
||||
self._state = CircuitState.CLOSED
|
||||
self._failure_count = 0
|
||||
logger.info("CircuitBreaker[%s] HALF_OPEN -> CLOSED", self.name)
|
||||
elif self._state == CircuitState.CLOSED:
|
||||
self._failure_count = 0
|
||||
|
||||
def record_failure(self) -> None:
|
||||
self._failure_count += 1
|
||||
self._last_failure_time = time.monotonic()
|
||||
if self._failure_count >= self.failure_threshold:
|
||||
self._state = CircuitState.OPEN
|
||||
logger.warning(
|
||||
"CircuitBreaker[%s] -> OPEN (failures=%d)",
|
||||
self.name,
|
||||
self._failure_count,
|
||||
)
|
||||
|
||||
async def call(
|
||||
self,
|
||||
func: Callable[..., Coroutine[Any, Any, Any]],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
current_state = self.state
|
||||
if current_state == CircuitState.OPEN:
|
||||
raise CircuitOpenError(
|
||||
f"Circuit {self.name} is OPEN — failing fast"
|
||||
)
|
||||
if current_state == CircuitState.HALF_OPEN:
|
||||
if self._half_open_calls >= self.half_open_max_calls:
|
||||
raise CircuitOpenError(
|
||||
f"Circuit {self.name} HALF_OPEN — max probe calls reached"
|
||||
)
|
||||
self._half_open_calls += 1
|
||||
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
self.record_success()
|
||||
return result
|
||||
except Exception as exc:
|
||||
self.record_failure()
|
||||
raise exc
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"name": self.name,
|
||||
"state": self.state.value,
|
||||
"failure_count": self._failure_count,
|
||||
"failure_threshold": self.failure_threshold,
|
||||
"recovery_timeout": self.recovery_timeout,
|
||||
}
|
||||
|
||||
|
||||
class CircuitOpenError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CircuitBreakerRegistry:
|
||||
"""Registry of named circuit breakers for external services."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._breakers: dict[str, CircuitBreaker] = {}
|
||||
|
||||
def get(
|
||||
self,
|
||||
name: str,
|
||||
failure_threshold: int = 5,
|
||||
recovery_timeout: float = 60.0,
|
||||
) -> CircuitBreaker:
|
||||
if name not in self._breakers:
|
||||
self._breakers[name] = CircuitBreaker(
|
||||
name=name,
|
||||
failure_threshold=failure_threshold,
|
||||
recovery_timeout=recovery_timeout,
|
||||
)
|
||||
return self._breakers[name]
|
||||
|
||||
def all_states(self) -> dict[str, dict]:
|
||||
return {name: cb.to_dict() for name, cb in self._breakers.items()}
|
||||
|
||||
|
||||
registry = CircuitBreakerRegistry()
|
||||
100
salesflow-saas/backend/pyproject.toml
Normal file
100
salesflow-saas/backend/pyproject.toml
Normal file
@ -0,0 +1,100 @@
|
||||
[project]
|
||||
name = "dealix-api"
|
||||
version = "0.1.0"
|
||||
description = "Dealix — Sovereign Deal, Growth & Commitment OS"
|
||||
requires-python = ">=3.12,<3.13"
|
||||
readme = "../README.md"
|
||||
license = { text = "Proprietary" }
|
||||
|
||||
dependencies = [
|
||||
# Core framework
|
||||
"fastapi>=0.115.0,<0.116.0",
|
||||
"uvicorn[standard]>=0.32.0,<0.33.0",
|
||||
"pydantic>=2.10.0,<3.0.0",
|
||||
"pydantic-settings>=2.10.1,<3.0.0",
|
||||
"pydantic-extra-types[phonenumbers]>=2.0.0",
|
||||
"python-multipart==0.0.12",
|
||||
|
||||
# Database
|
||||
"sqlalchemy==2.0.36",
|
||||
"asyncpg==0.30.0",
|
||||
"psycopg2-binary==2.9.10",
|
||||
"alembic==1.14.0",
|
||||
"pgvector==0.3.6",
|
||||
|
||||
# AI / LLM Providers
|
||||
"litellm>=1.74.0,<2",
|
||||
"instructor>=1.14.0",
|
||||
"groq==0.12.0",
|
||||
"openai>=2.8.0,<3",
|
||||
|
||||
# Async tasks
|
||||
"celery>=5.4.0,<6",
|
||||
"redis>=5.2.0,<6",
|
||||
|
||||
# Auth & Security
|
||||
"pyjwt>=2.10.0",
|
||||
"passlib[bcrypt]>=1.7.4",
|
||||
"bcrypt>=4.2.0",
|
||||
"python-jose>=3.3.0",
|
||||
"slowapi>=0.1.9",
|
||||
|
||||
# Communication
|
||||
"httpx>=0.28.1,<0.29.0",
|
||||
|
||||
# Arabic NLP
|
||||
"pyarabic>=0.6.0",
|
||||
|
||||
# PDF + docs
|
||||
"weasyprint>=60.0",
|
||||
|
||||
# Observability
|
||||
"sentry-sdk>=2.0.0",
|
||||
"prometheus-client>=0.21.0",
|
||||
"prometheus-fastapi-instrumentator>=7.0.0",
|
||||
"structlog>=24.0.0",
|
||||
"opentelemetry-api>=1.27.0,<2",
|
||||
"opentelemetry-sdk>=1.27.0,<2",
|
||||
"opentelemetry-instrumentation-fastapi>=0.48b0",
|
||||
"opentelemetry-instrumentation-sqlalchemy>=0.48b0",
|
||||
|
||||
# Utils
|
||||
"tenacity>=9.0.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest==8.3.4",
|
||||
"pytest-asyncio==0.24.0",
|
||||
"pytest-cov==5.0.0",
|
||||
"aiosqlite==0.20.0",
|
||||
"factory-boy>=3.3.0",
|
||||
"ruff>=0.7.0",
|
||||
"mypy>=1.13.0",
|
||||
"testcontainers>=4.8.0",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = ["ignore::DeprecationWarning"]
|
||||
markers = [
|
||||
"launch: pre-release surface & scenario checks",
|
||||
"slow: tests that hit external IO or long LangGraph paths",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "W", "F", "I", "B", "C4", "UP"]
|
||||
ignore = ["E501"] # line too long handled by formatter
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
ignore_missing_imports = true
|
||||
warn_return_any = false
|
||||
warn_unused_configs = true
|
||||
9
salesflow-saas/backend/railway.toml
Normal file
9
salesflow-saas/backend/railway.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[build]
|
||||
dockerfilePath = "Dockerfile"
|
||||
|
||||
[deploy]
|
||||
healthcheckPath = "/api/v1/health"
|
||||
healthcheckTimeout = 120
|
||||
startCommand = "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000} --workers 2"
|
||||
restartPolicyType = "ON_FAILURE"
|
||||
restartPolicyMaxRetries = 3
|
||||
@ -1,4 +1,5 @@
|
||||
# Dev / CI — not required in production images
|
||||
pytest>=8.0.0
|
||||
pytest-asyncio>=0.24.0
|
||||
aiosqlite>=0.20.0
|
||||
pytest==8.3.4
|
||||
pytest-asyncio==0.24.0
|
||||
aiosqlite==0.20.0
|
||||
httpx>=0.28.1,<0.29.0
|
||||
|
||||
@ -62,9 +62,9 @@ prometheus-fastapi-instrumentator>=7.0.0 # Prometheus metrics
|
||||
structlog>=24.0.0 # Structured JSON logging with tenant context
|
||||
|
||||
# === Testing ===
|
||||
pytest>=8.0.0
|
||||
pytest-asyncio>=0.23.0 # Async test support
|
||||
pytest-cov>=5.0.0 # Coverage reporting
|
||||
pytest==8.3.4
|
||||
pytest-asyncio==0.24.0 # Async test support — pinned for CI stability
|
||||
pytest-cov==5.0.0 # Coverage reporting — pinned for stability
|
||||
factory-boy>=3.3.0 # Test data factories for SQLAlchemy models
|
||||
|
||||
# === Forecasting ===
|
||||
|
||||
115
salesflow-saas/backend/scripts/k6_smoke_test.js
Normal file
115
salesflow-saas/backend/scripts/k6_smoke_test.js
Normal file
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Dealix Production Smoke Test — k6 load test
|
||||
*
|
||||
* Usage:
|
||||
* k6 run --env API_BASE=https://api.dealix.me scripts/k6_smoke_test.js
|
||||
* k6 run --env API_BASE=http://localhost:8001 --env API_KEY=your-key scripts/k6_smoke_test.js
|
||||
*
|
||||
* Thresholds:
|
||||
* - p95 response time < 500ms
|
||||
* - error rate < 1%
|
||||
* - http_req_duration p99 < 2000ms
|
||||
*/
|
||||
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
|
||||
const errorRate = new Rate('errors');
|
||||
const healthDuration = new Trend('health_duration');
|
||||
const pricingDuration = new Trend('pricing_duration');
|
||||
|
||||
const BASE = __ENV.API_BASE || 'http://localhost:8001';
|
||||
const API_KEY = __ENV.API_KEY || '';
|
||||
|
||||
const headers = API_KEY ? { 'Authorization': `Bearer ${API_KEY}` } : {};
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '10s', target: 5 }, // ramp up
|
||||
{ duration: '30s', target: 10 }, // steady
|
||||
{ duration: '10s', target: 20 }, // peak
|
||||
{ duration: '10s', target: 0 }, // ramp down
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500', 'p(99)<2000'],
|
||||
errors: ['rate<0.01'],
|
||||
health_duration: ['p(95)<200'],
|
||||
pricing_duration: ['p(95)<300'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
// 1. Health check (public, no auth)
|
||||
const healthRes = http.get(`${BASE}/api/v1/health`);
|
||||
healthDuration.add(healthRes.timings.duration);
|
||||
check(healthRes, {
|
||||
'health 200': (r) => r.status === 200,
|
||||
'health has status': (r) => JSON.parse(r.body).status !== undefined,
|
||||
}) || errorRate.add(1);
|
||||
|
||||
// 2. Pricing plans (public, no auth)
|
||||
const pricingRes = http.get(`${BASE}/api/v1/pricing/plans`);
|
||||
pricingDuration.add(pricingRes.timings.duration);
|
||||
check(pricingRes, {
|
||||
'pricing 200': (r) => r.status === 200,
|
||||
'pricing has plans': (r) => JSON.parse(r.body).plans.length >= 3,
|
||||
'pricing SAR': (r) => JSON.parse(r.body).currency === 'SAR',
|
||||
}) || errorRate.add(1);
|
||||
|
||||
// 3. Pricing single plan
|
||||
const planRes = http.get(`${BASE}/api/v1/pricing/plans/growth`);
|
||||
check(planRes, {
|
||||
'plan 200': (r) => r.status === 200,
|
||||
'plan is growth': (r) => JSON.parse(r.body).plan.id === 'growth',
|
||||
}) || errorRate.add(1);
|
||||
|
||||
// 4. Deep health (with auth if configured)
|
||||
if (API_KEY) {
|
||||
const deepRes = http.get(`${BASE}/api/v1/health/deep`, { headers });
|
||||
check(deepRes, {
|
||||
'deep health 200': (r) => r.status === 200,
|
||||
}) || errorRate.add(1);
|
||||
|
||||
// 5. Admin stats
|
||||
const statsRes = http.get(`${BASE}/api/v1/admin/dlq/queues`, { headers });
|
||||
check(statsRes, {
|
||||
'dlq queues 200': (r) => r.status === 200,
|
||||
}) || errorRate.add(1);
|
||||
|
||||
// 6. Circuit breaker status
|
||||
const cbRes = http.get(`${BASE}/api/v1/admin/circuit-breakers`, { headers });
|
||||
check(cbRes, {
|
||||
'circuit breakers 200': (r) => r.status === 200,
|
||||
}) || errorRate.add(1);
|
||||
|
||||
// 7. Approval stats
|
||||
const approvalRes = http.get(`${BASE}/api/v1/approval-center/stats`, { headers });
|
||||
check(approvalRes, {
|
||||
'approval stats 200': (r) => r.status === 200,
|
||||
}) || errorRate.add(1);
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
export function handleSummary(data) {
|
||||
const p95 = data.metrics.http_req_duration.values['p(95)'];
|
||||
const p99 = data.metrics.http_req_duration.values['p(99)'];
|
||||
const errRate = data.metrics.errors ? data.metrics.errors.values.rate : 0;
|
||||
const totalReqs = data.metrics.http_reqs.values.count;
|
||||
|
||||
const summary = {
|
||||
timestamp: new Date().toISOString(),
|
||||
total_requests: totalReqs,
|
||||
p95_ms: Math.round(p95),
|
||||
p99_ms: Math.round(p99),
|
||||
error_rate: errRate,
|
||||
pass: p95 < 500 && errRate < 0.01,
|
||||
};
|
||||
|
||||
return {
|
||||
'stdout': JSON.stringify(summary, null, 2) + '\n',
|
||||
'k6_results.json': JSON.stringify(summary),
|
||||
};
|
||||
}
|
||||
111
salesflow-saas/backend/tests/security/test_rls_fuzz.py
Normal file
111
salesflow-saas/backend/tests/security/test_rls_fuzz.py
Normal file
@ -0,0 +1,111 @@
|
||||
"""V002 — Runtime RLS Fuzz Test.
|
||||
|
||||
10,000 cross-tenant queries. Tenant A's session attempts to read rows from
|
||||
Tenant B's context. Expected: zero rows returned from B's data.
|
||||
|
||||
Any violation = P0 incident. This test is added to nightly CI.
|
||||
|
||||
Run:
|
||||
pytest backend/tests/security/test_rls_fuzz.py -v
|
||||
pytest backend/tests/security/test_rls_fuzz.py::test_cross_tenant_isolation_fuzz -v --count=10000
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from typing import Iterator
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import async_session_factory
|
||||
from app.database_rls import set_tenant_context
|
||||
|
||||
FUZZ_ITERATIONS = int(os.getenv("RLS_FUZZ_ITERATIONS", "10000"))
|
||||
|
||||
TENANT_SCOPED_TABLES = [
|
||||
"deals",
|
||||
"leads",
|
||||
"approval_requests",
|
||||
"evidence_packs",
|
||||
"contradictions",
|
||||
"compliance_controls",
|
||||
"ai_conversations",
|
||||
"audit_logs",
|
||||
"integration_sync_states",
|
||||
"strategic_deals",
|
||||
"durable_checkpoints",
|
||||
"idempotency_keys",
|
||||
]
|
||||
|
||||
|
||||
async def _seed_two_tenants(session: AsyncSession) -> tuple[uuid.UUID, uuid.UUID]:
|
||||
"""Create two tenant rows in each table for isolation testing."""
|
||||
tenant_a = uuid.uuid4()
|
||||
tenant_b = uuid.uuid4()
|
||||
return tenant_a, tenant_b
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cross_tenant_isolation_fuzz() -> None:
|
||||
"""Fuzz test: iterate switching tenant context and confirm zero bleed."""
|
||||
async with async_session_factory() as session:
|
||||
tenant_a, tenant_b = await _seed_two_tenants(session)
|
||||
|
||||
violations: list[tuple[str, str, int]] = []
|
||||
|
||||
for i in range(FUZZ_ITERATIONS):
|
||||
# Alternate contexts
|
||||
current = tenant_a if i % 2 == 0 else tenant_b
|
||||
other = tenant_b if i % 2 == 0 else tenant_a
|
||||
|
||||
await set_tenant_context(session, str(current))
|
||||
|
||||
for table in TENANT_SCOPED_TABLES:
|
||||
result = await session.execute(
|
||||
text(f"SELECT COUNT(*) FROM {table} WHERE tenant_id = :other"),
|
||||
{"other": str(other)},
|
||||
)
|
||||
leaked = result.scalar_one()
|
||||
if leaked and leaked > 0:
|
||||
violations.append((table, str(current), leaked))
|
||||
|
||||
assert not violations, (
|
||||
f"RLS FUZZ FAILURE — {len(violations)} cross-tenant leaks detected: "
|
||||
f"{violations[:10]}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rls_policies_enabled_on_all_tables() -> None:
|
||||
"""Every tenant-scoped table must have RLS enabled."""
|
||||
async with async_session_factory() as session:
|
||||
result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT tablename, rowsecurity
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename = ANY(:tables)
|
||||
"""
|
||||
),
|
||||
{"tables": TENANT_SCOPED_TABLES},
|
||||
)
|
||||
unprotected = [row[0] for row in result if not row[1]]
|
||||
assert not unprotected, f"RLS disabled on: {unprotected}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rls_default_deny_with_no_tenant_context() -> None:
|
||||
"""Queries without tenant context must return zero rows."""
|
||||
async with async_session_factory() as session:
|
||||
# Intentionally NOT calling set_tenant_context
|
||||
for table in TENANT_SCOPED_TABLES:
|
||||
result = await session.execute(text(f"SELECT COUNT(*) FROM {table}"))
|
||||
count = result.scalar_one()
|
||||
assert count == 0, (
|
||||
f"RLS default-deny FAILURE — {table} returned {count} rows "
|
||||
f"without tenant context"
|
||||
)
|
||||
325
salesflow-saas/backend/tests/test_d0_launch_hardening.py
Normal file
325
salesflow-saas/backend/tests/test_d0_launch_hardening.py
Normal file
@ -0,0 +1,325 @@
|
||||
"""Tests for D0 Launch Hardening modules — DLQ, PostHog, Circuit Breaker, Pricing."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import pytest
|
||||
|
||||
|
||||
# ── DLQ Tests ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class FakeRedis:
|
||||
"""Minimal async Redis mock for DLQ tests."""
|
||||
|
||||
def __init__(self):
|
||||
self._data: dict[str, list[str]] = {}
|
||||
|
||||
async def rpush(self, key: str, value: str) -> int:
|
||||
self._data.setdefault(key, []).append(value)
|
||||
return len(self._data[key])
|
||||
|
||||
async def lpop(self, key: str) -> str | None:
|
||||
lst = self._data.get(key, [])
|
||||
return lst.pop(0) if lst else None
|
||||
|
||||
async def lrange(self, key: str, start: int, end: int) -> list[str]:
|
||||
lst = self._data.get(key, [])
|
||||
return lst[start : end + 1]
|
||||
|
||||
async def llen(self, key: str) -> int:
|
||||
return len(self._data.get(key, []))
|
||||
|
||||
async def delete(self, key: str) -> int:
|
||||
removed = len(self._data.pop(key, []))
|
||||
return removed
|
||||
|
||||
async def scan(self, cursor: int, match: str = "*", count: int = 100):
|
||||
keys = [k for k in self._data if k.startswith(match.replace("*", ""))]
|
||||
return (0, keys)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_redis():
|
||||
return FakeRedis()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dlq_push_and_peek(fake_redis):
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=fake_redis)
|
||||
entry_id = await dlq.push("webhooks", {"url": "/test"}, "timeout error")
|
||||
assert entry_id is not None
|
||||
|
||||
entries = await dlq.peek("webhooks")
|
||||
assert len(entries) == 1
|
||||
assert entries[0].queue == "webhooks"
|
||||
assert entries[0].error == "timeout error"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dlq_depth(fake_redis):
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=fake_redis)
|
||||
await dlq.push("webhooks", {"a": 1}, "err1")
|
||||
await dlq.push("webhooks", {"b": 2}, "err2")
|
||||
assert await dlq.depth("webhooks") == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dlq_drain_success(fake_redis):
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=fake_redis)
|
||||
await dlq.push("webhooks", {"x": 1}, "err")
|
||||
|
||||
async def handler(payload):
|
||||
pass # success
|
||||
|
||||
result = await dlq.drain("webhooks", handler)
|
||||
assert result["processed"] == 1
|
||||
assert result["succeeded"] == 1
|
||||
assert result["re_queued"] == 0
|
||||
assert await dlq.depth("webhooks") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dlq_drain_retry(fake_redis):
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=fake_redis)
|
||||
await dlq.push("webhooks", {"x": 1}, "err", max_retries=3)
|
||||
|
||||
async def handler(payload):
|
||||
raise RuntimeError("still broken")
|
||||
|
||||
result = await dlq.drain("webhooks", handler, batch_size=1)
|
||||
assert result["processed"] == 1
|
||||
assert result["re_queued"] == 1
|
||||
assert await dlq.depth("webhooks") == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dlq_drain_dead(fake_redis):
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=fake_redis)
|
||||
await dlq.push("webhooks", {"x": 1}, "err", attempt=4, max_retries=5)
|
||||
|
||||
async def handler(payload):
|
||||
raise RuntimeError("permanent failure")
|
||||
|
||||
result = await dlq.drain("webhooks", handler)
|
||||
assert result["dead"] == 1
|
||||
assert await dlq.depth("webhooks") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dlq_purge(fake_redis):
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=fake_redis)
|
||||
await dlq.push("old", {"x": 1}, "err")
|
||||
await dlq.push("old", {"x": 2}, "err")
|
||||
purged = await dlq.purge("old")
|
||||
assert purged == 2
|
||||
assert await dlq.depth("old") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dlq_all_queues(fake_redis):
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=fake_redis)
|
||||
await dlq.push("webhooks", {}, "e")
|
||||
await dlq.push("payments", {}, "e")
|
||||
await dlq.push("payments", {}, "e")
|
||||
queues = await dlq.all_queues()
|
||||
assert queues.get("webhooks") == 1
|
||||
assert queues.get("payments") == 2
|
||||
|
||||
|
||||
# ── PostHog Tests ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_posthog_disabled_without_key():
|
||||
from app.services.posthog_client import PostHogClient
|
||||
|
||||
client = PostHogClient(api_key="")
|
||||
assert not client._enabled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_posthog_skip_when_disabled():
|
||||
from app.services.posthog_client import PostHogClient, FunnelEvent
|
||||
|
||||
client = PostHogClient(api_key="")
|
||||
result = await client.capture("user-1", FunnelEvent.LEAD_CAPTURED)
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_posthog_enabled_with_key():
|
||||
from app.services.posthog_client import PostHogClient
|
||||
|
||||
client = PostHogClient(api_key="phc_test123")
|
||||
assert client._enabled
|
||||
|
||||
|
||||
def test_funnel_events_values():
|
||||
from app.services.posthog_client import FunnelEvent
|
||||
|
||||
assert FunnelEvent.LANDING_VIEW.value == "landing_view"
|
||||
assert FunnelEvent.DEAL_WON.value == "deal_won"
|
||||
assert FunnelEvent.PAYMENT_SUCCEEDED.value == "payment_succeeded"
|
||||
assert len(FunnelEvent) >= 10
|
||||
|
||||
|
||||
# ── Circuit Breaker Tests ────────────────────────────────────────
|
||||
|
||||
|
||||
def test_circuit_breaker_starts_closed():
|
||||
from app.utils.circuit_breaker import CircuitBreaker
|
||||
|
||||
cb = CircuitBreaker("test")
|
||||
assert cb.state.value == "closed"
|
||||
|
||||
|
||||
def test_circuit_breaker_opens_on_threshold():
|
||||
from app.utils.circuit_breaker import CircuitBreaker
|
||||
|
||||
cb = CircuitBreaker("test", failure_threshold=3)
|
||||
cb.record_failure()
|
||||
cb.record_failure()
|
||||
assert cb.state.value == "closed"
|
||||
cb.record_failure()
|
||||
assert cb.state.value == "open"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_circuit_breaker_fails_fast_when_open():
|
||||
from app.utils.circuit_breaker import CircuitBreaker, CircuitOpenError
|
||||
|
||||
cb = CircuitBreaker("test", failure_threshold=1)
|
||||
cb.record_failure()
|
||||
assert cb.state.value == "open"
|
||||
|
||||
async def dummy():
|
||||
return "ok"
|
||||
|
||||
with pytest.raises(CircuitOpenError):
|
||||
await cb.call(dummy)
|
||||
|
||||
|
||||
def test_circuit_breaker_resets_on_success():
|
||||
from app.utils.circuit_breaker import CircuitBreaker
|
||||
|
||||
cb = CircuitBreaker("test", failure_threshold=3)
|
||||
cb.record_failure()
|
||||
cb.record_failure()
|
||||
cb.record_success()
|
||||
assert cb._failure_count == 0
|
||||
assert cb.state.value == "closed"
|
||||
|
||||
|
||||
def test_circuit_breaker_registry():
|
||||
from app.utils.circuit_breaker import CircuitBreakerRegistry
|
||||
|
||||
reg = CircuitBreakerRegistry()
|
||||
cb1 = reg.get("hubspot")
|
||||
cb2 = reg.get("hubspot")
|
||||
assert cb1 is cb2
|
||||
cb3 = reg.get("calendly")
|
||||
assert cb3 is not cb1
|
||||
states = reg.all_states()
|
||||
assert "hubspot" in states
|
||||
assert "calendly" in states
|
||||
|
||||
|
||||
# ── Pricing Tests ────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pricing_plans_endpoint():
|
||||
from fastapi.testclient import TestClient
|
||||
from app.api.v1.pricing import router
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.get("/pricing/plans")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "plans" in data
|
||||
assert len(data["plans"]) >= 3
|
||||
assert data["currency"] == "SAR"
|
||||
|
||||
starter = next(p for p in data["plans"] if p["id"] == "starter")
|
||||
assert starter["price_sar"] == 990
|
||||
assert "features_ar" in starter
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pricing_plan_by_id():
|
||||
from fastapi.testclient import TestClient
|
||||
from app.api.v1.pricing import router
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.get("/pricing/plans/growth")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["plan"]["id"] == "growth"
|
||||
|
||||
resp404 = client.get("/pricing/plans/nonexistent")
|
||||
assert resp404.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_checkout_no_moyasar_key():
|
||||
from fastapi.testclient import TestClient
|
||||
from app.api.v1.pricing import router
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.post(
|
||||
"/pricing/checkout",
|
||||
json={
|
||||
"plan_id": "starter",
|
||||
"customer_name": "Test User",
|
||||
"customer_email": "test@example.com",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "checkout_unavailable"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_checkout_enterprise_contact_sales():
|
||||
from fastapi.testclient import TestClient
|
||||
from app.api.v1.pricing import router
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.post(
|
||||
"/pricing/checkout",
|
||||
json={
|
||||
"plan_id": "enterprise",
|
||||
"customer_name": "Corp",
|
||||
"customer_email": "ceo@corp.sa",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "contact_sales"
|
||||
172
salesflow-saas/backend/tests/test_dlq_fault_injection.py
Normal file
172
salesflow-saas/backend/tests/test_dlq_fault_injection.py
Normal file
@ -0,0 +1,172 @@
|
||||
"""DLQ Fault Injection Tests — verify failure paths work correctly.
|
||||
|
||||
These tests simulate real failure scenarios:
|
||||
1. Webhook handler crashes → entry lands in DLQ
|
||||
2. DLQ drain retries and succeeds on second attempt
|
||||
3. DLQ drain exhausts retries → entry marked dead
|
||||
4. Circuit breaker opens after repeated failures
|
||||
5. Circuit breaker recovers after timeout
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
|
||||
|
||||
class FakeRedis:
|
||||
def __init__(self):
|
||||
self._data: dict[str, list[str]] = {}
|
||||
|
||||
async def rpush(self, key, value):
|
||||
self._data.setdefault(key, []).append(value)
|
||||
return len(self._data[key])
|
||||
|
||||
async def lpop(self, key):
|
||||
lst = self._data.get(key, [])
|
||||
return lst.pop(0) if lst else None
|
||||
|
||||
async def lrange(self, key, start, end):
|
||||
return self._data.get(key, [])[start : end + 1]
|
||||
|
||||
async def llen(self, key):
|
||||
return len(self._data.get(key, []))
|
||||
|
||||
async def delete(self, key):
|
||||
return len(self._data.pop(key, []))
|
||||
|
||||
async def scan(self, cursor, match="*", count=100):
|
||||
keys = [k for k in self._data if k.startswith(match.replace("*", ""))]
|
||||
return (0, keys)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_crash_lands_in_dlq():
|
||||
"""Simulate: Moyasar webhook handler throws → payload goes to DLQ."""
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=FakeRedis())
|
||||
webhook_payload = {
|
||||
"type": "payment_paid",
|
||||
"data": {"id": "pay_test_123", "amount": 99000},
|
||||
}
|
||||
|
||||
try:
|
||||
raise ConnectionError("DB connection lost during webhook processing")
|
||||
except ConnectionError as exc:
|
||||
await dlq.push("moyasar_webhooks", webhook_payload, str(exc))
|
||||
|
||||
assert await dlq.depth("moyasar_webhooks") == 1
|
||||
entries = await dlq.peek("moyasar_webhooks")
|
||||
assert entries[0].payload["data"]["id"] == "pay_test_123"
|
||||
assert "DB connection lost" in entries[0].error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dlq_drain_succeeds_on_second_attempt():
|
||||
"""Simulate: first retry fails, second succeeds."""
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=FakeRedis())
|
||||
await dlq.push("hubspot_sync", {"lead_id": "abc"}, "timeout", max_retries=5)
|
||||
|
||||
call_count = 0
|
||||
|
||||
async def flaky_handler(payload):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise TimeoutError("HubSpot timeout")
|
||||
|
||||
# First drain: fails, re-queues
|
||||
r1 = await dlq.drain("hubspot_sync", flaky_handler, batch_size=1)
|
||||
assert r1["re_queued"] == 1
|
||||
|
||||
# Second drain: succeeds
|
||||
r2 = await dlq.drain("hubspot_sync", flaky_handler, batch_size=1)
|
||||
assert r2["succeeded"] == 1
|
||||
assert await dlq.depth("hubspot_sync") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dlq_exhausts_retries_marks_dead():
|
||||
"""Simulate: permanent failure exhausts all retries."""
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
dlq = DeadLetterQueue(redis_client=FakeRedis())
|
||||
await dlq.push("calendly_webhooks", {"event": "booked"}, "err", attempt=4, max_retries=5)
|
||||
|
||||
async def always_fail(payload):
|
||||
raise RuntimeError("Calendly API permanently broken")
|
||||
|
||||
result = await dlq.drain("calendly_webhooks", always_fail, batch_size=1)
|
||||
assert result["dead"] == 1
|
||||
assert result["re_queued"] == 0
|
||||
assert await dlq.depth("calendly_webhooks") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_circuit_breaker_opens_and_recovers():
|
||||
"""Simulate: HubSpot fails 3x → circuit opens → recovers after timeout."""
|
||||
from app.utils.circuit_breaker import CircuitBreaker, CircuitOpenError
|
||||
|
||||
cb = CircuitBreaker("hubspot_api", failure_threshold=3, recovery_timeout=0.1)
|
||||
|
||||
# 3 failures → opens
|
||||
for _ in range(3):
|
||||
cb.record_failure()
|
||||
assert cb.state.value == "open"
|
||||
|
||||
# Fails fast when open
|
||||
async def hubspot_call():
|
||||
return {"contacts": []}
|
||||
|
||||
with pytest.raises(CircuitOpenError):
|
||||
await cb.call(hubspot_call)
|
||||
|
||||
# Wait for recovery timeout
|
||||
time.sleep(0.15)
|
||||
|
||||
# Should be half-open now → probe succeeds → closes
|
||||
result = await cb.call(hubspot_call)
|
||||
assert result == {"contacts": []}
|
||||
assert cb.state.value == "closed"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_circuit_breaker_stays_open_on_probe_failure():
|
||||
"""Simulate: probe call also fails → stays open."""
|
||||
from app.utils.circuit_breaker import CircuitBreaker
|
||||
|
||||
cb = CircuitBreaker("moyasar_api", failure_threshold=2, recovery_timeout=0.1)
|
||||
cb.record_failure()
|
||||
cb.record_failure()
|
||||
assert cb.state.value == "open"
|
||||
|
||||
time.sleep(0.15) # allow half-open
|
||||
|
||||
async def still_broken():
|
||||
raise ConnectionError("Moyasar still down")
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
await cb.call(still_broken)
|
||||
|
||||
assert cb.state.value == "open"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_queue_dlq_isolation():
|
||||
"""Verify different queues don't interfere with each other."""
|
||||
from app.services.dlq import DeadLetterQueue
|
||||
|
||||
redis = FakeRedis()
|
||||
dlq = DeadLetterQueue(redis_client=redis)
|
||||
|
||||
await dlq.push("webhooks", {"src": "webhook"}, "err1")
|
||||
await dlq.push("webhooks", {"src": "webhook2"}, "err2")
|
||||
await dlq.push("payments", {"src": "payment"}, "err3")
|
||||
|
||||
assert await dlq.depth("webhooks") == 2
|
||||
assert await dlq.depth("payments") == 1
|
||||
|
||||
await dlq.purge("webhooks")
|
||||
assert await dlq.depth("webhooks") == 0
|
||||
assert await dlq.depth("payments") == 1 # untouched
|
||||
109
salesflow-saas/commercial/claims_registry.yaml
Normal file
109
salesflow-saas/commercial/claims_registry.yaml
Normal file
@ -0,0 +1,109 @@
|
||||
# claims_registry.yaml — Dealix Commercial Claims Registry
|
||||
# Rule: No marketing material may state a capability unless it exists here with status=approved.
|
||||
# Last updated: 2026-04-17
|
||||
|
||||
claims:
|
||||
# ── APPROVED (backed by runtime evidence) ──────────────
|
||||
- id: golden_path_works
|
||||
claim_en: "End-to-end partner workflow with structured outputs, approval enforcement, and evidence packs"
|
||||
claim_ar: "مسار شراكة كامل من البداية للنهاية مع مخرجات مهيكلة وموافقات إلزامية وحزم أدلة"
|
||||
status: approved
|
||||
evidence: "POST /api/v1/golden-path/run — creates dossier, economics, approval, evidence"
|
||||
disclaimer_required: false
|
||||
|
||||
- id: evidence_packs_sha256
|
||||
claim_en: "Tamper-evident evidence packs with SHA256 hash verification"
|
||||
claim_ar: "حزم أدلة مقاومة للتلاعب مع تحقق SHA256"
|
||||
status: approved
|
||||
evidence: "backend/app/services/evidence_pack_service.py — hash computed and stored"
|
||||
|
||||
- id: executive_room_live
|
||||
claim_en: "Real-time Executive Room aggregating live data from 7 sources"
|
||||
claim_ar: "غرفة قيادة تنفيذية لحظية تجمع بيانات من 7 مصادر"
|
||||
status: approved
|
||||
evidence: "GET /api/v1/executive-room/snapshot — queries Deal, Approval, Connector, Compliance, Contradiction, StrategicDeal, EvidencePack tables"
|
||||
|
||||
- id: approval_sla
|
||||
claim_en: "Approval Center with SLA tracking and escalation"
|
||||
claim_ar: "مركز موافقات مع تتبع SLA وتصعيد"
|
||||
status: approved
|
||||
evidence: "sla_escalation_alerts.py — escalation levels 0-3"
|
||||
|
||||
- id: arabic_first
|
||||
claim_en: "Arabic-first UI with full RTL support"
|
||||
claim_ar: "واجهة عربية أولاً مع دعم RTL كامل"
|
||||
status: approved
|
||||
evidence: "9 frontend components with Arabic labels, RTL layout, i18n"
|
||||
|
||||
- id: pdpl_consent_checks
|
||||
claim_en: "PDPL consent verification before outbound messaging"
|
||||
claim_ar: "التحقق من موافقة PDPL قبل الرسائل الصادرة"
|
||||
status: approved
|
||||
evidence: "services/pdpl/consent_manager.py — check before send"
|
||||
|
||||
- id: trust_enforcement
|
||||
claim_en: "Class B actions blocked without correlation_id traceability"
|
||||
claim_ar: "الإجراءات الحساسة محظورة بدون معرف تتبع"
|
||||
status: approved
|
||||
evidence: "openclaw/approval_bridge.py — missing_correlation_id check"
|
||||
|
||||
- id: seventeen_schemas
|
||||
claim_en: "17 structured output schemas with Provenance (trace_id, confidence, freshness)"
|
||||
claim_ar: "17 مخطط مخرج مهيكل مع بيانات المصدر والثقة"
|
||||
status: approved
|
||||
evidence: "schemas/structured_outputs.py + services/structured_output_producers.py"
|
||||
|
||||
# ── RESTRICTED (partially true, needs qualifier) ──────
|
||||
- id: rls_isolation
|
||||
claim_en: "Database-level tenant isolation via PostgreSQL RLS"
|
||||
claim_ar: "عزل المستأجرين على مستوى قاعدة البيانات عبر RLS"
|
||||
status: restricted
|
||||
qualifier: "Migration exists; production deployment pending. Say 'RLS-ready architecture' not 'RLS-enforced'."
|
||||
evidence: "alembic/versions/20260417_0002_add_rls.py"
|
||||
|
||||
- id: durable_execution
|
||||
claim_en: "Crash-safe durable workflows with persistent checkpoints"
|
||||
claim_ar: "تنفيذ متين مع نقاط حفظ دائمة"
|
||||
status: restricted
|
||||
qualifier: "Checkpointer exists; not yet integrated into golden path. Say 'durable execution architecture' not 'crash-proof workflows'."
|
||||
|
||||
# ── FORBIDDEN (never claim) ────────────────────────────
|
||||
- id: soc2_compliant
|
||||
claim_en: "SOC 2 Type II compliant"
|
||||
status: forbidden
|
||||
reason: "No auditor report. Can only say 'SOC 2 readiness in progress'."
|
||||
|
||||
- id: ai_perfect
|
||||
claim_en: "100% AI accuracy"
|
||||
status: forbidden
|
||||
reason: "No ML system achieves 100% accuracy."
|
||||
|
||||
- id: better_than_salesforce
|
||||
claim_en: "Better than Salesforce"
|
||||
status: forbidden
|
||||
reason: "Different positioning, not direct comparison. Say 'complementary' or 'specialized for GCC'."
|
||||
|
||||
- id: temporal_production
|
||||
claim_en: "Temporal in production"
|
||||
status: forbidden
|
||||
reason: "Temporal is Watch tier. No code exists."
|
||||
|
||||
- id: opa_production
|
||||
claim_en: "OPA policy engine in production"
|
||||
status: forbidden
|
||||
reason: "OPA is Watch tier. No code exists."
|
||||
|
||||
- id: full_autonomy
|
||||
claim_en: "Fully autonomous AI decisions"
|
||||
status: forbidden
|
||||
reason: "HITL is mandatory for Class B actions. Never claim full autonomy."
|
||||
|
||||
- id: enterprise_grade
|
||||
claim_en: "Enterprise-grade"
|
||||
status: forbidden
|
||||
reason: "No SOC 2, no pentest, no production deployment yet. Too early."
|
||||
|
||||
- id: ten_x_revenue
|
||||
claim_en: "10x revenue increase"
|
||||
status: forbidden
|
||||
reason: "No customer data supports this claim."
|
||||
213
salesflow-saas/docs/FULL_NEXT_STEP_AND_STACK_EXPANSION_AR.md
Normal file
213
salesflow-saas/docs/FULL_NEXT_STEP_AND_STACK_EXPANSION_AR.md
Normal file
@ -0,0 +1,213 @@
|
||||
# خطة التنفيذ الكاملة — ما بعد الإغلاق
|
||||
|
||||
> **الهدف**: تحويل النظام من "مغلق نظرياً" إلى "مثبت تشغيلياً وقابل للبيع"
|
||||
> **القاعدة**: لا تبيع إلا ما يشتغل. لا تدّعي إلا ما هو حي.
|
||||
|
||||
---
|
||||
|
||||
## البوابات الثمانية — معيار "كل شيء تمام"
|
||||
|
||||
| # | البوابة | الحالة |
|
||||
|---|---------|--------|
|
||||
| 1 | **Truth** — مصدر واحد للحقيقة، لا overclaim | **PASS** |
|
||||
| 2 | **Contract** — كل output حرج schema-bound | **PARTIAL** — 3/17 schemas مستخدمة (golden path) |
|
||||
| 3 | **Trust** — Class B يفشل بدون correlation_id | **PASS** — مفعّل في approval_bridge |
|
||||
| 4 | **Durable** — مسار واحد resumable end-to-end | **PARTIAL** — golden path حي لكن بدون persistence |
|
||||
| 5 | **Executive** — Executive Room تجيب 5 أسئلة | **PASS** — weekly-pack endpoint حي |
|
||||
| 6 | **Release** — CI يحرس الإطلاق | **PARTIAL** — architecture_brief في CI |
|
||||
| 7 | **Saudi** — workflow حساس واحد مربوط | **TARGET** |
|
||||
| 8 | **Commercial** — التسويق يطابق الواقع | **PASS** — marketer hub مع forbidden claims |
|
||||
|
||||
---
|
||||
|
||||
## ما أُنجز (الوضع الحالي)
|
||||
|
||||
### حي فعلاً
|
||||
- Golden Path: `POST /api/v1/golden-path/run` → PartnerDossier → EconomicsModel → ApprovalPacket → EvidencePack
|
||||
- Trust enforcement: Class B actions تفشل بدون correlation_id
|
||||
- Auto evidence: deal close → auto evidence pack assembly
|
||||
- Executive Room: weekly-pack contract → ExecWeeklyPack schema
|
||||
- 9/9 frontend مربوطة بـ APIs حقيقية
|
||||
- 8/8 backend APIs تقرأ من DB حقيقية
|
||||
- Architecture Brief: 40/40 PASS
|
||||
|
||||
### ناقص
|
||||
- 14/17 structured output schemas غير مستخدمة
|
||||
- Connector live health probes
|
||||
- Saudi compliance live validation
|
||||
- OpenTelemetry
|
||||
- OIDC + attestations
|
||||
- OpenFGA
|
||||
|
||||
---
|
||||
|
||||
## الأدوات المطلوب إضافتها
|
||||
|
||||
### أضف الآن
|
||||
|
||||
| الأداة | لماذا | أين |
|
||||
|--------|-------|-----|
|
||||
| **OpenTelemetry** | traces + logs + metrics مترابطة | `requirements.txt` + gateway + services |
|
||||
| **GitHub OIDC** | بدل long-lived secrets | `.github/workflows/` |
|
||||
| **Artifact attestations** | provenance مثبت | CI build step |
|
||||
| **OpenFGA** | object-level authorization | approval_bridge + evidence packs |
|
||||
|
||||
### أضف قريباً
|
||||
|
||||
| الأداة | لماذا | متى |
|
||||
|--------|-------|-----|
|
||||
| **Great Expectations** | data quality gates | قبل evidence pack assembly |
|
||||
| **Unstructured** | استخراج عقود وDD docs | عند تفعيل M&A flow |
|
||||
| **Airbyte** | data movement موحد | عند 5+ مصادر |
|
||||
|
||||
### في الرادار
|
||||
|
||||
| الأداة | لماذا | متى |
|
||||
|--------|-------|-----|
|
||||
| **OPA** | policy engine | عندما القواعد > 50 |
|
||||
| **Temporal** | durable execution | بعد نجاح المسار الذهبي |
|
||||
| **MCP expansion** | أدوات أكثر | بعد استقرار المسارات |
|
||||
|
||||
---
|
||||
|
||||
## Backend — ما يجب تثبيته
|
||||
|
||||
### Endpoint Inventory
|
||||
|
||||
| المجموعة | عدد الـ endpoints | Classification |
|
||||
|----------|------------------|---------------|
|
||||
| Auth | 8 | Internal — no side effects |
|
||||
| Leads | 12 | External — Class A (read) / Class B (import) |
|
||||
| Deals | 10 | External — Class B (stage change triggers evidence) |
|
||||
| Approvals | 6 | Critical — Class B (approve/reject) |
|
||||
| Contradictions | 5 | Internal — Class A |
|
||||
| Evidence Packs | 5 | Critical — Class B (assemble/review) |
|
||||
| Executive Room | 5 | Internal — Class A (read-only) |
|
||||
| Compliance | 5 | Internal — Class A |
|
||||
| Connectors | 4 | Internal — Class A |
|
||||
| Golden Path | 2 | Critical — Class B (creates approval + evidence) |
|
||||
| Strategic Deals | 8 | External — Class B |
|
||||
| Outreach | 6 | External — Class B (sends messages) |
|
||||
|
||||
### Trust Enforcement Coverage
|
||||
|
||||
| Enforcement | المُغطى | الفجوة |
|
||||
|-------------|---------|--------|
|
||||
| Policy gate (A/B/C) | All OpenClaw actions | Direct API calls bypass OpenClaw |
|
||||
| correlation_id required | Class B via gateway | API routes don't enforce yet |
|
||||
| Auto evidence on deal close | deals.py | Other entity closes not covered |
|
||||
| Structured output validation | Golden path only | Other flows use free-form |
|
||||
|
||||
### ما يجب فعله
|
||||
|
||||
1. **كل Class B API route**: تحقق من correlation_id في payload
|
||||
2. **كل outreach endpoint**: تحقق من PDPL consent قبل الإرسال
|
||||
3. **كل strategic deal endpoint**: log to audit_log
|
||||
4. **idempotency key**: على كل POST يسبب side effects
|
||||
|
||||
---
|
||||
|
||||
## Frontend — ما يجب تثبيته
|
||||
|
||||
### Surface Maturity
|
||||
|
||||
| Surface | API Wired | Contract-Driven | States Complete | Arabic RTL |
|
||||
|---------|-----------|-----------------|-----------------|-----------|
|
||||
| Executive Room | ✅ | ✅ (weekly-pack) | Partial | ✅ |
|
||||
| Approval Center | ✅ | Partial | Partial | ✅ |
|
||||
| Evidence Viewer | ✅ | Partial | Partial | ✅ |
|
||||
| Compliance | ✅ | Partial | Partial | ✅ |
|
||||
| Connectors | ✅ | ✅ | Partial | ✅ |
|
||||
| Forecast | ✅ | Partial | Partial | ✅ |
|
||||
| Risk Heatmap | ✅ | Partial | Partial | ✅ |
|
||||
| Violations | ✅ | Partial | Partial | ✅ |
|
||||
| Partner Pipeline | ✅ | Partial | Partial | ✅ |
|
||||
|
||||
### ما يجب فعله
|
||||
|
||||
1. **Executive Room**: استهلك weekly-pack endpoint كمصدر وحيد
|
||||
2. **كل surface**: أضف loading spinner + empty state message + error handler
|
||||
3. **Demo indicator**: badge يوضح "بيانات تجريبية" vs "بيانات حية"
|
||||
|
||||
---
|
||||
|
||||
## الوثائق — ما يجب إضافته
|
||||
|
||||
### For Customer (العميل)
|
||||
|
||||
| الوثيقة | الحالة | الأولوية |
|
||||
|---------|--------|----------|
|
||||
| Onboarding guide | Done (LIVE_DEPLOYMENT_GUIDE) | — |
|
||||
| Admin setup guide | Target | P1 |
|
||||
| Executive quickstart | Target | P1 |
|
||||
| FAQ | Target | P2 |
|
||||
|
||||
### For Team (الفريق)
|
||||
|
||||
| الوثيقة | الحالة |
|
||||
|---------|--------|
|
||||
| Architecture docs | Done (26+ docs) |
|
||||
| Release docs | Done (release-prep) |
|
||||
| Runbooks | Done (memory/runbooks/) |
|
||||
| Closure program | Done (10 tracks) |
|
||||
|
||||
### For Sales (البيع)
|
||||
|
||||
| الوثيقة | الحالة |
|
||||
|---------|--------|
|
||||
| One-pager | Done |
|
||||
| Marketer Hub | Done |
|
||||
| Outreach sequences | Done |
|
||||
| Demo seeder | Done |
|
||||
| Deployment guide | Done |
|
||||
| Revenue engine plan | Done |
|
||||
|
||||
---
|
||||
|
||||
## الترتيب الأمثل — 5 مراحل
|
||||
|
||||
### المرحلة 1: Assurance (أسبوع 1)
|
||||
- [ ] فعّل OpenTelemetry (trace_id + span_id في gateway + services)
|
||||
- [ ] فعّل OIDC في CI/deploy
|
||||
- [ ] أضف attestation step في CI
|
||||
- [ ] اربط Release Matrix بـ PR checks
|
||||
|
||||
### المرحلة 2: Live Path (أسبوع 1-2)
|
||||
- [x] Golden Path حي end-to-end
|
||||
- [x] Trust enforcement (correlation_id required)
|
||||
- [x] Auto evidence on deal close
|
||||
- [x] Executive weekly-pack contract
|
||||
- [ ] Contradiction auto-detection في golden path
|
||||
|
||||
### المرحلة 3: Saudi Activation (أسبوع 2-3)
|
||||
- [ ] اختر workflow: مشاركة بيانات شريك
|
||||
- [ ] اربطه بـ PDPL data classification
|
||||
- [ ] اربطه بـ retention/export rules
|
||||
- [ ] اربطه بـ audit path + AI risk overlay
|
||||
|
||||
### المرحلة 4: Productization (أسبوع 3-4)
|
||||
- [ ] Admin setup guide
|
||||
- [ ] Executive quickstart
|
||||
- [ ] Customer FAQ
|
||||
- [ ] Public landing page copy
|
||||
- [ ] Trust/compliance page
|
||||
|
||||
### المرحلة 5: Expansion (شهر 2+)
|
||||
- [ ] Procurement/vendor deal flow
|
||||
- [ ] Renewal/expansion deal flow
|
||||
- [ ] Deeper M&A DD orchestration
|
||||
- [ ] More connectors with governance
|
||||
- [ ] Broader OpenFGA coverage
|
||||
|
||||
---
|
||||
|
||||
## تجنب الآن
|
||||
|
||||
| ما تتجنبه | السبب |
|
||||
|-----------|-------|
|
||||
| Temporal قبل golden path يستقر | over-engineering |
|
||||
| أكثر من 2 golden paths بنفس الوقت | تشتت |
|
||||
| MCP tools expansion | agent sprawl |
|
||||
| Industry pages قبل case study حقيقي | لا proof |
|
||||
| SOC2 certification claim | لا نملكها |
|
||||
| "أفضل من Salesforce" messaging | مختلف ≠ أفضل |
|
||||
215
salesflow-saas/docs/MASTER_REMAINING_SCOPE_MAP.md
Normal file
215
salesflow-saas/docs/MASTER_REMAINING_SCOPE_MAP.md
Normal file
@ -0,0 +1,215 @@
|
||||
# Master Remaining Scope Map — Dealix Tier-1 Completion
|
||||
|
||||
> **Status**: Active
|
||||
> **Updated**: 2026-04-17
|
||||
> **Rule**: Core System = done. Remaining = Productization + Operability + Revenue Enablement.
|
||||
|
||||
---
|
||||
|
||||
## Summary of What's Done
|
||||
|
||||
| Layer | Status |
|
||||
|-------|--------|
|
||||
| Governance docs (26+) | Done |
|
||||
| Backend models (3 Tier-1) | Done |
|
||||
| Backend services (6 Tier-1 + all real DB) | Done |
|
||||
| Backend APIs (8 Tier-1, all wired) | Done |
|
||||
| Frontend components (9 Tier-1, all wired to APIs) | Done |
|
||||
| Structured output schemas (17 Pydantic) | Done (defined, not yet enforced) |
|
||||
| Architecture brief (40/40) | Done |
|
||||
| Revenue activation docs | Done |
|
||||
| CODEOWNERS | Done |
|
||||
|
||||
---
|
||||
|
||||
## 1. Backend Remaining
|
||||
|
||||
### Must Now
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Runtime enforcement inventory | Every endpoint needs approval_class + sensitivity + reversibility | Target |
|
||||
| Enforce ApprovalPacket schema on Class B actions | No free-form approval payloads | Target |
|
||||
| Auto-assemble evidence pack on deal close | Currently manual only | Target |
|
||||
| Wire LeadScoreCard to lead qualification agent | 17 schemas defined, 0 used | Target |
|
||||
| correlation_id propagation through OpenClaw gateway | Needed for trust audit trail | Target |
|
||||
|
||||
### Should Next
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Idempotency keys for all side-effect endpoints | Prevent duplicate actions on retry | Target |
|
||||
| Connector health probes (live WhatsApp/Stripe check) | Currently only tracks status, no probe | Target |
|
||||
| Telemetry: trace propagation + structured logs | Needed for production observability | Target |
|
||||
| Saudi compliance live validation (actual consent coverage check) | Currently seeds controls as PARTIAL | Target |
|
||||
|
||||
### Strategic Later
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| OPA policy engine | Replace/augment policy.py when rules exceed 50 | Watch |
|
||||
| OpenFGA authorization | When RBAC insufficient for relationship-based access | Watch |
|
||||
| Temporal for durable workflows | When partner/DD/signature flows need crash-proof execution | Watch |
|
||||
| Compensation/rollback logic | Required before Temporal adoption | Target |
|
||||
|
||||
---
|
||||
|
||||
## 2. Frontend Remaining
|
||||
|
||||
### Must Now
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Loading/empty/error states for all 9 components | Professional UX | Partial |
|
||||
| Demo mode vs live mode indicator | Prevent confusion between demo and production | Target |
|
||||
|
||||
### Should Next
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Executive readability polish | Layout for CEO, not engineer | Partial |
|
||||
| Print/export modes for executive surfaces | Board pack export | Target |
|
||||
| Arabic/RTL typography polish | Professional Arabic rendering | Partial |
|
||||
| State badges for approval severity | Visual trust indicators | Target |
|
||||
|
||||
### Strategic Later
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Role-personalized surfaces (CEO vs Operator vs Admin) | Different views per role | Target |
|
||||
| Timeline views for approvals and commitments | Historical decision tracking | Target |
|
||||
| Embedded playbooks in UI | Inline guidance for users | Target |
|
||||
|
||||
---
|
||||
|
||||
## 3. Documentation Remaining
|
||||
|
||||
### Must Now
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Customer onboarding guide | For pilot clients | Partial (deployment guide exists) |
|
||||
| Admin setup guide | For client IT team | Target |
|
||||
| Executive quickstart | For CEO first use | Target |
|
||||
|
||||
### Should Next
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Operator guide | Day-to-day operations | Target |
|
||||
| FAQ (operational) | Common questions | Target |
|
||||
| Implementation checklist | Pre-deployment verification | Partial |
|
||||
|
||||
### Strategic Later
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Deployment models document | Cloud/on-prem/hybrid options | Target |
|
||||
| Integration playbooks per connector | WhatsApp, Salesforce, Stripe setup | Partial |
|
||||
| Incident handbook | Production incident response | Target |
|
||||
|
||||
---
|
||||
|
||||
## 4. Marketing Remaining
|
||||
|
||||
### Must Now
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| One-line positioning | What Dealix is in one sentence | Partial (market-dominance-plan.md) |
|
||||
| ICP definition | Who to sell to | Done (FIRST_3_CLIENTS_PLAN.md) |
|
||||
| Why not CRM / RPA / copilot | Competitive differentiation | Done (market-dominance-plan.md) |
|
||||
|
||||
### Should Next
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Homepage copy | Public website | Target |
|
||||
| Trust/compliance page | Enterprise buyer requirement | Target |
|
||||
| Saudi/GCC readiness page | Regional differentiation | Target |
|
||||
| Use-case pages | Sector-specific value | Partial (presentations exist) |
|
||||
|
||||
### Strategic Later
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Industry pages | Vertical GTM | Target |
|
||||
| Category creation narrative | Thought leadership | Target |
|
||||
| Analyst positioning pack | For Gartner/Forrester | Target |
|
||||
|
||||
---
|
||||
|
||||
## 5. Sales Remaining
|
||||
|
||||
### Must Now
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Pilot scope template | For first 3 clients | Done (FIRST_3_CLIENTS_PLAN.md) |
|
||||
| Demo script | Executive simulation | Done (FIRST_3_CLIENTS_PLAN.md) |
|
||||
| Pricing sheet | 3-tier pricing | Done (FIRST_3_CLIENTS_PLAN.md) |
|
||||
| Outreach scripts | WhatsApp/LinkedIn/Email | Done (whatsapp-sequences.json) |
|
||||
| Objection handling | Common objections + responses | Done (FIRST_3_CLIENTS_PLAN.md) |
|
||||
|
||||
### Should Next
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| ROI calculator | Quantified value for prospects | Target |
|
||||
| Security/compliance brief | For enterprise procurement | Partial |
|
||||
| Case study template | After first pilot | Done (FIRST_3_CLIENTS_PLAN.md) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Customer Success Remaining
|
||||
|
||||
### Must Now
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Kickoff checklist | First day with client | Done (LIVE_DEPLOYMENT_GUIDE.md) |
|
||||
| First 14 days success plan | Pilot monitoring | Done (LIVE_DEPLOYMENT_GUIDE.md) |
|
||||
| Post-pilot conversion script | Convert to paid | Done (FIRST_3_CLIENTS_PLAN.md) |
|
||||
|
||||
### Should Next
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Weekly adoption review | Ongoing engagement | Partial |
|
||||
| Monthly ROI review | Value demonstration | Target |
|
||||
| Support severity model | SLA for client support | Target |
|
||||
|
||||
---
|
||||
|
||||
## 7. Release Remaining
|
||||
|
||||
### Must Now
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| CI backend tests passing | Current blocker | In Progress (pinned deps) |
|
||||
| Architecture brief in CI | Governance gate | Done (in CI YAML) |
|
||||
| CODEOWNERS enforced | Protect sensitive paths | Done |
|
||||
|
||||
### Should Next
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| Branch protection on main | Prevent direct push | Target (GitHub settings) |
|
||||
| Required CI checks | Block merge on failure | Target (GitHub settings) |
|
||||
| Secret scanning | Prevent credential leaks | Target (GitHub settings) |
|
||||
| Release readiness matrix as PR gate | Block RC without evidence | Target |
|
||||
|
||||
### Strategic Later
|
||||
| Item | Why | Status |
|
||||
|------|-----|--------|
|
||||
| OIDC for cloud access | Eliminate long-lived secrets | Watch |
|
||||
| Artifact attestations | Build provenance | Watch |
|
||||
| Canary deployment | Gradual rollout | Target |
|
||||
| Audit log streaming | Long-term retention | Target |
|
||||
|
||||
---
|
||||
|
||||
## Priority Summary
|
||||
|
||||
### 🔴 Must Fix Now (blocks launch/sale)
|
||||
1. CI backend tests passing
|
||||
2. Runtime enforcement on Class B paths
|
||||
3. Schema enforcement on approval/evidence outputs
|
||||
4. Auto-assemble evidence on deal close
|
||||
|
||||
### 🟡 Should Do Next (improves quality/trust)
|
||||
5. Frontend loading/empty/error states
|
||||
6. Telemetry + correlation propagation
|
||||
7. Connector live health probes
|
||||
8. Saudi compliance live validation
|
||||
9. Branch protection + required checks
|
||||
|
||||
### 🟢 Strategic Later (expands market/moat)
|
||||
10. OPA/OpenFGA/Temporal adoption
|
||||
11. Role-personalized surfaces
|
||||
12. Industry GTM pages
|
||||
13. OIDC + artifact attestations
|
||||
14. Renewal/expansion automation
|
||||
170
salesflow-saas/docs/NEXT_STEP_AND_STACK_RECOMMENDATIONS_AR.md
Normal file
170
salesflow-saas/docs/NEXT_STEP_AND_STACK_RECOMMENDATIONS_AR.md
Normal file
@ -0,0 +1,170 @@
|
||||
# الخطوة التالية + توصيات المكدس — NEXT STEP & STACK RECOMMENDATIONS
|
||||
|
||||
> **القاعدة**: Core System = done. الآن = Live Path + Enforcement + Release Gate.
|
||||
> **المرجع**: `MASTER_OPERATING_PROMPT.md` + `tier1-master-closure-checklist.md`
|
||||
|
||||
---
|
||||
|
||||
## الخطوة التالية الواحدة الآن
|
||||
|
||||
### أغلق المسار الذهبي end-to-end
|
||||
|
||||
```
|
||||
Partner intake → Partner dossier (PartnerDossier schema)
|
||||
→ Economics model (EconomicsModel schema)
|
||||
→ Approval packet (ApprovalPacket schema)
|
||||
→ Approval Center (Class B enforcement)
|
||||
→ Workflow commitment (DurableTaskFlow checkpoint)
|
||||
→ Evidence pack (auto-assembled, SHA256)
|
||||
→ Executive weekly summary (ExecWeeklyPack schema)
|
||||
```
|
||||
|
||||
**لماذا هذا المسار؟**
|
||||
- يختبر القرار + الثقة + التنفيذ + الواجهة في تشغيل واحد
|
||||
- أسرع wedge لإظهار قيمة حقيقية
|
||||
- يثبت 5 من 6 اختبارات الإغلاق (truth, schema, workflow, trust, executive)
|
||||
|
||||
---
|
||||
|
||||
## 6 اختبارات الإغلاق
|
||||
|
||||
| # | الاختبار | المعيار | الحالة |
|
||||
|---|---------|---------|--------|
|
||||
| 1 | **Truth** | مصدر واحد للحقيقة يحدد current/partial/pilot/production | **PASS** — `current-vs-target-register.md` |
|
||||
| 2 | **Schema** | كل output حرج schema-bound مع validation | **FAIL** — 17 schemas defined, 0 enforced |
|
||||
| 3 | **Workflow** | مسار حي واحد end-to-end بدون تعديل يدوي | **FAIL** — لا يوجد مسار مكتمل |
|
||||
| 4 | **Trust** | external commitment يفشل بدون approval + evidence + correlation | **PARTIAL** — policy gate موجود، enforcement غير مكتمل |
|
||||
| 5 | **Release** | Release Readiness Matrix توقف الإصدار فعلاً | **FAIL** — architecture_brief في CI لكن ليس gate |
|
||||
| 6 | **Executive** | Executive Room حية تُستخدم أسبوعياً | **PARTIAL** — مربوطة بـ API، لكن بحاجة بيانات + استخدام |
|
||||
|
||||
---
|
||||
|
||||
## أضف الآن — Stack Additions
|
||||
|
||||
### 1. OpenTelemetry (correlation)
|
||||
**لماذا**: ربط trace_id/span_id عبر approval → execution → evidence
|
||||
**كيف**: إضافة `opentelemetry-api` + `opentelemetry-sdk` لـ requirements.txt
|
||||
**أين**: `openclaw/gateway.py` — generate trace_id at entry, propagate downstream
|
||||
**الأثر**: كل قرار قابل للتتبع من البداية للنهاية
|
||||
|
||||
### 2. GitHub OIDC
|
||||
**لماذا**: استبدال long-lived secrets بـ short-lived tokens
|
||||
**كيف**: في `.github/workflows/dealix-ci.yml` — إضافة `permissions: id-token: write`
|
||||
**أين**: deploy steps + cloud access
|
||||
**الأثر**: أمان أفضل + compliance ready
|
||||
|
||||
### 3. Artifact Attestations
|
||||
**لماذا**: إثبات provenance لكل build
|
||||
**كيف**: `actions/attest-build-provenance@v1` في CI
|
||||
**متطلب**: GitHub Enterprise Cloud لـ private repos
|
||||
**الأثر**: كل artifact مربوط بـ commit SHA + workflow + environment
|
||||
|
||||
### 4. OpenFGA (أقل تكامل حي)
|
||||
**لماذا**: object-level authorization لمسار approval/evidence
|
||||
**كيف**: ابدأ بـ authorization_model_id pinned لمسار واحد
|
||||
**أين**: approval_bridge.py — check can_user_approve(resource)
|
||||
**الأثر**: صلاحيات دقيقة بدل RBAC عام
|
||||
|
||||
---
|
||||
|
||||
## أضف بعده مباشرة
|
||||
|
||||
### 5. Great Expectations (data quality)
|
||||
**لماذا**: جودة البيانات كجزء من workflow preconditions
|
||||
**أين**: قبل evidence pack assembly + forecast calculations
|
||||
**الأثر**: بيانات موثوقة في Executive Room
|
||||
|
||||
### 6. Connector Governance Layer
|
||||
**لماذا**: فرض contract موحد لكل connector
|
||||
**ما المطلوب**: version, timeout, retry, health, freshness, audit mapping
|
||||
**الأثر**: لا direct vendor bindings من agents
|
||||
|
||||
### 7. Unstructured (document extraction)
|
||||
**لماذا**: استخراج DD docs, contracts, CIMs, PDFs
|
||||
**متى**: عند تفعيل M&A DD workflow
|
||||
**الأثر**: evidence pipeline أقوى
|
||||
|
||||
---
|
||||
|
||||
## احتفظ به في الرادار (لا تضفه الآن)
|
||||
|
||||
| التقنية | السبب | متى |
|
||||
|---------|-------|-----|
|
||||
| Temporal | durable workflows | بعد نجاح المسار الذهبي |
|
||||
| OPA | policy engine | عندما تتجاوز القواعد 50 |
|
||||
| MCP expansion | tool connectors كثيرة | بعد استقرار المسارات الأولى |
|
||||
| Airbyte | data ingestion | عند 5+ مصادر بيانات |
|
||||
|
||||
---
|
||||
|
||||
## Backend Upgrades — الترتيب
|
||||
|
||||
### الآن
|
||||
1. **correlation_id propagation**: `openclaw/gateway.py` → agent → audit → evidence
|
||||
2. **Schema enforcement**: LeadScoreCard + ApprovalPacket في live flows
|
||||
3. **Auto evidence pack**: on deal close → assemble from 6 tables
|
||||
4. **Approval enforcement**: Class B actions MUST have ApprovalPacket schema
|
||||
|
||||
### بعده
|
||||
5. **Idempotency keys**: لكل endpoint يسبب side effects
|
||||
6. **Retry/compensation**: لمسارات الشراكة والتوقيع
|
||||
7. **Verification receipts**: لكل tool call عبر OpenClaw
|
||||
8. **Telemetry**: structured logs + approval SLA metrics + contradiction counters
|
||||
|
||||
---
|
||||
|
||||
## Frontend Upgrades — الترتيب
|
||||
|
||||
### الآن
|
||||
1. **Contract-driven rendering**: Executive Room يستهلك ExecWeeklyPack مباشرة
|
||||
2. **Loading/empty/error states**: لكل surface
|
||||
3. **Demo vs live indicator**: فصل واضح
|
||||
|
||||
### بعده
|
||||
4. **Arabic/RTL polish**: جداول، تقارير، تصدير
|
||||
5. **Print/export modes**: للواجهات التنفيذية
|
||||
6. **State badges**: severity + trust indicators
|
||||
|
||||
---
|
||||
|
||||
## Docs/Sales/Marketing Additions — الترتيب
|
||||
|
||||
### الآن
|
||||
1. **Customer onboarding guide** (pilot clients)
|
||||
2. **Admin setup guide** (IT team)
|
||||
3. **Executive quickstart** (CEO first use)
|
||||
4. **Pilot sales pack**: one-pager + deck + ROI + scope
|
||||
5. **Marketer hub**: positioning + ICPs + objection handling + claims allowed
|
||||
|
||||
### بعده
|
||||
6. **Operator guide**
|
||||
7. **Implementation checklist**
|
||||
8. **Trust/compliance page** (public)
|
||||
9. **Saudi/GCC readiness page** (public)
|
||||
10. **Industry use-case pages**
|
||||
|
||||
---
|
||||
|
||||
## الترتيب الأمثل — 7 خطوات
|
||||
|
||||
```
|
||||
1. فعّل Docs/Governance/Contracts CI بالكامل ✅ (architecture_brief في CI)
|
||||
2. أغلق المسار الذهبي end-to-end ← الآن
|
||||
3. حوّل Executive Room إلى contract-driven
|
||||
4. اجعل Release Readiness Matrix gate فعلية
|
||||
5. فعّل workflow سعودي حساس واحد
|
||||
6. أضف OpenTelemetry + OIDC + attestations
|
||||
7. جهّز pilot sales pack + marketer hub + customer docs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## تجنب الآن
|
||||
|
||||
| ما تتجنبه | السبب |
|
||||
|-----------|-------|
|
||||
| إضافة agents جديدة | agent sprawl قبل القيمة |
|
||||
| MCP heavy expansion | تعقيد قبل استقرار |
|
||||
| Temporal قبل المسار الذهبي | over-engineering |
|
||||
| Industry pages قبل pilot | لا عميل = لا case study |
|
||||
| Perfect CI قبل المنتج | CI يُصلح لاحقاً، المنتج أولاً |
|
||||
90
salesflow-saas/docs/SPECTRUM_COMPETITIVE_ANALYSIS.md
Normal file
90
salesflow-saas/docs/SPECTRUM_COMPETITIVE_ANALYSIS.md
Normal file
@ -0,0 +1,90 @@
|
||||
# Spectrum Digital AI vs Dealix — Competitive Analysis
|
||||
|
||||
**Date:** 2026-04-23
|
||||
**Competitor:** spectrumdigital.ai (Australia-based)
|
||||
|
||||
---
|
||||
|
||||
## Key Finding
|
||||
|
||||
Spectrum is a **GoHighLevel white-label reskin** — no proprietary AI, no defensible moat. Targets solopreneurs/micro-businesses. Zero MENA presence.
|
||||
|
||||
---
|
||||
|
||||
## Spectrum Products & Pricing
|
||||
|
||||
| Product | What it does | Monthly | Annual |
|
||||
|---------|-------------|---------|--------|
|
||||
| InfinityCall | AI receptionist, 24/7 calls, FAQ, booking | $197/mo + $0.26/min | $1,997/yr |
|
||||
| TrustLoop | Review management, AI review responses | $397/mo | $3,997/yr |
|
||||
| AutoEngage | Lead nurture: email/SMS/WhatsApp drips, webchat, FB ads | $797/mo | $7,997/yr |
|
||||
|
||||
30-day free trial on all products.
|
||||
|
||||
---
|
||||
|
||||
## Feature Comparison
|
||||
|
||||
| Feature | Spectrum | Dealix | Winner |
|
||||
|---------|----------|--------|--------|
|
||||
| AI lead scoring | No | Yes | Dealix |
|
||||
| WhatsApp outreach | Yes (via GHL) | Yes (native) | Tie |
|
||||
| CRM sync (HubSpot/Salesforce) | No | Yes | Dealix |
|
||||
| Calendly booking | Own calendar only | Yes | Dealix |
|
||||
| Approval workflows | No | Yes | Dealix |
|
||||
| Arabic-first UI | No | Yes | Dealix |
|
||||
| Saudi market focus | No | Yes | Dealix |
|
||||
| Multi-LLM routing | No (GHL) | Yes (Groq/OpenAI/Anthropic) | Dealix |
|
||||
| Durable workflows | No | Yes | Dealix |
|
||||
| Audit trail / evidence | No | Yes | Dealix |
|
||||
| PDPL compliance | No | Yes | Dealix |
|
||||
| AI voice receptionist | Yes ($0.26/min) | Not yet | Spectrum |
|
||||
| Review/reputation mgmt | Yes | Not yet | Spectrum |
|
||||
| Facebook ad management | Yes | Not yet | Spectrum |
|
||||
| Database reactivation | Yes | Partial | Spectrum |
|
||||
| AI webchat | Yes | Not yet | Spectrum |
|
||||
|
||||
**Score: Dealix 11 — Spectrum 5**
|
||||
|
||||
---
|
||||
|
||||
## Where Dealix is Ahead (Defend These)
|
||||
|
||||
1. **Enterprise-grade** — CRM sync, approvals, audit trails
|
||||
2. **PDPL compliance** — Saudi regulatory requirement
|
||||
3. **Arabic-first** — native RTL, Saudi dialect, Hijri dates, SAR
|
||||
4. **Proprietary AI** — multi-LLM routing vs GHL reskin
|
||||
5. **Revenue OS** — unified system vs disconnected tool bundle
|
||||
|
||||
## Where Dealix is Behind (Close These Gaps)
|
||||
|
||||
1. **AI voice receptionist** — Spectrum's InfinityCall (P1, post-launch)
|
||||
2. **Review management** — TrustLoop equivalent (P2)
|
||||
3. **Facebook ad management** — AutoEngage feature (Backlog)
|
||||
4. **AI webchat** — live chat widget (P1)
|
||||
5. **Database reactivation** — dormant lead re-engagement (P1)
|
||||
|
||||
---
|
||||
|
||||
## Top 5 Competitive Messages for Saudi Market
|
||||
|
||||
1. **"مبني للسوق السعودي"** — Arabic-first, PDPL compliant, Saudi team. Spectrum is Australian with zero MENA presence.
|
||||
|
||||
2. **"نظام تشغيل إيرادات، مو أدوات مفرّقة"** — Dealix is one unified system. Spectrum is 3 separate tools ($1,391/mo combined).
|
||||
|
||||
3. **"ذكاء اصطناعي حقيقي، مو تغليف"** — Dealix has multi-LLM routing + lead scoring. Spectrum is a GoHighLevel reskin.
|
||||
|
||||
4. **"جاهز للمؤسسات"** — HubSpot sync, approval chains, evidence audit. Spectrum targets solopreneurs.
|
||||
|
||||
5. **"بياناتك تبقى في يدك"** — PDPL compliance, data sovereignty. Spectrum has no data residency guarantees.
|
||||
|
||||
---
|
||||
|
||||
## Strategic Decision
|
||||
|
||||
**Do NOT copy Spectrum's playbook.** Their GHL-reskin model targets a different market (micro-businesses). Dealix should position UP-market:
|
||||
|
||||
- Target: Saudi mid-market companies (50-500 employees)
|
||||
- Differentiator: compliance + Arabic + enterprise integrations
|
||||
- Pricing: higher than Spectrum (value-based, not volume-based)
|
||||
- Message: "The revenue OS Saudi enterprises trust"
|
||||
74
salesflow-saas/docs/VIDEO_PRODUCTION_GUIDE.md
Normal file
74
salesflow-saas/docs/VIDEO_PRODUCTION_GUIDE.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Dealix Promo Video Production Guide
|
||||
|
||||
## Tools (Ranked by Quality, April 2026)
|
||||
|
||||
### Video Generation
|
||||
1. **Google Veo 3.1** (RECOMMENDED) — 9:16 native, 4K, 60fps, built-in audio sync
|
||||
2. **Kling 3.0** — best character consistency across scenes
|
||||
3. **Runway Gen-4.5** — best creative control
|
||||
|
||||
> Sora is shutting down April 26, 2026. Do NOT use it.
|
||||
|
||||
### Saudi Arabic Voiceover
|
||||
1. **Nabarati** (nabarati.ai) — Arabic-first, 1000+ dialect tones, Saudi/Gulf dialect
|
||||
2. **Lahajati** (lahajati.ai) — 192+ Arabic dialects, 98-99% accuracy
|
||||
3. **ElevenLabs** — broader platform, good fallback
|
||||
|
||||
---
|
||||
|
||||
## Script (25 seconds, Saudi Dialect)
|
||||
|
||||
```
|
||||
[0:00-0:08 — Scene 1: The Pain]
|
||||
وش لو عندك نظام ذكي يسوق لك، يتابع العملاء، ويقفل الصفقة؟
|
||||
|
||||
[0:08-0:17 — Scene 2: The Solution]
|
||||
ديلكس — نظام الإيرادات الذكي. يكتشف لك العميل المناسب،
|
||||
يتواصل معه بالطريقة الصح، ويحوّل كل فرصة لصفقة.
|
||||
|
||||
[0:17-0:25 — Scene 3: The CTA]
|
||||
شركتك تستاهل نظام يشتغل بذكاء. جرّب ديلكس — مجاناً.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scene Prompts (Veo 3.1)
|
||||
|
||||
### Scene 1 — "The Pain" (0:00-0:08)
|
||||
```
|
||||
Cinematic 9:16 vertical video. Saudi businessman in white thobe and
|
||||
shemagh standing alone on rooftop overlooking Riyadh skyline at golden
|
||||
hour. Kingdom Tower visible in background. He looks at his phone with
|
||||
frustrated expression, puts it down. Camera slowly pushes in. Shallow
|
||||
depth of field, warm amber lighting, anamorphic lens flare.
|
||||
Photorealistic, 4K.
|
||||
```
|
||||
|
||||
### Scene 2 — "The Solution" (0:08-0:17)
|
||||
```
|
||||
Cinematic 9:16 vertical video. Same Saudi businessman now sitting in
|
||||
ultra-modern glass office in Riyadh, holographic data dashboard floating
|
||||
in front of him showing deal pipeline with Arabic text. He smiles
|
||||
confidently, swipes through deals. Teal and white UI elements glow.
|
||||
Clean minimal interior, marble and glass. Camera orbits 180 degrees.
|
||||
Cool blue-teal corporate lighting. Photorealistic, 4K.
|
||||
```
|
||||
|
||||
### Scene 3 — "The CTA" (0:17-0:25)
|
||||
```
|
||||
Cinematic 9:16 vertical video. Wide aerial drone shot of Riyadh
|
||||
financial district at night, lights glowing. Smooth transition to
|
||||
close-up of Dealix logo appearing on glass screen with teal glow effect.
|
||||
Text appears in elegant Arabic typography. Cinematic depth, bokeh city
|
||||
lights in background. Premium tech aesthetic. Photorealistic, 4K.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Production Workflow
|
||||
|
||||
1. Generate 3 clips in **Veo 3.1** (Google AI Studio)
|
||||
2. Record voiceover in **Nabarati** (Saudi male, confident tone)
|
||||
3. Combine in **CapCut** or **Runway editor**
|
||||
4. Add Dealix logo + CTA overlay + background music
|
||||
5. Export 9:16 for Instagram Reels / TikTok / YouTube Shorts
|
||||
103
salesflow-saas/docs/adr/0001-tier1-execution-policy-spikes.md
Normal file
103
salesflow-saas/docs/adr/0001-tier1-execution-policy-spikes.md
Normal file
@ -0,0 +1,103 @@
|
||||
# ADR 0001: Tier-1 Execution Policy Spikes
|
||||
|
||||
> **Status**: Accepted
|
||||
> **Date**: 2026-04-16
|
||||
> **Deciders**: Engineering, Product, Governance
|
||||
> **Parent**: [`MASTER_OPERATING_PROMPT.md`](../../MASTER_OPERATING_PROMPT.md)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Dealix is transitioning from a strong CRM/Revenue OS to a full Sovereign Enterprise Growth OS (Tier-1). This transition requires architectural decisions about how new governance, trust, and compliance components are built.
|
||||
|
||||
The codebase already has:
|
||||
- OpenClaw execution framework with policy classes (A/B/C)
|
||||
- Approval bridge with canary enforcement
|
||||
- Durable task flows with checkpointing
|
||||
- PDPL compliance engine
|
||||
- 30+ SQLAlchemy models following TenantModel pattern
|
||||
- 50+ API routes following FastAPI + Pydantic pattern
|
||||
- 38+ frontend components following Next.js + Tailwind RTL pattern
|
||||
|
||||
---
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Docs-First for Tier-1
|
||||
|
||||
**Decision**: Governance documentation is written before code implementation.
|
||||
|
||||
**Rationale**: The governance layer defines contracts that code must fulfill. Writing docs first prevents overclaim (docs describing code that doesn't exist) and ensures alignment between strategy and implementation.
|
||||
|
||||
**Consequence**: Every new code component references its governance doc. Every governance doc has a "Current vs Target" section.
|
||||
|
||||
---
|
||||
|
||||
### Decision 2: Contradiction Engine Uses Event-Sourced Model
|
||||
|
||||
**Decision**: Contradictions are recorded as immutable events, not CRUD records.
|
||||
|
||||
**Rationale**: Contradictions represent facts about system state at a point in time. Modifying them would destroy evidence. Resolution is a new event, not an update.
|
||||
|
||||
**Consequence**: `Contradiction` model uses status transitions (detected → reviewing → resolved/accepted). Resolution creates a new record, not an update to the original detection.
|
||||
|
||||
---
|
||||
|
||||
### Decision 3: Evidence Packs Aggregate Existing Data
|
||||
|
||||
**Decision**: Evidence packs are assembled from existing models, not from new data collection.
|
||||
|
||||
**Rationale**: The system already captures audit logs, consent records, AI conversations, approval decisions, and domain events. Evidence packs simply aggregate and hash this data for tamper-evident presentation.
|
||||
|
||||
**Consequence**: `EvidencePackService` queries existing tables. No new data capture mechanisms needed.
|
||||
|
||||
---
|
||||
|
||||
### Decision 4: Saudi Compliance Matrix Is Live
|
||||
|
||||
**Decision**: The compliance matrix is a live, queryable control system that executes checks against the running system.
|
||||
|
||||
**Rationale**: Static checklists become stale. Live controls provide continuous compliance assurance and can generate evidence on demand.
|
||||
|
||||
**Consequence**: `ComplianceControl` model includes `evidence_source` (which service provides the check) and `last_checked_at`. Controls are runnable, not just documentable.
|
||||
|
||||
---
|
||||
|
||||
### Decision 5: New Services Follow Existing Async Pattern
|
||||
|
||||
**Decision**: All new backend services follow the established pattern: `AsyncSession` injection, `tenant_id` scoping, Pydantic schemas for input/output.
|
||||
|
||||
**Rationale**: Consistency reduces cognitive load and ensures all code works within the existing testing and deployment infrastructure.
|
||||
|
||||
**Consequence**: No new frameworks or patterns introduced for Tier-1 services.
|
||||
|
||||
---
|
||||
|
||||
### Decision 6: New Frontend Components Follow Existing Pattern
|
||||
|
||||
**Decision**: All new frontend components use `"use client"`, functional components, Tailwind CSS, RTL-first layout, `text-right` alignment, and `fetch` for API calls.
|
||||
|
||||
**Rationale**: Consistency with the 38 existing Dealix components.
|
||||
|
||||
**Consequence**: No new UI frameworks or state management libraries for Tier-1 components.
|
||||
|
||||
---
|
||||
|
||||
### Decision 7: No Overclaim on Watch/Hold Technologies
|
||||
|
||||
**Decision**: Technologies in Watch or Hold tiers (Temporal, OPA, OpenFGA, Vault, Keycloak) are never referenced as "in production" or "deployed" in any document.
|
||||
|
||||
**Rationale**: Enterprise buyers and auditors will verify claims. Overclaim destroys trust.
|
||||
|
||||
**Consequence**: All docs use explicit "Current vs Target" tables. Watch technologies are listed as "Not evaluated" or "Watch" with clear criteria for adoption.
|
||||
|
||||
---
|
||||
|
||||
### Decision 8: Root-Anchored Execution
|
||||
|
||||
**Decision**: All scripts and commands execute from the repository root (`salesflow-saas/`). No path assumptions within scripts.
|
||||
|
||||
**Rationale**: Previous hooks and scripts had path bugs when run from different directories. The architecture brief script (`scripts/architecture_brief.py`) serves as the official preflight check.
|
||||
|
||||
**Consequence**: All new scripts use `Path(__file__).resolve().parent.parent` for root detection.
|
||||
221
salesflow-saas/docs/ai-operating-model.md
Normal file
221
salesflow-saas/docs/ai-operating-model.md
Normal file
@ -0,0 +1,221 @@
|
||||
# Dealix AI Operating Model — Five-Plane Architecture
|
||||
|
||||
> **Parent**: [`MASTER_OPERATING_PROMPT.md`](../MASTER_OPERATING_PROMPT.md)
|
||||
> **Version**: 1.0 | **Status**: Canonical
|
||||
> **Tracks**: All six tracks
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Dealix separates concerns into five architectural planes. Each plane has a distinct responsibility, clear boundaries, and explicit contracts with adjacent planes.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ DECISION PLANE │
|
||||
│ Strategy · Forecasting · Memos · Evidence │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ EXECUTION PLANE │
|
||||
│ OpenClaw · Durable Flows · Agents · Celery │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ TRUST PLANE │
|
||||
│ Policy Gates · Approvals · Audit · Compliance │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ DATA PLANE │
|
||||
│ PostgreSQL · pgvector · Redis · Events · RAG │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ OPERATING PLANE │
|
||||
│ CI/CD · Monitoring · Self-Improvement · Flags │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Decision Plane
|
||||
|
||||
**Purpose**: Where strategic decisions are made, forecasts generated, and executive memos assembled.
|
||||
|
||||
### Current State
|
||||
| Component | File | Status |
|
||||
|-----------|------|--------|
|
||||
| Executive ROI Service | `services/executive_roi_service.py` | Live (basic) |
|
||||
| Analytics Service | `services/analytics_service.py` | Live |
|
||||
| Management Summary Agent | `ai-agents/prompts/management-summary-agent.md` | Live |
|
||||
| Revenue Attribution Agent | `ai-agents/prompts/revenue-attribution-agent.md` | Live |
|
||||
| Predictive Revenue | `services/predictive_revenue_service.py` | Live |
|
||||
| Strategic Simulator | `services/strategic_deals/strategic_simulator.py` | Live |
|
||||
| ROI Engine | `services/strategic_deals/roi_engine.py` | Live |
|
||||
|
||||
### Target State
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Executive Room (full aggregation) | Building |
|
||||
| Evidence Pack Assembly | Building |
|
||||
| Actual vs Forecast Control Center | Building |
|
||||
| Contradiction-aware decisioning | Building |
|
||||
| Board Pack Generator | Planned |
|
||||
|
||||
### Structured Outputs
|
||||
All Decision Plane outputs must be structured:
|
||||
- `LeadScoreCard` — qualification score + signals + recommendation
|
||||
- `QualificationMemo` — deal qualification with evidence
|
||||
- `ProposalPack` — pricing + terms + value proposition
|
||||
- `ExecutiveSnapshot` — KPIs + risks + pending decisions
|
||||
- `EvidencePack` — assembled proof for audit/board review
|
||||
- `ForecastVariance` — actual vs forecast with root causes
|
||||
|
||||
---
|
||||
|
||||
## 2. Execution Plane
|
||||
|
||||
**Purpose**: Where work gets done. Durable, checkpointed, retriable workflows.
|
||||
|
||||
### Current State
|
||||
| Component | File | Status |
|
||||
|-----------|------|--------|
|
||||
| OpenClaw Gateway | `openclaw/gateway.py` | Live |
|
||||
| Durable Task Flow | `openclaw/durable_flow.py` | Live |
|
||||
| Task Router | `openclaw/task_router.py` | Live |
|
||||
| Policy Engine | `openclaw/policy.py` | Live |
|
||||
| Approval Bridge | `openclaw/approval_bridge.py` | Live |
|
||||
| Observability Bridge | `openclaw/observability_bridge.py` | Live |
|
||||
| Hooks | `openclaw/hooks.py` | Live |
|
||||
| Canary Context | `openclaw/canary_context.py` | Live |
|
||||
| Plugins (5) | `openclaw/plugins/` | Live |
|
||||
| Agent Executor | `services/agents/` | Live |
|
||||
| Celery Workers | `workers/` | Live |
|
||||
| Sequence Engine | `services/sequence_engine.py` | Live |
|
||||
|
||||
### Execution Flow
|
||||
```
|
||||
Request → OpenClaw Gateway
|
||||
→ Policy Gate (policy.py: A/B/C classification)
|
||||
→ Observability (start run, trace)
|
||||
→ Approval Bridge (if Class B: check approval_token)
|
||||
→ Canary Context (if canary enforcement: tenant check)
|
||||
→ Task Router (dispatch to registered handler)
|
||||
→ Durable Flow (checkpoint state)
|
||||
→ Agent Executor / Celery Task
|
||||
→ Action Handler (DB write, message send, etc.)
|
||||
→ Observability (finish run)
|
||||
```
|
||||
|
||||
### Target State
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Temporal for long-running workflows | Watch |
|
||||
| Compensation policies (rollback) | Planned |
|
||||
| Idempotency keys for all writes | Planned |
|
||||
| Dead letter queue with alerting | Planned |
|
||||
|
||||
---
|
||||
|
||||
## 3. Trust Plane
|
||||
|
||||
**Purpose**: Where governance is enforced. No sensitive action bypasses this plane.
|
||||
|
||||
### Current State
|
||||
| Component | File | Status |
|
||||
|-----------|------|--------|
|
||||
| Policy Classes (A/B/C) | `openclaw/policy.py` | Live |
|
||||
| Approval Bridge | `openclaw/approval_bridge.py` | Live |
|
||||
| Trust Score Service | `services/trust_score_service.py` | Live |
|
||||
| Security Gate | `services/security_gate.py` | Live |
|
||||
| Shannon Security | `services/shannon_security.py` | Live |
|
||||
| PDPL Consent Manager | `services/pdpl/consent_manager.py` | Live |
|
||||
| PDPL Data Rights | `services/pdpl/data_rights.py` | Live |
|
||||
| Audit Service | `services/audit_service.py` | Live |
|
||||
| Audit Log Model | `models/audit_log.py` | Live |
|
||||
| Outbound Governance | `services/outbound_governance.py` | Live |
|
||||
| Tool Verification | `services/tool_verification.py` | Live |
|
||||
| Tool Receipts | `services/tool_receipts.py` | Live |
|
||||
| SLA Escalation Alerts | `services/sla_escalation_alerts.py` | Live |
|
||||
| Skill Governance | `services/skill_governance.py` | Live |
|
||||
|
||||
### Target State
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Contradiction Engine | Building |
|
||||
| Saudi Compliance Matrix (live controls) | Building |
|
||||
| OPA policy engine | Watch |
|
||||
| OpenFGA authorization graph | Watch |
|
||||
| Vault secrets governance | Watch |
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Plane
|
||||
|
||||
**Purpose**: Where data lives, moves, and is enriched.
|
||||
|
||||
### Current State
|
||||
| Component | Status |
|
||||
|-----------|------|
|
||||
| PostgreSQL 16 + asyncpg | Live |
|
||||
| pgvector embeddings | Live |
|
||||
| Redis 7 (cache + broker) | Live |
|
||||
| Multi-tenant data isolation | Live |
|
||||
| Alembic migrations | Live |
|
||||
| Knowledge Service (RAG) | Live |
|
||||
| Domain Events | Live |
|
||||
| Integration Sync State | Live |
|
||||
| 30+ SQLAlchemy models | Live |
|
||||
| Mem0 memory engine | Live |
|
||||
|
||||
### Data Governance Rules
|
||||
1. All tables include `tenant_id` (via `TenantModel` base)
|
||||
2. Money fields use `Numeric(12,2)`, never Float
|
||||
3. Timezone is `Asia/Riyadh` (UTC+3)
|
||||
4. Currency defaults to SAR
|
||||
5. Soft deletes via `deleted_at` field
|
||||
6. PII never stored in logs
|
||||
7. pgvector kept updated (security patches)
|
||||
8. No external RAG SaaS — PostgreSQL + pgvector + KnowledgeService only
|
||||
|
||||
### Target State
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| CloudEvents for event schema | Planned |
|
||||
| AsyncAPI for event documentation | Planned |
|
||||
| Data quality automated checks | Planned |
|
||||
| Lineage/catalog layer | Watch |
|
||||
|
||||
---
|
||||
|
||||
## 5. Operating Plane
|
||||
|
||||
**Purpose**: Where the system monitors, improves, and governs itself.
|
||||
|
||||
### Current State
|
||||
| Component | File | Status |
|
||||
|-----------|------|--------|
|
||||
| Observability | `services/observability.py` | Live |
|
||||
| Self-Improvement Loop | `services/self_improvement.py` | Live |
|
||||
| Feature Flags | `services/feature_flags.py` | Live |
|
||||
| Go-Live Matrix | `services/go_live_matrix.py` | Live |
|
||||
| Operations Hub | `services/operations_hub.py` | Live |
|
||||
| GitHub Actions CI | `.github/workflows/dealix-ci.yml` | Live |
|
||||
| Claude Commands | `.claude/commands/` | Live |
|
||||
| Claude Hooks | `.claude/hooks/` | Live |
|
||||
|
||||
### Target State
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Architecture Brief preflight | Building |
|
||||
| Connector Governance Board | Building |
|
||||
| Model Routing Dashboard | Building |
|
||||
| OIDC authentication | Planned |
|
||||
| Artifact attestations | Planned |
|
||||
| Audit log external streaming | Planned |
|
||||
| Protected branch rulesets | Planned |
|
||||
|
||||
---
|
||||
|
||||
## Plane Interaction Rules
|
||||
|
||||
1. **Decision → Execution**: Decision Plane emits structured directives; Execution Plane processes them as tasks.
|
||||
2. **Execution → Trust**: Every execution step checks Trust Plane before performing sensitive actions.
|
||||
3. **Trust → Data**: Trust Plane reads audit logs and compliance state from Data Plane.
|
||||
4. **Data → Operating**: Operating Plane monitors Data Plane health and triggers alerts.
|
||||
5. **Operating → All**: Operating Plane can pause, resume, or rollback any plane component.
|
||||
|
||||
No plane bypasses Trust for Class B or C actions. This is enforced at the OpenClaw Gateway level.
|
||||
32
salesflow-saas/docs/baselines/README.md
Normal file
32
salesflow-saas/docs/baselines/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Performance & Accessibility Baselines
|
||||
|
||||
> Every future "faster than X" or "WCAG compliant" claim must reference a file in this directory.
|
||||
|
||||
## Contents
|
||||
|
||||
| File pattern | Produced by | Update frequency |
|
||||
|--------------|-------------|------------------|
|
||||
| `perf_YYYYMMDD.json` | `k6 run infra/load-tests/baseline.js` | Monthly + before each release |
|
||||
| `a11y_YYYYMMDD.json` | `pnpm run test:a11y` (Playwright + axe) | Monthly + before each release |
|
||||
|
||||
## Interpretation
|
||||
|
||||
### Performance (V006)
|
||||
- Source: k6 stages → 10 → 50 → 200 VUs over 5 minutes
|
||||
- Target: p95 golden_path <2s, weekly_pack <1.5s, approval_center <800ms
|
||||
- Error budget: <1%
|
||||
|
||||
### Accessibility (V007)
|
||||
- Source: axe-core via @axe-core/playwright
|
||||
- Target: 0 violations on routes: `/`, `/login`, `/deals`, `/approvals`, `/executive-room`
|
||||
- Checks both LTR (en) and RTL (ar) layouts
|
||||
|
||||
## Rule
|
||||
|
||||
- **Never** cite performance or a11y numbers from memory, screenshots, or CI badges.
|
||||
- Reference the JSON file in commit messages, marketing claims, security questionnaires, customer demos.
|
||||
- If claiming an improvement, include the baseline JSON **and** the new JSON in the PR.
|
||||
|
||||
## Current baselines
|
||||
|
||||
*(Empty until V006 + V007 first runs. Do not claim perf/a11y numbers until populated.)*
|
||||
258
salesflow-saas/docs/current-vs-target-register.md
Normal file
258
salesflow-saas/docs/current-vs-target-register.md
Normal file
@ -0,0 +1,258 @@
|
||||
# Current vs Target Register — Dealix Subsystem Maturity
|
||||
|
||||
> **Parent**: [`MASTER_OPERATING_PROMPT.md`](../MASTER_OPERATING_PROMPT.md)
|
||||
> **Purpose**: Single source of truth for what is deployed vs what is planned.
|
||||
> **Rule**: No document may claim "production" for anything marked Target/Pilot here.
|
||||
> **Version**: 1.0 | **Last Audited**: 2026-04-16
|
||||
|
||||
---
|
||||
|
||||
## Legend
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| **Production** | Deployed, tested, used by tenants |
|
||||
| **Partial** | Code exists, not fully integrated or tested |
|
||||
| **Pilot** | Behind feature flag, limited testing |
|
||||
| **Target** | Designed/documented, no production code |
|
||||
| **Watch** | Evaluating, no code at all |
|
||||
|
||||
---
|
||||
|
||||
## 1. Decision Plane
|
||||
|
||||
| Component | Status | Evidence | Gap |
|
||||
|-----------|--------|----------|-----|
|
||||
| Executive ROI Service | **Partial** | `services/executive_roi_service.py` (20 lines, basic snapshot) | Needs full aggregation from 6+ services |
|
||||
| Analytics Service | **Production** | `services/analytics_service.py` | — |
|
||||
| Management Summary Agent | **Production** | `ai-agents/prompts/management-summary-agent.md` | — |
|
||||
| Revenue Attribution Agent | **Production** | `ai-agents/prompts/revenue-attribution-agent.md` | — |
|
||||
| Predictive Revenue | **Production** | `services/predictive_revenue_service.py` | — |
|
||||
| Strategic Simulator | **Production** | `services/strategic_deals/strategic_simulator.py` | — |
|
||||
| ROI Engine | **Production** | `services/strategic_deals/roi_engine.py` | — |
|
||||
| Executive Room (full) | **Partial** | `api/v1/executive_room.py` + `components/dealix/executive-room.tsx` | Returns placeholder data; needs real aggregation |
|
||||
| Evidence Pack Assembly | **Partial** | `services/evidence_pack_service.py` + `models/evidence_pack.py` | Model + service exist; needs integration with deal/compliance flows |
|
||||
| Forecast Control Center | **Partial** | `services/forecast_control_center.py` + `api/v1/forecast_control.py` | Returns placeholder; needs real forecast data |
|
||||
| Structured Output Schemas | **Target** | — | Need Pydantic schemas for LeadScoreCard, QualificationMemo, ProposalPack, etc. |
|
||||
| Board Pack Generator | **Target** | — | No code |
|
||||
|
||||
---
|
||||
|
||||
## 2. Execution Plane
|
||||
|
||||
| Component | Status | Evidence | Gap |
|
||||
|-----------|--------|----------|-----|
|
||||
| OpenClaw Gateway | **Production** | `openclaw/gateway.py` | — |
|
||||
| Policy Engine (A/B/C) | **Production** | `openclaw/policy.py` | — |
|
||||
| Approval Bridge | **Production** | `openclaw/approval_bridge.py` | — |
|
||||
| Durable Task Flow | **Production** | `openclaw/durable_flow.py` | In-memory checkpoints; no persistent storage |
|
||||
| Task Router | **Production** | `openclaw/task_router.py` | — |
|
||||
| Observability Bridge | **Production** | `openclaw/observability_bridge.py` | — |
|
||||
| Canary Context | **Production** | `openclaw/canary_context.py` | — |
|
||||
| Hooks | **Production** | `openclaw/hooks.py` | — |
|
||||
| Celery Workers | **Production** | `workers/` | — |
|
||||
| Sequence Engine | **Production** | `services/sequence_engine.py` | — |
|
||||
| Plugin: WhatsApp | **Production** | `openclaw/plugins/whatsapp_plugin.py` | — |
|
||||
| Plugin: Salesforce | **Partial** | `openclaw/plugins/salesforce_agentforce_plugin.py` | Needs OAuth flow testing |
|
||||
| Plugin: Stripe | **Partial** | `openclaw/plugins/stripe_plugin.py` | Webhook testing incomplete |
|
||||
| Plugin: Voice | **Pilot** | `openclaw/plugins/voice_plugin.py` | Behind flag, limited |
|
||||
| Plugin: Contract Intel | **Pilot** | `openclaw/plugins/contract_intelligence_plugin.py` | Early stage |
|
||||
| Temporal Integration | **Watch** | ADR spike planned | No code; requires evidence before adoption |
|
||||
| Compensation/Rollback | **Target** | Documented in execution-fabric.md | No code |
|
||||
| Idempotency Keys | **Target** | — | No code |
|
||||
| Dead Letter Queue | **Target** | — | No code |
|
||||
|
||||
---
|
||||
|
||||
## 3. Trust Plane
|
||||
|
||||
| Component | Status | Evidence | Gap |
|
||||
|-----------|--------|----------|-----|
|
||||
| Policy Classes (A/B/C) | **Production** | `openclaw/policy.py` | — |
|
||||
| Approval Bridge | **Production** | `openclaw/approval_bridge.py` | — |
|
||||
| Trust Score Service | **Production** | `services/trust_score_service.py` | — |
|
||||
| Security Gate | **Production** | `services/security_gate.py` | — |
|
||||
| Shannon Security | **Production** | `services/shannon_security.py` | — |
|
||||
| PDPL Consent Manager | **Production** | `services/pdpl/consent_manager.py` | — |
|
||||
| PDPL Data Rights | **Production** | `services/pdpl/data_rights.py` | — |
|
||||
| Audit Service | **Production** | `services/audit_service.py` | — |
|
||||
| Outbound Governance | **Production** | `services/outbound_governance.py` | — |
|
||||
| Tool Verification | **Production** | `services/tool_verification.py` | — |
|
||||
| Tool Receipts | **Production** | `services/tool_receipts.py` | — |
|
||||
| SLA Escalation Alerts | **Production** | `services/sla_escalation_alerts.py` | — |
|
||||
| Skill Governance | **Production** | `services/skill_governance.py` | — |
|
||||
| Contradiction Engine | **Partial** | `services/contradiction_engine.py` + `models/contradiction.py` | Model + service + API exist; no AI scan integration yet |
|
||||
| Evidence Pack System | **Partial** | `services/evidence_pack_service.py` + `models/evidence_pack.py` | Model + service + API exist; no auto-assembly from deal flows |
|
||||
| Saudi Compliance Matrix | **Partial** | `services/saudi_compliance_matrix.py` + `models/compliance_control.py` | Seed controls exist; live checks not wired to real services |
|
||||
| Approval Center (SLA) | **Partial** | `api/v1/approval_center.py` | API exists; SLA fields not on ApprovalRequest model yet |
|
||||
| OPA Policy Engine | **Watch** | Documented in trust-fabric.md | No code; requires ADR + spike |
|
||||
| OpenFGA Authorization | **Watch** | Documented in trust-fabric.md | No code; requires ADR + spike |
|
||||
| Vault Secrets Mgmt | **Watch** | Documented in trust-fabric.md | No code |
|
||||
| Keycloak Identity | **Watch** | Documented in trust-fabric.md | No code |
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Plane
|
||||
|
||||
| Component | Status | Evidence | Gap |
|
||||
|-----------|--------|----------|-----|
|
||||
| PostgreSQL 16 + asyncpg | **Production** | `database.py`, `docker-compose.yml` | — |
|
||||
| pgvector Embeddings | **Production** | In requirements.txt, used by KnowledgeService | — |
|
||||
| Redis 7 (cache + broker) | **Production** | `docker-compose.yml` | — |
|
||||
| Multi-tenant Isolation | **Production** | `TenantModel` base class, JWT middleware | — |
|
||||
| Alembic Migrations | **Production** | `alembic/` | — |
|
||||
| Knowledge Service (RAG) | **Production** | `services/knowledge_service.py` | — |
|
||||
| Domain Events | **Production** | `models/operations.py (DomainEvent)` | — |
|
||||
| Integration Sync State | **Production** | `models/operations.py (IntegrationSyncState)` | — |
|
||||
| Mem0 Memory Engine | **Partial** | In requirements.txt | Integration depth unclear |
|
||||
| Connector Governance Board | **Partial** | `services/connector_governance.py` + `api/v1/connector_governance.py` | Returns known connectors; no live probe |
|
||||
| CloudEvents Schema | **Target** | Documented in ai-operating-model.md | No code |
|
||||
| AsyncAPI Event Docs | **Target** | — | No code |
|
||||
| Semantic Metrics Layer | **Target** | — | No code |
|
||||
| Data Quality Checks | **Target** | — | No code |
|
||||
| Lineage/Catalog | **Watch** | — | No code |
|
||||
| Connector Facade Standard | **Target** | Documented in trust-fabric.md | No formalized interface |
|
||||
|
||||
---
|
||||
|
||||
## 5. Operating Plane
|
||||
|
||||
| Component | Status | Evidence | Gap |
|
||||
|-----------|--------|----------|-----|
|
||||
| Observability | **Production** | `services/observability.py` | — |
|
||||
| Self-Improvement Loop | **Production** | `services/self_improvement.py` | — |
|
||||
| Feature Flags | **Production** | `services/feature_flags.py` | — |
|
||||
| Go-Live Matrix | **Production** | `services/go_live_matrix.py` | — |
|
||||
| Operations Hub | **Production** | `services/operations_hub.py` | — |
|
||||
| GitHub Actions CI | **Production** | `.github/workflows/dealix-ci.yml` | Backend + frontend jobs |
|
||||
| Claude Commands | **Production** | `.claude/commands/` (5 commands) | — |
|
||||
| Claude Hooks | **Production** | `.claude/hooks/` | — |
|
||||
| Architecture Brief | **Production** | `scripts/architecture_brief.py` | 40/40 checks pass |
|
||||
| Model Routing Dashboard | **Partial** | `services/model_routing_dashboard.py` + `api/v1/model_routing.py` | Static provider list; no live metrics collection |
|
||||
| Docker Compose | **Production** | `docker-compose.yml` (7 services) | — |
|
||||
| Protected Branches | **Target** | — | Not configured on GitHub |
|
||||
| Required Checks | **Target** | — | CI exists but not required |
|
||||
| CODEOWNERS | **Target** | — | File not created |
|
||||
| Environments | **Target** | — | Not configured on GitHub |
|
||||
| Deployment Protection | **Target** | — | No rules configured |
|
||||
| OIDC Auth | **Target** | — | Using long-lived secrets |
|
||||
| Artifact Attestations | **Target** | — | Requires Enterprise plan for private repos |
|
||||
| Audit Log Streaming | **Target** | — | No external streaming |
|
||||
| Rulesets | **Target** | — | Not configured |
|
||||
|
||||
---
|
||||
|
||||
## 6. Revenue OS
|
||||
|
||||
| Component | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| Lead Capture (WhatsApp/Web) | **Production** | `api/v1/leads.py`, `whatsapp_webhook.py` |
|
||||
| Lead Enrichment | **Production** | `services/company_research.py`, `services/osint_service.py` |
|
||||
| Lead Qualification (0-100) | **Production** | `ai-agents/prompts/lead-qualification-agent.md` |
|
||||
| Multi-channel Outreach | **Production** | `services/sequence_engine.py`, outreach plugins |
|
||||
| Meeting Orchestration | **Production** | `api/v1/meetings.py` |
|
||||
| Proposal / CPQ | **Production** | `services/cpq/`, `ai-agents/prompts/proposal-drafting-agent.md` |
|
||||
| Deal Pipeline | **Production** | `api/v1/deals.py`, `services/deal_service.py` |
|
||||
| Commission Engine | **Production** | `api/v1/commissions.py` |
|
||||
| Affiliate System | **Production** | `api/v1/affiliates.py`, `affiliate-system/` |
|
||||
| Invoice / ZATCA | **Partial** | `services/zatca_compliance.py` |
|
||||
| Renewal / Upsell | **Partial** | `services/predictive_revenue_service.py` |
|
||||
| Account Expansion Intel | **Partial** | Signal intelligence exists |
|
||||
|
||||
---
|
||||
|
||||
## 7. Partnership OS
|
||||
|
||||
| Component | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| Partner Scouting | **Production** | `services/strategic_deals/ecosystem_mapper.py` |
|
||||
| Strategic Fit Scoring | **Production** | `services/strategic_deals/deal_matcher.py` |
|
||||
| Term Negotiation | **Production** | `services/strategic_deals/deal_negotiator.py` |
|
||||
| Deal Room | **Production** | `services/strategic_deals/deal_room.py` |
|
||||
| Partner Pipeline Board | **Partial** | `components/dealix/partner-pipeline-board.tsx` (UI ready, needs data) |
|
||||
| Partner Scorecards | **Target** | — |
|
||||
| Co-sell Workflows | **Target** | — |
|
||||
|
||||
---
|
||||
|
||||
## 8. Corporate Development / M&A OS
|
||||
|
||||
| Component | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| Acquisition Scouting | **Production** | `services/strategic_deals/acquisition_scouting.py` |
|
||||
| Company Profiling | **Production** | `services/strategic_deals/company_profiler.py` |
|
||||
| Portfolio Intelligence | **Production** | `services/strategic_deals/portfolio_intelligence.py` |
|
||||
| Strategic Simulation | **Production** | `services/strategic_deals/strategic_simulator.py` |
|
||||
| ROI Engine | **Production** | `services/strategic_deals/roi_engine.py` |
|
||||
| DD Orchestration | **Target** | Governance doc exists, no durable workflow |
|
||||
| IC Memo Generator | **Target** | — |
|
||||
| Board Pack Draft | **Target** | — |
|
||||
|
||||
---
|
||||
|
||||
## 9. Expansion OS
|
||||
|
||||
| Component | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| Territory Manager | **Production** | `services/territory_manager.py` |
|
||||
| Feature Flags (canary) | **Production** | `services/feature_flags.py`, `openclaw/canary_context.py` |
|
||||
| Industry Templates (5) | **Production** | `seeds/` |
|
||||
| Sector Presentations (11) | **Production** | `presentations/` |
|
||||
| Dialect Detection | **Production** | `ai/saudi_dialect.py`, `ai/arabic_nlp.py` |
|
||||
| Market Scanning | **Target** | Governance doc exists |
|
||||
| Stop-Loss Logic | **Target** | Documented, no live triggers |
|
||||
| Post-Launch Actual vs Forecast | **Partial** | `forecast_control_center.py` (placeholder) |
|
||||
|
||||
---
|
||||
|
||||
## 10. PMI / Strategic PMO OS
|
||||
|
||||
| Component | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| PMI Framework | **Target** | `docs/governance/pmi-os.md` documented |
|
||||
| Day-1 Readiness Checklist | **Target** | Template in doc |
|
||||
| 30/60/90 Plans | **Target** | Template in doc |
|
||||
| Dependency Tracking | **Target** | — |
|
||||
| Escalation Engine | **Target** | SLA escalation exists for approvals |
|
||||
| Synergy Realization | **Target** | — |
|
||||
| Exec Weekly Pack | **Target** | — |
|
||||
|
||||
---
|
||||
|
||||
## 11. Executive / Governance OS
|
||||
|
||||
| Component | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| Executive Room | **Partial** | `executive-room.tsx` + `executive_room.py` (UI + API, placeholder data) |
|
||||
| Approval Center | **Partial** | `approval-center.tsx` + `approval_center.py` (UI + API, placeholder data) |
|
||||
| Evidence Pack Viewer | **Partial** | `evidence-pack-viewer.tsx` + `evidence_packs.py` (UI + API) |
|
||||
| Risk Heatmap | **Partial** | `risk-heatmap.tsx` (UI ready, needs aggregated data) |
|
||||
| Actual vs Forecast | **Partial** | `actual-vs-forecast-dashboard.tsx` + `forecast_control.py` |
|
||||
| Policy Violations Board | **Partial** | `policy-violations-board.tsx` (UI ready) |
|
||||
| Saudi Compliance Dashboard | **Partial** | `saudi-compliance-dashboard.tsx` + `saudi_compliance.py` |
|
||||
| Connector Governance Board | **Partial** | `connector-governance-board.tsx` + `connector_governance.py` |
|
||||
| Partner Pipeline Board | **Partial** | `partner-pipeline-board.tsx` (UI ready) |
|
||||
| Board Pack Export | **Target** | — |
|
||||
| Next-Best-Action Board | **Target** | — |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Plane / OS | Production | Partial | Pilot | Target | Watch |
|
||||
|-----------|-----------|---------|-------|--------|-------|
|
||||
| Decision | 7 | 4 | 0 | 2 | 0 |
|
||||
| Execution | 12 | 2 | 2 | 3 | 1 |
|
||||
| Trust | 13 | 4 | 0 | 0 | 4 |
|
||||
| Data | 8 | 2 | 0 | 4 | 1 |
|
||||
| Operating | 10 | 1 | 0 | 7 | 0 |
|
||||
| Revenue OS | 9 | 3 | 0 | 0 | 0 |
|
||||
| Partnership OS | 4 | 1 | 0 | 2 | 0 |
|
||||
| M&A OS | 5 | 0 | 0 | 3 | 0 |
|
||||
| Expansion OS | 5 | 1 | 0 | 2 | 0 |
|
||||
| PMI OS | 0 | 0 | 0 | 7 | 0 |
|
||||
| Executive OS | 0 | 9 | 0 | 2 | 0 |
|
||||
| **TOTAL** | **73** | **27** | **2** | **32** | **6** |
|
||||
|
||||
**Maturity Score**: 73 Production / 140 Total = **52.1%**
|
||||
**With Partial**: (73+27) / 140 = **71.4%**
|
||||
39
salesflow-saas/docs/customer_learnings/README.md
Normal file
39
salesflow-saas/docs/customer_learnings/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# §3 — Customer Validation Program
|
||||
|
||||
> Hard rule: **no Phase 2 feature ships until pilot customers drive the backlog.**
|
||||
> All customer-facing artifacts live here.
|
||||
|
||||
## Contents
|
||||
|
||||
| Artifact | Purpose | Owner |
|
||||
|----------|---------|-------|
|
||||
| [pilot_agreement_template.md](pilot_agreement_template.md) | Design-partner + paid-pilot contract (draft — counsel reviews before signing) | Founder + Head of CS |
|
||||
| [pilot_template/success_criteria.md](pilot_template/success_criteria.md) | Per-pilot success definition signed before onboarding | Head of CS |
|
||||
| [pilot_template/kickoff_checklist.md](pilot_template/kickoff_checklist.md) | 14-point onboarding checklist | Head of CS |
|
||||
| [friction_log.md](friction_log.md) | Weekly running log of every customer friction | Head of CS |
|
||||
| [feature_requests.yaml](feature_requests.yaml) | Structured registry of customer-requested features with 3-pilot threshold | Founder |
|
||||
| [weekly_review_template.md](weekly_review_template.md) | Format for the Wed customer-learnings synthesis | Founder + Head of CS |
|
||||
| [hypotheses.yaml](hypotheses.yaml) | 12 viability hypotheses tracked to SUPPORTED/FALSIFIED/AMBIGUOUS | Founder |
|
||||
| [interviews/_template_ar.md](interviews/_template_ar.md) | Arabic 45-min discovery call script + log | Founder |
|
||||
| [interviews/_template_en.md](interviews/_template_en.md) | English 45-min discovery call script + log | Founder |
|
||||
| [founder_dashboard.md](founder_dashboard.md) | Weekly Monday printable dashboard (Business Viability Kit §8) | Founder |
|
||||
| [pricing_discovery.md](pricing_discovery.md) | Van Westendorp + value-based pricing worksheet | Founder |
|
||||
| [unit_economics.md](unit_economics.md) | Per-customer economics (fill after 3 paying customers) | Founder |
|
||||
| [defensibility_scorecard.md](defensibility_scorecard.md) | 5-moat scorecard, measured Week 12 + quarterly | Founder |
|
||||
|
||||
## Rules
|
||||
|
||||
1. **No feature enters the Wave backlog unless it appears in the Friction Log or Feature Requests registry with a customer reference.**
|
||||
2. **Every pilot's Success Criteria is signed before kickoff.** No verbal commitments.
|
||||
3. **Founder personally attends ≥1 customer call/week** for 90 days. Customer Proxy Syndrome is a named failure mode (§6).
|
||||
4. **Friction Log entries must be written within 24h of the conversation.**
|
||||
5. **If a feature is requested by <3 pilots in ≥30 days, it stays out of the roadmap** (prevents Integration Sprawl — §6).
|
||||
|
||||
## Week-12 Phase Gate Inputs
|
||||
|
||||
Data used to color the gate (§3 of Execution Waves):
|
||||
- Signed success criteria completion rates (from `pilot_template/success_criteria.md` per pilot)
|
||||
- Golden-path completion rate (from Dealix analytics)
|
||||
- NPS scores (from weekly reviews)
|
||||
- Reference willingness (captured in friction_log entries)
|
||||
- Renewal intent (captured in weekly reviews)
|
||||
@ -0,0 +1,107 @@
|
||||
# Dealix — Strategic Defensibility Scorecard
|
||||
|
||||
> First measurement at Week 12. Re-measured every quarter.
|
||||
> Each question: 0–2 points. Max 20.
|
||||
> Honest scoring only — be more generous with your competitors than yourself.
|
||||
|
||||
---
|
||||
|
||||
## Scoring rubric (per question)
|
||||
|
||||
- **0** — No evidence we have this
|
||||
- **1** — Directional evidence, not yet strong
|
||||
- **2** — Strong evidence (data, customer language, wins)
|
||||
|
||||
---
|
||||
|
||||
## 1. Data Moat
|
||||
|
||||
**Q1.1** — Do we have data competitors cannot access (e.g. Arabic formal-register corpus, PDPL evidence patterns, board-pack styles)?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Q1.2** — Does every customer interaction make the product measurably better for the *next* customer?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Subtotal**: __ / 4
|
||||
|
||||
---
|
||||
|
||||
## 2. Distribution Moat
|
||||
|
||||
**Q2.1** — Are customers actively referring peers within 6 months of sign-up (H12 check)?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Q2.2** — Do we have 2+ acquisition channels that competitors cannot easily copy (e.g. KSA CFO community, Arabic enterprise content depth, government procurement whitelist)?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Subtotal**: __ / 4
|
||||
|
||||
---
|
||||
|
||||
## 3. Technical Moat
|
||||
|
||||
**Q3.1** — Is there a capability that would take a competitor ≥ 6 months to replicate (durable execution + evidence graph + RLS + OTel compliance correlation stack)?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Q3.2** — Is Arabic-first quality *measurably* better than any alternative (blind customer test, eval suite results, documented examples)?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Subtotal**: __ / 4
|
||||
|
||||
---
|
||||
|
||||
## 4. Brand Moat
|
||||
|
||||
**Q4.1** — If a customer switches away, is there emotional / identity loss ("this is *our* board room tool")?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Q4.2** — Would customers defend Dealix unprompted in public forums or peer conversations?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Subtotal**: __ / 4
|
||||
|
||||
---
|
||||
|
||||
## 5. Switching-Cost Moat
|
||||
|
||||
**Q5.1** — Is there data / workflow / integration lock-in that is painful to migrate out of?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Q5.2** — Do users build Dealix-specific expertise (advanced admins, workflow owners) that would be lost on churn?
|
||||
Score: __ / 2 — evidence:
|
||||
|
||||
**Subtotal**: __ / 4
|
||||
|
||||
---
|
||||
|
||||
## Final Score
|
||||
|
||||
**Total**: __ / 20
|
||||
|
||||
### Interpretation
|
||||
|
||||
| Score | Meaning |
|
||||
|-------|---------|
|
||||
| 16–20 | Category-defining trajectory. Compound. |
|
||||
| 10–15 | Strong but not yet defensible. Invest in weakest moat. |
|
||||
| 5–9 | Commoditizable. Rethink positioning or narrow scope. |
|
||||
| 0–4 | No moat. Change fundamentals, or the business will be out-executed. |
|
||||
|
||||
---
|
||||
|
||||
## Improvement plan (top 1 action per weakest moat)
|
||||
|
||||
- Weakest moat identified:
|
||||
- Single action for next quarter:
|
||||
- Evidence we will accept as success:
|
||||
|
||||
---
|
||||
|
||||
## History
|
||||
|
||||
| Quarter | Score | Weakest moat | Top action taken |
|
||||
|---------|-------|--------------|------------------|
|
||||
| Q1 2026 (W12) | __ | __ | __ |
|
||||
| Q2 2026 | __ | __ | __ |
|
||||
| Q3 2026 | __ | __ | __ |
|
||||
| Q4 2026 | __ | __ | __ |
|
||||
29
salesflow-saas/docs/customer_learnings/feature_requests.yaml
Normal file
29
salesflow-saas/docs/customer_learnings/feature_requests.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
# Customer Feature Request Registry
|
||||
#
|
||||
# Rule (from §6 Failure Modes — Integration Sprawl):
|
||||
# A feature enters the Wave backlog ONLY after ≥3 pilot customers
|
||||
# independently request it within a 60-day window.
|
||||
#
|
||||
# Owner: Founder (reviewed weekly Wednesday).
|
||||
# Schema is stable — do not rename fields.
|
||||
|
||||
version: 1
|
||||
|
||||
requests:
|
||||
# Example entry. Delete or overwrite on first real request.
|
||||
- id: FR-0001
|
||||
title: "Exportable Evidence Pack as signed PDF for board meetings"
|
||||
requested_by:
|
||||
- customer: "example-retail-group"
|
||||
role: "CFO"
|
||||
date: "2026-04-17"
|
||||
quote: "لو نقدر نطلع الإيفيدنس باك كـPDF موقع لمجلس الإدارة يكون رهيب"
|
||||
theme_tags: ["evidence", "executive_room", "reporting"]
|
||||
estimated_effort: "M" # S/M/L/XL
|
||||
threshold_met: false # flips true when ≥3 distinct customers request
|
||||
in_backlog: false
|
||||
wave: null # one of A/B/C/D/E once promoted
|
||||
notes: |
|
||||
Currently Evidence Packs export as JSON + SHA256 manifest.
|
||||
PDF export would require: signed PDF service, bilingual rendering,
|
||||
sponsor signature block. Watch for 2 more requests before promoting.
|
||||
101
salesflow-saas/docs/customer_learnings/founder_dashboard.md
Normal file
101
salesflow-saas/docs/customer_learnings/founder_dashboard.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Dealix Founder Execution Dashboard
|
||||
|
||||
> One page. Updated every Monday morning. Print it or pin it at top of your working doc.
|
||||
> If hidden in a Notion folder you never open, it does not exist.
|
||||
|
||||
---
|
||||
|
||||
## Week of: **YYYY-MM-DD**
|
||||
|
||||
---
|
||||
|
||||
## ★ North Star
|
||||
- **Paid pilot customers**: ___ / 3 (target Week 12)
|
||||
- **Days to Phase Gate**: ___ / 84
|
||||
|
||||
---
|
||||
|
||||
## ★ Founder-only blockers
|
||||
(If any overdue, nothing else on this dashboard matters)
|
||||
|
||||
- [ ] FD001 Legal entity decided (due W2)
|
||||
- [ ] FD002 Counsel engaged + retainer signed (due W2)
|
||||
- [ ] FD003 Repo extracted to new GitHub org (due W1)
|
||||
- [ ] FD004 SAIP trademark filed (due W3)
|
||||
- [ ] FD005 Job specs posted (due W4)
|
||||
|
||||
---
|
||||
|
||||
## ★ This week (planned Monday AM)
|
||||
|
||||
| Activity | Minimum | Actual |
|
||||
|----------|---------|--------|
|
||||
| Customer calls scheduled | 5 | ___ |
|
||||
| Customer calls completed | 5 | ___ |
|
||||
| Interview logs written | = calls completed | ___ |
|
||||
| Demos delivered | as booked | ___ |
|
||||
| Pilots signed this week | — | ___ |
|
||||
|
||||
---
|
||||
|
||||
## ★ External verification (V003 + V001)
|
||||
|
||||
- Pentest vendor engaged? [ ] Y [ ] N
|
||||
- Pentest report received? [ ] Y [ ] N
|
||||
- Open Critical findings: ___
|
||||
- Open High findings: ___
|
||||
- V001 last run: ____-__-__ · verified findings: ___
|
||||
|
||||
---
|
||||
|
||||
## ★ Hypotheses status (of 12)
|
||||
|
||||
Read `hypotheses.yaml` before filling.
|
||||
|
||||
- SUPPORTED: ___
|
||||
- FALSIFIED: ___
|
||||
- AMBIGUOUS: ___
|
||||
- UNTESTED: ___
|
||||
|
||||
---
|
||||
|
||||
## ★ Hire pipeline
|
||||
|
||||
| Role | Screened | Interviewed | Offer out |
|
||||
|------|----------|-------------|-----------|
|
||||
| Design Engineer | ___ | ___ | ___ |
|
||||
| Backend Engineer | ___ | ___ | ___ |
|
||||
| Head of CS | ___ | ___ | ___ |
|
||||
|
||||
---
|
||||
|
||||
## ★ Red flags — mark any that are true this week
|
||||
|
||||
- [ ] Zero customer calls this week
|
||||
- [ ] Any FD task >7 days overdue
|
||||
- [ ] Feature work shipped without customer pull
|
||||
- [ ] A new plan or blueprint was written this week
|
||||
- [ ] Metric claimed without evidence (baseline file, interview log, invoice)
|
||||
|
||||
**Rule**: 2+ red flags = stop. Escalate to advisor before next Monday.
|
||||
|
||||
---
|
||||
|
||||
## ★ Cash & runway
|
||||
|
||||
- Cash on hand: ______ SAR
|
||||
- Monthly burn: ______ SAR
|
||||
- Months of runway: ___
|
||||
- Est. months to breakeven: ___
|
||||
|
||||
---
|
||||
|
||||
## ★ One-line narrative for the week
|
||||
|
||||
Write a single sentence capturing what you learned this week from paying or nearly-paying customers. If you cannot, you did not have enough conversations.
|
||||
|
||||
>
|
||||
|
||||
---
|
||||
|
||||
*Next Monday: copy this page, date it, fill it again. Keep all versions — the trend line is the real signal.*
|
||||
50
salesflow-saas/docs/customer_learnings/friction_log.md
Normal file
50
salesflow-saas/docs/customer_learnings/friction_log.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Customer Friction Log
|
||||
|
||||
> One entry per friction. No aggregation, no editorializing. Raw source of truth.
|
||||
> Head of CS owns; Founder reads weekly on Wednesday.
|
||||
> **Rule**: entry written within 24h of the conversation. No exceptions.
|
||||
|
||||
---
|
||||
|
||||
## Entry Template (copy for each new entry)
|
||||
|
||||
```
|
||||
### YYYY-MM-DD — [customer_short_name] — [short_title]
|
||||
|
||||
- **Reporter**: [dealix_team_member]
|
||||
- **Customer role**: [CFO / COO / Sales Ops / Admin / End user]
|
||||
- **Severity**: [P0 show-stopper | P1 major | P2 annoyance | P3 nice-to-have]
|
||||
- **Theme tag**: [auth | arabic | approval | evidence | reporting | integration | perf | a11y | other]
|
||||
- **Context** (1–2 sentences describing what customer was trying to do):
|
||||
|
||||
- **What they said** (direct quote when possible, Arabic OK):
|
||||
>
|
||||
|
||||
- **What actually happened** (observed behavior, steps to reproduce):
|
||||
|
||||
- **Workaround used (if any)**:
|
||||
|
||||
- **Linked GitHub issue / ticket**: #____
|
||||
|
||||
- **Status**: [open | in-progress | resolved | won't-fix-with-rationale]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entries
|
||||
|
||||
### [Seed — example of the format; delete on first real entry]
|
||||
|
||||
### 2026-04-17 — Example Retail Group — Approval Card Arabic RTL label truncation
|
||||
|
||||
- **Reporter**: Head of CS
|
||||
- **Customer role**: CFO
|
||||
- **Severity**: P2 annoyance
|
||||
- **Theme tag**: arabic, a11y
|
||||
- **Context**: CFO trying to approve a deal from mobile Safari in Arabic locale.
|
||||
- **What they said**:
|
||||
> "الزر الأخضر يخفي نصف السطر العلوي. ما أقدر أقرأ اسم الصفقة."
|
||||
- **What actually happened**: Approve button overlaps deal title in RTL at viewport <375px.
|
||||
- **Workaround used**: Customer approved from desktop instead.
|
||||
- **Linked GitHub issue / ticket**: #TBD
|
||||
- **Status**: open (queued for Wave A)
|
||||
117
salesflow-saas/docs/customer_learnings/hypotheses.yaml
Normal file
117
salesflow-saas/docs/customer_learnings/hypotheses.yaml
Normal file
@ -0,0 +1,117 @@
|
||||
# 12 Business Viability Hypotheses
|
||||
#
|
||||
# Each hypothesis MUST reach verdict by Week 12: SUPPORTED, FALSIFIED, or AMBIGUOUS.
|
||||
# Edited only after customer interactions — never from founder intuition alone.
|
||||
#
|
||||
# Rule: an interview log in docs/customer_learnings/interviews/ must be cited
|
||||
# as evidence for every status change.
|
||||
|
||||
version: 1
|
||||
kit: "DEALIX_BUSINESS_VIABILITY_KIT.md v1.0.0"
|
||||
window_weeks: [4, 12]
|
||||
|
||||
hypotheses:
|
||||
- id: H1
|
||||
claim: "The commitment-tracking problem is real and painful for 200–2000-employee KSA firms"
|
||||
falsification_trigger: "10 CFO/COO interviews answer 'can't remember' or take >60s to cite an example"
|
||||
status: UNTESTED # UNTESTED | SUPPORTED | FALSIFIED | AMBIGUOUS
|
||||
evidence_interviews: [] # e.g. ["acme_20260422.md"]
|
||||
confidence: null # null | low | medium | high
|
||||
last_update: null
|
||||
|
||||
- id: H2
|
||||
claim: "Pain clears the $10K/yr willingness-to-pay threshold"
|
||||
falsification_trigger: "Unprompted annual-value answers cluster below $10K across interviews"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H3
|
||||
claim: "Decision maker is reachable within 3 meetings of first call"
|
||||
falsification_trigger: "'Procurement committee + 6 months' is the dominant answer"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H4
|
||||
claim: "Clear trigger event (board pressure, audit fail, reg change) drives purchase"
|
||||
falsification_trigger: "No identifiable triggers across 10 interviews"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H5
|
||||
claim: "Buyer owns ≥ $50K/yr budget for ContOps / RevOps / Governance tech"
|
||||
falsification_trigger: "No comparable tool purchase in last 12 months"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H6
|
||||
claim: "Saudi enterprises genuinely prefer Arabic-first over translated English SaaS"
|
||||
falsification_trigger: "'We stayed on the English interface' is the pattern"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H7
|
||||
claim: "PDPL + ZATCA depth is a stated RFP criterion, not a late-added checkbox"
|
||||
falsification_trigger: "'Compliance added at end' is typical"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H8
|
||||
claim: "Serious customers accept paid pilot (50% of Business tier upfront)"
|
||||
falsification_trigger: "'We pay only after value proven' pattern blocks close"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H9
|
||||
claim: "Successful pilot customers will be public references (logo, testimonial, case)"
|
||||
falsification_trigger: "Refusal in pilot negotiations across 3+ candidates"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H10
|
||||
claim: "Land-and-expand viable: after one workflow wins, customers want more"
|
||||
falsification_trigger: "Month-3 review: 'no other workflows' dominant"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H11
|
||||
claim: "Quantified customer value ≥ 10× price"
|
||||
falsification_trigger: "Ratio under 10× in 3+ pilots once measured"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
- id: H12
|
||||
claim: "Organic referrals emerge within 6 months of first successful pilot"
|
||||
falsification_trigger: "Zero unprompted referrals after 3 successful wins + 6 months"
|
||||
status: UNTESTED
|
||||
evidence_interviews: []
|
||||
confidence: null
|
||||
last_update: null
|
||||
|
||||
# Weekly rollup (auto-filled by founder Friday review)
|
||||
rollup:
|
||||
total: 12
|
||||
supported: 0
|
||||
falsified: 0
|
||||
ambiguous: 0
|
||||
untested: 12
|
||||
notes: "Kit issued Week 4. No interactions logged yet."
|
||||
@ -0,0 +1,139 @@
|
||||
# مكالمة اكتشاف عميل — {اسم الشركة}
|
||||
|
||||
> **النوع**: مكالمة اكتشاف (ليست عرضاً، ليست بيعاً)
|
||||
> **المدة المستهدفة**: 45 دقيقة
|
||||
> **القاعدة الأساسية**: اسأل. لا تبِع. لا تصف Dealix في أول 30 دقيقة.
|
||||
|
||||
---
|
||||
|
||||
## معلومات المكالمة
|
||||
|
||||
- **الشركة**: ________________
|
||||
- **المُحاوَر**: ________________
|
||||
- **الدور**: ________________
|
||||
- **عدد الموظفين**: ________________
|
||||
- **القطاع**: ________________
|
||||
- **المدة الفعلية**: ________________
|
||||
- **رابط التسجيل** (بإذن): ________________
|
||||
- **التاريخ**: YYYY-MM-DD
|
||||
|
||||
---
|
||||
|
||||
## ١) الإطار (٥ دقائق)
|
||||
|
||||
> "شكراً لوقتك. هذه محادثة استكشاف، ليست بيعاً. أبحث في كيف تُدار الالتزامات التجارية في الشركات مثل شركتكم. في نهاية المكالمة، لو لم يكن هناك تطابق، سأكون صريحاً. هل يناسبك هذا الإطار؟"
|
||||
|
||||
تأكيد الإطار: [ ] نعم [ ] لا
|
||||
|
||||
## ٢) الخلفية (٥ دقائق)
|
||||
|
||||
- كم موظفاً في الشركة؟
|
||||
- في أي قطاع؟
|
||||
- ما دورك تحديداً؟
|
||||
- منذ متى في هذا الدور؟
|
||||
|
||||
## ٣) اكتشاف المشكلة (٢٠ دقيقة)
|
||||
|
||||
### س١ — "احكِ لي عن آخر مرة ضاع فيها التزام مهم بين فرقكم أو بينكم وبين طرف خارجي."
|
||||
اقتباس حرفي:
|
||||
>
|
||||
|
||||
### س٢ — "كيف تعرفون حالياً ما الذي وعدتم به لعملائكم/شركائكم في الـ 6 أشهر الماضية؟"
|
||||
اقتباس:
|
||||
>
|
||||
|
||||
### س٣ — "عند تحضير اجتماع مجلس، من أين تأتي الأرقام؟ ومتى آخر مرة اكتُشِف خطأ فيها أمام المجلس؟"
|
||||
اقتباس:
|
||||
>
|
||||
|
||||
### س٤ — "لو تخيلت يوم عمل مثالياً لك كـ [CFO/COO]، ما الذي يختلف عن اليوم الحالي؟"
|
||||
اقتباس:
|
||||
>
|
||||
|
||||
### س٥ — "ما الأدوات/البرامج التي استخدمتَها لحل هذه المشكلة ولم تنجح؟ ولماذا؟"
|
||||
الأدوات المذكورة:
|
||||
|
||||
### س٦ — "لو لم تُحَل هذه المشكلة خلال 12 شهراً، ماذا يحدث؟"
|
||||
اقتباس:
|
||||
>
|
||||
|
||||
## ٤) الاستكشاف الكمّي (١٠ دقائق)
|
||||
|
||||
### س٧ — ساعات أسبوعية في التقارير التنفيذية
|
||||
الإجابة: ______ ساعة/أسبوع
|
||||
|
||||
### س٨ — تأخّر قرار بسبب غياب معلومة + كلفة التأخير
|
||||
الإجابة:
|
||||
|
||||
### س٩ — "ما القيمة السنوية العقلانية لنظام يحل 80% من هذا؟"
|
||||
**القيمة غير المُحفَّزة** (بدون اقتراح منك): ______ ر.س/سنة
|
||||
|
||||
### س١٠ — "من يقرر شراء نظام كهذا؟ من يمكنه تعطيل القرار؟"
|
||||
متخذ القرار: ______
|
||||
الخطوات حتى التوقيع: ______
|
||||
|
||||
## ٥) الإغلاق (٥ دقائق)
|
||||
|
||||
### س١١ — عرض Dealix في 15 دقيقة الأسبوع القادم؟
|
||||
[ ] نعم، وعد بموعد: __________
|
||||
[ ] لاحقاً: __________
|
||||
[ ] لا: السبب __________
|
||||
|
||||
### س١٢ — إحالات
|
||||
أسماء مقترحة: __________
|
||||
|
||||
---
|
||||
|
||||
## الاستنتاجات (تُكتب خلال 24 ساعة)
|
||||
|
||||
### أعلى ٥ اقتباسات
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
5.
|
||||
|
||||
### مستوى الألم (١-١٠)
|
||||
**النتيجة**: _/10
|
||||
**المبرر**:
|
||||
|
||||
### Trigger Event
|
||||
- [ ] موجود: __________
|
||||
- [ ] غير موجود
|
||||
|
||||
### إمكانية الوصول للمشتري
|
||||
- متخذ القرار: __________
|
||||
- خطوات حتى القرار: __________
|
||||
|
||||
### الاستعداد للدفع (ر.س سنوياً)
|
||||
- غير مُحفَّز: __________
|
||||
- بعد وصف Dealix: __________
|
||||
|
||||
### المنافسون المذكورون
|
||||
-
|
||||
|
||||
### الخطوة التالية
|
||||
- [ ] عرض محدد في: __________
|
||||
- [ ] متابعة في: __________
|
||||
- [ ] انسحاب: السبب __________
|
||||
|
||||
---
|
||||
|
||||
## تأثير على الفرضيات
|
||||
|
||||
راجع `docs/customer_learnings/hypotheses.yaml` وحدّث فقط ما غيّره هذا الاجتماع:
|
||||
|
||||
- H1 — مشكلة حقيقية ومؤلمة: SUPPORTED / FALSIFIED / AMBIGUOUS — لماذا:
|
||||
- H2 — ألم ≥ $10K/yr: ...
|
||||
- H3 — مشتري قابل للوصول: ...
|
||||
- H4 — trigger event: ...
|
||||
- H5 — ميزانية ≥ $50K: ...
|
||||
- H6 — عربي مقبل > إنجليزي مترجم: ...
|
||||
- H7 — PDPL/ZATCA ميزة: ...
|
||||
- (ما لم تتم محادثته اتركه UNTESTED)
|
||||
|
||||
---
|
||||
|
||||
## ملاحظات شخصية (للمؤسس فقط)
|
||||
|
||||
>
|
||||
@ -0,0 +1,143 @@
|
||||
# Customer Discovery Call — {Company}
|
||||
|
||||
> **Type**: Discovery call (not a demo, not a sales pitch)
|
||||
> **Target length**: 45 minutes
|
||||
> **Prime directive**: Ask. Don't sell. Don't describe Dealix in the first 30 minutes.
|
||||
|
||||
---
|
||||
|
||||
## Meta
|
||||
|
||||
- **Company**: ________________
|
||||
- **Interviewee**: ________________
|
||||
- **Role**: ________________
|
||||
- **Headcount**: ________________
|
||||
- **Sector**: ________________
|
||||
- **Actual duration**: ________________
|
||||
- **Recording link** (with consent): ________________
|
||||
- **Date**: YYYY-MM-DD
|
||||
|
||||
---
|
||||
|
||||
## 1) Frame (5 min)
|
||||
|
||||
> "Thanks for the time. This is a discovery conversation, not a sales pitch. I'm researching how commercial commitments are managed in companies like yours. By the end, if there's no fit, I'll say so directly. Does that framing work?"
|
||||
|
||||
Frame confirmed: [ ] Yes [ ] No
|
||||
|
||||
## 2) Background (5 min)
|
||||
|
||||
- Headcount?
|
||||
- Sector?
|
||||
- Your exact role?
|
||||
- Time in role?
|
||||
|
||||
## 3) Problem Discovery (20 min)
|
||||
|
||||
### Q1 — "Walk me through the last time a commitment slipped between teams — or between your company and a partner."
|
||||
Verbatim:
|
||||
>
|
||||
|
||||
### Q2 — "How do you currently know what you've committed to, 6 months back?"
|
||||
Verbatim:
|
||||
>
|
||||
|
||||
### Q3 — "When prepping a board meeting, where do the numbers come from? When did someone last catch an error in a board pack?"
|
||||
Verbatim:
|
||||
>
|
||||
|
||||
### Q4 — "If I could design your ideal Monday morning as [CFO/COO], what's different from today?"
|
||||
Verbatim:
|
||||
>
|
||||
|
||||
### Q5 — "What tools have you tried that failed to solve this? Why did they fail?"
|
||||
Tools cited:
|
||||
|
||||
### Q6 — "If this problem isn't solved in 12 months, what happens?"
|
||||
Verbatim:
|
||||
>
|
||||
|
||||
## 4) Quantitative Discovery (10 min)
|
||||
|
||||
### Q7 — Hours/week on executive reports
|
||||
Answer: ______ hrs/week
|
||||
|
||||
### Q8 — Last decision delayed for missing information + cost of that delay
|
||||
Answer:
|
||||
|
||||
### Q9 — "If a system solved 80% of this, what's a reasonable annual value from your perspective?"
|
||||
**Unprompted figure** (no anchor from you): ______ SAR/year
|
||||
|
||||
### Q10 — "Who would decide to buy? Who could block it?"
|
||||
Decision maker: ______
|
||||
Steps to decision: ______
|
||||
|
||||
## 5) Close (5 min)
|
||||
|
||||
### Q11 — 15-minute Dealix demo next week?
|
||||
[ ] Yes, scheduled: __________
|
||||
[ ] Later: __________
|
||||
[ ] No: reason __________
|
||||
|
||||
### Q12 — Referrals
|
||||
Suggested names: __________
|
||||
|
||||
---
|
||||
|
||||
## Conclusions (written within 24h)
|
||||
|
||||
### Top 5 verbatim quotes
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
5.
|
||||
|
||||
### Pain level (1–10)
|
||||
**Score**: _/10
|
||||
**Rationale**:
|
||||
|
||||
### Trigger event
|
||||
- [ ] Present: __________
|
||||
- [ ] Absent
|
||||
|
||||
### Buyer accessibility
|
||||
- Decision maker: __________
|
||||
- Steps to decision: __________
|
||||
|
||||
### Stated willingness to pay (SAR/yr)
|
||||
- Unprompted: __________
|
||||
- After Dealix described: __________
|
||||
|
||||
### Competing alternatives mentioned
|
||||
-
|
||||
|
||||
### Next step
|
||||
- [ ] Demo scheduled: __________
|
||||
- [ ] Follow-up: __________
|
||||
- [ ] Declined: reason __________
|
||||
|
||||
---
|
||||
|
||||
## Hypotheses impact
|
||||
|
||||
Update only those affected; cite this file in `hypotheses.yaml`:
|
||||
|
||||
- H1 problem real & painful: SUPPORTED / FALSIFIED / AMBIGUOUS — why:
|
||||
- H2 pain ≥ $10K/yr:
|
||||
- H3 buyer reachable ≤3 meetings:
|
||||
- H4 clear trigger event:
|
||||
- H5 ≥ $50K budget:
|
||||
- H6 Arabic-first preferred:
|
||||
- H7 PDPL/ZATCA a stated criterion:
|
||||
- H8 paid pilot accepted:
|
||||
- H9 reference-willing on success:
|
||||
- H10 land-and-expand signals:
|
||||
- H11 value ≥ 10× price:
|
||||
- H12 organic referrals likely:
|
||||
|
||||
---
|
||||
|
||||
## Founder private notes
|
||||
|
||||
>
|
||||
@ -0,0 +1,120 @@
|
||||
# Dealix — Pilot Agreement Template
|
||||
|
||||
> **NOTICE**: Draft. Not a legal document. Must be reviewed and adapted by counsel (FD002) before execution.
|
||||
|
||||
---
|
||||
|
||||
## Parties
|
||||
|
||||
- **Provider**: Dealix (exact entity name per FD001 decision) ("Dealix")
|
||||
- **Customer**: [Company legal name] ("Customer")
|
||||
|
||||
## Effective Date
|
||||
|
||||
[YYYY-MM-DD]
|
||||
|
||||
## Pilot Term
|
||||
|
||||
90 days from the Effective Date. May be extended by mutual written agreement.
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope
|
||||
|
||||
Dealix will provide Customer with access to the Dealix Enterprise Growth OS, including:
|
||||
- Core platform (Deals, Approvals, Evidence, Executive Room)
|
||||
- Saudi Compliance module (PDPL consent tracking, ZATCA invoicing, optional SDAIA/NCA reporting)
|
||||
- Up to [N] named users
|
||||
- Standard onboarding and weekly 30-minute feedback session
|
||||
|
||||
**Excluded** from pilot scope: custom development, dedicated infrastructure, on-premises deployment, custom SLAs above standard.
|
||||
|
||||
## 2. Commercial Terms
|
||||
|
||||
### Design-partner pilots (first 3 customers)
|
||||
- Fee: **zero** during Pilot Term in exchange for obligations in §6.
|
||||
- Credit: **6 months** free on post-pilot Business tier if renewal executed.
|
||||
|
||||
### Paid pilots (customers 4+)
|
||||
- Fee: **1,500 USD** payable upfront (= 50% of Business tier 3-month value).
|
||||
- Credit: Pilot fee fully applied to year-1 subscription if renewed within 30 days of pilot end.
|
||||
|
||||
## 3. Success Criteria
|
||||
|
||||
Per pilot, Dealix and Customer will sign a Success Criteria document (see `pilot_template/success_criteria.md`) **before Kickoff**. Default criteria:
|
||||
- 90%+ Golden Path completion rate across Customer's first 10 partner/deal flows
|
||||
- At least 3 Customer-approved Evidence Packs generated
|
||||
- At least 1 Executive Weekly Pack delivered and acted upon
|
||||
- NPS ≥30 at Pilot end
|
||||
- No P0 incidents attributable to Dealix
|
||||
|
||||
## 4. Data
|
||||
|
||||
- Data residency: me-south-1 (AWS Bahrain) by default. KSA region available on request.
|
||||
- All Customer data remains Customer-owned.
|
||||
- Dealix may use aggregated, de-identified telemetry to improve the platform.
|
||||
- Dealix will NOT use Customer data to train external LLMs.
|
||||
- DPA (Data Processing Agreement) signed alongside this pilot — see `docs/legal/templates/DPA_EN.md`.
|
||||
|
||||
## 5. Privacy & Compliance (KSA-specific)
|
||||
|
||||
- Dealix processes Personal Data in accordance with the PDPL.
|
||||
- Customer is the Data Controller; Dealix is the Data Processor.
|
||||
- Dealix maintains appropriate safeguards (PostgreSQL RLS, encryption at rest + transit, audit logging).
|
||||
- Sub-processor list disclosed at `docs/trust/subprocessors.md` (TBD — Wave E).
|
||||
|
||||
## 6. Customer Obligations (design-partner pilots only)
|
||||
|
||||
In exchange for fee-free pilot, Customer agrees to:
|
||||
1. Designate a named executive sponsor (CFO/COO/GM).
|
||||
2. Attend weekly 30-minute feedback session for 90 days.
|
||||
3. Permit Dealix to record sessions for internal research.
|
||||
4. Upon successful pilot, permit Dealix to:
|
||||
- Publish Customer's name and logo as a reference.
|
||||
- Record a ≤30-minute case study interview.
|
||||
- Invite Customer to speak at first Dealix community event.
|
||||
5. Provide timely feedback on friction, bugs, and requested features.
|
||||
|
||||
## 7. Support & SLA
|
||||
|
||||
- Business-hours support (Sunday–Thursday 09:00–17:00 AST).
|
||||
- Best-effort response target: 4 hours for P1, 1 business day for P2.
|
||||
- Pilots do NOT include 24/7 or weekend pager rotation.
|
||||
|
||||
## 8. Intellectual Property
|
||||
|
||||
- Dealix retains all IP in the platform.
|
||||
- Customer retains all IP in Customer's data and business processes.
|
||||
- Any jointly developed configuration becomes jointly owned; subject to standard non-exclusive license to each party.
|
||||
|
||||
## 9. Confidentiality
|
||||
|
||||
Each party shall protect the other's Confidential Information with the same care as its own, and for no less than 3 years after Pilot end.
|
||||
|
||||
## 10. Termination
|
||||
|
||||
- Either party may terminate with 30 days' written notice.
|
||||
- Customer data export available for 60 days post-termination in JSON + CSV formats.
|
||||
- Design-partner credits forfeited if Customer terminates without cause within first 60 days.
|
||||
|
||||
## 11. Limitation of Liability
|
||||
|
||||
Pilot is provided "as is." Dealix's total liability is capped at the Pilot Fee (or 1,500 USD where no fee paid). Neither party liable for indirect, incidental, or consequential damages.
|
||||
|
||||
## 12. Governing Law
|
||||
|
||||
[Per FD001 selection: KSA law if MISA LLC; DIFC law if DIFC; Delaware law if C-Corp]
|
||||
|
||||
## 13. Dispute Resolution
|
||||
|
||||
Good-faith negotiation for 30 days. Then binding arbitration in [per FD001].
|
||||
|
||||
---
|
||||
|
||||
## Signatures
|
||||
|
||||
**Dealix**
|
||||
Name: ________________ Title: ________________ Date: ________________ Signature: ________________
|
||||
|
||||
**Customer**
|
||||
Name: ________________ Title: ________________ Date: ________________ Signature: ________________
|
||||
@ -0,0 +1,52 @@
|
||||
# Pilot Kickoff Checklist — [CUSTOMER NAME]
|
||||
|
||||
> Complete in the 10 business days before pilot start.
|
||||
> Head of CS owns; Founder reviews.
|
||||
|
||||
---
|
||||
|
||||
## Week −2
|
||||
|
||||
- [ ] Pilot agreement signed (`pilot_agreement_template.md` adapted)
|
||||
- [ ] DPA signed (`docs/legal/templates/DPA_EN.md`)
|
||||
- [ ] Success Criteria document signed (`success_criteria.md`)
|
||||
- [ ] Executive Sponsor + Operational Lead identified and intro'd
|
||||
- [ ] Invoice sent if paid pilot; receipt confirmed
|
||||
|
||||
## Week −1
|
||||
|
||||
- [ ] Tenant provisioned in production (region: me-south-1)
|
||||
- [ ] Named users created with appropriate roles (per RBAC)
|
||||
- [ ] SSO configured (if Wave B shipped and requested)
|
||||
- [ ] ZATCA e-invoicing enabled if applicable
|
||||
- [ ] Integration seeds loaded (industry template if applicable)
|
||||
- [ ] Arabic locale confirmed default for all users
|
||||
- [ ] Demo data seeded in sandbox for training
|
||||
- [ ] Runbook shared with Customer IT (`revenue-activation/deployment/ADMIN_SETUP_GUIDE.md`)
|
||||
|
||||
## Kickoff Day
|
||||
|
||||
- [ ] 90-minute kickoff call with Sponsor + Ops Lead
|
||||
- [ ] Success Criteria re-read aloud
|
||||
- [ ] First Golden Path walked through live
|
||||
- [ ] Weekly 30-min session scheduled for 12 weeks (same time weekly)
|
||||
- [ ] Slack/WhatsApp/Email channel for async support agreed
|
||||
- [ ] Friction Log public link shared so Customer can add entries
|
||||
|
||||
## Week +1
|
||||
|
||||
- [ ] First Golden Path run by Customer (unassisted) logged
|
||||
- [ ] First Evidence Pack generated
|
||||
- [ ] Weekly check-in #1 completed, notes in `friction_log.md`
|
||||
- [ ] Any P0/P1 issues resolved within SLA
|
||||
|
||||
---
|
||||
|
||||
## Red Flags to Halt Kickoff
|
||||
|
||||
Do NOT go live until resolved:
|
||||
- Executive Sponsor is a proxy (delegate), not the named sponsor
|
||||
- Success Criteria unsigned at T-2 days
|
||||
- Named users not provisioned 48h before kickoff
|
||||
- Customer pushing for features outside Scope before first run
|
||||
- Compliance questionnaire unanswered
|
||||
@ -0,0 +1,78 @@
|
||||
# Pilot Success Criteria — [CUSTOMER NAME]
|
||||
|
||||
> Signed before kickoff. Amended only by written mutual agreement.
|
||||
> Drives the Week-12 Phase Gate verdict.
|
||||
|
||||
---
|
||||
|
||||
## Meta
|
||||
|
||||
- **Customer**: ________________
|
||||
- **Executive Sponsor**: ________________ (name + title)
|
||||
- **Operational Lead**: ________________ (name + title)
|
||||
- **Dealix Owner**: ________________ (Head of CS)
|
||||
- **Pilot Start**: ________________ | **Pilot End**: ________________
|
||||
- **Tier**: [ ] Design Partner (free) [ ] Paid ($1,500)
|
||||
|
||||
---
|
||||
|
||||
## Customer's "Why"
|
||||
|
||||
One paragraph, in Customer's own words, describing the problem they hope Dealix solves.
|
||||
|
||||
> ________________________________________________________________
|
||||
> ________________________________________________________________
|
||||
|
||||
---
|
||||
|
||||
## Quantitative Success Criteria
|
||||
|
||||
Complete BEFORE kickoff. Must be measurable at Pilot End.
|
||||
|
||||
| # | Metric | Baseline | Target | Actual | Pass? |
|
||||
|---|--------|----------|--------|--------|-------|
|
||||
| 1 | Golden Path completion rate across first 10 partner flows | — | ≥90% | __ | [ ] |
|
||||
| 2 | Evidence Packs generated and approved | 0 | ≥3 | __ | [ ] |
|
||||
| 3 | Executive Weekly Pack delivered + acted upon | 0 | ≥6 of 12 weeks | __ | [ ] |
|
||||
| 4 | Named users actively using (≥1 action/week) | 0 | ≥[N] | __ | [ ] |
|
||||
| 5 | P0 incidents attributable to Dealix | — | 0 | __ | [ ] |
|
||||
| 6 | NPS at Pilot end | — | ≥30 | __ | [ ] |
|
||||
| 7 | Time to first Golden Path run from onboarding | — | ≤5 business days | __ | [ ] |
|
||||
| 8 | Custom customer metric 1 | __ | __ | __ | [ ] |
|
||||
| 9 | Custom customer metric 2 | __ | __ | __ | [ ] |
|
||||
|
||||
---
|
||||
|
||||
## Qualitative Success Signals
|
||||
|
||||
| # | Signal | Collected via | Pass? |
|
||||
|---|--------|---------------|-------|
|
||||
| 1 | Customer describes Dealix unprompted as a "trusted system of record" for commercial ops | Weekly interview | [ ] |
|
||||
| 2 | At least 1 executive uses the Weekly Pack in a board or leadership meeting | Confirmation + artifact | [ ] |
|
||||
| 3 | Arabic-first UX praised in Customer's own words (capture quote) | Transcript | [ ] |
|
||||
| 4 | Customer willing to take a reference call from another prospect | Direct confirmation | [ ] |
|
||||
| 5 | Customer willing to publish logo + case study | Signed addendum | [ ] |
|
||||
|
||||
---
|
||||
|
||||
## Renewal Intent Capture (Week 11)
|
||||
|
||||
- [ ] Verbal intent expressed by Executive Sponsor
|
||||
- [ ] Verbal intent expressed by Operational Lead
|
||||
- [ ] Proposal requested for year-1 subscription
|
||||
- [ ] Procurement process initiated
|
||||
|
||||
---
|
||||
|
||||
## Verdict (Week 12)
|
||||
|
||||
- [ ] **GREEN** — ≥7 of 9 quantitative met AND ≥3 of 5 qualitative + verbal renewal intent
|
||||
- [ ] **YELLOW** — 5–6 quantitative met; extend pilot 60 days
|
||||
- [ ] **RED** — ≤4 quantitative met; document learnings, do not force renewal
|
||||
|
||||
---
|
||||
|
||||
## Signatures
|
||||
|
||||
Customer Sponsor: ________________ Date: ________________
|
||||
Dealix (Head of CS): ________________ Date: ________________
|
||||
109
salesflow-saas/docs/customer_learnings/pricing_discovery.md
Normal file
109
salesflow-saas/docs/customer_learnings/pricing_discovery.md
Normal file
@ -0,0 +1,109 @@
|
||||
# Dealix — Pricing Discovery Worksheet
|
||||
|
||||
> **Never ask "what would you pay?"** — answer is biased toward zero.
|
||||
> Use Van Westendorp Price Sensitivity Meter (PSM) after 8+ discovery interviews.
|
||||
> Combine with value-based sanity check.
|
||||
|
||||
---
|
||||
|
||||
## 1. Van Westendorp — four questions
|
||||
|
||||
Asked at end of discovery call, after pain + quantitative discovery:
|
||||
|
||||
1. **Too cheap**: "At what annual price would this feel so cheap you'd question its quality or seriousness?"
|
||||
2. **Bargain**: "At what annual price would this be a good deal — strong value for the cost?"
|
||||
3. **Getting expensive**: "At what price would this start feeling expensive, but you'd still consider if the value is there?"
|
||||
4. **Too expensive**: "At what price is it simply too expensive, regardless of value?"
|
||||
|
||||
Record in SAR/year, unprompted. No anchoring from you.
|
||||
|
||||
---
|
||||
|
||||
## 2. Raw data table
|
||||
|
||||
| # | Company | Interview date | Too cheap (SAR/yr) | Bargain | Getting expensive | Too expensive |
|
||||
|---|---------|----------------|--------------------|----|-------------------|---------------|
|
||||
| 1 | | | | | | |
|
||||
| 2 | | | | | | |
|
||||
| 3 | | | | | | |
|
||||
| 4 | | | | | | |
|
||||
| 5 | | | | | | |
|
||||
| 6 | | | | | | |
|
||||
| 7 | | | | | | |
|
||||
| 8 | | | | | | |
|
||||
| 9 | | | | | | |
|
||||
| 10 | | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## 3. Intersections (fill after ≥8 data points)
|
||||
|
||||
Plot cumulative curves; find intersection points.
|
||||
|
||||
| Point | Definition | Value (SAR/yr) |
|
||||
|-------|------------|----------------|
|
||||
| Point of Marginal Cheapness | "too cheap" ∩ "getting expensive" | |
|
||||
| Optimal Price Point (indifference) | "bargain" ∩ "getting expensive" | |
|
||||
| Point of Marginal Expensiveness | "bargain" ∩ "too expensive" | |
|
||||
|
||||
**Acceptable pricing band**: Marginal Cheapness → Marginal Expensiveness.
|
||||
**Initial list price**: start at Optimal Price Point, test both sides.
|
||||
|
||||
---
|
||||
|
||||
## 4. Value-based sanity check (per customer)
|
||||
|
||||
For each interviewed customer, compute:
|
||||
|
||||
```
|
||||
Annual value =
|
||||
(hours_saved_per_week × 52 × avg_hourly_cost_of_role)
|
||||
+ (num_better_decisions × avg_decision_value)
|
||||
+ (risk_avoided_per_year)
|
||||
```
|
||||
|
||||
**Rule**: price ≤ 20% of annual value; customers rarely accept above 25%.
|
||||
|
||||
| Company | Hours saved/wk | Hourly cost | Better decisions/yr | Risk avoided | Annual value | Max price (25%) |
|
||||
|---------|---------------|-------------|---------------------|--------------|--------------|-----------------|
|
||||
| | | | | | | |
|
||||
| | | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## 5. Pricing-model A/B experiment matrix
|
||||
|
||||
After first 5 interviews, prototype and test **one model per prospect** (never three — creates indecision).
|
||||
|
||||
| Model | Structure | Best when |
|
||||
|-------|-----------|-----------|
|
||||
| Per seat | SAR/user/month | Predictable user count, horizontal role |
|
||||
| Per workflow | SAR/workflow/month + seats | Workflow count drives value |
|
||||
| Platform + usage | Base SAR + SAR/approval or SAR/evidence-pack | Usage tracks with realized value |
|
||||
|
||||
Track acceptance rate:
|
||||
|
||||
| Model | Offered to (# prospects) | Continued to demo | Signed pilot |
|
||||
|-------|---------------------------|-------------------|--------------|
|
||||
| Per seat | | | |
|
||||
| Per workflow | | | |
|
||||
| Platform + usage | | | |
|
||||
|
||||
---
|
||||
|
||||
## 6. Red flags in pricing discovery
|
||||
|
||||
- All "too cheap" answers ≥ current plan price → pricing too low; room to raise.
|
||||
- Large gap between "bargain" and "too expensive" across interviews → market isn't segmented yet; stratify by company size/sector.
|
||||
- "Annual value" computed < 5× price → cannot justify the ROI pitch; either raise value or lower price.
|
||||
- Customer names zero competing tools → category is unknown to them; education cost is your hidden CAC.
|
||||
|
||||
---
|
||||
|
||||
## 7. Pricing decision log
|
||||
|
||||
Every price change logged here with reason + evidence:
|
||||
|
||||
| Date | Tier | Old price | New price | Reason | Evidence source |
|
||||
|------|------|-----------|-----------|--------|-----------------|
|
||||
| | | | | | |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user