system-prompts-and-models-o.../dealix/api/routers/cards.py
Sami Assiri e1c629bacf feat(dealix): role-based Revenue Command Cards API and factory
Add cards schema (max 3 buttons, forbidden automation patterns), deterministic card_factory per CEO/Sales/Growth/Agency/Support/Delivery, FastAPI routes GET /api/v1/cards/feed, GET whatsapp daily-brief (no auto-send), POST decision (draft_only). Wire router in main, extend smoke_inprocess, enrich command-center.html with role switcher + live feed. Tests: test_role_based_cards.py.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 16:05:57 +03:00

113 lines
3.6 KiB
Python

"""Role-based Revenue Command Cards API — feeds + decisions (draft/approval-first)."""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from auto_client_acquisition.revenue_company_os.card_factory import (
build_role_command_feed,
build_whatsapp_daily_brief_lines,
)
from auto_client_acquisition.revenue_company_os.cards import (
UserRole,
is_known_role,
normalize_role_param,
)
router = APIRouter(prefix="/api/v1/cards", tags=["revenue-command-cards"])
# Demo-only: in-process decision log (not durable storage).
_decision_log: dict[str, dict[str, Any]] = {}
class CardDecisionBody(BaseModel):
"""Human decision on a card — never triggers live channel send."""
action: str | None = Field(None, description="approve | edit | skip")
button_action: str | None = Field(None, description="machine key from pressed button")
note: str | None = Field(None, max_length=2000)
def _find_card(card_id: str) -> dict[str, Any] | None:
for role in UserRole:
for c in build_role_command_feed(role.value)["cards"]:
if str(c.get("card_id")) == card_id:
return c
return None
def _allowed_roles() -> list[str]:
return [e.value for e in UserRole]
@router.get("/feed")
async def get_card_feed(role: str = Query("ceo", description="ceo | sales_manager | growth_manager | ...")) -> dict[str, Any]:
nr = normalize_role_param(role)
if not is_known_role(nr):
raise HTTPException(
status_code=400,
detail={"error": "unknown_role", "allowed": _allowed_roles()},
)
return build_role_command_feed(nr)
@router.get("/whatsapp/daily-brief")
async def get_whatsapp_daily_brief(
role: str = Query("ceo", description="Role for brief lines (still approval-first)"),
) -> dict[str, Any]:
"""Compact lines for WhatsApp-style surfaces — no auto-send."""
nr = normalize_role_param(role)
if not is_known_role(nr):
raise HTTPException(
status_code=400,
detail={"error": "unknown_role", "allowed": _allowed_roles()},
)
return {
"role": nr,
"lines_ar": build_whatsapp_daily_brief_lines(nr),
"demo": True,
"no_auto_send": True,
}
@router.post("/{card_id}/decision")
async def post_card_decision(card_id: str, body: CardDecisionBody) -> dict[str, Any]:
card = _find_card(card_id)
if not card:
raise HTTPException(status_code=404, detail={"error": "unknown_card_id"})
base_proof = list(card.get("proof_impact") or [])
extra: list[str] = ["decision_recorded"]
if body.button_action:
extra.append(f"button:{body.button_action}")
if body.action:
extra.append(f"action:{body.action}")
proof_events = base_proof + extra
record = {
"card_id": card_id,
"role": card.get("role"),
"action": body.action,
"button_action": body.button_action,
"note": body.note,
"proof_events": proof_events,
"execution_mode": "draft_only",
"draft_export_ar": (
"مسودة تنفيذية: راجع المحتوى ثم نفّذ يدوياً عبر قناتك المعتمدة. "
"لا يُرسل Dealix نيابةً عنك في وضع Paid Beta الحالي."
),
}
_decision_log[card_id] = record
return {
"card_id": card_id,
"status": "logged",
"proof_events": proof_events,
"execution_mode": record["execution_mode"],
"draft_export_ar": record["draft_export_ar"],
"demo": True,
}