system-prompts-and-models-o.../salesflow-saas/backend/app/core/database.py
Sami Assiri 8075e63c7e fix(db): prevent duplicate audit seed on repeated init_db() calls
Guard: only seed audit chain if audit_log is empty.
Prevents chain breaks when server restarts re-run init_db().
2026-04-20 06:38:40 +00:00

565 lines
23 KiB
Python

"""Dealix Database Core — SQLite with full schema for 9 OS modules"""
import sqlite3
import hashlib
import json
import time
from contextlib import contextmanager
from pathlib import Path
DB_PATH = Path(__file__).parent.parent.parent / "dealix.db"
def get_connection():
conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=DELETE")
conn.execute("PRAGMA foreign_keys=ON")
return conn
@contextmanager
def db():
conn = get_connection()
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def init_db():
with db() as conn:
conn.executescript("""
-- Users & Auth
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'sales',
org_id TEXT NOT NULL DEFAULT 'dealix',
password_hash TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
-- ============================================================
-- 1. REVENUE OS
-- ============================================================
CREATE TABLE IF NOT EXISTS leads (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
company_name TEXT NOT NULL,
contact_name TEXT,
contact_email TEXT,
contact_phone TEXT,
source TEXT DEFAULT 'website',
industry TEXT,
company_size TEXT,
annual_revenue TEXT,
region TEXT,
status TEXT DEFAULT 'new',
score INTEGER DEFAULT 0,
stage TEXT DEFAULT 'intake',
assigned_to TEXT,
notes TEXT,
enriched_data TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS deals (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
lead_id TEXT,
title TEXT NOT NULL,
value REAL DEFAULT 0,
currency TEXT DEFAULT 'SAR',
stage TEXT DEFAULT 'discovery',
probability INTEGER DEFAULT 0,
close_date TEXT,
owner_id TEXT,
account_id TEXT,
notes TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS accounts (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
company_name TEXT NOT NULL,
industry TEXT,
tier TEXT DEFAULT 'standard',
arr REAL DEFAULT 0,
health_score INTEGER DEFAULT 75,
csm_id TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- ============================================================
-- 2. PRICING & MARGIN CONTROL OS
-- ============================================================
CREATE TABLE IF NOT EXISTS quotes (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
deal_id TEXT,
account_id TEXT,
line_items TEXT,
subtotal REAL DEFAULT 0,
discount_pct REAL DEFAULT 0,
discount_reason TEXT,
final_price REAL DEFAULT 0,
margin_pct REAL DEFAULT 0,
approval_status TEXT DEFAULT 'pending',
approved_by TEXT,
approved_at TEXT,
valid_until TEXT,
created_by TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS discount_policies (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
max_discount_pct REAL NOT NULL,
approver_role TEXT NOT NULL,
deal_value_min REAL DEFAULT 0,
deal_value_max REAL,
active INTEGER DEFAULT 1
);
-- ============================================================
-- 3. PARTNERSHIP & ALLIANCE OS
-- ============================================================
CREATE TABLE IF NOT EXISTS partners (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
company_name TEXT NOT NULL,
partner_type TEXT DEFAULT 'reseller',
status TEXT DEFAULT 'prospect',
fit_score INTEGER DEFAULT 0,
revenue_contribution REAL DEFAULT 0,
health_score INTEGER DEFAULT 75,
contact_name TEXT,
contact_email TEXT,
notes TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS alliance_workflows (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
partner_id TEXT NOT NULL,
stage TEXT DEFAULT 'scouting',
economics_model TEXT,
term_sheet TEXT,
approval_status TEXT DEFAULT 'pending',
approved_by TEXT,
activation_date TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- ============================================================
-- 4. PROCUREMENT / VENDOR OS
-- ============================================================
CREATE TABLE IF NOT EXISTS vendors (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
vendor_name TEXT NOT NULL,
category TEXT,
risk_level TEXT DEFAULT 'medium',
spend REAL DEFAULT 0,
health_score INTEGER DEFAULT 75,
contract_expiry TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS procurement_requests (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
vendor_id TEXT,
title TEXT NOT NULL,
amount REAL NOT NULL,
justification TEXT,
status TEXT DEFAULT 'draft',
approval_status TEXT DEFAULT 'pending',
approved_by TEXT,
approved_at TEXT,
created_by TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- ============================================================
-- 5. RENEWAL & EXPANSION OS
-- ============================================================
CREATE TABLE IF NOT EXISTS renewals (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
account_id TEXT NOT NULL,
current_arr REAL DEFAULT 0,
renewal_date TEXT,
churn_risk_score INTEGER DEFAULT 0,
expansion_score INTEGER DEFAULT 0,
status TEXT DEFAULT 'upcoming',
rescue_play_active INTEGER DEFAULT 0,
assigned_to TEXT,
notes TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- ============================================================
-- 6. EXPANSION / MARKET ENTRY OS
-- ============================================================
CREATE TABLE IF NOT EXISTS market_entries (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
market_name TEXT NOT NULL,
segment TEXT,
readiness_score INTEGER DEFAULT 0,
status TEXT DEFAULT 'scanning',
gtm_plan TEXT,
launch_date TEXT,
stop_loss_triggered INTEGER DEFAULT 0,
actual_vs_forecast TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- ============================================================
-- 7. M&A / CORPORATE DEVELOPMENT OS
-- ============================================================
CREATE TABLE IF NOT EXISTS ma_targets (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
target_name TEXT NOT NULL,
industry TEXT,
estimated_value REAL,
fit_score INTEGER DEFAULT 0,
stage TEXT DEFAULT 'screening',
dd_findings TEXT,
valuation_memo TEXT,
synergy_model TEXT,
ic_pack_status TEXT DEFAULT 'pending',
board_pack_ready INTEGER DEFAULT 0,
close_date TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- ============================================================
-- 8. PMI / STRATEGIC PMO OS
-- ============================================================
CREATE TABLE IF NOT EXISTS pmo_projects (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
title TEXT NOT NULL,
type TEXT DEFAULT 'pmi',
status TEXT DEFAULT 'active',
day1_readiness INTEGER DEFAULT 0,
plan_30_60_90 TEXT,
synergy_target REAL DEFAULT 0,
synergy_realized REAL DEFAULT 0,
blockers TEXT,
health TEXT DEFAULT 'green',
created_at TEXT DEFAULT (datetime('now'))
);
-- ============================================================
-- 9. EXECUTIVE / BOARD OS
-- ============================================================
CREATE TABLE IF NOT EXISTS approvals (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
module TEXT NOT NULL,
reference_id TEXT NOT NULL,
title TEXT NOT NULL,
amount REAL,
risk_level TEXT DEFAULT 'medium',
status TEXT DEFAULT 'pending',
requested_by TEXT,
approved_by TEXT,
decision_at TEXT,
evidence_pack TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS executive_packs (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
week_label TEXT,
actual_revenue REAL DEFAULT 0,
forecast_revenue REAL DEFAULT 0,
open_approvals INTEGER DEFAULT 0,
blockers TEXT,
next_best_actions TEXT,
risk_heatmap TEXT,
generated_at TEXT DEFAULT (datetime('now'))
);
-- ============================================================
-- AUDIT CHAIN (cross-module)
-- ============================================================
-- =============================================
-- Revenue Intelligence OS — Lead Machine Tables
-- =============================================
-- ICP configs per org
CREATE TABLE IF NOT EXISTS icp_configs (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
name TEXT NOT NULL,
config TEXT NOT NULL, -- JSON ICPConfig
is_active INTEGER DEFAULT 1,
created_by TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- Discovered leads (raw, before enrichment)
CREATE TABLE IF NOT EXISTS intelligence_leads (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
company_name TEXT NOT NULL,
domain TEXT,
industry TEXT,
region TEXT,
company_size TEXT,
description TEXT,
website TEXT,
tech_stack TEXT, -- JSON list
signals TEXT, -- JSON list
recent_news TEXT, -- JSON list
contact_name TEXT,
contact_title TEXT,
contact_email TEXT,
contact_phone TEXT,
contact_linkedin TEXT,
decision_maker_score INTEGER DEFAULT 0,
enrichment_source TEXT DEFAULT 'web',
enrichment_confidence REAL DEFAULT 0.5,
source TEXT,
source_url TEXT,
raw_snippet TEXT,
trigger TEXT,
-- Scores
score_fit INTEGER DEFAULT 0,
score_intent INTEGER DEFAULT 0,
score_access INTEGER DEFAULT 0,
score_value INTEGER DEFAULT 0,
score_urgency INTEGER DEFAULT 0,
score_master INTEGER DEFAULT 0,
priority_tier TEXT DEFAULT 'P4',
score_reasons TEXT, -- JSON list
next_action TEXT,
next_action_ar TEXT,
-- Outreach
outreach_whatsapp_ar TEXT,
outreach_email_subject_ar TEXT,
outreach_email_body_ar TEXT,
outreach_linkedin_ar TEXT,
outreach_angle TEXT,
-- Pipeline tracking
pipeline_run_id TEXT,
crm_lead_id TEXT, -- linked to leads table
status TEXT DEFAULT 'discovered', -- discovered | contacted | qualified | archived
reviewed_by TEXT,
reviewed_at TEXT,
enriched_at TEXT,
discovered_at TEXT DEFAULT (datetime('now')),
created_at TEXT DEFAULT (datetime('now'))
);
-- Pipeline run history
CREATE TABLE IF NOT EXISTS intelligence_runs (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
icp_id TEXT,
run_mode TEXT DEFAULT 'auto', -- auto | manual | triggered
motion TEXT DEFAULT 'sales',
total_discovered INTEGER DEFAULT 0,
total_deduped INTEGER DEFAULT 0,
total_enriched INTEGER DEFAULT 0,
tier_p1 INTEGER DEFAULT 0,
tier_p2 INTEGER DEFAULT 0,
tier_p3 INTEGER DEFAULT 0,
tier_p4 INTEGER DEFAULT 0,
duration_sec REAL DEFAULT 0,
status TEXT DEFAULT 'running', -- running | complete | error
error_message TEXT,
created_by TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- Watchlist for trigger alerts
CREATE TABLE IF NOT EXISTS intelligence_watchlist (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
company_name TEXT NOT NULL,
domain TEXT,
priority INTEGER DEFAULT 0,
last_scanned TEXT,
active INTEGER DEFAULT 1,
added_by TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- Trigger events detected
CREATE TABLE IF NOT EXISTS intelligence_triggers (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
company_name TEXT NOT NULL,
trigger_type TEXT NOT NULL,
trigger_label_ar TEXT,
signal_strength INTEGER DEFAULT 0,
evidence TEXT,
source_url TEXT,
recommended_action_ar TEXT,
recommended_action_en TEXT,
is_actioned INTEGER DEFAULT 0,
actioned_by TEXT,
detected_at TEXT DEFAULT (datetime('now'))
);
-- Entity registry (deduplication)
CREATE TABLE IF NOT EXISTS intelligence_entities (
id TEXT PRIMARY KEY,
canonical_name TEXT NOT NULL,
normalized_name TEXT,
domain TEXT,
aliases TEXT, -- JSON list
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL,
module TEXT NOT NULL,
action TEXT NOT NULL,
actor_id TEXT,
resource_id TEXT,
payload TEXT,
prev_hash TEXT,
entry_hash TEXT,
ts TEXT DEFAULT (datetime('now'))
);
""")
# Seed admin users
import hashlib, uuid
users = [
("admin-001", "admin@dealix.io", "Admin", "admin"),
("mgr-001", "manager@dealix.io", "Manager", "manager"),
("sales-001", "sales@dealix.io", "Sales Rep", "sales"),
]
passwords = {"admin": "Admin1234!", "manager": "Manager1234!", "sales": "Sales1234!"}
for uid, email, name, role in users:
pw = hashlib.sha256(passwords[role].encode()).hexdigest()
conn.execute("""
INSERT OR IGNORE INTO users (id, email, name, role, password_hash)
VALUES (?, ?, ?, ?, ?)
""", (uid, email, name, role, pw))
# Seed discount policies
conn.execute("""
INSERT OR IGNORE INTO discount_policies (id, org_id, max_discount_pct, approver_role, deal_value_min, deal_value_max)
VALUES ('dp-1','dealix',10,'sales',0,50000),
('dp-2','dealix',20,'manager',50000,200000),
('dp-3','dealix',35,'admin',200000,NULL)
""")
# Seed sample data for dashboard
_seed_sample_data(conn)
def _seed_sample_data(conn):
import uuid
# Sample leads
leads = [
("lead-001","dealix","البنك الأهلي","محمد الغامدي","m@anb.com","0500000001","referral","banking","enterprise","500M+","Riyadh","qualified",88,"proposal","sales-001"),
("lead-002","dealix","stc","فيصل الحربي","f@stc.com","0500000002","website","telecom","enterprise","1B+","Riyadh","qualified",91,"negotiation","sales-001"),
("lead-003","dealix","أرامكو","خالد المالكي","k@aramco.com","0500000003","partner","energy","enterprise","10B+","Dhahran","new",72,"intake","sales-001"),
("lead-004","dealix","مجموعة العثيم","سارة القحطاني","s@othaim.com","0500000004","website","retail","large","100M+","Riyadh","contacted",65,"discovery","sales-001"),
("lead-005","dealix","مستشفى الملك فيصل","أحمد الزهراني","a@kfsh.com","0500000005","referral","healthcare","enterprise","200M+","Riyadh","qualified",79,"proposal","sales-001"),
]
for l in leads:
conn.execute("""INSERT OR IGNORE INTO leads
(id,org_id,company_name,contact_name,contact_email,contact_phone,source,industry,company_size,annual_revenue,region,status,score,stage,assigned_to)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", l)
# Sample deals
deals = [
("deal-001","dealix","lead-001","صفقة البنك الأهلي — Revenue OS",850000,"SAR","proposal",75,"2026-06-30","sales-001","acc-001"),
("deal-002","dealix","lead-002","stc — Enterprise Suite",1200000,"SAR","negotiation",85,"2026-05-31","sales-001","acc-002"),
("deal-003","dealix","lead-003","أرامكو — Executive OS",2500000,"SAR","discovery",40,"2026-09-30","sales-001","acc-003"),
("deal-004","dealix","lead-005","KFSH — Procurement OS",420000,"SAR","proposal",65,"2026-07-15","sales-001","acc-004"),
]
for d in deals:
conn.execute("""INSERT OR IGNORE INTO deals
(id,org_id,lead_id,title,value,currency,stage,probability,close_date,owner_id,account_id)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""", d)
# Sample accounts
accounts = [
("acc-001","dealix","البنك الأهلي","banking","enterprise",850000,82,"sales-001"),
("acc-002","dealix","stc","telecom","strategic",1200000,91,"sales-001"),
("acc-003","dealix","أرامكو","energy","strategic",0,88,"sales-001"),
("acc-004","dealix","KFSH","healthcare","enterprise",420000,74,"sales-001"),
]
for a in accounts:
conn.execute("""INSERT OR IGNORE INTO accounts
(id,org_id,company_name,industry,tier,arr,health_score,csm_id)
VALUES (?,?,?,?,?,?,?,?)""", a)
# Sample partners
partners = [
("part-001","dealix","Oracle Arabia","technology","active",87,320000,82,"علي الدوسري","a@oracle.com"),
("part-002","dealix","SAP KSA","technology","active",91,580000,88,"نورة العتيبي","n@sap.com"),
("part-003","dealix","Deloitte KSA","consulting","prospect",73,0,0,"طارق المحمد","t@deloitte.com"),
]
for p in partners:
conn.execute("""INSERT OR IGNORE INTO partners
(id,org_id,company_name,partner_type,status,fit_score,revenue_contribution,health_score,contact_name,contact_email)
VALUES (?,?,?,?,?,?,?,?,?,?)""", p)
# Sample M&A targets
conn.execute("""INSERT OR IGNORE INTO ma_targets
(id,org_id,target_name,industry,estimated_value,fit_score,stage)
VALUES ('ma-001','dealix','Salesbook KSA','SaaS',8500000,84,'due_diligence')""")
# Sample renewals
conn.execute("""INSERT OR IGNORE INTO renewals
(id,org_id,account_id,current_arr,renewal_date,churn_risk_score,expansion_score,status)
VALUES ('ren-001','dealix','acc-001',850000,'2026-12-31',22,67,'upcoming'),
('ren-002','dealix','acc-002',1200000,'2026-10-31',8,88,'upcoming')""")
# Sample approvals
conn.execute("""INSERT OR IGNORE INTO approvals
(id,org_id,module,reference_id,title,amount,risk_level,status,requested_by)
VALUES
('appr-001','dealix','pricing','deal-001','خصم 25% — البنك الأهلي',212500,'high','pending','sales-001'),
('appr-002','dealix','procurement','pr-001','تجديد عقد Oracle',145000,'medium','pending','mgr-001'),
('appr-003','dealix','partnership','part-003','تفعيل شراكة Deloitte',0,'low','pending','mgr-001')""")
# Executive pack
conn.execute("""INSERT OR IGNORE INTO executive_packs
(id,org_id,week_label,actual_revenue,forecast_revenue,open_approvals,blockers,next_best_actions)
VALUES ('ep-001','dealix','الأسبوع 16 — 2026',3850000,4200000,3,
'["صفقة أرامكو: تأخر RFP","تجديد Oracle: انتهاء العقد في 30 يوم"]',
'["أغلق خصم البنك الأهلي — 25%","ادفع تجديد Oracle قبل الانتهاء","جدول kickoff مع Deloitte"]')""")
# Audit chain seed — only insert if no entries exist yet
existing = conn.execute("SELECT COUNT(*) FROM audit_log").fetchone()[0]
if existing > 0:
return
prev = "GENESIS"
entries = [
("dealix","revenue","lead_created","admin-001","lead-001","{}"),
("dealix","pricing","quote_created","sales-001","deal-001","{}"),
("dealix","partnership","partner_added","mgr-001","part-001","{}"),
("dealix","executive","pack_generated","admin-001","ep-001","{}"),
]
for org, module, action, actor, resource, payload in entries:
import hashlib, time
content = f"{org}:{module}:{action}:{actor}:{resource}:{time.time()}"
entry_hash = hashlib.sha256(f"{prev}:{content}".encode()).hexdigest()
conn.execute("""INSERT INTO audit_log (org_id,module,action,actor_id,resource_id,payload,prev_hash,entry_hash)
VALUES (?,?,?,?,?,?,?,?)""", (org, module, action, actor, resource, payload, prev, entry_hash))
prev = entry_hash