import uuid from fastapi import FastAPI, Depends, HTTPException, status, Query, Body, Request from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlmodel import SQLModel, Field, create_engine, Session, select from passlib.context import CryptContext from typing import Optional, List, Dict from datetime import datetime, timedelta from bff_models import Token, AccountProfileUpdateRequest, AccountPasswordChangeRequest, AgentTransactionResponse, AutoApproveSettingsRequest, ApproveTransactionsRequest, TransactionStatus, RegisterResponse, DashboardCardsResponse, DashboardChartTotalResponse, DashboardChartAgentResponse, StatAgentsResponse, StatReferralsResponse, StatSalesResponse, BillingCardsResponse, BillingChartStatResponse, BillingChartPieResponse, AccountResponse, CompanyProfileResponse, AccountProfileResponse, AccountProfileUpdateResponse, AccountPasswordChangeResponse, AutoApproveSettingsGetResponse, AutoApproveSettingsUpdateResponse, ApproveTransactionsResult, TgAuthResponse, BillingPayoutsTransactionsResponse from tg_models import RefAddRequest, RefResponse, RegisterRequest, TokenRequest, RefAddResponse, RefStatResponse, StatResponse from uuid import uuid4 from fastapi.responses import JSONResponse from sqlalchemy import func from hashlib import sha256 import jwt from jwt.exceptions import InvalidTokenError from pydantic import BaseModel, EmailStr from enum import Enum # Конфигурация AUTH_DATABASE_ADDRESS = "sqlite:///partner.db" #SQLModel class Company(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str commission: float # процент комиссии key: str = Field(index=True, unique=True) create_dttm: datetime = Field(default_factory=datetime.utcnow) update_dttm: datetime = Field(default_factory=datetime.utcnow) auto_approve_transactions: bool = Field(default=False) # Новое поле для автоподтверждения class TgAgent(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) tg_id: int = Field(index=True, unique=True) chat_id: Optional[int] = None phone: Optional[str] = None name: Optional[str] = None login: Optional[str] = None hash: Optional[str] = None company_id: int = Field(foreign_key="company.id") create_dttm: datetime = Field(default_factory=datetime.utcnow) update_dttm: datetime = Field(default_factory=datetime.utcnow) class Ref(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) tg_agent_id: int = Field(foreign_key="tgagent.id") ref: str description: Optional[str] = None create_dttm: datetime = Field(default_factory=datetime.utcnow) update_dttm: datetime = Field(default_factory=datetime.utcnow) class Sale(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) cost: float crediting: float # сколько начислено за продажу ref: int = Field(foreign_key="ref.id") sale_id: str company_id: int = Field(foreign_key="company.id") create_dttm: datetime = Field(default_factory=datetime.utcnow) update_dttm: datetime = Field(default_factory=datetime.utcnow) class AgentTransaction(SQLModel, table=True): __tablename__ = "agent_transactions" # Указываем имя таблицы явно id: Optional[int] = Field(default=None, primary_key=True) tg_agent_id: int = Field(foreign_key="tgagent.id") # ID агента, связь с TgAgent amount: float # Используем float для DECIMAL(15,2) status: str # 'waiting', 'process', 'done', 'reject', 'error' transaction_group: uuid.UUID = Field(default_factory=uuid.uuid4) # UUID для группировки create_dttm: datetime = Field(default_factory=datetime.utcnow) update_dttm: datetime = Field(default_factory=datetime.utcnow) class PartnerTransaction(SQLModel, table=True): __tablename__ = "partner_transactions" # Указываем имя таблицы явно id: Optional[int] = Field(default=None, primary_key=True) company_id: int = Field(foreign_key="company.id") # ID партнера, связь с Company type: str # 'deposit', 'agent_payout', 'service_fee' amount: float # Используем float для DECIMAL(15,2) status: str # 'process', 'done', 'error' transaction_group: uuid.UUID # UUID для группировки, может быть связан с agent_transactions agent_transaction_id: Optional[int] = Field(default=None, foreign_key="agent_transactions.id") # Связь с агентской транзакцией create_dttm: datetime = Field(default_factory=datetime.utcnow) update_dttm: datetime = Field(default_factory=datetime.utcnow) class CompanyBalance(SQLModel, table=True): __tablename__ = "company_balances" # Указываем имя таблицы явно id: Optional[int] = Field(default=None, primary_key=True) company_id: int = Field(foreign_key="company.id", unique=True) # ID компании, уникальный баланс на компанию available_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2) pending_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2) updated_dttm: datetime = Field(default_factory=datetime.utcnow) class AgentBalance(SQLModel, table=True): __tablename__ = "agent_balances" # Указываем имя таблицы явно id: Optional[int] = Field(default=None, primary_key=True) tg_agent_id: int = Field(foreign_key="tgagent.id", unique=True) # ID агента, уникальный баланс на агента available_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2) frozen_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2) updated_dttm: datetime = Field(default_factory=datetime.utcnow) class Account(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) login: str = Field(index=True, unique=True) password_hash: str firstName: Optional[str] = None surname: Optional[str] = None phone: Optional[str] = None email: Optional[str] = None company_id: int = Field(foreign_key="company.id") create_dttm: datetime = Field(default_factory=datetime.utcnow) update_dttm: datetime = Field(default_factory=datetime.utcnow) class AccountProfileUpdateRequest(BaseModel): firstName: str surname: str email: EmailStr phone: str class AccountPasswordChangeRequest(BaseModel): currentPassword: str newPassword: str class TransactionStatus(str, Enum): # Определяем Enum для статусов WAITING = 'waiting' PROCESS = 'process' DONE = 'done' REJECT = 'reject' ERROR = 'error' NEW = 'new' # Новый статус # Новая модель ответа для агентских транзакций с именем агента class AgentTransactionResponse(BaseModel): amount: float status: TransactionStatus # Используем Enum transaction_group: uuid.UUID create_dttm: datetime update_dttm: datetime agent_name: Optional[str] = None # Поле для имени агента # Создание движка базы данных AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True) SQLModel.metadata.create_all(AUTH_DB_ENGINE) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") # FastAPI app app = FastAPI() # CRUD def get_tg_agent_by_tg_id(db: Session, tg_id: int) -> Optional[TgAgent]: statement = select(TgAgent).where(TgAgent.tg_id == tg_id) return db.exec(statement).first() # Dependency def get_db(): with Session(AUTH_DB_ENGINE) as session: yield session def get_current_account(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) login: str = payload.get("sub") if login is None: raise credentials_exception except InvalidTokenError: raise credentials_exception account = get_account_by_login(db, login) if account is None: raise credentials_exception return account # Авторизация async def get_current_tg_agent(request: Request, db: Session = Depends(get_db)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) auth_header = request.headers.get("Authorization") if not auth_header or not auth_header.startswith("Bearer "): raise credentials_exception hash_value = auth_header.replace("Bearer ", "").strip() tg_agent = db.exec(select(TgAgent).where(TgAgent.hash == hash_value)).first() if tg_agent is None: raise credentials_exception return tg_agent # Регистрация @app.post("/register", tags=["partner-tg"], response_model=RegisterResponse) def register(req: RegisterRequest, db: Session = Depends(get_db)): """ Регистрирует нового Telegram-агента в системе. """ tg_id = req.tg_id chat_id = req.chat_id phone = req.phone name = getattr(req, 'name', None) login = getattr(req, 'login', None) company_key = req.company_key print(f'tg_id: {tg_id}, chat_id: {chat_id}, phone: {phone}, name: {name}, login: {login}, company_key: {company_key}') tg_agent = get_tg_agent_by_tg_id(db, tg_id) if tg_agent: raise HTTPException(status_code=400, detail="tg_id already registered") # Поиск компании по ключу company = db.exec(select(Company).where(Company.key == company_key)).first() if not company: raise HTTPException(status_code=400, detail="Компания с таким ключом не найдена") hash_value = sha256(f"{tg_id}sold".encode()).hexdigest() new_tg_agent = TgAgent( tg_id=tg_id, chat_id=chat_id, phone=phone, name=name, login=login, hash=hash_value, company_id=company.id ) db.add(new_tg_agent) db.commit() db.refresh(new_tg_agent) return {"msg": "TgAgent registered successfully"} def authenticate_tg_agent(engine, tg_id: int): with Session(engine) as db: tg_agent = get_tg_agent_by_tg_id(db, tg_id) if not tg_agent: return None return tg_agent # Авторизация SECRET_KEY = "supersecretkey" # Лучше вынести в .env ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 def create_access_token(data: dict, expires_delta: timedelta = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @app.post("/token", response_model=Token, tags=["bff", "token"]) def login_account_for_access_token( form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db) ): """ Авторизует аккаунт и возвращает токен доступа. """ # login: str = Body(...), # password: str = Body(...), account = get_account_by_login(db, form_data.username) if not account or not verify_password(form_data.password, account.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect login or password", headers={"WWW-Authenticate": "Bearer"}, ) access_token = create_access_token( data={"sub": account.login}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) ) return Token(access_token=access_token, token_type="bearer") @app.get("/ref", response_model=List[RefResponse], tags=["partner-tg"]) def get_refs(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): """ Возвращает список реферальных ссылок текущего Telegram-агента. """ refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all() return [RefResponse(ref=r.ref, description=r.description or "") for r in refs] @app.post("/ref/add", tags=["partner-tg"], response_model=RefAddResponse) def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): """ Добавляет новую реферальную ссылку для текущего Telegram-агента. """ new_ref = Ref( tg_agent_id=current_tg_agent.id, ref=str(uuid4()), description=req.description ) db.add(new_ref) db.commit() db.refresh(new_ref) return {"ref": new_ref.ref} @app.get("/ref/stat", tags=["partner-tg"], response_model=RefStatResponse) def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): """ Возвращает статистику по реферальным ссылкам текущего Telegram-агента. """ # 1. Получаем все реферальные ссылки пользователя refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all() result = [] for ref in refs: # 2. Для каждой ссылки считаем продажи и сумму sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all() sales_count = len(sales) income = sum(sale.crediting for sale in sales) result.append({ "description": ref.description or "", "sales": sales_count, "income": income }) return {"refData": result} @app.get("/stat", tags=["partner-tg"], response_model=StatResponse) def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): """ Возвращает общую статистику для текущего Telegram-агента. """ # 1. Получаем все реферальные ссылки пользователя refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all() ref_ids = [r.id for r in refs] # 2. Считаем totalSales (продажи по всем рефам пользователя) total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all() totalSales = len(total_sales) totalIncome = sum(sale.crediting for sale in total_sales) # Заменено получение доступного остатка из AgentBalance agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == current_tg_agent.id)).first() availableWithdrawal = agent_balance.available_balance if agent_balance else 0.0 return { "totalSales": totalSales, "totalIncome": totalIncome, "availableWithdrawal": availableWithdrawal } @app.get("/dashboard/cards", tags=["bff", "dashboard"], response_model=DashboardCardsResponse) def get_dashboard_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): """ Возвращает данные для карточек на главной панели (dashboard) пользователя, включая общий доход, выплаты, активных рефералов, ожидающие выплаты и общее количество продаж. """ # 1. Общий доход - сумма всех Sale.cost total_revenue = db.exec(select(Sale).where(Sale.company_id == current_account.company_id)).all() totalRevenue = sum(sale.cost for sale in total_revenue) # 2. Общие выплаты - сумма всех Sale.crediting totalPayouts = sum(sale.crediting for sale in total_revenue) # 3. Активные рефералы - количество уникальных TgAgent.tg_id unique_agents = db.exec(select(TgAgent.tg_id).where(TgAgent.company_id == current_account.company_id)).all() activeReferrals = len(set(unique_agents)) # 4. Ожидающие выплаты - сумма AgentTransaction со статусом 'waiting' pending_agent_transactions = db.exec(select(AgentTransaction).join(TgAgent).where(TgAgent.company_id == current_account.company_id).where(AgentTransaction.status == 'waiting')).all() pendingPayouts = sum(t.amount for t in pending_agent_transactions) # 5. Количество продаж totalSales = len(total_revenue) return { "totalRevenue": totalRevenue, "totalPayouts": totalPayouts, "activeReferrals": activeReferrals, "pendingPayouts": pendingPayouts, "totalSales": totalSales } @app.get("/dashboard/chart/total", tags=["bff", "dashboard"], response_model=DashboardChartTotalResponse) def get_dashboard_chart_total(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): """ Возвращает данные для графика "Общий доход и продажи по датам" на главной панели (dashboard). """ # Группируем продажи по дате (день) result = db.exec( select( func.strftime('%Y-%m-%d', Sale.create_dttm).label('date'), func.sum(Sale.cost).label('revenue'), func.count(Sale.id).label('sales') ).where(Sale.company_id == current_account.company_id).group_by(func.strftime('%Y-%m-%d', Sale.create_dttm)) .order_by(func.strftime('%Y-%m-%d', Sale.create_dttm)) ).all() # Преобразуем результат в нужный формат data = [ {"date": row.date, "revenue": row.revenue or 0, "sales": row.sales or 0} for row in result ] return DashboardChartTotalResponse(items=data) @app.get("/dashboard/chart/agent", tags=["bff", "dashboard"], response_model=DashboardChartAgentResponse) def get_dashboard_chart_agent(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): """ Возвращает данные для графика "Продажи по агентам" на главной панели (dashboard). """ # Получаем всех агентов agents = db.exec(select(TgAgent).where(TgAgent.company_id == current_account.company_id)).all() result = [] for agent in agents: # Получаем все рефы этого агента refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all() ref_ids = [r.id for r in refs] if not ref_ids: result.append({ "name": agent.name or f"Агент {agent.id}", "count": 0, "sum": 0.0 }) continue # Получаем все продажи по этим рефам sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all() sales_count = len(sales) sales_sum = sum(sale.cost for sale in sales) result.append({ "name": agent.name or f"Агент {agent.id}", "count": sales_count, "sum": sales_sum }) return DashboardChartAgentResponse(items=result) @app.get("/stat/agents", tags=["bff", "stat"], response_model=StatAgentsResponse) def get_agents_stat( db: Session = Depends(get_db), date_start: str = Query(None), date_end: str = Query(None), current_account: Account = Depends(get_current_account), ): """ Возвращает статистику по агентам компании, включая количество рефералов, продаж, сумму продаж и начислений. Возможна фильтрация по дате создания агентов. """ agents_query = select(TgAgent).where(TgAgent.company_id == current_account.company_id) if date_start: agents_query = agents_query.where(TgAgent.create_dttm >= date_start) if date_end: agents_query = agents_query.where(TgAgent.create_dttm <= date_end) agents = db.exec(agents_query).all() result = [] for agent in agents: refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all() ref_ids = [r.id for r in refs] ref_count = len(ref_ids) if not ref_ids: result.append({ "name": agent.name or f"Агент {agent.id}", "refCount": 0, "salesCount": 0, "salesSum": 0.0, "crediting": 0.0 }) continue sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all() sales_count = len(sales) sales_sum = sum(sale.cost for sale in sales) crediting_sum = sum(sale.crediting for sale in sales) result.append({ "name": agent.name or f"Агент {agent.id}", "refCount": ref_count, "salesCount": sales_count, "salesSum": sales_sum, "crediting": crediting_sum }) return StatAgentsResponse(items=result) @app.get("/stat/referrals", tags=["bff", "stat"], response_model=StatReferralsResponse) def get_referrals_stat( db: Session = Depends(get_db), date_start: str = Query(None), date_end: str = Query(None), current_account: Account = Depends(get_current_account), ): """ Возвращает статистику по реферальным ссылкам компании, включая имя агента, описание, сумму и количество продаж. Возможна фильтрация по дате создания рефералов. """ refs_query = select(Ref).join(TgAgent).where(TgAgent.company_id == current_account.company_id) if date_start: refs_query = refs_query.where(Ref.create_dttm >= date_start) if date_end: refs_query = refs_query.where(Ref.create_dttm <= date_end) refs = db.exec(refs_query).all() result = [] for ref in refs: agent = db.exec(select(TgAgent).where(TgAgent.id == ref.tg_agent_id)).first() sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all() sales_count = len(sales) sales_sum = sum(sale.cost for sale in sales) result.append({ "ref": ref.ref, "agent": agent.name if agent and agent.name else f"Агент {ref.tg_agent_id}", "description": ref.description or "", "salesSum": sales_sum, "salesCount": sales_count }) return StatReferralsResponse(items=result) @app.get("/stat/sales", tags=["bff", "stat"], response_model=StatSalesResponse) def get_sales_stat( db: Session = Depends(get_db), date_start: str = Query(None), date_end: str = Query(None), current_account: Account = Depends(get_current_account), ): """ Возвращает статистику по продажам компании, включая ID продажи, стоимость, начисления, реферала и имя агента. Возможна фильтрация по дате создания продаж. """ sales_query = select(Sale).where(Sale.company_id == current_account.company_id) if date_start: sales_query = sales_query.where(Sale.create_dttm >= date_start) if date_end: sales_query = sales_query.where(Sale.create_dttm <= date_end) sales = db.exec(sales_query).all() ref_ids = list(set(sale.ref for sale in sales)) refs = db.exec(select(Ref).where(Ref.id.in_(ref_ids))).all() if ref_ids else [] ref_map = {ref.id: ref for ref in refs} agent_ids = list(set(ref.tg_agent_id for ref in refs)) if refs else [] agents = db.exec(select(TgAgent).where(TgAgent.id.in_(agent_ids))).all() if agent_ids else [] agent_map = {agent.id: agent for agent in agents} result = [] for sale in sales: ref_obj = ref_map.get(sale.ref) agent_obj = agent_map.get(ref_obj.tg_agent_id) if ref_obj else None result.append({ "saleId": sale.sale_id, "cost": sale.cost, "crediting": sale.crediting, "ref": ref_obj.ref if ref_obj else None, "name": agent_obj.name if agent_obj else None }) return StatSalesResponse(items=result) @app.get("/billing/cards", tags=["bff", "billing"], response_model=BillingCardsResponse) def get_billing_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): """ Возвращает ключевые показатели биллинга для компании, включая общий заработок, общие выплаты и доступные к выводу средства. """ # 1. cost - Общий заработок (сумма всех Sale.cost) sales = db.exec(select(Sale).where(Sale.company_id == current_account.company_id)).all() cost = sum(sale.cost for sale in sales) # 2. crediting - Общие выплаты (сумма PartnerTransaction типа 'agent_payout' со статусом 'done') completed_payouts = db.exec(select(PartnerTransaction).where(PartnerTransaction.type == 'agent_payout').where(PartnerTransaction.status == 'done').where(PartnerTransaction.company_id == current_account.company_id)).all() crediting = sum(t.amount for t in completed_payouts) # 3. pendingPayouts - Доступно к выводу всеми партнерами (сумма всех доступных балансов агентов) agent_balances = db.exec(select(AgentBalance).join(TgAgent).where(TgAgent.company_id == current_account.company_id)).all() pendingPayouts = sum(balance.available_balance for balance in agent_balances) return { "cost": cost, "crediting": crediting, "pendingPayouts": pendingPayouts } @app.get("/billing/payouts/transactions", tags=["bff", "billing"], response_model=BillingPayoutsTransactionsResponse) def get_billing_payouts_transactions( db: Session = Depends(get_db), date_start: str = Query(None), date_end: str = Query(None), current_account: Account = Depends(get_current_account), ): """ Возвращает список транзакций выплат для компании текущего пользователя. Возможна фильтрация по дате создания. """ # Используем AgentTransaction вместо Transaction # Явно выбираем обе модели для корректной распаковки query = select(AgentTransaction, TgAgent).join(TgAgent).where(TgAgent.company_id == current_account.company_id) if date_start: query = query.where(AgentTransaction.create_dttm >= date_start) if date_end: query = query.where(AgentTransaction.create_dttm <= date_end) # Заказываем по дате создания query = query.order_by(AgentTransaction.create_dttm.desc()) # Выполняем запрос и формируем результат results = db.exec(query).all() result = [] for agent_trans, agent in results: try: status_enum = TransactionStatus(agent_trans.status) except ValueError: # Если статус из БД не соответствует Enum, используем статус ERROR status_enum = TransactionStatus.ERROR result.append({ "id": agent_trans.transaction_group, # Используем id, как и запрошено "amount": agent_trans.amount, "agent": agent.name if agent else None, "status": status_enum, "create_dttm": agent_trans.create_dttm, "update_dttm": agent_trans.update_dttm, }) return BillingPayoutsTransactionsResponse(items=result) @app.get("/billing/chart/stat", tags=["bff", "billing"], response_model=BillingChartStatResponse) def get_billing_chart_stat(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): """ Возвращает статистику выплат по датам и статусам для компании текущего пользователя. """ # Группируем агентские транзакции по дате (день) и статусу result = db.exec( select( func.strftime('%Y-%m-%d', AgentTransaction.create_dttm).label('date'), AgentTransaction.status.label('status'), func.count(AgentTransaction.id).label('count') ).join(TgAgent).where(TgAgent.company_id == current_account.company_id).group_by( func.strftime('%Y-%m-%d', AgentTransaction.create_dttm), AgentTransaction.status ).order_by( func.strftime('%Y-%m-%d', AgentTransaction.create_dttm), AgentTransaction.status ) ).all() data = [ {"date": row.date, "status": row.status, "count": row.count} for row in result ] return BillingChartStatResponse(items=data) @app.get("/billing/chart/pie", tags=["bff", "billing"], response_model=BillingChartPieResponse) def get_billing_chart_pie(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): """ Возвращает круговую диаграмму статистики выплат по статусам для компании текущего пользователя. """ # Группируем агентские транзакции по статусу result = db.exec( select( AgentTransaction.status.label('status'), func.count(AgentTransaction.id).label('count') ).join(TgAgent).where(TgAgent.company_id == current_account.company_id).group_by(AgentTransaction.status) ).all() data = [ {"status": row.status, "count": row.count} for row in result ] return BillingChartPieResponse(items=data) @app.post("/tg_auth", tags=["partner-tg"], response_model=TgAuthResponse) def tg_auth(hash: str = Body(..., embed=True), db: Session = Depends(get_db)): """ Авторизует Telegram-агента по хешу. """ tg_agent = db.exec(select(TgAgent).where(TgAgent.hash == hash)).first() if not tg_agent: raise HTTPException(status_code=401, detail="Hash not found") return {"msg": "Auth success", "tg_id": tg_agent.tg_id} # --- Новый функционал для Account --- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) def get_account_by_login(db: Session, login: str) -> Optional[Account]: statement = select(Account).where(Account.login == login) return db.exec(statement).first() @app.get("/account", tags=["bff", "account"], response_model=AccountResponse) def get_account(current_account: Account = Depends(get_current_account)): """ Возвращает базовую информацию об аккаунте текущего пользователя (имя и фамилию). """ return { "firstName": current_account.firstName, "surname": current_account.surname } @app.get("/account/profile", tags=["bff", "account"], response_model=AccountProfileResponse) def get_account_profile(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): """ Возвращает полную информацию о профиле аккаунта текущего пользователя, включая данные компании. """ company = db.exec(select(Company).where(Company.id == current_account.company_id)).first() if not company: raise HTTPException(status_code=404, detail="Компания не найдена") return { "firstName": current_account.firstName, "surname": current_account.surname, "phone": current_account.phone, "email": current_account.email, "create_dttm": current_account.create_dttm, "company": { "name": company.name, "key": company.key, "commission": company.commission } } @app.post("/account/profile", tags=["bff", "account"], response_model=AccountProfileUpdateResponse) def update_account_profile( req: AccountProfileUpdateRequest, current_account: Account = Depends(get_current_account), db: Session = Depends(get_db) ): """ Обновляет информацию профиля текущего пользователя. """ # Проверка, что все поля заполнены (Pydantic уже валидирует email и обязательность) if not req.firstName.strip() or not req.surname.strip() or not req.email or not req.phone.strip(): raise HTTPException(status_code=400, detail="Все поля должны быть заполнены") # Обновляем поля current_account.firstName = req.firstName.strip() current_account.surname = req.surname.strip() current_account.email = req.email current_account.phone = req.phone.strip() db.add(current_account) db.commit() db.refresh(current_account) return {"msg": "Профиль обновлён успешно"} @app.post("/account/password", tags=["bff", "account"], response_model=AccountPasswordChangeResponse) def change_account_password( req: AccountPasswordChangeRequest, current_account: Account = Depends(get_current_account), db: Session = Depends(get_db) ): """ Изменяет пароль текущего пользователя. """ # Проверяем текущий пароль if not verify_password(req.currentPassword, current_account.password_hash): raise HTTPException(status_code=400, detail="Текущий пароль неверный") # Проверяем, что новый пароль не пустой и отличается от текущего if not req.newPassword.strip(): raise HTTPException(status_code=400, detail="Новый пароль не может быть пустым") if verify_password(req.newPassword, current_account.password_hash): raise HTTPException(status_code=400, detail="Новый пароль не должен совпадать с текущим") # Хешируем и сохраняем новый пароль current_account.password_hash = pwd_context.hash(req.newPassword) db.add(current_account) db.commit() db.refresh(current_account) return {"msg": "Пароль успешно изменён"} # --- Новый функционал для агентских транзакций партнера --- @app.get("/account/agent-transaction", response_model=List[AgentTransactionResponse], tags=["bff", "account"]) def get_account_agent_transactions( statuses: Optional[List[TransactionStatus]] = Query(None), # Изменено на List[TransactionStatus] date_start: str = Query(None), # Добавлен параметр date_start date_end: str = Query(None), # Добавлен параметр date_end current_account: Account = Depends(get_current_account), db: Session = Depends(get_db) ): """ Возвращает список агентских транзакций для компании текущего пользователя, с возможностью фильтрации по статусу и дате создания. """ # Получаем ID компании текущего аккаунта company_id = current_account.company_id # Строим базовый запрос: выбрать AgentTransaction и TgAgent, связанные с агентами этой компании query = select(AgentTransaction, TgAgent).join(TgAgent).where(TgAgent.company_id == company_id) # Если переданы статусы, добавляем фильтрацию по статусам if statuses: query = query.where(AgentTransaction.status.in_(statuses)) # Если передана дата начала, добавляем фильтрацию по дате создания >= date_start if date_start: query = query.where(AgentTransaction.create_dttm >= date_start) # Если передана дата окончания, добавляем фильтрацию по дате создания <= date_end if date_end: query = query.where(AgentTransaction.create_dttm <= date_end) # Выполняем запрос results = db.exec(query).all() # Формируем список ответов в формате AgentTransactionResponse agent_transactions_response = [] for agent_trans, agent in results: agent_transactions_response.append( AgentTransactionResponse( amount=agent_trans.amount, status=agent_trans.status, transaction_group=agent_trans.transaction_group, create_dttm=agent_trans.create_dttm, update_dttm=agent_trans.update_dttm, agent_name=agent.name # Используем имя агента ) ) return agent_transactions_response # Модель запроса для POST /account/auto-approve class AutoApproveSettingsRequest(BaseModel): auto_approve: bool apply_to_current: Optional[bool] = False @app.get("/account/auto-approve", tags=["bff", "account"], response_model=AutoApproveSettingsGetResponse) def get_auto_approve_settings( current_account: Account = Depends(get_current_account), db: Session = Depends(get_db) ): """ Возвращает текущую настройку автоматического подтверждения для компании пользователя. """ company = db.exec(select(Company).where(Company.id == current_account.company_id)).first() if not company: raise HTTPException(status_code=404, detail="Компания не найдена") return {"auto_approve_transactions": company.auto_approve_transactions} @app.post("/account/auto-approve", tags=["bff", "account"], response_model=AutoApproveSettingsUpdateResponse) def update_auto_approve_settings( req: AutoApproveSettingsRequest, current_account: Account = Depends(get_current_account), db: Session = Depends(get_db) ): """ Обновляет настройку автоматического подтверждения транзакций для компании пользователя. При необходимости переводит транзакции из 'waiting' в 'new'. """ company = db.exec(select(Company).where(Company.id == current_account.company_id)).first() if not company: raise HTTPException(status_code=404, detail="Компания не найдена") company.auto_approve_transactions = req.auto_approve company.update_dttm = datetime.utcnow() db.add(company) if req.apply_to_current and req.auto_approve: # Применяем только если авто-аппрув включается и запрошено применение к текущим # Находим все агентские транзакции компании в статусе 'waiting' agent_transactions_to_update = db.exec( select(AgentTransaction) .join(TgAgent) .where(TgAgent.company_id == company.id) .where(AgentTransaction.status == TransactionStatus.WAITING) ).all() for agent_trans in agent_transactions_to_update: agent_trans.status = TransactionStatus.NEW agent_trans.update_dttm = datetime.utcnow() db.add(agent_trans) # Находим соответствующие партнерские транзакции и обновляем их статус partner_transactions_to_update = db.exec( select(PartnerTransaction) .where(PartnerTransaction.agent_transaction_id == agent_trans.id) # Используем связь по ID .where(PartnerTransaction.status == TransactionStatus.PROCESS) # Предполагаем, что связанные партнерские транзакции в статусе PROCESS ).all() for partner_trans in partner_transactions_to_update: partner_trans.status = TransactionStatus.NEW partner_trans.update_dttm = datetime.utcnow() db.add(partner_trans) db.commit() db.refresh(company) return {"msg": "Настройка автоматического подтверждения обновлена", "auto_approve_transactions": company.auto_approve_transactions} # Модель запроса для POST /account/approve-transactions class ApproveTransactionsRequest(BaseModel): transaction_ids: List[uuid.UUID] @app.post("/account/approve-transactions", tags=["bff", "account"], response_model=ApproveTransactionsResult) def approve_agent_transactions( req: ApproveTransactionsRequest, current_account: Account = Depends(get_current_account), db: Session = Depends(get_db) ): """ Утверждение выбранных агентских транзакций для компании текущего пользователя. Переводит транзакции из статуса 'waiting' в 'new'. """ company_id = current_account.company_id approved_count = 0 if not req.transaction_ids: return {"msg": "Нет транзакций для утверждения", "approved_count": 0} # Find transactions belonging to the company and with specified IDs and statuses transactions_to_approve = db.exec( select(AgentTransaction) .join(TgAgent) .where(TgAgent.company_id == company_id) .where(AgentTransaction.transaction_group.in_(req.transaction_ids)) .where(AgentTransaction.status == TransactionStatus.WAITING) # Утверждаем только транзакции в статусе 'waiting' ).all() for agent_trans in transactions_to_approve: agent_trans.status = TransactionStatus.NEW # Переводим в статус 'new' agent_trans.update_dttm = datetime.utcnow() db.add(agent_trans) approved_count += 1 db.commit() return {"msg": f"Переведено в статус NEW {approved_count} транзакций", "approved_count": approved_count}