Compare commits

...

4 Commits

Author SHA1 Message Date
Redsandyg
7045d6790a Добавлены новые эндпоинты для работы с реферальными ссылками и статистикой Telegram-агентов в integration_api.py. Реализованы функции для получения списка реферальных ссылок, добавления новых ссылок, получения статистики по ссылкам и общей статистики для агентов. Также добавлена функция авторизации Telegram-агента по хешу и регистрация новых агентов. Удалены устаревшие функции из main.py для улучшения структуры кода. 2025-06-12 16:27:40 +03:00
Redsandyg
16973bbb64 Обновлены модели Sale, AgentTransaction и PartnerTransaction: добавлено уникальное ограничение для поля transaction_group в AgentTransaction, а также исправлены пробелы в определениях полей sale_id и transaction_group. Улучшена читаемость кода. 2025-06-12 16:03:03 +03:00
Redsandyg
736f04bb7e Обновлены идентификаторы API и добавлен генератор уникальных идентификаторов для продаж. В функции создания продажи добавлены проверки на наличие балансов агента и компании, а также улучшена логика обновления балансов. Установлен уровень изоляции для транзакций. Обновлены комментарии для ясности кода. 2025-06-11 11:01:26 +03:00
Redsandyg
1cc18e0364 Добавлены новые поля для агентской комиссии в модели Company и CompanyProfileResponse. Реализованы функции для обработки продаж через интеграционный API, включая создание и регистрацию продаж с учетом агентской комиссии. Обновлены соответствующие эндпоинты и модели для работы с токенами и продажами. Улучшена логика обработки транзакций и обновления балансов компаний и агентов. 2025-06-10 14:15:46 +03:00
9 changed files with 405 additions and 285 deletions

View File

@ -144,6 +144,7 @@ class CompanyProfileResponse(BaseModel):
name: str
key: str
commission: float
agent_commission: float
class AccountProfileResponse(BaseModel):
firstName: Optional[str] = None
@ -186,4 +187,4 @@ class IntegrationTokenCreateRequest(BaseModel):
class IntegrationTokenUpdateRequest(BaseModel):
id: int
description: str
description: str

61
call_sale_api.py Normal file
View File

@ -0,0 +1,61 @@
import uuid
import requests
import json
# Конфигурация API
BASE_URL = "http://127.0.0.1:8001"
API_KEY = "672a1437-70e8-461f-9bff-20f5ce4a023d"
REF = "9bd1a6bd-98e1-48f4-a120-3b3d016011c0"
# Данные для запроса на создание продажи
# Замените эти значения на актуальные для вашей продажи
sale_data = {
"cost": 100.50, # Стоимость продажи
"ref": REF, # Ваш реферальный код
"sale_id": str(uuid.uuid4()) # Уникальный идентификатор продажи для вашей компании
}
# Эндпоинты
token_endpoint = f"{BASE_URL}/token"
sale_endpoint = f"{BASE_URL}/sale"
# Шаг 1: Получение JWT токена
print(f"Отправка запроса на получение токена на {token_endpoint}")
token_headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json"
}
try:
token_response = requests.post(token_endpoint, headers=token_headers)
token_response.raise_for_status()
token_data = token_response.json()
jwt_token = token_data["access_token"]
print("JWT токен успешно получен.")
except requests.exceptions.RequestException as e:
print(f"Произошла ошибка при получении токена: {e}")
if hasattr(e, 'response') and e.response is not None:
print("Тело ответа с ошибкой:", e.response.json())
exit() # Прерываем выполнение, если не удалось получить токен
# Шаг 2: Вызов эндпоинта /sale с использованием полученного JWT токена
headers_with_jwt = {
"Authorization": f"Bearer {jwt_token}",
"Content-Type": "application/json"
}
print(f"Отправка запроса на {sale_endpoint} с данными: {sale_data}")
try:
sale_response = requests.post(sale_endpoint, headers=headers_with_jwt, data=json.dumps(sale_data))
sale_response.raise_for_status() # Вызовет исключение для ошибок HTTP (4xx или 5xx)
print("Статус ответа:", sale_response.status_code)
print("Тело ответа:", sale_response.json())
except requests.exceptions.RequestException as e:
print(f"Произошла ошибка при вызове API sale: {e}")
if hasattr(e, 'response') and e.response is not None:
print("Тело ответа с ошибкой:", e.response.json())

View File

@ -85,6 +85,7 @@ def fill_db():
company = Company(
name="RE: Premium",
commission=10.0,
agent_commission=15.0,
key="re-premium-key",
)
session.add(company)
@ -174,7 +175,7 @@ def fill_db():
sale_count = random.randint(20, int(20 * 1.25)) # от 20 до 25
for _ in range(sale_count):
cost = round(random.uniform(100, 1000), 2)
crediting = round(cost * random.uniform(0.5, 1.0), 2)
crediting = round(cost * (company.agent_commission / 100.0), 2)
# Генерируем случайную дату и время в пределах последних 7 дней
end_dttm = datetime.utcnow()

View File

@ -3,12 +3,13 @@ from passlib.context import CryptContext
from typing import Optional
from datetime import datetime, timedelta
from bff_models import Token, TransactionStatus
from sql_models import Company, TgAgent, Account, AgentBalance, AgentTransaction, PartnerTransaction, Sale, Ref
from sql_models import Company, TgAgent, Account, AgentBalance, AgentTransaction, PartnerTransaction, Sale, Ref, IntegrationToken, CompanyBalance
from hashlib import sha256
import jwt
from jwt.exceptions import InvalidTokenError
from fastapi import HTTPException, status, Depends, Request
from fastapi import HTTPException, status, Depends, Request, Header
from fastapi.security import OAuth2PasswordBearer
import hashlib
# Конфигурация
AUTH_DATABASE_ADDRESS = "sqlite:///partner.db"
@ -18,9 +19,60 @@ SECRET_KEY = "supersecretkey"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60
# JWT Configuration for Integration API
INTEGRATION_SECRET_KEY = "your-super-secret-jwt-key" # TODO: Замените на реальный секретный ключ из переменных окружения
INTEGRATION_ALGORITHM = "HS256"
INTEGRATION_ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # Токен действителен 7 дней
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_integration_db():
with Session(AUTH_DB_ENGINE) as session:
yield session
def create_integration_jwt_token(company_id: int):
expires = datetime.utcnow() + timedelta(minutes=INTEGRATION_ACCESS_TOKEN_EXPIRE_MINUTES)
payload = {
"sub": str(company_id),
"exp": expires,
"type": "access"
}
return jwt.encode(payload, INTEGRATION_SECRET_KEY, algorithm=INTEGRATION_ALGORITHM)
async def get_current_company_from_jwt(
token: str = Depends(OAuth2PasswordBearer(tokenUrl="/token")),
db: Session = Depends(get_integration_db)
):
"""
Зависимость для получения текущей компании на основе JWT токена для Integration API.
"""
try:
payload = jwt.decode(token, INTEGRATION_SECRET_KEY, algorithms=[INTEGRATION_ALGORITHM])
company_id: int = int(payload.get("sub"))
if company_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Недействительная полезная нагрузка токена",
headers={"WWW-Authenticate": "Bearer"},
)
company = db.exec(select(Company).where(Company.id == company_id)).first()
if not company:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Компания не найдена")
return company
except InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Недействительный токен",
headers={"WWW-Authenticate": "Bearer"},
)
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Срок действия токена истек",
headers={"WWW-Authenticate": "Bearer"},
)
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()

View File

@ -1,75 +1,256 @@
from fastapi import FastAPI, HTTPException, status, Depends, Request
from sqlmodel import Session, select
from typing import Optional
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 jwt
from jwt.exceptions import InvalidTokenError
from pydantic import BaseModel
import hashlib
import uuid
from sql_models import Sale
from helpers_bff import get_db, AUTH_DB_ENGINE # Assuming these are the correct imports
from sql_models import Company, IntegrationToken, Ref, Sale, AgentTransaction, PartnerTransaction, AgentBalance, TgAgent, CompanyBalance
from integration_models import Token, SaleCreateRequest, SaleCreateResponse, TransactionStatus
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()
# Конфигурация для интеграционного API токена
INTEGRATION_SECRET_KEY = "your-integration-super-secret-key" # Смените это на безопасный ключ!
INTEGRATION_ALGORITHM = "HS256"
INTEGRATION_TOKEN_EXPIRE_MINUTES = 60 * 24 # 24 часа
#7c06945b-f4b8-4929-8350-b9841405a609
class IntegrationTokenData(BaseModel):
client_id: str
class SaleCreate(BaseModel):
cost: float
crediting: float
ref_id: int
sale_id: str
company_id: int
sale_date: datetime = datetime.utcnow()
@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 токен.
"""
def create_integration_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=INTEGRATION_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, INTEGRATION_SECRET_KEY, algorithm=INTEGRATION_ALGORITHM)
return encoded_jwt
if not x_api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="API-ключ не предоставлен",
headers={"WWW-Authenticate": "Bearer"},
)
async def verify_integration_token(request: Request):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate integration credentials",
headers={"WWW-Authenticate": "Bearer"},
)
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise credentials_exception
token = auth_header.replace("Bearer ", "").strip()
try:
payload = jwt.decode(token, INTEGRATION_SECRET_KEY, algorithms=[INTEGRATION_ALGORITHM])
client_id: str = payload.get("client_id")
if client_id is None:
raise credentials_exception
# Здесь вы можете добавить логику для проверки client_id, например, из базы данных
except InvalidTokenError:
raise credentials_exception
return True # Токен действителен
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()
@app.post("/sale", status_code=status.HTTP_201_CREATED)
async def upload_sale(sale_data: SaleCreate, db: Session = Depends(get_db), verified: bool = Depends(verify_integration_token)):
if not verified:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authorized")
if not integration_token_db:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверный API-ключ",
headers={"WWW-Authenticate": "Bearer"},
)
db_sale = Sale(cost=sale_data.cost, crediting=sale_data.crediting, ref=sale_data.ref_id, sale_id=sale_data.sale_id, company_id=sale_data.company_id, sale_date=sale_data.sale_date)
db.add(db_sale)
# Обновляем use_dttm токена
integration_token_db.use_dttm = datetime.utcnow()
db.add(integration_token_db)
db.commit()
db.refresh(db_sale)
return {"message": "Sale uploaded successfully", "sale_id": db_sale.id}
db.refresh(integration_token_db)
@app.get("/generate-integration-token")
async def generate_token_endpoint(client_id: str):
token_data = {"client_id": client_id}
token = create_integration_access_token(token_data)
return {"access_token": token, "token_type": "bearer"}
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=["partner-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=["partner-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=["partner-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=["partner-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=["partner-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("/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=["partner-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"}

34
integration_models.py Normal file
View File

@ -0,0 +1,34 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
import uuid
from enum import Enum
# Models for /token endpoint
class Token(BaseModel):
access_token: str
token_type: str
class IntegrationTokenResponse(BaseModel):
msg: str
company_name: str
company_key: str
# Models for /sale endpoint
class SaleCreateRequest(BaseModel):
ref: str
sale_id: str
cost: float
class SaleCreateResponse(BaseModel):
msg: str
sale_id: str
crediting: float
class TransactionStatus(str, Enum):
NEW = "new"
PROCESS = "process"
WAITING = "waiting"
DONE = "done"
CANCELED = "canceled"
ERROR = "error"

123
main.py
View File

@ -1,4 +1,3 @@
import uuid
from fastapi import (
FastAPI,
Depends,
@ -13,7 +12,6 @@ from typing import Optional, List, Dict
from datetime import timedelta, datetime
from bff_models import (
Token,
RegisterResponse,
DashboardCardsResponse,
DashboardChartTotalResponse,
DashboardChartAgentResponse,
@ -30,7 +28,6 @@ from bff_models import (
AutoApproveSettingsGetResponse,
AutoApproveSettingsUpdateResponse,
ApproveTransactionsResult,
TgAuthResponse,
BillingPayoutsTransactionsResponse,
AccountProfileUpdateRequest,
AccountPasswordChangeRequest,
@ -42,7 +39,6 @@ from bff_models import (
IntegrationTokenCreateRequest,
IntegrationTokenUpdateRequest
)
from tg_models import RefAddRequest, RefResponse, RegisterRequest, RefAddResponse, RefStatResponse, StatResponse
from sql_models import (
Company,
TgAgent,
@ -59,9 +55,7 @@ import hashlib
from helpers_bff import (
AUTH_DB_ENGINE,
get_db,
get_tg_agent_by_tg_id,
get_current_account,
get_current_tg_agent,
create_access_token,
verify_password,
get_account_by_login,
@ -77,41 +71,6 @@ SQLModel.metadata.create_all(AUTH_DB_ENGINE)
app = FastAPI()
@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 = 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"}
@app.post("/token", response_model=Token, tags=["bff", "token"])
def login_account_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
@ -136,72 +95,6 @@ def login_account_for_access_token(
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(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=["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)):
"""
@ -504,19 +397,6 @@ def get_billing_chart_pie(current_account: Account = Depends(get_current_account
@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 ---
@app.get("/account", tags=["bff", "account"], response_model=AccountResponse)
def get_account(current_account: Account = Depends(get_current_account)):
"""
@ -544,7 +424,8 @@ def get_account_profile(current_account: Account = Depends(get_current_account),
"company": {
"name": company.name,
"key": company.key,
"commission": company.commission
"commission": company.commission,
"agent_commission": company.agent_commission
}
}

View File

@ -1,146 +1,54 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt
annotated-types==0.7.0
# via pydantic
anyio==4.8.0
# via
# httpx
# starlette
# watchfiles
bcrypt==4.2.1
# via passlib
behave==1.2.6
# via epai-auth (pyproject.toml)
certifi==2024.12.14
# via
# httpcore
# httpx
# requests
cffi==1.17.1
# via cryptography
charset-normalizer==3.4.2
# via requests
click==8.1.8
# via
# rich-toolkit
# typer
# uvicorn
colorama==0.4.6
cryptography==44.0.0
# via epai-auth (pyproject.toml)
dnspython==2.7.0
# via email-validator
email-validator==2.2.0
# via fastapi
fastapi==0.115.6
# via epai-auth (pyproject.toml)
fastapi-cli==0.0.7
# via fastapi
greenlet==3.1.1
# via sqlalchemy
h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==1.0.7
# via httpx
httptools==0.6.4
# via uvicorn
httpx==0.28.1
# via fastapi
idna==3.10
# via
# anyio
# email-validator
# httpx
# requests
jinja2==3.1.5
# via fastapi
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
# via jinja2
mdurl==0.1.2
# via markdown-it-py
parse==1.20.2
# via
# behave
# parse-type
parse-type==0.6.4
# via behave
passlib==1.7.4
# via epai-auth (pyproject.toml)
psycopg2==2.9.10
# via epai-auth (pyproject.toml)
pycparser==2.22
# via cffi
pydantic==2.10.5
# via
# fastapi
# pydantic-settings
# sqlmodel
pydantic-core==2.27.2
# via pydantic
pydantic-settings==2.9.1
# via epai-auth (pyproject.toml)
pygments==2.19.1
# via rich
pyjwt==2.10.1
# via epai-auth (pyproject.toml)
python-dotenv==1.0.1
# via
# pydantic-settings
# uvicorn
python-multipart==0.0.20
# via fastapi
pyyaml==6.0.2
# via uvicorn
requests==2.32.3
# via epai-auth (pyproject.toml)
rich==13.9.4
# via
# rich-toolkit
# typer
rich-toolkit==0.12.0
# via fastapi-cli
ruff==0.9.1
# via epai-auth (pyproject.toml)
shellingham==1.5.4
# via typer
six==1.17.0
# via
# behave
# parse-type
sniffio==1.3.1
# via anyio
sqlalchemy==2.0.37
# via sqlmodel
sqlmodel==0.0.22
# via epai-auth (pyproject.toml)
starlette==0.41.3
# via fastapi
typer==0.15.1
# via fastapi-cli
typing-extensions==4.12.2
# via
# anyio
# fastapi
# pydantic
# pydantic-core
# rich-toolkit
# sqlalchemy
# typer
# typing-inspection
typing-inspection==0.4.0
# via pydantic-settings
urllib3==2.4.0
# via requests
uvicorn==0.34.0
# via
# fastapi
# fastapi-cli
#uvloop==0.21.0
# via uvicorn
watchfiles==1.0.4
# via uvicorn
websockets==14.1
# via uvicorn

View File

@ -7,11 +7,12 @@ from sqlalchemy import Column, String
class Company(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
commission: float # процент комиссии
commission: float # процент комиссии, который взымается за пользование сервисом
agent_commission: float = Field(default=0.0) # процент от продаж, который начисляется агенту
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)
auto_approve_transactions: bool = Field(default=False) # Отвечает за автоматическое одобрение агентских транзакций на вывод.
integration_tokens: List["IntegrationToken"] = Relationship(back_populates="company")
@ -40,7 +41,7 @@ class Sale(SQLModel, table=True):
cost: float
crediting: float # сколько начислено за продажу
ref: int = Field(foreign_key="ref.id")
sale_id: str
sale_id: str
company_id: int = Field(foreign_key="company.id")
sale_dttm: datetime = Field(default_factory=datetime.utcnow)
create_dttm: datetime = Field(default_factory=datetime.utcnow)
@ -52,7 +53,7 @@ class AgentTransaction(SQLModel, table=True):
tg_agent_id: int = Field(foreign_key="tgagent.id")
amount: float
status: str
transaction_group: uuid.UUID = Field(default_factory=uuid.uuid4)
transaction_group: uuid.UUID = Field(default_factory=uuid.uuid4, unique=True)
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
@ -63,7 +64,7 @@ class PartnerTransaction(SQLModel, table=True):
type: str
amount: float
status: str
transaction_group: uuid.UUID
transaction_group: uuid.UUID
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)