system-prompts-and-models-o.../dealix/db/models.py
2026-05-01 14:03:52 +03:00

488 lines
26 KiB
Python

"""
SQLAlchemy 2.0 async ORM models.
نماذج قاعدة البيانات.
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import JSON, Boolean, Float, ForeignKey, Integer, String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from core.utils import utcnow
class Base(DeclarativeBase):
"""Base class for all models."""
class LeadRecord(Base):
__tablename__ = "leads"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
source: Mapped[str] = mapped_column(String(32), index=True)
company_name: Mapped[str] = mapped_column(String(255), default="")
contact_name: Mapped[str] = mapped_column(String(255), default="")
contact_email: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
contact_phone: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
sector: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
region: Mapped[str | None] = mapped_column(String(128), nullable=True)
company_size: Mapped[str | None] = mapped_column(String(32), nullable=True)
budget: Mapped[float | None] = mapped_column(Float, nullable=True)
status: Mapped[str] = mapped_column(String(32), default="new", index=True)
fit_score: Mapped[float] = mapped_column(Float, default=0.0)
urgency_score: Mapped[float] = mapped_column(Float, default=0.0)
locale: Mapped[str] = mapped_column(String(4), default="ar")
message: Mapped[str | None] = mapped_column(Text, nullable=True)
pain_points: Mapped[list] = mapped_column(JSON, default=list)
meta_json: Mapped[dict] = mapped_column("metadata", JSON, default=dict)
dedup_hash: Mapped[str] = mapped_column(String(32), default="", index=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
deals: Mapped[list[DealRecord]] = relationship(back_populates="lead")
class DealRecord(Base):
__tablename__ = "deals"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
lead_id: Mapped[str] = mapped_column(ForeignKey("leads.id"), index=True)
hubspot_deal_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
hubspot_contact_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
amount: Mapped[float] = mapped_column(Float, default=0.0)
currency: Mapped[str] = mapped_column(String(8), default="SAR")
stage: Mapped[str] = mapped_column(String(64), default="new")
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
lead: Mapped[LeadRecord] = relationship(back_populates="deals")
class AgentRunRecord(Base):
"""Audit log — every agent invocation."""
__tablename__ = "agent_runs"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
agent_name: Mapped[str] = mapped_column(String(64), index=True)
lead_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
status: Mapped[str] = mapped_column(String(16), default="success")
duration_ms: Mapped[float | None] = mapped_column(Float, nullable=True)
input_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
output_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)
class ConversationRecord(Base):
"""Inbound message + outbound auto-response — full audit log."""
__tablename__ = "conversations"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
lead_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
channel: Mapped[str] = mapped_column(String(32), index=True) # whatsapp/email/form/sms/linkedin
sender: Mapped[str | None] = mapped_column(String(255), nullable=True)
inbound_message: Mapped[str] = mapped_column(Text, default="")
outbound_response: Mapped[str | None] = mapped_column(Text, nullable=True)
classification: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
sentiment: Mapped[str | None] = mapped_column(String(16), nullable=True)
next_action: Mapped[str | None] = mapped_column(String(64), nullable=True)
escalation_required: Mapped[bool] = mapped_column(default=False)
auto_sent: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)
class TaskRecord(Base):
"""Follow-up tasks scheduled by the autonomous engine."""
__tablename__ = "tasks"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
lead_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
deal_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
task_type: Mapped[str] = mapped_column(String(32), index=True) # follow_up, demo, payment_check, onboarding
due_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)
status: Mapped[str] = mapped_column(String(16), default="pending", index=True) # pending, done, skipped
owner: Mapped[str] = mapped_column(String(64), default="auto")
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
class CompanyRecord(Base):
"""Subscriber company profile — one per Dealix customer."""
__tablename__ = "companies"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
name: Mapped[str] = mapped_column(String(255))
website: Mapped[str | None] = mapped_column(String(255), nullable=True)
industry: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
country: Mapped[str | None] = mapped_column(String(64), nullable=True)
city: Mapped[str | None] = mapped_column(String(128), nullable=True)
products: Mapped[str | None] = mapped_column(Text, nullable=True)
target_customer_type: Mapped[str | None] = mapped_column(Text, nullable=True)
average_deal_value: Mapped[float | None] = mapped_column(Float, nullable=True)
sales_cycle_length_days: Mapped[float | None] = mapped_column(Float, nullable=True)
current_lead_sources: Mapped[str | None] = mapped_column(Text, nullable=True)
current_crm: Mapped[str | None] = mapped_column(String(64), nullable=True)
booking_link: Mapped[str | None] = mapped_column(String(255), nullable=True)
sales_team_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
whatsapp_number: Mapped[str | None] = mapped_column(String(32), nullable=True)
tone_of_voice: Mapped[str] = mapped_column(String(64), default="professional_khaliji")
languages: Mapped[str] = mapped_column(String(64), default="ar,en")
pricing_rules: Mapped[str | None] = mapped_column(Text, nullable=True)
handoff_rules: Mapped[str | None] = mapped_column(Text, nullable=True)
privacy_requirements: Mapped[str | None] = mapped_column(Text, nullable=True)
success_metric: Mapped[str | None] = mapped_column(Text, nullable=True)
icp_profile: Mapped[dict] = mapped_column("icp_profile", JSON, default=dict)
channel_plan: Mapped[dict] = mapped_column("channel_plan", JSON, default=dict)
offer_ladder: Mapped[dict] = mapped_column("offer_ladder", JSON, default=dict)
automation_policy: Mapped[dict] = mapped_column("automation_policy", JSON, default=dict)
status: Mapped[str] = mapped_column(String(32), default="active", index=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class PartnerRecord(Base):
"""Partner/agency record for distribution channel."""
__tablename__ = "partners"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
company_name: Mapped[str] = mapped_column(String(255))
partner_type: Mapped[str] = mapped_column(String(32), index=True) # AGENCY/IMPLEMENTATION/REFERRAL/STRATEGIC
contact_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
contact_email: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
status: Mapped[str] = mapped_column(String(32), default="prospecting", index=True) # prospecting/active/paused
commission_terms: Mapped[str | None] = mapped_column(Text, nullable=True)
setup_fee_sar: Mapped[float] = mapped_column(Float, default=0.0)
mrr_share_pct: Mapped[float] = mapped_column(Float, default=0.0)
clients_signed: Mapped[int] = mapped_column(default=0)
next_action: Mapped[str | None] = mapped_column(String(64), nullable=True)
next_action_at: Mapped[datetime | None] = mapped_column(nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class CustomerRecord(Base):
"""Customer = subscribed paying company. One per closed deal."""
__tablename__ = "customers"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
company_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
deal_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
plan: Mapped[str] = mapped_column(String(32), default="pilot") # pilot/starter/growth/scale
onboarding_status: Mapped[str] = mapped_column(String(32), default="kickoff_pending", index=True)
pilot_start_at: Mapped[datetime | None] = mapped_column(nullable=True)
pilot_end_at: Mapped[datetime | None] = mapped_column(nullable=True)
success_metric: Mapped[str | None] = mapped_column(Text, nullable=True)
daily_report_sent: Mapped[int] = mapped_column(default=0)
nps_score: Mapped[int | None] = mapped_column(nullable=True)
churn_risk: Mapped[str] = mapped_column(String(16), default="low") # low/medium/high
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class OutreachQueueRecord(Base):
"""Outreach message queue — auto or human-approval."""
__tablename__ = "outreach_queue"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
lead_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
channel: Mapped[str] = mapped_column(String(32), index=True)
message: Mapped[str] = mapped_column(Text)
approval_required: Mapped[bool] = mapped_column(default=True)
status: Mapped[str] = mapped_column(String(32), default="queued", index=True) # queued/approved/sent/skipped
due_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)
sent_at: Mapped[datetime | None] = mapped_column(nullable=True)
risk_reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
# ── Data Lake + Lead Graph (Phase 12) ──────────────────────────────
# Compliant ingestion: every row carries source/allowed_use/risk/opt-out.
class RawLeadImport(Base):
"""One row per uploaded dataset (CSV/Excel/JSON)."""
__tablename__ = "raw_lead_imports"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
source_name: Mapped[str] = mapped_column(String(255), index=True)
source_type: Mapped[str] = mapped_column(String(32), index=True) # owned/public/paid/partner/google_maps/google_search/manual
file_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
imported_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
allowed_use: Mapped[str] = mapped_column(String(128), default="business_contact_research_only")
consent_status: Mapped[str] = mapped_column(String(32), default="unknown") # unknown/opted_in/legitimate_interest/owned
risk_level: Mapped[str] = mapped_column(String(16), default="medium", index=True) # low/medium/high
rows_total: Mapped[int] = mapped_column(Integer, default=0)
rows_normalized: Mapped[int] = mapped_column(Integer, default=0)
rows_rejected: Mapped[int] = mapped_column(Integer, default=0)
rows_duplicate: Mapped[int] = mapped_column(Integer, default=0)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[str] = mapped_column(String(32), default="raw", index=True) # raw/normalizing/normalized/deduped/done/error
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class RawLeadRow(Base):
"""One row per record inside an import."""
__tablename__ = "raw_lead_rows"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
import_id: Mapped[str] = mapped_column(String(64), index=True)
raw_json: Mapped[dict] = mapped_column(JSON, default=dict)
normalized_status: Mapped[str] = mapped_column(String(32), default="pending", index=True) # pending/ok/rejected/duplicate
account_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
error: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
class AccountRecord(Base):
"""Canonical company entity in the lead graph."""
__tablename__ = "accounts"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
company_name: Mapped[str] = mapped_column(String(255), index=True)
normalized_name: Mapped[str] = mapped_column(String(255), index=True)
domain: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
website: Mapped[str | None] = mapped_column(String(500), nullable=True)
city: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
country: Mapped[str | None] = mapped_column(String(64), nullable=True, default="SA")
sector: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
google_place_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
source_count: Mapped[int] = mapped_column(Integer, default=1)
best_source: Mapped[str | None] = mapped_column(String(64), nullable=True)
risk_level: Mapped[str] = mapped_column(String(16), default="medium", index=True)
status: Mapped[str] = mapped_column(String(32), default="new", index=True) # new/enriched/qualified/blocked
data_quality_score: Mapped[float] = mapped_column(Float, default=0.0)
extra: Mapped[dict] = mapped_column("extra_json", JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class ContactRecord(Base):
"""Person attached to an account. PDPL-aware: opt_out + consent_status mandatory."""
__tablename__ = "contacts"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
account_id: Mapped[str] = mapped_column(String(64), index=True)
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
role: Mapped[str | None] = mapped_column(String(128), nullable=True)
email: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
phone: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
linkedin_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
source: Mapped[str] = mapped_column(String(64), default="manual", index=True)
consent_status: Mapped[str] = mapped_column(String(32), default="unknown", index=True)
opt_out: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
risk_level: Mapped[str] = mapped_column(String(16), default="medium")
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class SignalRecord(Base):
"""Time-series signals attached to an account (tech, intent, hire, news)."""
__tablename__ = "signals"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
account_id: Mapped[str] = mapped_column(String(64), index=True)
signal_type: Mapped[str] = mapped_column(String(64), index=True) # tech/intent/hire/news/funding/integration
signal_value: Mapped[str] = mapped_column(String(500))
source_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
confidence: Mapped[float] = mapped_column(Float, default=0.5)
detected_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)
class LeadScoreRecord(Base):
"""Latest score per account (one current row per account)."""
__tablename__ = "lead_scores"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
account_id: Mapped[str] = mapped_column(String(64), index=True)
fit_score: Mapped[float] = mapped_column(Float, default=0.0)
intent_score: Mapped[float] = mapped_column(Float, default=0.0)
urgency_score: Mapped[float] = mapped_column(Float, default=0.0)
risk_score: Mapped[float] = mapped_column(Float, default=0.0)
total_score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
priority: Mapped[str] = mapped_column(String(8), default="P3", index=True) # P0/P1/P2/P3/BACKLOG
recommended_channel: Mapped[str | None] = mapped_column(String(32), nullable=True)
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)
class SuppressionRecord(Base):
"""Opt-out / do-not-contact list. Checked before any outbound queue write."""
__tablename__ = "data_suppression_list"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
email: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
phone: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
domain: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
reason: Mapped[str] = mapped_column(String(128), default="opt_out")
created_at: Mapped[datetime] = mapped_column(default=utcnow)
class GmailDraftRecord(Base):
"""Gmail draft created by the revenue machine — Sami reviews + sends."""
__tablename__ = "gmail_drafts"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
account_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
queue_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
to_email: Mapped[str] = mapped_column(String(255), index=True)
subject: Mapped[str] = mapped_column(String(500))
body_plain: Mapped[str] = mapped_column(Text)
sender_email: Mapped[str] = mapped_column(String(255), default="")
gmail_draft_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
gmail_message_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
status: Mapped[str] = mapped_column(String(32), default="created", index=True)
# created | reviewed | sent | discarded | failed
sequence_step: Mapped[int] = mapped_column(Integer, default=0)
sent_at: Mapped[datetime | None] = mapped_column(nullable=True, index=True)
discarded_reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class LinkedInDraftRecord(Base):
"""LinkedIn drafts — manual send only (no automation per LinkedIn ToS)."""
__tablename__ = "linkedin_drafts"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
account_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
company_name: Mapped[str] = mapped_column(String(255))
contact_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
profile_search_query: Mapped[str] = mapped_column(String(500))
company_context: Mapped[str | None] = mapped_column(Text, nullable=True)
reason_for_outreach: Mapped[str | None] = mapped_column(Text, nullable=True)
message_ar: Mapped[str] = mapped_column(Text)
message_en: Mapped[str | None] = mapped_column(Text, nullable=True)
followup_day_3: Mapped[str | None] = mapped_column(Text, nullable=True)
followup_day_7: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[str] = mapped_column(String(32), default="draft", index=True)
# draft | sent | replied | unreachable
sent_at: Mapped[datetime | None] = mapped_column(nullable=True)
reply_text: Mapped[str | None] = mapped_column(Text, nullable=True)
reply_received_at: Mapped[datetime | None] = mapped_column(nullable=True)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class DataSourceProvenance(Base):
"""Provenance record — every signal/account/contact pointer back to source.
Required for PDPL audit trail + to dedupe "same lead from 3 sources".
"""
__tablename__ = "data_source_provenance"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
entity_type: Mapped[str] = mapped_column(String(32), index=True) # account|contact|signal|deal
entity_id: Mapped[str] = mapped_column(String(64), index=True)
source_name: Mapped[str] = mapped_column(String(255), index=True)
source_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
source_type: Mapped[str] = mapped_column(String(32)) # public|paid|partner|owned|google_maps|google_search|manual
allowed_use: Mapped[str] = mapped_column(String(128), default="business_contact_research_only")
collected_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)
confidence: Mapped[float] = mapped_column(Float, default=0.5)
refresh_needed: Mapped[bool] = mapped_column(Boolean, default=False)
class EmailSendLog(Base):
"""Auditable log of every email send attempt — required for compliance + bounce tracking."""
__tablename__ = "email_send_log"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
account_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
queue_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
to_email: Mapped[str] = mapped_column(String(255), index=True)
subject: Mapped[str] = mapped_column(String(500))
body_preview: Mapped[str] = mapped_column(Text, default="")
sender_email: Mapped[str] = mapped_column(String(255), default="")
status: Mapped[str] = mapped_column(String(32), default="queued", index=True)
# queued | sent | bounced | replied | opt_out | blocked_compliance | failed
gmail_message_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
bounce_reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
reply_classification: Mapped[str | None] = mapped_column(String(64), nullable=True)
reply_received_at: Mapped[datetime | None] = mapped_column(nullable=True)
sent_at: Mapped[datetime | None] = mapped_column(nullable=True, index=True)
batch_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
sequence_step: Mapped[int] = mapped_column(Integer, default=0) # 0=initial, 2/5/10 for follow-ups
compliance_check: Mapped[dict] = mapped_column("compliance_check_json", JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class WebhookSubscriptionRecord(Base):
"""Outbound webhook subscription — Scale tier ecosystem play.
Customers register an HTTPS endpoint + secret. Dealix POSTs signed events
when matching activity occurs (lead.created, deal.won, payment.received...).
"""
__tablename__ = "webhook_subscriptions"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
customer_id: Mapped[str] = mapped_column(String(64), index=True)
endpoint_url: Mapped[str] = mapped_column(String(500))
secret: Mapped[str] = mapped_column(String(128)) # HMAC signing key — never exposed back
events: Mapped[list] = mapped_column(JSON, default=list) # empty list = all events
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
enabled: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
last_delivery_at: Mapped[datetime | None] = mapped_column(nullable=True)
last_status_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
consecutive_failures: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(default=utcnow)
updated_at: Mapped[datetime] = mapped_column(default=utcnow, onupdate=utcnow)
class ProofLedgerEventRecord(Base):
"""أحداث إثبات إيراد — تقديرات تشغيلية حتى ربط CRM محاسبي."""
__tablename__ = "proof_ledger_events"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
event_type: Mapped[str] = mapped_column(String(64), index=True)
revenue_influenced_sar_estimate: Mapped[float] = mapped_column(Float, default=0.0)
notes_ar: Mapped[str] = mapped_column(Text, default="")
extra_json: Mapped[dict] = mapped_column(JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)
class WebhookDeliveryRecord(Base):
"""Per-attempt delivery audit — debug + replay support."""
__tablename__ = "webhook_deliveries"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
subscription_id: Mapped[str] = mapped_column(String(64), index=True)
customer_id: Mapped[str] = mapped_column(String(64), index=True)
event_id: Mapped[str] = mapped_column(String(64), index=True)
event_type: Mapped[str] = mapped_column(String(64), index=True)
attempt: Mapped[int] = mapped_column(Integer, default=1)
endpoint_url: Mapped[str] = mapped_column(String(500))
status_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
success: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
error: Mapped[str | None] = mapped_column(String(500), nullable=True)
duration_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
request_signature: Mapped[str] = mapped_column(String(255), default="")
payload: Mapped[dict] = mapped_column("payload_json", JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)