import json import logging from contextlib import asynccontextmanager from datetime import datetime from typing import Optional from fastapi import FastAPI, Request, Depends, Query from fastapi.responses import JSONResponse, HTMLResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_ from app.config import get_settings from app.database import get_db, async_session, engine from app.models.models import Base from app.max_api import MaxAPIClient from app.settings_cache import SettingsCache from app.bot_engine import handle_update from app.text_renderer import render_template, make_clickable_links settings = get_settings() logger = logging.getLogger(__name__) max_api: Optional[MaxAPIClient] = None @asynccontextmanager async def lifespan(app: FastAPI): global max_api async with engine.begin() as conn: pass async with async_session() as db: await SettingsCache.refresh(db) token = SettingsCache.get_bot_token() if token: max_api = MaxAPIClient(token) try: me = await max_api.get_me() logger.info(f"Bot connected: {me}") except Exception as e: logger.error(f"Bot connection failed: {e}") else: logger.warning("No bot token configured") yield if max_api: await max_api.close() app = FastAPI(title="AegisOne Max Bot — София", lifespan=lifespan) @app.post(settings.WEBHOOK_PATH) async def webhook(request: Request, db: AsyncSession = Depends(get_db)): secret_header = request.headers.get("X-Max-Bot-Api-Secret", "") webhook_secret = settings.WEBHOOK_SECRET if webhook_secret and secret_header != webhook_secret: logger.warning("Invalid webhook secret") return JSONResponse({"error": "Invalid secret"}, status_code=403) raw = await request.body() try: body = json.loads(raw) except json.JSONDecodeError as e: logger.error(f"Invalid JSON from MAX: {e}, raw={raw[:500]}") return JSONResponse({"ok": False}, status_code=200) logger.info(f"Webhook update: {body.get('update_type', 'unknown')}") global max_api if not max_api: token = SettingsCache.get_bot_token() if token: max_api = MaxAPIClient(token) try: await handle_update(None, body, db, max_api) await db.commit() except Exception as e: logger.error(f"Webhook handler error: {e}", exc_info=True) await db.rollback() return JSONResponse({"ok": True}) @app.get("/api/1c/response") @app.post("/api/1c/response") async def api_1c_response(request: Request, db: AsyncSession = Depends(get_db)): try: body = await request.json() from app.integrations import _1c_unf as ic_integration success = await ic_integration.handle_1c_response(db, body) await db.commit() if success: conv_id = body.get("conversation_id") user_id = body.get("user_id") if user_id and max_api: status = body.get("status", "в работе") engineer = body.get("engineer", "") from app.conversation import get_template_rendered text = await get_template_rendered(db, "status_update", conv_id=conv_id, status=status, engineer=engineer) await max_api.send_message(user_id, text, format="markdown") return JSONResponse({"ok": success}) except Exception as e: logger.error(f"1C response error: {e}", exc_info=True) await db.rollback() return JSONResponse({"ok": False, "error": str(e)}, status_code=500) @app.get("/health") async def health(): return {"status": "ok", "bot": "София"} # ===================== Service Portal API ===================== @app.get("/api/bot/settings") async def api_bot_settings(db: AsyncSession = Depends(get_db)): await SettingsCache.refresh(db) return JSONResponse(SettingsCache.get_all()) @app.post("/api/bot/settings/save") async def api_bot_settings_save(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotSetting body = await request.json() for key, value in body.items(): result = await db.execute(select(BotSetting).where(BotSetting.key == key)) setting = result.scalar_one_or_none() if setting: setting.value = str(value) else: db.add(BotSetting(key=key, value=str(value))) await db.commit() SettingsCache._last_update = 0 return JSONResponse({"ok": True}) @app.get("/api/bot/templates") async def api_bot_templates(db: AsyncSession = Depends(get_db)): await SettingsCache.refresh(db) return JSONResponse(SettingsCache.get_all_templates()) @app.post("/api/bot/templates/save") async def api_bot_templates_save(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotResponseTemplate body = await request.json() for key, value in body.items(): result = await db.execute(select(BotResponseTemplate).where(BotResponseTemplate.template_key == key)) tmpl = result.scalar_one_or_none() if tmpl: tmpl.template_text = value else: db.add(BotResponseTemplate(template_key=key, template_text=value)) await db.commit() SettingsCache._last_update = 0 return JSONResponse({"ok": True}) @app.get("/api/bot/features") async def api_bot_features(db: AsyncSession = Depends(get_db)): from app.models.models import BotFeature result = await db.execute(select(BotFeature).order_by(BotFeature.sort_order)) features = result.scalars().all() return JSONResponse([ {"key": f.feature_key, "name": f.name, "description": f.description, "enabled": f.enabled} for f in features ]) @app.post("/api/bot/features/toggle") async def api_bot_features_toggle(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotFeature body = await request.json() key = body.get("feature_key") enabled = body.get("enabled", False) result = await db.execute(select(BotFeature).where(BotFeature.feature_key == key)) feature = result.scalar_one_or_none() if feature: feature.enabled = enabled await db.commit() SettingsCache._last_update = 0 return JSONResponse({"ok": True}) @app.get("/api/bot/categories") async def api_bot_categories(db: AsyncSession = Depends(get_db)): from app.models.models import BotCategory result = await db.execute(select(BotCategory).order_by(BotCategory.sort_order)) cats = result.scalars().all() return JSONResponse([ {"id": c.id, "name": c.name, "description": c.description, "active": c.active, "icon_emoji": c.icon_emoji, "sort_order": c.sort_order, "keywords": c.keywords} for c in cats ]) @app.post("/api/bot/categories/create") async def api_bot_categories_create(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotCategory body = await request.json() db.add(BotCategory( name=body.get("name", ""), description=body.get("description", ""), active=body.get("active", True), icon_emoji=body.get("icon_emoji", "📋"), sort_order=body.get("sort_order", 0), keywords=body.get("keywords", []), )) await db.commit() SettingsCache._last_update = 0 return JSONResponse({"ok": True}) @app.post("/api/bot/categories/edit") async def api_bot_categories_edit(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotCategory body = await request.json() result = await db.execute(select(BotCategory).where(BotCategory.id == body.get("id"))) cat = result.scalar_one_or_none() if cat: for f in ("name", "description", "active", "icon_emoji", "sort_order", "keywords"): if f in body: setattr(cat, f, body[f]) await db.commit() SettingsCache._last_update = 0 return JSONResponse({"ok": True}) @app.post("/api/bot/categories/delete") async def api_bot_categories_delete(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotCategory body = await request.json() result = await db.execute(select(BotCategory).where(BotCategory.id == body.get("id"))) cat = result.scalar_one_or_none() if cat: await db.delete(cat) await db.commit() SettingsCache._last_update = 0 return JSONResponse({"ok": True}) @app.get("/api/bot/kb") async def api_bot_kb(db: AsyncSession = Depends(get_db)): from app.models.models import BotKnowledgeBase result = await db.execute(select(BotKnowledgeBase).order_by(BotKnowledgeBase.sort_order)) entries = result.scalars().all() return JSONResponse([ {"id": e.id, "question": e.question, "answer": e.answer, "category": e.category, "active": e.active, "sort_order": e.sort_order, "keywords": e.keywords} for e in entries ]) @app.post("/api/bot/kb/create") async def api_bot_kb_create(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotKnowledgeBase body = await request.json() db.add(BotKnowledgeBase( question=body.get("question", ""), answer=body.get("answer", ""), category=body.get("category", ""), active=body.get("active", True), sort_order=body.get("sort_order", 0), keywords=body.get("keywords", []), )) await db.commit() return JSONResponse({"ok": True}) @app.post("/api/bot/kb/edit") async def api_bot_kb_edit(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotKnowledgeBase body = await request.json() result = await db.execute(select(BotKnowledgeBase).where(BotKnowledgeBase.id == body.get("id"))) entry = result.scalar_one_or_none() if entry: for f in ("question", "answer", "category", "active", "sort_order", "keywords"): if f in body: setattr(entry, f, body[f]) await db.commit() return JSONResponse({"ok": True}) @app.post("/api/bot/kb/delete") async def api_bot_kb_delete(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotKnowledgeBase body = await request.json() result = await db.execute(select(BotKnowledgeBase).where(BotKnowledgeBase.id == body.get("id"))) entry = result.scalar_one_or_none() if entry: await db.delete(entry) await db.commit() return JSONResponse({"ok": True}) @app.get("/api/bot/conversations") async def api_bot_conversations(db: AsyncSession = Depends(get_db), status: str = Query(None), page: int = Query(1), limit: int = Query(20)): from app.models.models import BotConversation, BotUser query = select(BotConversation, BotUser.first_name).join(BotUser) if status: query = query.where(BotConversation.status == status) query = query.order_by(BotConversation.created_at.desc()).offset((page - 1) * limit).limit(limit) result = await db.execute(query) rows = result.all() return JSONResponse([ {"id": c.id, "user_name": fn, "status": c.status, "inquiry_text": c.inquiry_text[:100], "created_at": c.created_at.isoformat() if c.created_at else "", "priority": c.priority} for c, fn in rows ]) @app.get("/api/bot/users") async def api_bot_users(db: AsyncSession = Depends(get_db), page: int = Query(1), limit: int = Query(20), sort_by: str = Query("created_at"), sort_dir: str = Query("desc"), consent: str = Query(None)): from app.models.models import BotUser allowed_sort = {"id", "max_user_id", "first_name", "last_name", "phone", "email", "consent_given", "created_at"} if sort_by not in allowed_sort: sort_by = "created_at" sort_col = getattr(BotUser, sort_by) order = sort_col.desc() if sort_dir == "desc" else sort_col.asc() filters = [] if consent == "true": filters.append(BotUser.consent_given == True) elif consent == "false": filters.append(BotUser.consent_given == False) count_q = select(func.count(BotUser.id)) if filters: count_q = count_q.where(and_(*filters)) total = (await db.execute(count_q)).scalar() or 0 query = select(BotUser).where(*filters).order_by(order).offset((page - 1) * limit).limit(limit) result = await db.execute(query) users = result.scalars().all() return JSONResponse({ "total": total, "users": [ {"id": u.id, "max_user_id": u.max_user_id, "first_name": u.first_name, "last_name": u.last_name, "phone": u.phone, "email": u.email, "consent_given": u.consent_given, "created_at": u.created_at.isoformat() if u.created_at else ""} for u in users ], }) @app.get("/api/bot/analytics") async def api_bot_analytics(db: AsyncSession = Depends(get_db), days: int = Query(30)): from app.models.models import BotConversation, BotUser from datetime import timedelta since = datetime.now() - timedelta(days=days) total_consented = await db.execute( select(func.count(BotUser.id)).where(BotUser.consent_given == True)) total_convs = await db.execute( select(func.count(BotConversation.id)).where(BotConversation.created_at >= since)) open_convs = await db.execute(select(func.count(BotConversation.id)).where( BotConversation.status == "open", BotConversation.created_at >= since)) closed_convs = await db.execute(select(func.count(BotConversation.id)).where( BotConversation.status == "closed", BotConversation.created_at >= since)) return JSONResponse({ "total_users": total_consented.scalar(), "conversations_period": total_convs.scalar(), "open_conversations": open_convs.scalar(), "closed_conversations": closed_convs.scalar(), "period_days": days, }) @app.post("/api/bot/test-run") async def api_bot_test_run(request: Request, db: AsyncSession = Depends(get_db)): body = await request.json() scenario = body.get("scenario", "new_dialog") messages = body.get("messages", []) settings = SettingsCache.get_all() features = {k: v.enabled for k, v in SettingsCache.get_enabled_features().items()} categories = [{"id": c.id, "name": c.name} for c in SettingsCache.get_categories()] test_api = MaxAPIClient("test_token", test_mode=True) conversation_log = [] try: from app.fsm import FSM, BotState from app.conversation import get_or_create_user, add_message await SettingsCache.refresh(db) mock_user_id = body.get("mock_user_id", 999999999) user = await get_or_create_user(db, mock_user_id) if not user: user = await get_or_create_user(db, mock_user_id) FSM.set_state(mock_user_id, BotState.IDLE) if scenario == "new_dialog": update = { "update_type": "bot_started", "user": {"user_id": mock_user_id}, } await handle_update(None, update, db, test_api) conversation_log = [{"role": "bot", "text": m["text"]} for m in test_api.test_messages] test_api.test_messages.clear() elif scenario == "out_of_hours": update = { "update_type": "bot_started", "user": {"user_id": mock_user_id}, } await handle_update(None, update, db, test_api) conversation_log = [{"role": "bot", "text": m["text"]} for m in test_api.test_messages] test_api.test_messages.clear() elif scenario == "returning": conv = await get_or_create_user(db, mock_user_id) user = await get_or_create_user(db, mock_user_id) from app.conversation import create_conversation from app.models.models import BotConversation existing = await db.execute( select(BotConversation).where(BotConversation.user_id == user.id).order_by(BotConversation.created_at.desc()) ) if not existing.scalar_one_or_none(): await create_conversation(db, user.id, "open", "Test inquiry") await db.commit() update = { "update_type": "bot_started", "user": {"user_id": mock_user_id}, } await handle_update(None, update, db, test_api) conversation_log = [{"role": "bot", "text": m["text"]} for m in test_api.test_messages] test_api.test_messages.clear() for msg_text in messages: update = { "update_type": "message_created", "user": {"user_id": mock_user_id}, "message": { "body": {"text": msg_text}, "attachments": [], }, } await handle_update(None, update, db, test_api) conversation_log.append({"role": "user", "text": msg_text}) for bot_msg in test_api.test_messages: conversation_log.append({"role": "bot", "text": bot_msg["text"]}) test_api.test_messages.clear() await db.rollback() FSM.clear_all() except Exception as e: logger.error(f"Test-run error: {e}", exc_info=True) await db.rollback() return JSONResponse({ "scenario": scenario, "conversation": conversation_log, "settings": settings, "enabled_features": features, "categories": categories, }) @app.get("/api/bot/broadcast/recipients") async def api_bot_broadcast_recipients(db: AsyncSession = Depends(get_db), q: str = Query(None), page: int = Query(1), limit: int = Query(200)): from app.models.models import BotUser filters = [BotUser.consent_given == True] if q: like = f"%{q}%" filters.append( BotUser.first_name.ilike(like) | BotUser.last_name.ilike(like) | BotUser.phone.ilike(like) | BotUser.email.ilike(like) ) count_q = select(func.count(BotUser.id)).where(and_(*filters)) total = (await db.execute(count_q)).scalar() or 0 query = select(BotUser).where(and_(*filters)).order_by(BotUser.first_name).offset((page - 1) * limit).limit(limit) result = await db.execute(query) users = result.scalars().all() return JSONResponse({ "total": total, "users": [ {"id": u.id, "max_user_id": u.max_user_id, "first_name": u.first_name, "last_name": u.last_name, "phone": u.phone, "email": u.email, "consent_given": u.consent_given, "created_at": u.created_at.isoformat() if u.created_at else ""} for u in users ], }) @app.post("/api/bot/broadcast") async def api_bot_broadcast(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotBroadcast, BotUser body = await request.json() text = body.get("text", "") if not text: return JSONResponse({"error": "Text is required"}, status_code=400) action = body.get("action", "send") user_ids = body.get("user_ids", None) # None or [] = all consenting users draft_id = body.get("draft_id") # Build recipient list: only consenting users filters = [BotUser.consent_given == True] if user_ids is not None and len(user_ids) > 0: filters.append(BotUser.max_user_id.in_(user_ids)) result = await db.execute(select(BotUser.max_user_id, BotUser.first_name, BotUser.last_name).where(and_(*filters))) recipients = result.all() recipient_ids = [r[0] for r in recipients] recipient_names = [f"{r[1]} {r[2]}".strip() or f"id{r[0]}" for r in recipients] if action == "preview": # Create or reuse draft if draft_id: result = await db.execute(select(BotBroadcast).where(BotBroadcast.id == draft_id)) broadcast = result.scalar_one_or_none() if not broadcast: return JSONResponse({"error": "Draft not found"}, status_code=404) broadcast.text = text broadcast.recipient_count = len(recipient_ids) broadcast.recipient_ids = recipient_ids else: broadcast = BotBroadcast(text=text, recipient_count=len(recipient_ids), recipient_ids=recipient_ids, status="draft") db.add(broadcast) await db.commit() return JSONResponse({ "draft_id": broadcast.id, "text": text, "recipient_count": len(recipient_ids), "recipients": recipient_names, }) # action == "send" if draft_id: result = await db.execute(select(BotBroadcast).where(BotBroadcast.id == draft_id)) broadcast = result.scalar_one_or_none() if broadcast: broadcast.text = text broadcast.recipient_count = len(recipient_ids) broadcast.recipient_ids = recipient_ids broadcast.status = "sent" broadcast.sent_at = datetime.now() else: return JSONResponse({"error": "Draft not found"}, status_code=404) else: broadcast = BotBroadcast(text=text, recipient_count=len(recipient_ids), recipient_ids=recipient_ids, status="sent", sent_at=datetime.now()) db.add(broadcast) await db.commit() sent_count = 0 if max_api: for uid in recipient_ids: try: await max_api.send_message(uid, text, format="markdown") sent_count += 1 except Exception: pass return JSONResponse({"ok": True, "sent_to": sent_count, "total_recipients": len(recipient_ids)}) @app.get("/api/bot/broadcast/history") async def api_bot_broadcast_history(db: AsyncSession = Depends(get_db), page: int = Query(1), limit: int = Query(20)): from app.models.models import BotBroadcast count_q = select(func.count(BotBroadcast.id)).where(BotBroadcast.status == "sent") total = (await db.execute(count_q)).scalar() or 0 query = select(BotBroadcast).where(BotBroadcast.status == "sent").order_by( BotBroadcast.sent_at.desc()).offset((page - 1) * limit).limit(limit) result = await db.execute(query) broadcasts = result.scalars().all() return JSONResponse({ "total": total, "broadcasts": [ {"id": b.id, "text": b.text[:200], "recipient_count": b.recipient_count, "sent_at": b.sent_at.isoformat() if b.sent_at else ""} for b in broadcasts ], }) # ===================== Risk Questions API ===================== @app.get("/api/bot/risk-questions") async def api_bot_risk_questions(db: AsyncSession = Depends(get_db)): from app.models.models import BotRiskQuestion result = await db.execute( select(BotRiskQuestion).order_by(BotRiskQuestion.sort_order) ) questions = result.scalars().all() return JSONResponse([ { "id": q.id, "question_key": q.question_key, "question": q.question, "weight": q.weight, "sort_order": q.sort_order, "is_active": q.is_active, } for q in questions ]) @app.post("/api/bot/risk-questions/create") async def api_bot_risk_questions_create(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotRiskQuestion body = await request.json() q = BotRiskQuestion( question_key=body["question_key"], question=body["question"], weight=int(body.get("weight", 10)), sort_order=int(body.get("sort_order", 0)), is_active=body.get("is_active", True), ) db.add(q) await db.commit() return JSONResponse({"ok": True, "id": q.id}) @app.post("/api/bot/risk-questions/edit") async def api_bot_risk_questions_edit(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotRiskQuestion body = await request.json() qid = body.get("id") if not qid: return JSONResponse({"ok": False, "error": "id required"}, status_code=400) q = await db.get(BotRiskQuestion, int(qid)) if not q: return JSONResponse({"ok": False, "error": "Not found"}, status_code=404) if "question_key" in body: q.question_key = body["question_key"] if "question" in body: q.question = body["question"] if "weight" in body: q.weight = int(body["weight"]) if "sort_order" in body: q.sort_order = int(body["sort_order"]) if "is_active" in body: q.is_active = body["is_active"] await db.commit() return JSONResponse({"ok": True}) @app.post("/api/bot/risk-questions/toggle") async def api_bot_risk_questions_toggle(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotRiskQuestion body = await request.json() q = await db.get(BotRiskQuestion, int(body["id"])) if not q: return JSONResponse({"ok": False, "error": "Not found"}, status_code=404) q.is_active = not q.is_active await db.commit() return JSONResponse({"ok": True}) @app.post("/api/bot/risk-questions/delete") async def api_bot_risk_questions_delete(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotRiskQuestion body = await request.json() q = await db.get(BotRiskQuestion, int(body["id"])) if not q: return JSONResponse({"ok": False, "error": "Not found"}, status_code=404) await db.delete(q) await db.commit() return JSONResponse({"ok": True}) # ===================== Tickets API ===================== @app.get("/api/bot/tickets") async def api_bot_tickets(db: AsyncSession = Depends(get_db), status: str = Query(None), priority: str = Query(None)): from app.models.models import BotTicket, BotUser query = select(BotTicket, BotUser.first_name, BotUser.last_name).join( BotUser, BotTicket.user_id == BotUser.id, isouter=True ) if status: query = query.where(BotTicket.status == status) if priority: query = query.where(BotTicket.priority == priority) query = query.order_by(BotTicket.created_at.desc()) result = await db.execute(query) rows = result.all() return JSONResponse([ { "id": t.id, "ticket_number": t.ticket_number, "title": t.title, "description": t.description[:200], "priority": t.priority, "status": t.status, "user_name": f"{fn or ''} {ln or ''}".strip() if fn else None, "object_name": None, "created_at": t.created_at.isoformat() if t.created_at else "", } for t, fn, ln in rows ]) @app.get("/api/bot/tickets/{ticket_id}") async def api_bot_ticket_get(ticket_id: int, db: AsyncSession = Depends(get_db)): from app.models.models import BotTicket, BotUser, BotTicketMessage t = await db.get(BotTicket, ticket_id) if not t: return JSONResponse({"error": "Not found"}, status_code=404) user = await db.get(BotUser, t.user_id) if t.user_id else None result = await db.execute( select(BotTicketMessage).where(BotTicketMessage.ticket_id == ticket_id) .order_by(BotTicketMessage.created_at) ) messages = result.scalars().all() return JSONResponse({ "id": t.id, "ticket_number": t.ticket_number, "title": t.title, "description": t.description, "priority": t.priority, "status": t.status, "user_name": f"{user.first_name} {user.last_name}".strip() if user else None, "object_name": None, "created_at": t.created_at.isoformat() if t.created_at else "", "messages": [ {"id": m.id, "direction": m.direction, "text": m.text, "sender": m.sender, "timestamp": m.created_at.isoformat() if m.created_at else ""} for m in messages ], }) @app.post("/api/bot/tickets/create") async def api_bot_tickets_create(request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotTicket, BotTicketMessage, BotTicketStatus from sqlalchemy import func body = await request.json() count = (await db.execute(select(func.count(BotTicket.id)))).scalar() or 0 ticket_num = f"T-{count + 1:04d}" t = BotTicket( ticket_number=ticket_num, title=body.get("title", ""), description=body.get("description", ""), priority=body.get("priority", "normal"), status="open", created_by=body.get("created_by", "portal"), ) db.add(t) await db.flush() db.add(BotTicketMessage(ticket_id=t.id, direction="in", text=body.get("description", ""), sender="client")) db.add(BotTicketStatus(ticket_id=t.id, old_status="", new_status="open", changed_by="portal")) await db.commit() return JSONResponse({"ok": True, "id": t.id, "ticket_number": ticket_num}) @app.post("/api/bot/tickets/{ticket_id}/message") async def api_bot_tickets_message(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotTicketMessage body = await request.json() m = BotTicketMessage( ticket_id=ticket_id, direction=body.get("direction", "in"), text=body.get("text", ""), sender=body.get("sender", ""), ) db.add(m) await db.commit() return JSONResponse({"ok": True}) @app.post("/api/bot/tickets/{ticket_id}/status") async def api_bot_tickets_status(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db)): from app.models.models import BotTicket, BotTicketStatus body = await request.json() t = await db.get(BotTicket, ticket_id) if not t: return JSONResponse({"ok": False, "error": "Not found"}, status_code=404) old = t.status new_status = body.get("status", old) db.add(BotTicketStatus(ticket_id=t.id, old_status=old, new_status=new_status, changed_by="portal")) t.status = new_status await db.commit() return JSONResponse({"ok": True}) @app.get("/api/bot/engineers") async def api_bot_engineers(db: AsyncSession = Depends(get_db)): from app.models.models import User result = await db.execute( select(User.id, User.full_name).where( User.is_active == True, User.role.in_(["engineer", "owner"]) ).order_by(User.full_name) ) return JSONResponse([{"id": row[0], "full_name": row[1]} for row in result.fetchall()]) @app.get("/api/bot/handoffs") async def api_bot_handoffs(db: AsyncSession = Depends(get_db)): from app.models.models import BotConversation, BotUser query = ( select(BotConversation, BotUser.first_name, BotUser.max_user_id) .join(BotUser) .where(BotConversation.status == "handoff") .order_by(BotConversation.created_at.desc()) ) result = await db.execute(query) rows = result.all() return JSONResponse([ { "id": c.id, "user_name": fn, "max_user_id": muid, "text": c.inquiry_text[:200] if c.inquiry_text else "", "status": c.status, "priority": c.priority, "created_at": c.created_at.isoformat() if c.created_at else "", } for c, fn, muid in rows ]) @app.post("/api/bot/handoffs/{handoff_id}/take") async def api_bot_handoff_take(handoff_id: int, db: AsyncSession = Depends(get_db)): from app.models.models import BotConversation conv = await db.get(BotConversation, handoff_id) if not conv or conv.status != "handoff": return JSONResponse({"ok": False, "error": "Not found or not handoff"}, status_code=404) conv.status = "open" conv.priority = "normal" await db.commit() return JSONResponse({"ok": True}) @app.post("/api/bot/handoffs/{handoff_id}/close") async def api_bot_handoff_close(handoff_id: int, db: AsyncSession = Depends(get_db)): from app.models.models import BotConversation from datetime import datetime conv = await db.get(BotConversation, handoff_id) if not conv or conv.status != "handoff": return JSONResponse({"ok": False, "error": "Not found or not handoff"}, status_code=404) conv.status = "closed" conv.closed_at = datetime.now() await db.commit() return JSONResponse({"ok": True}) @app.get("/api/bot/notifications") async def api_bot_notifications(db: AsyncSession = Depends(get_db), page: int = Query(1), limit: int = Query(50)): from app.models.models import BotNotification count_q = select(func.count(BotNotification.id)) total = (await db.execute(count_q)).scalar() or 0 query = ( select(BotNotification) .order_by(BotNotification.created_at.desc()) .offset((page - 1) * limit).limit(limit) ) result = await db.execute(query) notifications = result.scalars().all() return JSONResponse({ "total": total, "notifications": [ { "id": n.id, "type": n.type, "recipient_type": n.recipient_type, "title": n.title, "body": n.body[:200], "is_read": n.is_read, "created_at": n.created_at.isoformat() if n.created_at else "", } for n in notifications ], }) @app.post("/api/bot/notifications/{notif_id}/read") async def api_bot_notification_read(notif_id: int, db: AsyncSession = Depends(get_db)): from app.models.models import BotNotification n = await db.get(BotNotification, notif_id) if not n: return JSONResponse({"ok": False, "error": "Not found"}, status_code=404) n.is_read = True await db.commit() return JSONResponse({"ok": True})