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 # Конфигурация 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) 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 Transaction(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) transaction_id: str = Field(default_factory=lambda: str(uuid4()), index=True, unique=True) sum: float tg_agent_id: int = Field(foreign_key="tgagent.id") status: str # 'process' || 'done' || 'error' || 'waiting' 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 Account(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) login: str = Field(index=True, unique=True) password_hash: str # теперь хранится hash пароля 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 # Создание движка базы данных 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 # Авторизация 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"]) def login_account_for_access_token( login: str = Body(...), password: str = Body(...), db: Session = Depends(get_db) ): account = get_account_by_login(db, login) if not account or not verify_password(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) withdrawals = db.exec( select(Transaction).where( Transaction.tg_agent_id == current_tg_agent.id ) ).all() availableWithdrawal = totalIncome - sum(t.sum for t in withdrawals) return { "totalSales": totalSales, "totalIncome": totalIncome, "availableWithdrawal": availableWithdrawal } @app.get("/dashboard/cards", tags=["bff"]) def get_dashboard_cards(db: Session = Depends(get_db)): # 1. Общий доход - сумма всех Sale.cost total_revenue = db.exec(select(Sale)).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)).all() activeReferrals = len(set(unique_agents)) # 4. Ожидающие выплаты - разница между суммой всех Sale.crediting и суммой всех Transaction.sum all_transactions = db.exec(select(Transaction)).all() totalTransactions = sum(t.sum for t in all_transactions) pendingPayouts = totalPayouts - totalTransactions # 5. Количество продаж totalSales = len(total_revenue) return { "totalRevenue": totalRevenue, "totalPayouts": totalPayouts, "activeReferrals": activeReferrals, "pendingPayouts": pendingPayouts, "totalSales": totalSales } @app.get("/dashboard/chart/total", tags=["bff"]) def get_dashboard_chart_total(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') ).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"]) def get_dashboard_chart_agent(db: Session = Depends(get_db)): # Получаем всех агентов agents = db.exec(select(TgAgent)).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"]) def get_agents_stat( db: Session = Depends(get_db), date_start: str = Query(None), date_end: str = Query(None), ): agents_query = select(TgAgent) 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"]) def get_referrals_stat( db: Session = Depends(get_db), date_start: str = Query(None), date_end: str = Query(None), ): refs_query = select(Ref) 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"]) def get_sales_stat( db: Session = Depends(get_db), date_start: str = Query(None), date_end: str = Query(None), ): sales_query = select(Sale) 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"]) def get_billing_cards(db: Session = Depends(get_db)): # 1. cost - сумма всех Sale.cost sales = db.exec(select(Sale)).all() cost = sum(sale.cost for sale in sales) # 2. crediting - сумма всех Sale.crediting crediting = sum(sale.crediting for sale in sales) # 3. pendingPayouts - разница между crediting и суммой всех Transaction.sum transactions = db.exec(select(Transaction)).all() total_transactions = sum(t.sum for t in transactions) pendingPayouts = crediting - total_transactions return { "cost": cost, "crediting": crediting, "pendingPayouts": pendingPayouts } @app.get("/billing/payouts/transactions", tags=["bff"]) def get_billing_payouts_transactions( db: Session = Depends(get_db), date_start: str = Query(None), date_end: str = Query(None), ): query = select(Transaction) if date_start: query = query.where(Transaction.create_dttm >= date_start) if date_end: query = query.where(Transaction.create_dttm <= date_end) transactions = db.exec(query).all() result = [] for t in transactions: agent = db.exec(select(TgAgent).where(TgAgent.id == t.tg_agent_id)).first() result.append({ "id": t.transaction_id, "sum": t.sum, "agent": agent.name if agent else None, "status": t.status, "create_dttm": t.create_dttm, "update_dttm": t.update_dttm, }) return result @app.get("/billing/chart/stat", tags=["bff"]) def get_billing_chart_stat(db: Session = Depends(get_db)): # Группируем транзакции по дате (день) и статусу result = db.exec( select( func.strftime('%Y-%m-%d', Transaction.create_dttm).label('date'), Transaction.status.label('status'), func.count(Transaction.id).label('count') ).group_by( func.strftime('%Y-%m-%d', Transaction.create_dttm), Transaction.status ).order_by( func.strftime('%Y-%m-%d', Transaction.create_dttm), Transaction.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"]) def get_billing_chart_pie(db: Session = Depends(get_db)): result = db.exec( select( Transaction.status.label('status'), func.count(Transaction.id).label('count') ).group_by(Transaction.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() 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 @app.get("/account", tags=["bff"]) 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"]) 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"]) 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"]) 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": "Пароль успешно изменён"}