from fastapi import FastAPI, Depends, HTTPException, status, Header, Body, Request from sqlmodel import Session, select, Field from typing import Optional, List, Dict from datetime import datetime, timedelta import hashlib import uuid from random import choices import string from sql_models import Company, IntegrationToken, Ref, Sale, AgentTransaction, PartnerTransaction, AgentBalance, TgAgent, CompanyBalance, SaleCategory from integration_models import Token, SaleCreateRequest, SaleCreateResponse, TransactionStatus, WithdrawRequest, WithdrawResponse, PromoValidationRequest, PromoValidationResponse from bff_models import RegisterResponse, TgAuthResponse from tg_models import RefAddRequest, RefResponse, RefAddResponse, RefStatResponse, RegisterRequest, StatResponse from helpers_bff import AUTH_DB_ENGINE, get_integration_db, create_integration_jwt_token, get_current_company_from_jwt, get_tg_agent_by_tg_id, get_current_tg_agent app = FastAPI() #7c06945b-f4b8-4929-8350-b9841405a609 @app.post("/token", tags=["integration"], response_model=Token) async def get_token_for_api_key( x_api_key: str = Header(..., alias="X-API-Key"), db: Session = Depends(get_integration_db) ): """ Обменивает API-ключ на JWT токен. """ if not x_api_key: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="API-ключ не предоставлен", headers={"WWW-Authenticate": "Bearer"}, ) api_key_hash = hashlib.sha256(x_api_key.encode()).hexdigest() integration_token_db = db.exec( select(IntegrationToken).where(IntegrationToken.token_hash == api_key_hash) ).first() if not integration_token_db: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Неверный API-ключ", headers={"WWW-Authenticate": "Bearer"}, ) # Обновляем use_dttm токена integration_token_db.use_dttm = datetime.utcnow() db.add(integration_token_db) db.commit() db.refresh(integration_token_db) jwt_token = create_integration_jwt_token(integration_token_db.company_id) return {"access_token": jwt_token, "token_type": "bearer"} @app.get("/ref", response_model=List[RefResponse], tags=["agent-tg"]) def get_refs(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_integration_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 "", promocode=r.promocode) for r in refs] @app.post("/ref/add", tags=["agent-tg"], response_model=RefAddResponse) def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_integration_db)): """ Добавляет новую реферальную ссылку для текущего Telegram-агента. """ # Генерация промокода (логика как была для промокодов) alphabet = string.ascii_letters + string.digits + "!@#$%^&*" promocode = ''.join(choices(alphabet, k=8)) while db.exec(select(Ref).where(Ref.promocode == promocode)).first(): promocode = ''.join(choices(alphabet, k=8)) new_ref = Ref( tg_agent_id=current_tg_agent.id, ref=str(uuid.uuid4()), description=req.description, promocode=promocode ) db.add(new_ref) db.commit() db.refresh(new_ref) return {"ref": new_ref.ref, "promocode": new_ref.promocode, "description": new_ref.description} @app.get("/ref/stat", tags=["agent-tg"], response_model=RefStatResponse) def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_integration_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=["agent-tg"], response_model=StatResponse) def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_integration_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.post("/tg_auth", tags=["agent-tg"], response_model=TgAuthResponse) def tg_auth(hash: str = Body(..., embed=True), db: Session = Depends(get_integration_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} @app.post("/withdraw", tags=["agent-tg"], response_model=WithdrawResponse) async def withdraw_funds( req: WithdrawRequest, db: Session = Depends(get_integration_db) ): """ Запрос на вывод средств для Telegram-агента. """ tg_agent = db.exec(select(TgAgent).where(TgAgent.tg_id == req.tg_id)).first() if not tg_agent: raise HTTPException(status_code=404, detail="Telegram-агент не найден") company = db.exec(select(Company).where(Company.id == tg_agent.company_id)).first() if not company: raise HTTPException(status_code=404, detail="Компания не найдена для агента") if req.amount <= 0: raise HTTPException(status_code=400, detail="Сумма для вывода должна быть положительной") agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == tg_agent.id)).first() if not agent_balance or agent_balance.available_balance < req.amount: raise HTTPException(status_code=400, detail="Недостаточно средств на балансе для вывода") # Определяем статус транзакции transaction_status = TransactionStatus.WAITING if company.auto_approve_transactions: transaction_status = TransactionStatus.NEW # Создаем запись AgentTransaction new_agent_transaction = AgentTransaction( tg_agent_id=tg_agent.id, amount=req.amount, status=transaction_status.value, transaction_group=uuid.uuid4() ) db.add(new_agent_transaction) # Обновляем баланс агента agent_balance.available_balance -= req.amount if transaction_status == TransactionStatus.WAITING: # Если автоматически одобряется, переводим на замороженный баланс компании (т.е. компания должна выплатить) agent_balance.frozen_balance += req.amount # Удерживаем средства, пока они не будут выведены db.add(agent_balance) db.commit() db.refresh(new_agent_transaction) db.refresh(agent_balance) return {"msg": "Запрос на вывод средств успешно создан", "transaction_id": new_agent_transaction.transaction_group} @app.post("/sale", tags=["integration"], response_model=SaleCreateResponse) async def create_sale( req: SaleCreateRequest, company: Company = Depends(get_current_company_from_jwt), db: Session = Depends(get_integration_db) ): """ Регистрирует новую продажу в системе. """ # Устанавливаем уровень изоляции для текущей транзакции db.connection(execution_options={'isolation_level': 'SERIALIZABLE'}) # Проверка входных данных if not req.ref and not req.promocode: raise HTTPException(status_code=400, detail="Необходимо передать либо ref, либо promocode") if not req.category: raise HTTPException(status_code=400, detail="Необходимо передать category (id категории)") # 1. Найти Ref по ref и/или promocode referral = None if req.ref and req.promocode: referral_by_ref = db.exec(select(Ref).where(Ref.ref == req.ref)).first() referral_by_code = db.exec(select(Ref).where(Ref.promocode == req.promocode)).first() if not referral_by_ref or not referral_by_code: raise HTTPException(status_code=404, detail="Реферальная ссылка или промокод не найдены") if referral_by_ref.id != referral_by_code.id: raise HTTPException(status_code=400, detail="ref и promocode не соответствуют одной ссылке") referral = referral_by_ref elif req.ref: referral = db.exec(select(Ref).where(Ref.ref == req.ref)).first() if not referral: raise HTTPException(status_code=404, detail="Реферальная ссылка не найдена") elif req.promocode: referral = db.exec(select(Ref).where(Ref.promocode == req.promocode)).first() if not referral: raise HTTPException(status_code=404, detail="Промокод не найден") # Проверяем, что реф действительно принадлежит компании tg_agent = db.exec(select(TgAgent).where(TgAgent.id == referral.tg_agent_id, TgAgent.company_id == company.id)).first() if not tg_agent: raise HTTPException(status_code=404, detail="Реферальная ссылка не принадлежит данной компании") # 2. Проверить, что sale_id уникален для данной компании existing_sale = db.exec( select(Sale) .where(Sale.company_id == company.id) .where(Sale.sale_id == req.sale_id) ).first() if existing_sale: raise HTTPException(status_code=400, detail="Продажа с таким sale_id уже существует для данной компании") # 3. Найти категорию и рассчитать crediting sale_category = db.exec(select(SaleCategory).where(SaleCategory.category == req.category, SaleCategory.company_id == company.id)).first() if not sale_category: raise HTTPException(status_code=404, detail="Категория продажи не найдена") crediting_amount = req.cost * (sale_category.perc / 100.0) # 4. Проверить и обновить AgentBalance и CompanyBalance agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == tg_agent.id)).first() if not agent_balance: raise HTTPException(status_code=404, detail="Баланс агента не найден") company_balance = db.exec(select(CompanyBalance).where(CompanyBalance.company_id == company.id)).first() if not company_balance: raise HTTPException(status_code=404, detail="Баланс компании не найден") # 5. Создать Sale new_sale = Sale( cost=req.cost, crediting=crediting_amount, ref=referral.id, sale_id=req.sale_id, group_sale_id=req.group_sale_id, company_id=company.id, category=sale_category.id, sale_dttm=datetime.utcnow() ) db.add(new_sale) # Создать AgentTransaction agent_transaction_status = TransactionStatus.DONE agent_transaction = AgentTransaction( tg_agent_id=tg_agent.id, amount=crediting_amount, status=agent_transaction_status.value, transaction_group=uuid.uuid4() ) db.add(agent_transaction) # Обновление балансов для продаж - всегда в замороженный/ожидающий баланс agent_balance.frozen_balance += crediting_amount company_balance.pending_balance -= crediting_amount company_balance.updated_dttm = datetime.utcnow() db.commit() db.refresh(new_sale) db.refresh(agent_balance) db.refresh(company_balance) db.refresh(agent_transaction) return { "msg": "Продажа успешно зарегистрирована", "sale_id": new_sale.sale_id, "crediting": new_sale.crediting } @app.post("/validationPromo", tags=["integration"], response_model=PromoValidationResponse) async def validate_promocode( req: PromoValidationRequest, company: Company = Depends(get_current_company_from_jwt), db: Session = Depends(get_integration_db) ): """ Проверяет валидность промокода для текущей компании. """ referral = db.exec(select(Ref).where(Ref.promocode == req.promocode)).first() if not referral: return {"validation": False} tg_agent = db.exec(select(TgAgent).where(TgAgent.id == referral.tg_agent_id, TgAgent.company_id == company.id)).first() if not tg_agent: return {"validation": False} return {"validation": True} @app.post("/register", tags=["agent-tg"], response_model=RegisterResponse) def register(req: RegisterRequest, db: Session = Depends(get_integration_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 = hashlib.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"}