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

140 lines
5.3 KiB
Python

"""Command Feed من أحداث DB — مع fallback إلى العرض التجريبي."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from typing import Any
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from auto_client_acquisition.innovation.command_feed import build_demo_command_feed
from db.models import EmailSendLog, OutreachQueueRecord, ProofLedgerEventRecord, TaskRecord
async def build_command_feed_from_db(
session: AsyncSession,
*,
tenant_id: str = "default",
limit_per_type: int = 3,
) -> dict[str, Any]:
"""
يبني بطاقات من طابور الموافقة، المهام المتأخرة، سجل الإيميل، آخر أحداث الدفتر.
عند غياب بيانات كافية يُرجع نفس بطاقات ``build_demo_command_feed`` مع ``source=demo_fallback``.
عند فشل الاستعلام (مثلاً جداول غير مهيأة في بيئة اختبار) يُرجع العرض التجريبي.
"""
try:
return await _build_command_feed_from_db_impl(
session, tenant_id=tenant_id, limit_per_type=limit_per_type
)
except SQLAlchemyError:
demo = build_demo_command_feed()
return {**demo, "source": "demo_fallback", "live": False}
async def _build_command_feed_from_db_impl(
session: AsyncSession,
*,
tenant_id: str = "default",
limit_per_type: int = 3,
) -> dict[str, Any]:
cards: list[dict[str, Any]] = []
now = datetime.now(tz=UTC)
lim = min(max(limit_per_type, 1), 10)
q_queue = (
select(OutreachQueueRecord)
.where(
OutreachQueueRecord.approval_required.is_(True),
OutreachQueueRecord.status == "queued",
)
.order_by(OutreachQueueRecord.created_at.desc())
.limit(lim)
)
res_q = await session.execute(q_queue)
for row in res_q.scalars():
cards.append(
{
"type": "approval_needed",
"title_ar": "موافقة مطلوبة — رسالة في الطابور",
"why": f"رسالة {row.channel} بانتظار الموافقة.",
"risk": row.risk_reason or "راجع المحتوى والامتثال قبل الإرسال.",
"suggested_action": "راجع النص ووافق أو عدّل من لوحة الإدارة.",
"cta": "مراجعة الطابور",
"source_ref": {"table": "outreach_queue", "id": row.id},
}
)
overdue = now - timedelta(days=3)
q_tasks = (
select(TaskRecord)
.where(
TaskRecord.status == "pending",
TaskRecord.due_at < overdue,
)
.order_by(TaskRecord.due_at.asc())
.limit(lim)
)
res_t = await session.execute(q_tasks)
for row in res_t.scalars():
cards.append(
{
"type": "leak",
"title_ar": "مهمة متابعة متأخرة",
"why": f"مهمة {row.task_type} تجاوزت موعدها.",
"risk": "تسريب زخم الصفقة أو انطباع ضعيف.",
"suggested_action": "حدّد خطوة تالية أو أغلق المهمة بتعليق.",
"cta": "عرض المهام",
"source_ref": {"table": "tasks", "id": row.id},
}
)
q_block = (
select(EmailSendLog)
.where(EmailSendLog.status == "blocked_compliance")
.order_by(EmailSendLog.created_at.desc())
.limit(lim)
)
res_b = await session.execute(q_block)
for row in res_b.scalars():
cards.append(
{
"type": "compliance_risk",
"title_ar": "إرسال بريد موقوف لأسباب امتثال",
"why": f"سجل إرسال إلى {row.to_email} بحالة blocked_compliance.",
"risk": "تكرار المحاولة دون مراجعة قد يخالف السياسة.",
"suggested_action": "راجع compliance_check_json وسجّل القرار.",
"cta": "سجل الإرسال",
"source_ref": {"table": "email_send_log", "id": row.id},
}
)
q_pl = (
select(ProofLedgerEventRecord)
.where(ProofLedgerEventRecord.tenant_id == tenant_id)
.order_by(ProofLedgerEventRecord.created_at.desc())
.limit(lim)
)
res_p = await session.execute(q_pl)
for row in res_p.scalars():
cards.append(
{
"type": "proof_update",
"title_ar": f"حدث دفتر إثبات — {row.event_type}",
"why": row.notes_ar or "حدث جديد في سجل الإثبات.",
"risk": "التقديرات تقريبية حتى ربط CRM.",
"suggested_action": "أدرج في تقرير الأسبوع للإدارة.",
"cta": "عرض الدفتر",
"source_ref": {"table": "proof_ledger_events", "id": row.id},
}
)
if not cards:
demo = build_demo_command_feed()
return {**demo, "source": "demo_fallback", "live": False}
return {"cards": cards[:25], "source": "database", "live": True, "demo": False}