307 lines
13 KiB
Python
307 lines
13 KiB
Python
from fastapi import FastAPI, Depends, HTTPException, status, Header, Body
|
||
from sqlmodel import Session, select, Field
|
||
from typing import Optional, List, Dict
|
||
from datetime import datetime, timedelta
|
||
import hashlib
|
||
import uuid
|
||
|
||
from sql_models import Company, IntegrationToken, Ref, Sale, AgentTransaction, PartnerTransaction, AgentBalance, TgAgent, CompanyBalance
|
||
from integration_models import Token, SaleCreateRequest, SaleCreateResponse, TransactionStatus, WithdrawRequest, WithdrawResponse
|
||
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 "") 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-агента.
|
||
"""
|
||
new_ref = Ref(
|
||
tg_agent_id=current_tg_agent.id,
|
||
ref=str(uuid.uuid4()),
|
||
description=req.description
|
||
)
|
||
db.add(new_ref)
|
||
db.commit()
|
||
db.refresh(new_ref)
|
||
return {"ref": new_ref.ref}
|
||
|
||
@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'})
|
||
|
||
# 1. Найти Ref по `ref` и `company.id`
|
||
# Сначала находим TgAgent, связанный с компанией, затем Ref
|
||
|
||
|
||
|
||
tg_agent = db.exec(
|
||
select(TgAgent)
|
||
.join(Ref)
|
||
.where(TgAgent.company_id == company.id)
|
||
.where(Ref.ref == req.ref)
|
||
).first()
|
||
|
||
if not tg_agent:
|
||
raise HTTPException(status_code=404, detail="Реферальная ссылка не найдена или не принадлежит данной компании")
|
||
|
||
referral = db.exec(select(Ref).where(Ref.ref == req.ref).where(Ref.tg_agent_id == tg_agent.id)).first()
|
||
if not referral:
|
||
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
|
||
crediting_amount = req.cost * (company.agent_commission / 100.0)
|
||
|
||
# 4. Проверить и обновить AgentBalance и CompanyBalance
|
||
# AgentBalance
|
||
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="Баланс агента не найден")
|
||
|
||
# CompanyBalance
|
||
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,
|
||
company_id=company.id,
|
||
sale_dttm=datetime.utcnow()
|
||
)
|
||
db.add(new_sale)
|
||
|
||
# Создать AgentTransaction
|
||
agent_transaction_status = TransactionStatus.DONE # auto_approve_transactions отвечает только за апрув агентских транзакций на вывод
|
||
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("/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"}
|