| """ |
| Marketing API Hardened - Entry point for Alice & Bob workflows. |
| Standardized RBAC Sealing with Full Test Compatibility. |
| """ |
|
|
| import logging |
|
|
| from fastapi import APIRouter, Depends, HTTPException, Query |
|
|
| from src.server.schemas.marketing import ( |
| ApprovalRequest, |
| DraftBlogRequest, |
| DraftFromLeadsRequest, |
| LeadCreateRequest, |
| LeadUpdateRequest, |
| LogoRequest, |
| PitchRequest, |
| PitchResponse, |
| PromoteLeadRequest, |
| RejectSuggestionRequest, |
| ) |
| from src.server.services.marketing_service import MarketingService |
|
|
| from ..auth.dependencies import get_current_user, requires_permission |
| from ..auth.permissions import ( |
| AGENT_TRIGGER_MKT, |
| BRAND_ASSET_MANAGE, |
| CONTENT_PUBLISH, |
| TASK_READ_TEAM, |
| ) |
|
|
| logger = logging.getLogger(__name__) |
|
|
| router: APIRouter = APIRouter(prefix="/api/marketing", tags=["marketing"]) |
|
|
|
|
| def get_marketing_service(): |
| """Export for test compatibility.""" |
| return MarketingService() |
|
|
|
|
| def _err(msg: str, code: int = 403): |
| raise HTTPException(status_code=code, detail=msg) |
|
|
|
|
| @router.get("/jobs") |
| async def search_jobs(keyword: str = Query(...), limit: int = 8, current_user: dict = Depends(get_current_user)): |
| service = MarketingService() |
| return await service.search_jobs(keyword, limit) |
|
|
|
|
| @router.get("/leads") |
| async def list_leads(current_user: dict = Depends(get_current_user)): |
| user_id = str(current_user.get("id")) |
| role = current_user.get("role", "member") |
| logger.info(f"DEBUG: list_leads accessed by UserID: {user_id}, Role: {role}") |
| service = MarketingService() |
| return await service.list_leads(user_id=user_id, role=role) |
|
|
|
|
| @router.post("/leads") |
| async def create_lead(req: LeadCreateRequest, current_user: dict = Depends(get_current_user)): |
| service = MarketingService() |
| success, res = await service.create_lead(req.model_dump(), creator_id=str(current_user.get("id"))) |
| if not success: |
| _err(str(res), 400) |
| return res |
|
|
|
|
| @router.post("/leads/reset") |
| async def reset_leads(current_user: dict = Depends(requires_permission(CONTENT_PUBLISH))): |
| service = MarketingService() |
| success = await service.reset_leads() |
| return {"success": success} |
|
|
|
|
| @router.patch("/leads/{lead_id}") |
| async def update_lead(lead_id: str, req: LeadUpdateRequest, current_user: dict = Depends(get_current_user)): |
| service = MarketingService() |
| success, res = await service.update_lead(lead_id, req.model_dump(exclude_unset=True)) |
| if not success: |
| _err(str(res), 400) |
| return res |
|
|
|
|
| @router.post("/leads/{lead_id}/promote") |
| async def promote_lead(lead_id: str, req: PromoteLeadRequest, current_user: dict = Depends(get_current_user)): |
| service = MarketingService() |
| success, res = await service.promote_to_vendor(lead_id, **req.model_dump(), owner_id=str(current_user.get("id"))) |
| if not success: |
| _err(str(res), 400) |
| return res |
|
|
|
|
| @router.post("/generate-pitch", response_model=PitchResponse) |
| async def generate_pitch(req: PitchRequest, current_user: dict = Depends(get_current_user)): |
| role = current_user.get("role", "viewer").lower() |
| if role not in ["system_admin", "admin", "marketing", "sales"]: |
| raise HTTPException(status_code=403, detail=f"Role '{role}' is unauthorized to generate pitches") |
|
|
| service = MarketingService() |
| res = await service.generate_pitch(req.company, req.job_title) |
| if isinstance(res, dict) and "error_code" in res: |
| raise HTTPException(status_code=res["error_code"], detail=res["message"]) |
| return res |
|
|
|
|
| @router.post("/draft-blog") |
| async def draft_blog_post(req: DraftBlogRequest, current_user: dict = Depends(requires_permission(AGENT_TRIGGER_MKT))): |
| service = MarketingService() |
| success, res = await service.draft_blog(req.topic, req.industry, req.keywords) |
| if not success: |
| _err(res.get("message", "AI Draft failed"), res.get("error_code", 500)) |
| return res |
|
|
|
|
| @router.post("/draft-from-leads") |
| async def draft_from_leads( |
| req: DraftFromLeadsRequest, current_user: dict = Depends(requires_permission(AGENT_TRIGGER_MKT)) |
| ): |
| service = MarketingService() |
| success, res = await service.draft_from_leads(req.lead_ids) |
| if not success: |
| _err(res.get("message", "AI Draft from leads failed"), res.get("error_code", 500)) |
| return res |
|
|
|
|
| @router.post("/blog/{post_id}/submit") |
| async def submit_blog(post_id: str, current_user: dict = Depends(get_current_user)): |
| service = MarketingService() |
| success, res = await service.submit_blog(post_id) |
| if not success: |
| _err(res.get("error", "Submission failed"), 400) |
| return res |
|
|
|
|
| @router.post("/generate-logo") |
| async def generate_logo(req: LogoRequest, current_user: dict = Depends(requires_permission(BRAND_ASSET_MANAGE))): |
| service = MarketingService() |
| return await service.generate_visual_asset(req.style) |
|
|
|
|
| @router.get("/sources") |
| async def get_combined_sources(current_user: dict = Depends(get_current_user)): |
| service = MarketingService() |
| return await service.get_combined_sources(user_id=str(current_user.get("id"))) |
|
|
|
|
| @router.get("/context/{source_id}") |
| async def get_content_context( |
| source_id: str, source_type: str = Query("lead"), current_user: dict = Depends(get_current_user) |
| ): |
| service = MarketingService() |
| return await service.get_content_context(source_id, source_type) |
|
|
|
|
| @router.get("/manager/alerts") |
| async def get_manager_alerts(current_user: dict = Depends(requires_permission(TASK_READ_TEAM))): |
| from ..utils import get_supabase_client |
|
|
| sb = get_supabase_client() |
| res = ( |
| sb.table("archon_logs") |
| .select("*") |
| .eq("level", "ALERT") |
| .in_("source", ["sentinel", "twin_scout", "LeadScoring"]) |
| .order("created_at", desc=True) |
| .limit(50) |
| .execute() |
| ) |
| return res.data or [] |
|
|
|
|
| @router.post("/manager/sentinel/run") |
| async def run_sentinel_manual(current_user: dict = Depends(requires_permission(AGENT_TRIGGER_MKT))): |
| service = MarketingService() |
| return await service.run_sentinel() |
|
|
|
|
| @router.get("/stats") |
| async def get_marketing_stats(current_user: dict = Depends(get_current_user)): |
| service = MarketingService() |
| return await service.get_marketing_stats() |
|
|
|
|
| @router.get("/trends") |
| async def get_marketing_trends(current_user: dict = Depends(get_current_user)): |
| service = MarketingService() |
| return await service.get_marketing_trends() |
|
|
|
|
| @router.get("/intelligence") |
| async def get_marketing_intelligence(current_user: dict = Depends(get_current_user)): |
| from ..services.stats.metrics import MetricsManager |
|
|
| metrics = MetricsManager() |
| return await metrics.get_marketing_intelligence(user_id=str(current_user.get("id"))) |
|
|
|
|
| @router.post("/knowledge/seed") |
| async def seed_knowledge_base(current_user: dict = Depends(requires_permission(CONTENT_PUBLISH))): |
| service = MarketingService() |
| return await service.seed_knowledge() |
|
|
|
|
| @router.get("/approvals") |
| async def get_pending_approvals(current_user: dict = Depends(requires_permission(TASK_READ_TEAM))): |
| service = MarketingService() |
| return await service.get_pending_approvals() |
|
|
|
|
| @router.post("/approvals/reject-suggestion") |
| async def generate_reject_suggestion( |
| req: RejectSuggestionRequest, |
| current_user: dict = Depends(requires_permission(CONTENT_PUBLISH)), |
| ): |
| service = MarketingService() |
| return await service.generate_reject_suggestion(req.item_type, req.item_id) |
|
|
|
|
| @router.post("/approvals/{item_type}/{item_id}/{action}") |
| async def process_approval( |
| item_type: str, |
| item_id: str, |
| action: str, |
| req: ApprovalRequest, |
| current_user: dict = Depends(requires_permission(CONTENT_PUBLISH)), |
| ): |
| service = MarketingService() |
| success = await service.process_approval(item_type, item_id, action, req.notes) |
| return {"success": success} |
|
|