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 models import RefAddRequest, RefResponse, RegisterRequest, Token, TokenRequest 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"]) def register(req: RegisterRequest, db: Session = Depends(get_db)): 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( # login: str = Body(...), # password: str = Body(...), form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db) ): 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)): 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"]) def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): 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"]) def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): # 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"]) def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): # 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"]) def get_dashboard_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): # 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"]) def get_dashboard_chart_total(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): # Группируем продажи по дате (день) 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 JSONResponse(content=data) @app.get("/dashboard/chart/agent", tags=["bff", "dashboard"]) def get_dashboard_chart_agent(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): # Получаем всех агентов 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 JSONResponse(content=result) @app.get("/stat/agents", tags=["bff", "stat"]) 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 JSONResponse(content=result) @app.get("/stat/referrals", tags=["bff", "stat"]) 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 JSONResponse(content=result) @app.get("/stat/sales", tags=["bff", "stat"]) 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), ): 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 JSONResponse(content=result) @app.get("/billing/cards", tags=["bff", "billing"]) 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"]) 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: result.append({ "id": agent_trans.transaction_group, # Используем transaction_group как ID транзакции группы "amount": agent_trans.amount, "agent": agent.name if agent else None, # Имя агента из join "status": agent_trans.status, "create_dttm": agent_trans.create_dttm, "update_dttm": agent_trans.update_dttm, }) return result @app.get("/billing/chart/stat", tags=["bff", "billing"]) 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 JSONResponse(content=data) @app.get("/billing/chart/pie", tags=["bff", "billing"]) 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 JSONResponse(content=data) @app.post("/tg_auth", tags=["partner-tg"]) def tg_auth(hash: str = Body(..., embed=True), db: Session = Depends(get_db)): 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"]) 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"]) 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"]) 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"]) 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"]) 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"]) 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"]) 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}