Compare commits

..

12 Commits

Author SHA1 Message Date
Redsandyg
2c8cbe753e Добавлен новый файл call_validation_promo_api.py для валидации промокодов через API. Реализована логика получения JWT токена и проверки валидности промокода. Обновлены модели в integration_models.py для поддержки запросов и ответов на валидацию промокодов. Добавлен новый эндпоинт /validationPromo в integration_api.py для обработки запросов на валидацию промокодов. 2025-06-22 14:39:14 +03:00
Redsandyg
a6308582cb Удалено поле agent_commission из модели CompanyProfileResponse и соответствующих мест в коде. Обновлены значения комиссии в функции fill_db для создания компании. Изменены теги в эндпоинтах API, убраны лишние теги "bff" для упрощения структуры. Улучшена читаемость кода. 2025-06-21 15:02:21 +03:00
Redsandyg
9157570e58 Обновлены модели и функции для поддержки групповых продаж. Внесены изменения в API для передачи идентификаторов групповых продаж. Обновлены SQL-скрипты и модели для учета нового поля group_sale_id. Изменены данные для создания продажи, включая категорию и стоимость. 2025-06-18 11:31:32 +03:00
Redsandyg
bf6a6a8987 Добавлены новые модели SaleCategoryRequest и SaleCategoryResponse в bff_models.py для работы с категориями продаж. Обновлены функции в fill_db.py для заполнения базы данных категориями продаж. Изменены эндпоинты в main.py для создания и получения категорий продаж. Обновлены модели и SQL-скрипты для поддержки новых полей и связей. Улучшена логика обработки продаж с учетом категорий. 2025-06-18 10:48:29 +03:00
Redsandyg
4f366680bf Обновлен эндпоинт создания продажи в integration_api.py: добавлена проверка на наличие ref и promocode, улучшена логика поиска реферальных ссылок и проверка их принадлежности компании. Модель SaleCreateRequest обновлена для поддержки опционального поля promocode. 2025-06-16 11:11:41 +03:00
Redsandyg
d113ae4adb Добавлено новое поле promocode в модель StatReferralsItem и модель Ref. Обновлены функции в fill_db.py для генерации уникальных промокодов при создании реферальных ссылок. Обновлены эндпоинты в integration_api.py для возврата промокодов вместе с реферальными ссылками. Удалены устаревшие функции работы с промокодами из кода. Обновлены SQL-скрипты и модели для учета новых полей. 2025-06-15 17:03:41 +03:00
Redsandyg
92df59ad23 Добавлены новые функции для работы с промокодами в integration_api.py, включая создание и получение промокодов для Telegram-агентов. Обновлены модели и добавлен новый файл generate_sql.py для генерации SQL-скрипта создания таблиц. Обновлены fill_db.py для генерации промокодов при заполнении базы данных. Также обновлены sql_models.py для добавления модели PromoCode и соответствующих связей. Улучшена структура кода и добавлены отладочные сообщения. 2025-06-15 15:20:28 +03:00
Redsandyg
3973d6404d Добавлен новый эндпоинт для запроса на вывод средств для Telegram-агентов в integration_api.py. Обновлены теги для существующих эндпоинтов, изменив их с "partner-tg" на "agent-tg". В integration_models.py добавлены модели WithdrawRequest и WithdrawResponse для обработки запросов на вывод средств. Улучшена логика обработки транзакций и проверок баланса. 2025-06-13 14:12:58 +03:00
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
13 changed files with 960 additions and 313 deletions

View File

@@ -89,6 +89,7 @@ class StatReferralsItem(BaseModel):
ref: str
agent: Optional[str] = None
description: str
promocode: str
salesSum: float
salesCount: int
@@ -187,3 +188,20 @@ class IntegrationTokenCreateRequest(BaseModel):
class IntegrationTokenUpdateRequest(BaseModel):
id: int
description: str
# New models for sale categories
class SaleCategoryRequest(BaseModel):
id: int | None = None
category: str
description: str | None = None
perc: float
class SaleCategoryResponse(BaseModel):
id: int
category: str
description: str | None = None
perc: float
create_dttm: datetime
update_dttm: datetime
model_config = ConfigDict(from_attributes=True)

63
call_sale_api.py Normal file
View File

@@ -0,0 +1,63 @@
import uuid
import requests
import json
# Конфигурация API
BASE_URL = "http://127.0.0.1:8001"
API_KEY = "de058226-37d3-4d0e-a483-3c2a7fac3573"
REF = "0d9aaa96-80e6-424c-84c9-ff70a6eb915e"
# Данные для запроса на создание продажи
# Замените эти значения на актуальные для вашей продажи
sale_data = {
"cost": 100, # Стоимость продажи
"ref": REF, # Ваш реферальный код
"sale_id": str(uuid.uuid4()), # Уникальный идентификатор продажи для вашей компании
"category": "vip", # название категории (например, 'basic', 'premium', 'vip')
"group_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

@@ -0,0 +1,63 @@
import requests
import json
# Конфигурация API
BASE_URL = "http://127.0.0.1:8001"
API_KEY = "9efb2df0-03d7-4212-93db-0ae6418667e1" # API-ключ
PROMOCODE = "brw0OMOz"
# Данные для запроса на валидацию промокода
validation_data = {
"promocode": PROMOCODE,
}
# Эндпоинты
token_endpoint = f"{BASE_URL}/token"
validation_endpoint = f"{BASE_URL}/validationPromo"
# Шаг 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:
try:
print("Тело ответа с ошибкой:", e.response.json())
except json.JSONDecodeError:
print("Тело ответа с ошибкой (не JSON):", e.response.text)
exit() # Прерываем выполнение, если не удалось получить токен
# Шаг 2: Вызов эндпоинта /validationPromo с использованием полученного JWT токена
headers_with_jwt = {
"Authorization": f"Bearer {jwt_token}",
"Content-Type": "application/json"
}
print(f"Отправка запроса на {validation_endpoint} с данными: {validation_data}")
try:
validation_response = requests.post(validation_endpoint, headers=headers_with_jwt, data=json.dumps(validation_data))
validation_response.raise_for_status() # Вызовет исключение для ошибок HTTP (4xx или 5xx)
print("Статус ответа:", validation_response.status_code)
print("Тело ответа:", validation_response.json())
except requests.exceptions.RequestException as e:
print(f"Произошла ошибка при вызове API validationPromo: {e}")
if hasattr(e, 'response') and e.response is not None:
try:
print("Тело ответа с ошибкой:", e.response.json())
except json.JSONDecodeError:
print("Тело ответа с ошибкой (не JSON):", e.response.text)

View File

@@ -1,11 +1,12 @@
import random
from uuid import uuid4
from sqlmodel import Session
from sql_models import TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance, IntegrationToken
from sql_models import TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance, IntegrationToken, SaleCategory
from sqlalchemy import text
from datetime import datetime, timedelta
from hashlib import sha256
from helpers_bff import AUTH_DB_ENGINE, get_password_hash
import string
# Константа: список user_ids
@@ -71,6 +72,7 @@ def fill_db():
date_list = get_date_list(7) # 8 дней: от недели назад до сегодня
with Session(AUTH_DB_ENGINE) as session:
# Очистка таблиц
# session.execute(text("DELETE FROM promocode"))
session.execute(text("DELETE FROM sale"))
session.execute(text("DELETE FROM ref"))
session.execute(text("DELETE FROM tgagent"))
@@ -79,12 +81,13 @@ def fill_db():
session.execute(text('DELETE FROM "partner_transactions"'))
session.execute(text('DELETE FROM "company_balances"'))
session.execute(text('DELETE FROM "agent_balances"'))
session.execute(text("DELETE FROM salecategory"))
session.execute(text("DELETE FROM company"))
session.commit()
# 0. Company
company = Company(
name="RE: Premium",
commission=10.0,
commission=0.0,
key="re-premium-key",
)
session.add(company)
@@ -107,6 +110,18 @@ def fill_db():
session.add(integration_token)
session.commit()
# 0.2 SaleCategory
sale_categories = [
SaleCategory(category="basic", description="Базовая продажа", perc=10.0, company_id=company.id),
SaleCategory(category="premium", description="Премиум продажа", perc=20.0, company_id=company.id),
SaleCategory(category="vip", description="VIP продажа", perc=30.0, company_id=company.id),
]
for cat in sale_categories:
session.add(cat)
session.commit()
for cat in sale_categories:
session.refresh(cat)
# 1. Accounts
accounts = []
for i in range(4):
@@ -148,19 +163,28 @@ def fill_db():
session.commit()
for tg_agent in tg_agents:
session.refresh(tg_agent)
# Отладка: количество агентов
agent_count = session.execute(text("SELECT COUNT(*) FROM tgagent")).scalar()
print(f'Агентов в базе: {agent_count}')
# 3. Refs (минимум 22 на агента)
refs = []
desc_count = len(ALL_DESCRIPTIONS)
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
for tg_agent in tg_agents:
ref_count = random.randint(22, int(22 * 1.25)) # от 22 до 27
for j in range(ref_count):
ref_val = str(uuid4())
desc_val = ALL_DESCRIPTIONS[(j % desc_count)]
dt = random.choice(date_list)
promocode = ''.join(random.choices(alphabet, k=8))
# Проверяем уникальность промокода среди уже созданных рефов
while any(r.promocode == promocode for r in refs):
promocode = ''.join(random.choices(alphabet, k=8))
ref = Ref(
tg_agent_id=tg_agent.id,
ref=ref_val,
description=desc_val,
promocode=promocode,
create_dttm=dt,
update_dttm=dt
)
@@ -170,25 +194,30 @@ def fill_db():
for ref in refs:
session.refresh(ref)
# 4. Sales (минимум 20 на каждый ref)
all_categories = session.query(SaleCategory).filter_by(company_id=company.id).all()
for ref in refs:
sale_count = random.randint(20, int(20 * 1.25)) # от 20 до 25
for _ in range(sale_count):
group_size = 5
group_sale_ids = [str(uuid4()) for _ in range((sale_count // group_size) + 1)]
for idx in range(sale_count):
cost = round(random.uniform(100, 1000), 2)
crediting = round(cost * random.uniform(0.5, 1.0), 2)
sale_category = random.choice(all_categories)
crediting = round(cost * (sale_category.perc / 100.0), 2)
# Генерируем случайную дату и время в пределах последних 7 дней
end_dttm = datetime.utcnow()
start_dttm = end_dttm - timedelta(days=7)
time_diff = end_dttm - start_dttm
random_seconds = random.uniform(0, time_diff.total_seconds())
sale_dttm = start_dttm + timedelta(seconds=random_seconds)
group_sale_id = group_sale_ids[idx // group_size]
sale = Sale(
cost=cost,
crediting=crediting,
ref=ref.id,
sale_id=str(uuid4()),
group_sale_id=group_sale_id,
company_id=company.id,
category=sale_category.id,
sale_dttm=sale_dttm,
create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне
update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне

22
generate_sql.py Normal file
View File

@@ -0,0 +1,22 @@
from sqlmodel import SQLModel
from helpers_bff import AUTH_DB_ENGINE
from sqlalchemy.schema import CreateTable
import os
# --- Отладочный вывод ---
print("Таблицы в metadata:", SQLModel.metadata.sorted_tables)
print("Все зарегистрированные модели:", SQLModel.__subclasses__())
# --- Генерация и сохранение SQL-скрипта создания таблиц ---
def get_sql_create_script():
script = []
for table in SQLModel.metadata.sorted_tables:
create_table_sql = str(CreateTable(table).compile(dialect=AUTH_DB_ENGINE.dialect))
script.append(f"{create_table_sql};")
return "\n\n".join(script)
sql_script = get_sql_create_script()
sql_file_path = os.path.join(os.path.dirname(__file__), "sql_create.sql")
with open(sql_file_path, "w", encoding="utf-8") as f:
f.write(sql_script)
print("SQL-скрипт успешно сгенерирован!")

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,343 @@
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, Request
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 random import choices
import string
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, 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()
# Конфигурация для интеграционного 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=["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"}

51
integration_models.py Normal file
View File

@@ -0,0 +1,51 @@
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: Optional[str] = None
promocode: Optional[str] = None
sale_id: str
cost: float
category: str # название категории продажи
group_sale_id: str # новое поле для группировки продаж
class SaleCreateResponse(BaseModel):
msg: str
sale_id: str
crediting: float
class WithdrawRequest(BaseModel):
tg_id: int
amount: float
class WithdrawResponse(BaseModel):
msg: str
transaction_id: uuid.UUID
class TransactionStatus(str, Enum):
NEW = "new"
PROCESS = "process"
WAITING = "waiting"
DONE = "done"
CANCELED = "canceled"
ERROR = "error"
class PromoValidationRequest(BaseModel):
promocode: str
class PromoValidationResponse(BaseModel):
validation: bool

232
main.py
View File

@@ -13,7 +13,6 @@ from typing import Optional, List, Dict
from datetime import timedelta, datetime
from bff_models import (
Token,
RegisterResponse,
DashboardCardsResponse,
DashboardChartTotalResponse,
DashboardChartAgentResponse,
@@ -30,7 +29,6 @@ from bff_models import (
AutoApproveSettingsGetResponse,
AutoApproveSettingsUpdateResponse,
ApproveTransactionsResult,
TgAuthResponse,
BillingPayoutsTransactionsResponse,
AccountProfileUpdateRequest,
AccountPasswordChangeRequest,
@@ -40,9 +38,10 @@ from bff_models import (
TransactionStatus,
IntegrationTokenResponse,
IntegrationTokenCreateRequest,
IntegrationTokenUpdateRequest
IntegrationTokenUpdateRequest,
SaleCategoryRequest,
SaleCategoryResponse
)
from tg_models import RefAddRequest, RefResponse, RegisterRequest, RefAddResponse, RefStatResponse, StatResponse
from sql_models import (
Company,
TgAgent,
@@ -52,22 +51,23 @@ from sql_models import (
PartnerTransaction,
AgentBalance,
Account,
IntegrationToken
IntegrationToken,
SaleCategory
)
from sqlalchemy import func
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,
ACCESS_TOKEN_EXPIRE_MINUTES,
pwd_context,
)
import os
from pydantic import BaseModel
# Создание движка базы данных
@@ -77,42 +77,9 @@ 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"])
@app.post("/token", response_model=Token, tags=[ "token"])
def login_account_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
@@ -136,73 +103,7 @@ 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)
@app.get("/dashboard/cards", tags=[ "dashboard"], response_model=DashboardCardsResponse)
def get_dashboard_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
"""
Возвращает данные для карточек на главной панели (dashboard) пользователя, включая общий доход, выплаты, активных рефералов, ожидающие выплаты и общее количество продаж.
@@ -233,7 +134,7 @@ def get_dashboard_cards(current_account: Account = Depends(get_current_account),
"totalSales": totalSales
}
@app.get("/dashboard/chart/total", tags=["bff", "dashboard"], response_model=DashboardChartTotalResponse)
@app.get("/dashboard/chart/total", tags=[ "dashboard"], response_model=DashboardChartTotalResponse)
def get_dashboard_chart_total(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
"""
Возвращает данные для графика "Общий доход и продажи по датам" на главной панели (dashboard).
@@ -254,7 +155,7 @@ def get_dashboard_chart_total(current_account: Account = Depends(get_current_acc
]
return DashboardChartTotalResponse(items=data)
@app.get("/dashboard/chart/agent", tags=["bff", "dashboard"], response_model=DashboardChartAgentResponse)
@app.get("/dashboard/chart/agent", tags=[ "dashboard"], response_model=DashboardChartAgentResponse)
def get_dashboard_chart_agent(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
"""
Возвращает данные для графика "Продажи по агентам" на главной панели (dashboard).
@@ -284,7 +185,7 @@ def get_dashboard_chart_agent(current_account: Account = Depends(get_current_acc
})
return DashboardChartAgentResponse(items=result)
@app.get("/stat/agents", tags=["bff", "stat"], response_model=StatAgentsResponse)
@app.get("/stat/agents", tags=[ "stat"], response_model=StatAgentsResponse)
def get_agents_stat(
db: Session = Depends(get_db),
date_start: str = Query(None),
@@ -328,7 +229,7 @@ def get_agents_stat(
})
return StatAgentsResponse(items=result)
@app.get("/stat/referrals", tags=["bff", "stat"], response_model=StatReferralsResponse)
@app.get("/stat/referrals", tags=[ "stat"], response_model=StatReferralsResponse)
def get_referrals_stat(
db: Session = Depends(get_db),
date_start: str = Query(None),
@@ -355,12 +256,13 @@ def get_referrals_stat(
"ref": ref.ref,
"agent": agent.name if agent and agent.name else f"Агент {ref.tg_agent_id}",
"description": ref.description or "",
"promocode": ref.promocode,
"salesSum": sales_sum,
"salesCount": sales_count
})
return StatReferralsResponse(items=result)
@app.get("/stat/sales", tags=["bff", "stat"], response_model=StatSalesResponse)
@app.get("/stat/sales", tags=[ "stat"], response_model=StatSalesResponse)
def get_sales_stat(
db: Session = Depends(get_db),
date_start: str = Query(None),
@@ -396,7 +298,7 @@ def get_sales_stat(
})
return StatSalesResponse(items=result)
@app.get("/billing/cards", tags=["bff", "billing"], response_model=BillingCardsResponse)
@app.get("/billing/cards", tags=[ "billing"], response_model=BillingCardsResponse)
def get_billing_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
"""
Возвращает ключевые показатели биллинга для компании, включая общий заработок, общие выплаты и доступные к выводу средства.
@@ -419,7 +321,7 @@ def get_billing_cards(current_account: Account = Depends(get_current_account), d
"pendingPayouts": pendingPayouts
}
@app.get("/billing/payouts/transactions", tags=["bff", "billing"], response_model=BillingPayoutsTransactionsResponse)
@app.get("/billing/payouts/transactions", tags=[ "billing"], response_model=BillingPayoutsTransactionsResponse)
def get_billing_payouts_transactions(
db: Session = Depends(get_db),
date_start: str = Query(None),
@@ -459,7 +361,7 @@ def get_billing_payouts_transactions(
})
return BillingPayoutsTransactionsResponse(items=result)
@app.get("/billing/chart/stat", tags=["bff", "billing"], response_model=BillingChartStatResponse)
@app.get("/billing/chart/stat", tags=[ "billing"], response_model=BillingChartStatResponse)
def get_billing_chart_stat(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
"""
Возвращает статистику выплат по датам и статусам для компании текущего пользователя.
@@ -484,7 +386,7 @@ def get_billing_chart_stat(current_account: Account = Depends(get_current_accoun
]
return BillingChartStatResponse(items=data)
@app.get("/billing/chart/pie", tags=["bff", "billing"], response_model=BillingChartPieResponse)
@app.get("/billing/chart/pie", tags=[ "billing"], response_model=BillingChartPieResponse)
def get_billing_chart_pie(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
"""
Возвращает круговую диаграмму статистики выплат по статусам для компании текущего пользователя.
@@ -504,20 +406,7 @@ 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)
@app.get("/account", tags=[ "account"], response_model=AccountResponse)
def get_account(current_account: Account = Depends(get_current_account)):
"""
Возвращает базовую информацию об аккаунте текущего пользователя (имя и фамилию).
@@ -527,7 +416,7 @@ def get_account(current_account: Account = Depends(get_current_account)):
"surname": current_account.surname
}
@app.get("/account/profile", tags=["bff", "account"], response_model=AccountProfileResponse)
@app.get("/account/profile", tags=[ "account"], response_model=AccountProfileResponse)
def get_account_profile(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
"""
Возвращает полную информацию о профиле аккаунта текущего пользователя, включая данные компании.
@@ -544,11 +433,11 @@ def get_account_profile(current_account: Account = Depends(get_current_account),
"company": {
"name": company.name,
"key": company.key,
"commission": company.commission
"commission": company.commission,
}
}
@app.post("/account/profile", tags=["bff", "account"], response_model=AccountProfileUpdateResponse)
@app.post("/account/profile", tags=[ "account"], response_model=AccountProfileUpdateResponse)
def update_account_profile(
req: AccountProfileUpdateRequest,
current_account: Account = Depends(get_current_account),
@@ -570,7 +459,7 @@ def update_account_profile(
db.refresh(current_account)
return {"msg": "Профиль обновлён успешно"}
@app.post("/account/password", tags=["bff", "account"], response_model=AccountPasswordChangeResponse)
@app.post("/account/password", tags=[ "account"], response_model=AccountPasswordChangeResponse)
def change_account_password(
req: AccountPasswordChangeRequest,
current_account: Account = Depends(get_current_account),
@@ -596,7 +485,7 @@ def change_account_password(
# --- Новый функционал для агентских транзакций партнера ---
@app.get("/account/agent-transaction", response_model=List[AgentTransactionResponse], tags=["bff", "account"])
@app.get("/account/agent-transaction", response_model=List[AgentTransactionResponse], tags=[ "account"])
def get_account_agent_transactions(
statuses: Optional[List[TransactionStatus]] = Query(None),
date_start: str = Query(None),
@@ -653,7 +542,7 @@ def get_account_agent_transactions(
@app.get("/account/auto-approve", tags=["bff", "account"], response_model=AutoApproveSettingsGetResponse)
@app.get("/account/auto-approve", tags=[ "account"], response_model=AutoApproveSettingsGetResponse)
def get_auto_approve_settings(
current_account: Account = Depends(get_current_account),
db: Session = Depends(get_db)
@@ -666,7 +555,7 @@ def get_auto_approve_settings(
raise HTTPException(status_code=404, detail="Компания не найдена")
return {"auto_approve_transactions": company.auto_approve_transactions}
@app.post("/account/auto-approve", tags=["bff", "account"], response_model=AutoApproveSettingsUpdateResponse)
@app.post("/account/auto-approve", tags=[ "account"], response_model=AutoApproveSettingsUpdateResponse)
def update_auto_approve_settings(
req: AutoApproveSettingsRequest,
current_account: Account = Depends(get_current_account),
@@ -716,7 +605,7 @@ def update_auto_approve_settings(
@app.post("/account/approve-transactions", tags=["bff", "account"], response_model=ApproveTransactionsResult)
@app.post("/account/approve-transactions", tags=[ "account"], response_model=ApproveTransactionsResult)
def approve_agent_transactions(
req: ApproveTransactionsRequest,
current_account: Account = Depends(get_current_account),
@@ -753,7 +642,7 @@ def approve_agent_transactions(
# --- Новый функционал для интеграционных токенов ---
@app.get("/account/integration-tokens", tags=["bff", "account"], response_model=List[IntegrationTokenResponse])
@app.get("/account/integration-tokens", tags=[ "account"], response_model=List[IntegrationTokenResponse])
def get_integration_tokens(
current_account: Account = Depends(get_current_account),
db: Session = Depends(get_db)
@@ -765,7 +654,7 @@ def get_integration_tokens(
return tokens # Позволяем FastAPI самостоятельно сериализовать объекты IntegrationToken в IntegrationTokenResponse
@app.post("/account/integration-tokens", tags=["bff", "account"], response_model=IntegrationTokenResponse)
@app.post("/account/integration-tokens", tags=[ "account"], response_model=IntegrationTokenResponse)
def create_integration_token(
req: IntegrationTokenCreateRequest,
current_account: Account = Depends(get_current_account),
@@ -798,7 +687,7 @@ def create_integration_token(
return response_token
@app.post("/account/integration-tokens/update-description", tags=["bff", "account"], response_model=Dict[str, str])
@app.post("/account/integration-tokens/update-description", tags=[ "account"], response_model=Dict[str, str])
def update_integration_token_description(
req: IntegrationTokenUpdateRequest,
current_account: Account = Depends(get_current_account),
@@ -821,7 +710,7 @@ def update_integration_token_description(
db.refresh(token)
return {"msg": "Описание токена обновлено успешно"}
@app.delete("/account/integration-tokens/{token_id}", tags=["bff", "account"], response_model=Dict[str, str])
@app.delete("/account/integration-tokens/{token_id}", tags=[ "account"], response_model=Dict[str, str])
def delete_integration_token(
token_id: int,
current_account: Account = Depends(get_current_account),
@@ -840,3 +729,60 @@ def delete_integration_token(
db.delete(token)
db.commit()
return {"msg": "Токен удален успешно"}
# --- Категории продаж ---
@app.get("/category", tags=["category"], response_model=List[SaleCategoryResponse])
def get_sale_categories(
current_account: Account = Depends(get_current_account),
db: Session = Depends(get_db)
):
"""
Возвращает список всех категорий продаж компании пользователя.
"""
categories = db.exec(select(SaleCategory).where(SaleCategory.company_id == current_account.company_id)).all()
return categories
@app.post("/category", tags=["category"], response_model=SaleCategoryResponse)
def create_or_update_sale_category(
req: SaleCategoryRequest,
current_account: Account = Depends(get_current_account),
db: Session = Depends(get_db)
):
"""
Создает новую или обновляет существующую категорию продаж для компании пользователя.
"""
# Проверка уникальности category для компании
existing = db.exec(
select(SaleCategory)
.where(SaleCategory.category == req.category)
.where(SaleCategory.company_id == current_account.company_id)
).first()
if req.id is not None:
category = db.exec(select(SaleCategory).where(SaleCategory.id == req.id, SaleCategory.company_id == current_account.company_id)).first()
if not category:
raise HTTPException(status_code=404, detail="Категория не найдена")
# Если меняем имя, оно не должно совпадать с другой категорией
if existing and existing.id != req.id:
raise HTTPException(status_code=400, detail="Категория с таким именем уже существует")
category.category = req.category
category.description = req.description
category.perc = req.perc
category.update_dttm = datetime.utcnow()
db.add(category)
else:
if existing:
raise HTTPException(status_code=400, detail="Категория с таким именем уже существует")
now = datetime.utcnow()
category = SaleCategory(
category=req.category,
description=req.description,
perc=req.perc,
company_id=current_account.company_id,
create_dttm=now,
update_dttm=now
)
db.add(category)
db.commit()
db.refresh(category)
return SaleCategoryResponse.model_validate(category)

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

179
sql_create.sql Normal file
View File

@@ -0,0 +1,179 @@
CREATE TABLE company (
id INTEGER NOT NULL,
name VARCHAR NOT NULL,
commission FLOAT NOT NULL,
agent_commission FLOAT NOT NULL,
"key" VARCHAR NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
auto_approve_transactions BOOLEAN NOT NULL,
PRIMARY KEY (id)
)
;
CREATE TABLE account (
id INTEGER NOT NULL,
login VARCHAR NOT NULL,
password_hash VARCHAR NOT NULL,
"firstName" VARCHAR,
surname VARCHAR,
phone VARCHAR,
email VARCHAR,
company_id INTEGER NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(company_id) REFERENCES company (id)
)
;
CREATE TABLE company_balances (
id INTEGER NOT NULL,
company_id INTEGER NOT NULL,
available_balance FLOAT NOT NULL,
pending_balance FLOAT NOT NULL,
updated_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE (company_id),
FOREIGN KEY(company_id) REFERENCES company (id)
)
;
CREATE TABLE integrationtoken (
id INTEGER NOT NULL,
description VARCHAR NOT NULL,
token_hash VARCHAR,
masked_token VARCHAR,
company_id INTEGER NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
use_dttm DATETIME,
PRIMARY KEY (id),
FOREIGN KEY(company_id) REFERENCES company (id)
)
;
CREATE TABLE salecategory (
id INTEGER NOT NULL,
category VARCHAR NOT NULL,
description VARCHAR,
perc FLOAT NOT NULL,
company_id INTEGER NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(company_id) REFERENCES company (id)
)
;
CREATE TABLE tgagent (
id INTEGER NOT NULL,
tg_id INTEGER NOT NULL,
chat_id INTEGER,
phone VARCHAR,
name VARCHAR,
login VARCHAR,
hash VARCHAR,
company_id INTEGER NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(company_id) REFERENCES company (id)
)
;
CREATE TABLE agent_balances (
id INTEGER NOT NULL,
tg_agent_id INTEGER NOT NULL,
available_balance FLOAT NOT NULL,
frozen_balance FLOAT NOT NULL,
updated_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE (tg_agent_id),
FOREIGN KEY(tg_agent_id) REFERENCES tgagent (id)
)
;
CREATE TABLE agent_transactions (
id INTEGER NOT NULL,
tg_agent_id INTEGER NOT NULL,
amount FLOAT NOT NULL,
status VARCHAR NOT NULL,
transaction_group CHAR(32) NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(tg_agent_id) REFERENCES tgagent (id),
UNIQUE (transaction_group)
)
;
CREATE TABLE ref (
id INTEGER NOT NULL,
tg_agent_id INTEGER NOT NULL,
ref VARCHAR NOT NULL,
description VARCHAR,
promocode VARCHAR(8) NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(tg_agent_id) REFERENCES tgagent (id)
)
;
CREATE TABLE partner_transactions (
id INTEGER NOT NULL,
company_id INTEGER NOT NULL,
type VARCHAR NOT NULL,
amount FLOAT NOT NULL,
status VARCHAR NOT NULL,
transaction_group CHAR(32) NOT NULL,
agent_transaction_id INTEGER,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(company_id) REFERENCES company (id),
FOREIGN KEY(agent_transaction_id) REFERENCES agent_transactions (id)
)
;
CREATE TABLE sale (
id INTEGER NOT NULL,
cost FLOAT NOT NULL,
crediting FLOAT NOT NULL,
ref INTEGER NOT NULL,
sale_id VARCHAR NOT NULL,
group_sale_id VARCHAR NOT NULL,
company_id INTEGER NOT NULL,
category INTEGER NOT NULL,
sale_dttm DATETIME NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(ref) REFERENCES ref (id),
FOREIGN KEY(company_id) REFERENCES company (id),
FOREIGN KEY(category) REFERENCES salecategory (id)
)
;

View File

@@ -7,13 +7,19 @@ 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 # процент комиссии, который взымается за пользование сервисом
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")
tg_agents: List["TgAgent"] = Relationship(back_populates="company")
sales: List["Sale"] = Relationship(back_populates="company")
partner_transactions: List["PartnerTransaction"] = Relationship(back_populates="company")
company_balance: Optional["CompanyBalance"] = Relationship(back_populates="company")
accounts: List["Account"] = Relationship(back_populates="company")
sale_categories: List["SaleCategory"] = Relationship(back_populates="company")
class TgAgent(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
@@ -27,35 +33,65 @@ class TgAgent(SQLModel, table=True):
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
company: "Company" = Relationship(back_populates="tg_agents")
refs: List["Ref"] = Relationship(back_populates="tg_agent")
agent_transactions: List["AgentTransaction"] = Relationship(back_populates="tg_agent")
agent_balance: Optional["AgentBalance"] = Relationship(back_populates="tg_agent")
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
promocode: str = Field(index=True, unique=True, max_length=8)
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
tg_agent: "TgAgent" = Relationship(back_populates="refs")
sales: List["Sale"] = Relationship(back_populates="ref_obj")
class SaleCategory(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
category: str
description: Optional[str] = None
perc: float # процент начисления партнеру
company_id: int = Field(foreign_key="company.id")
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
company: "Company" = Relationship(back_populates="sale_categories")
sales: List["Sale"] = Relationship(back_populates="sale_category")
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
group_sale_id: str # новое поле для группировки продаж
company_id: int = Field(foreign_key="company.id")
category: int = Field(foreign_key="salecategory.id") # новая ссылка на категорию
sale_dttm: datetime = Field(default_factory=datetime.utcnow)
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
ref_obj: "Ref" = Relationship(back_populates="sales")
company: "Company" = Relationship(back_populates="sales")
sale_category: "SaleCategory" = Relationship(back_populates="sales")
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")
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)
tg_agent: "TgAgent" = Relationship(back_populates="agent_transactions")
partner_transactions: List["PartnerTransaction"] = Relationship(back_populates="agent_transaction")
class PartnerTransaction(SQLModel, table=True):
__tablename__ = "partner_transactions"
id: Optional[int] = Field(default=None, primary_key=True)
@@ -68,6 +104,9 @@ class PartnerTransaction(SQLModel, table=True):
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
company: "Company" = Relationship(back_populates="partner_transactions")
agent_transaction: Optional["AgentTransaction"] = Relationship(back_populates="partner_transactions")
class CompanyBalance(SQLModel, table=True):
__tablename__ = "company_balances"
id: Optional[int] = Field(default=None, primary_key=True)
@@ -76,6 +115,8 @@ class CompanyBalance(SQLModel, table=True):
pending_balance: float = Field(default=0.0)
updated_dttm: datetime = Field(default_factory=datetime.utcnow)
company: "Company" = Relationship(back_populates="company_balance")
class AgentBalance(SQLModel, table=True):
__tablename__ = "agent_balances"
id: Optional[int] = Field(default=None, primary_key=True)
@@ -84,6 +125,8 @@ class AgentBalance(SQLModel, table=True):
frozen_balance: float = Field(default=0.0)
updated_dttm: datetime = Field(default_factory=datetime.utcnow)
tg_agent: "TgAgent" = Relationship(back_populates="agent_balance")
class Account(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
login: str = Field(index=True, unique=True)
@@ -96,6 +139,8 @@ class Account(SQLModel, table=True):
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
company: "Company" = Relationship(back_populates="accounts")
# Новая модель для интеграционных токенов
class IntegrationToken(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
@@ -107,4 +152,4 @@ class IntegrationToken(SQLModel, table=True):
update_dttm: datetime = Field(default_factory=datetime.utcnow, nullable=False)
use_dttm: Optional[datetime] = None
company: Company = Relationship(back_populates="integration_tokens")
company: "Company" = Relationship(back_populates="integration_tokens")

View File

@@ -7,6 +7,7 @@ from uuid import UUID
class RefResponse(BaseModel):
ref: str
description: str
promocode: str
class RefAddRequest(BaseModel):
description: str
@@ -25,6 +26,8 @@ class RegisterRequest(BaseModel):
# New Response Models for TG APIs
class RefAddResponse(BaseModel):
ref: str
promocode: str
description: str
class RefStatItem(BaseModel):
description: str