Compare commits
16 Commits
899d7040b4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c8cbe753e | ||
|
|
a6308582cb | ||
|
|
9157570e58 | ||
|
|
bf6a6a8987 | ||
|
|
4f366680bf | ||
|
|
d113ae4adb | ||
|
|
92df59ad23 | ||
|
|
3973d6404d | ||
|
|
7045d6790a | ||
|
|
16973bbb64 | ||
|
|
736f04bb7e | ||
|
|
1cc18e0364 | ||
|
|
076cdd1828 | ||
|
|
57188186c0 | ||
|
|
6e804953c0 | ||
|
|
155d1002fc |
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, Field, EmailStr
|
||||
from pydantic import BaseModel, Field, EmailStr, ConfigDict
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
@@ -89,6 +89,7 @@ class StatReferralsItem(BaseModel):
|
||||
ref: str
|
||||
agent: Optional[str] = None
|
||||
description: str
|
||||
promocode: str
|
||||
salesSum: float
|
||||
salesCount: int
|
||||
|
||||
@@ -169,3 +170,38 @@ class AutoApproveSettingsUpdateResponse(BaseModel):
|
||||
class ApproveTransactionsResult(BaseModel):
|
||||
msg: str
|
||||
approved_count: int
|
||||
|
||||
# New models for integration tokens
|
||||
class IntegrationTokenResponse(BaseModel):
|
||||
id: int
|
||||
description: str
|
||||
masked_token: str
|
||||
rawToken: Optional[str] = None
|
||||
create_dttm: datetime
|
||||
use_dttm: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class IntegrationTokenCreateRequest(BaseModel):
|
||||
description: str
|
||||
|
||||
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
63
call_sale_api.py
Normal 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())
|
||||
63
call_validation_promo_api.py
Normal file
63
call_validation_promo_api.py
Normal 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)
|
||||
85
fill_db.py
85
fill_db.py
@@ -1,11 +1,12 @@
|
||||
import random
|
||||
from uuid import uuid4
|
||||
from sqlmodel import Session
|
||||
from main import AUTH_DB_ENGINE, TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance
|
||||
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 passlib.context import CryptContext
|
||||
from helpers_bff import AUTH_DB_ENGINE, get_password_hash
|
||||
import string
|
||||
|
||||
|
||||
# Константа: список user_ids
|
||||
@@ -63,11 +64,6 @@ LOGINS = [
|
||||
ALL_DESCRIPTIONS = DESCRIPTIONS
|
||||
|
||||
# ---
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def get_date_list(days=7):
|
||||
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return [today - timedelta(days=i) for i in range(days, -1, -1)]
|
||||
@@ -76,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"))
|
||||
@@ -84,17 +81,47 @@ 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)
|
||||
session.commit()
|
||||
session.refresh(company)
|
||||
|
||||
# 0.1 IntegrationTokens
|
||||
for _ in range(3): # Создаем 3 токена для каждой компании
|
||||
new_token_value = str(uuid4()) # Генерируем уникальный токен
|
||||
token_hash = sha256(new_token_value.encode()).hexdigest() # Хешируем токен для хранения
|
||||
masked_token = new_token_value[:5] + "***********************" + new_token_value[-4:] # Генерируем замаскированный токен
|
||||
|
||||
integration_token = IntegrationToken(
|
||||
description=random.choice(DESCRIPTIONS), # Используем существующие описания
|
||||
token_hash=token_hash,
|
||||
masked_token=masked_token,
|
||||
company_id=company.id,
|
||||
use_dttm=random.choice(date_list) if random.random() < 0.7 else None # Пример: 70% токенов будут иметь дату использования
|
||||
)
|
||||
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):
|
||||
@@ -136,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
|
||||
)
|
||||
@@ -158,20 +194,33 @@ 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)
|
||||
dt = random.choice(date_list)
|
||||
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,
|
||||
create_dttm=dt,
|
||||
update_dttm=dt
|
||||
category=sale_category.id,
|
||||
sale_dttm=sale_dttm,
|
||||
create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне
|
||||
update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне
|
||||
)
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
@@ -208,13 +257,21 @@ def fill_db():
|
||||
PARTNER_TRANSACTION_TYPES = ['deposit', 'agent_payout', 'service_fee']
|
||||
PARTNER_TRANSACTION_STATUSES = ['process', 'done', 'error', 'new']
|
||||
|
||||
waiting_transactions_to_ensure = 7
|
||||
waiting_transactions_count = 0
|
||||
|
||||
for tg_agent in tg_agents:
|
||||
# Генерируем несколько групп транзакций для каждого агента
|
||||
for _ in range(random.randint(3, 6)): # От 3 до 6 групп на агента
|
||||
transaction_group_id = uuid4()
|
||||
dt = random.choice(date_list)
|
||||
agent_trans_amount = round(random.uniform(500, 3000), 2)
|
||||
agent_trans_status = random.choice(AGENT_TRANSACTION_STATUSES)
|
||||
|
||||
if waiting_transactions_count < waiting_transactions_to_ensure:
|
||||
agent_trans_status = 'waiting'
|
||||
waiting_transactions_count += 1
|
||||
else:
|
||||
agent_trans_status = random.choice(AGENT_TRANSACTION_STATUSES)
|
||||
|
||||
# Создаем AgentTransaction
|
||||
agent_transaction = AgentTransaction(
|
||||
|
||||
22
generate_sql.py
Normal file
22
generate_sql.py
Normal 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-скрипт успешно сгенерирован!")
|
||||
142
helpers_bff.py
Normal file
142
helpers_bff.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from sqlmodel import Session, select, create_engine
|
||||
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, IntegrationToken, CompanyBalance
|
||||
from hashlib import sha256
|
||||
import jwt
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
from fastapi import HTTPException, status, Depends, Request, Header
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
import hashlib
|
||||
|
||||
# Конфигурация
|
||||
AUTH_DATABASE_ADDRESS = "sqlite:///partner.db"
|
||||
AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True)
|
||||
|
||||
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()
|
||||
|
||||
def get_db():
|
||||
with Session(AUTH_DB_ENGINE) as session:
|
||||
yield session
|
||||
|
||||
def get_current_account(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
login: str = payload.get("sub")
|
||||
if login is None:
|
||||
raise credentials_exception
|
||||
except InvalidTokenError:
|
||||
raise credentials_exception
|
||||
account = get_account_by_login(db, login)
|
||||
if account is None:
|
||||
raise credentials_exception
|
||||
return account
|
||||
|
||||
async def get_current_tg_agent(request: Request, db: Session = Depends(get_db)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
raise credentials_exception
|
||||
hash_value = auth_header.replace("Bearer ", "").strip()
|
||||
tg_agent = db.exec(select(TgAgent).where(TgAgent.hash == hash_value)).first()
|
||||
if tg_agent is None:
|
||||
raise credentials_exception
|
||||
return tg_agent
|
||||
|
||||
def authenticate_tg_agent(engine, tg_id: int):
|
||||
with Session(engine) as db:
|
||||
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
|
||||
if not tg_agent:
|
||||
return None
|
||||
return tg_agent
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def get_account_by_login(db: Session, login: str) -> Optional[Account]:
|
||||
statement = select(Account).where(Account.login == login)
|
||||
return db.exec(statement).first()
|
||||
343
integration_api.py
Normal file
343
integration_api.py
Normal file
@@ -0,0 +1,343 @@
|
||||
from fastapi import FastAPI, Depends, HTTPException, status, Header, Body, Request
|
||||
from sqlmodel import Session, select, Field
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime, timedelta
|
||||
import hashlib
|
||||
import uuid
|
||||
from random import choices
|
||||
import string
|
||||
|
||||
from sql_models import Company, IntegrationToken, Ref, Sale, AgentTransaction, PartnerTransaction, AgentBalance, TgAgent, CompanyBalance, SaleCategory
|
||||
from integration_models import Token, SaleCreateRequest, SaleCreateResponse, TransactionStatus, WithdrawRequest, WithdrawResponse, PromoValidationRequest, PromoValidationResponse
|
||||
from bff_models import RegisterResponse, TgAuthResponse
|
||||
from tg_models import RefAddRequest, RefResponse, RefAddResponse, RefStatResponse, RegisterRequest, StatResponse
|
||||
from helpers_bff import AUTH_DB_ENGINE, get_integration_db, create_integration_jwt_token, get_current_company_from_jwt, get_tg_agent_by_tg_id, get_current_tg_agent
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
#7c06945b-f4b8-4929-8350-b9841405a609
|
||||
|
||||
|
||||
@app.post("/token", tags=["integration"], response_model=Token)
|
||||
async def get_token_for_api_key(
|
||||
x_api_key: str = Header(..., alias="X-API-Key"),
|
||||
db: Session = Depends(get_integration_db)
|
||||
):
|
||||
"""
|
||||
Обменивает API-ключ на JWT токен.
|
||||
"""
|
||||
|
||||
if not x_api_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="API-ключ не предоставлен",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
api_key_hash = hashlib.sha256(x_api_key.encode()).hexdigest()
|
||||
integration_token_db = db.exec(
|
||||
select(IntegrationToken).where(IntegrationToken.token_hash == api_key_hash)
|
||||
).first()
|
||||
|
||||
if not integration_token_db:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверный API-ключ",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Обновляем use_dttm токена
|
||||
integration_token_db.use_dttm = datetime.utcnow()
|
||||
db.add(integration_token_db)
|
||||
db.commit()
|
||||
db.refresh(integration_token_db)
|
||||
|
||||
jwt_token = create_integration_jwt_token(integration_token_db.company_id)
|
||||
return {"access_token": jwt_token, "token_type": "bearer"}
|
||||
|
||||
@app.get("/ref", response_model=List[RefResponse], tags=["agent-tg"])
|
||||
def get_refs(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_integration_db)):
|
||||
"""
|
||||
Возвращает список реферальных ссылок текущего Telegram-агента.
|
||||
"""
|
||||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||||
return [RefResponse(ref=r.ref, description=r.description or "", promocode=r.promocode) for r in refs]
|
||||
|
||||
@app.post("/ref/add", tags=["agent-tg"], response_model=RefAddResponse)
|
||||
def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_integration_db)):
|
||||
"""
|
||||
Добавляет новую реферальную ссылку для текущего Telegram-агента.
|
||||
"""
|
||||
# Генерация промокода (логика как была для промокодов)
|
||||
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
promocode = ''.join(choices(alphabet, k=8))
|
||||
while db.exec(select(Ref).where(Ref.promocode == promocode)).first():
|
||||
promocode = ''.join(choices(alphabet, k=8))
|
||||
new_ref = Ref(
|
||||
tg_agent_id=current_tg_agent.id,
|
||||
ref=str(uuid.uuid4()),
|
||||
description=req.description,
|
||||
promocode=promocode
|
||||
)
|
||||
db.add(new_ref)
|
||||
db.commit()
|
||||
db.refresh(new_ref)
|
||||
return {"ref": new_ref.ref, "promocode": new_ref.promocode, "description": new_ref.description}
|
||||
|
||||
@app.get("/ref/stat", tags=["agent-tg"], response_model=RefStatResponse)
|
||||
def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_integration_db)):
|
||||
"""
|
||||
Возвращает статистику по реферальным ссылкам текущего Telegram-агента.
|
||||
"""
|
||||
# 1. Получаем все реферальные ссылки пользователя
|
||||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||||
result = []
|
||||
for ref in refs:
|
||||
# 2. Для каждой ссылки считаем продажи и сумму
|
||||
sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all()
|
||||
sales_count = len(sales)
|
||||
income = sum(sale.crediting for sale in sales)
|
||||
result.append({
|
||||
"description": ref.description or "",
|
||||
"sales": sales_count,
|
||||
"income": income
|
||||
})
|
||||
return {"refData": result}
|
||||
|
||||
@app.get("/stat", tags=["agent-tg"], response_model=StatResponse)
|
||||
def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_integration_db)):
|
||||
"""
|
||||
Возвращает общую статистику для текущего Telegram-агента.
|
||||
"""
|
||||
# 1. Получаем все реферальные ссылки пользователя
|
||||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||||
ref_ids = [r.id for r in refs]
|
||||
|
||||
# 2. Считаем totalSales (продажи по всем рефам пользователя)
|
||||
total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
|
||||
totalSales = len(total_sales)
|
||||
totalIncome = sum(sale.crediting for sale in total_sales)
|
||||
# Заменено получение доступного остатка из AgentBalance
|
||||
agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == current_tg_agent.id)).first()
|
||||
availableWithdrawal = agent_balance.available_balance if agent_balance else 0.0
|
||||
return {
|
||||
"totalSales": totalSales,
|
||||
"totalIncome": totalIncome,
|
||||
"availableWithdrawal": availableWithdrawal
|
||||
}
|
||||
|
||||
@app.post("/tg_auth", tags=["agent-tg"], response_model=TgAuthResponse)
|
||||
def tg_auth(hash: str = Body(..., embed=True), db: Session = Depends(get_integration_db)):
|
||||
"""
|
||||
Авторизует Telegram-агента по хешу.
|
||||
"""
|
||||
tg_agent = db.exec(select(TgAgent).where(TgAgent.hash == hash)).first()
|
||||
if not tg_agent:
|
||||
raise HTTPException(status_code=401, detail="Hash not found")
|
||||
return {"msg": "Auth success", "tg_id": tg_agent.tg_id}
|
||||
|
||||
@app.post("/withdraw", tags=["agent-tg"], response_model=WithdrawResponse)
|
||||
async def withdraw_funds(
|
||||
req: WithdrawRequest,
|
||||
db: Session = Depends(get_integration_db)
|
||||
):
|
||||
"""
|
||||
Запрос на вывод средств для Telegram-агента.
|
||||
"""
|
||||
tg_agent = db.exec(select(TgAgent).where(TgAgent.tg_id == req.tg_id)).first()
|
||||
if not tg_agent:
|
||||
raise HTTPException(status_code=404, detail="Telegram-агент не найден")
|
||||
|
||||
company = db.exec(select(Company).where(Company.id == tg_agent.company_id)).first()
|
||||
if not company:
|
||||
raise HTTPException(status_code=404, detail="Компания не найдена для агента")
|
||||
|
||||
if req.amount <= 0:
|
||||
raise HTTPException(status_code=400, detail="Сумма для вывода должна быть положительной")
|
||||
|
||||
agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == tg_agent.id)).first()
|
||||
if not agent_balance or agent_balance.available_balance < req.amount:
|
||||
raise HTTPException(status_code=400, detail="Недостаточно средств на балансе для вывода")
|
||||
|
||||
# Определяем статус транзакции
|
||||
transaction_status = TransactionStatus.WAITING
|
||||
if company.auto_approve_transactions:
|
||||
transaction_status = TransactionStatus.NEW
|
||||
|
||||
# Создаем запись AgentTransaction
|
||||
new_agent_transaction = AgentTransaction(
|
||||
tg_agent_id=tg_agent.id,
|
||||
amount=req.amount,
|
||||
status=transaction_status.value,
|
||||
transaction_group=uuid.uuid4()
|
||||
)
|
||||
db.add(new_agent_transaction)
|
||||
|
||||
|
||||
# Обновляем баланс агента
|
||||
agent_balance.available_balance -= req.amount
|
||||
if transaction_status == TransactionStatus.WAITING: # Если автоматически одобряется, переводим на замороженный баланс компании (т.е. компания должна выплатить)
|
||||
agent_balance.frozen_balance += req.amount # Удерживаем средства, пока они не будут выведены
|
||||
db.add(agent_balance)
|
||||
|
||||
db.commit()
|
||||
db.refresh(new_agent_transaction)
|
||||
db.refresh(agent_balance)
|
||||
|
||||
return {"msg": "Запрос на вывод средств успешно создан", "transaction_id": new_agent_transaction.transaction_group}
|
||||
|
||||
@app.post("/sale", tags=["integration"], response_model=SaleCreateResponse)
|
||||
async def create_sale(
|
||||
req: SaleCreateRequest,
|
||||
company: Company = Depends(get_current_company_from_jwt),
|
||||
db: Session = Depends(get_integration_db)
|
||||
):
|
||||
"""
|
||||
Регистрирует новую продажу в системе.
|
||||
"""
|
||||
# Устанавливаем уровень изоляции для текущей транзакции
|
||||
db.connection(execution_options={'isolation_level': 'SERIALIZABLE'})
|
||||
|
||||
# Проверка входных данных
|
||||
if not req.ref and not req.promocode:
|
||||
raise HTTPException(status_code=400, detail="Необходимо передать либо ref, либо promocode")
|
||||
if not req.category:
|
||||
raise HTTPException(status_code=400, detail="Необходимо передать category (id категории)")
|
||||
|
||||
# 1. Найти Ref по ref и/или promocode
|
||||
referral = None
|
||||
if req.ref and req.promocode:
|
||||
referral_by_ref = db.exec(select(Ref).where(Ref.ref == req.ref)).first()
|
||||
referral_by_code = db.exec(select(Ref).where(Ref.promocode == req.promocode)).first()
|
||||
if not referral_by_ref or not referral_by_code:
|
||||
raise HTTPException(status_code=404, detail="Реферальная ссылка или промокод не найдены")
|
||||
if referral_by_ref.id != referral_by_code.id:
|
||||
raise HTTPException(status_code=400, detail="ref и promocode не соответствуют одной ссылке")
|
||||
referral = referral_by_ref
|
||||
elif req.ref:
|
||||
referral = db.exec(select(Ref).where(Ref.ref == req.ref)).first()
|
||||
if not referral:
|
||||
raise HTTPException(status_code=404, detail="Реферальная ссылка не найдена")
|
||||
elif req.promocode:
|
||||
referral = db.exec(select(Ref).where(Ref.promocode == req.promocode)).first()
|
||||
if not referral:
|
||||
raise HTTPException(status_code=404, detail="Промокод не найден")
|
||||
|
||||
# Проверяем, что реф действительно принадлежит компании
|
||||
tg_agent = db.exec(select(TgAgent).where(TgAgent.id == referral.tg_agent_id, TgAgent.company_id == company.id)).first()
|
||||
if not tg_agent:
|
||||
raise HTTPException(status_code=404, detail="Реферальная ссылка не принадлежит данной компании")
|
||||
|
||||
# 2. Проверить, что sale_id уникален для данной компании
|
||||
existing_sale = db.exec(
|
||||
select(Sale)
|
||||
.where(Sale.company_id == company.id)
|
||||
.where(Sale.sale_id == req.sale_id)
|
||||
).first()
|
||||
if existing_sale:
|
||||
raise HTTPException(status_code=400, detail="Продажа с таким sale_id уже существует для данной компании")
|
||||
|
||||
# 3. Найти категорию и рассчитать crediting
|
||||
sale_category = db.exec(select(SaleCategory).where(SaleCategory.category == req.category, SaleCategory.company_id == company.id)).first()
|
||||
if not sale_category:
|
||||
raise HTTPException(status_code=404, detail="Категория продажи не найдена")
|
||||
crediting_amount = req.cost * (sale_category.perc / 100.0)
|
||||
|
||||
# 4. Проверить и обновить AgentBalance и CompanyBalance
|
||||
agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == tg_agent.id)).first()
|
||||
if not agent_balance:
|
||||
raise HTTPException(status_code=404, detail="Баланс агента не найден")
|
||||
company_balance = db.exec(select(CompanyBalance).where(CompanyBalance.company_id == company.id)).first()
|
||||
if not company_balance:
|
||||
raise HTTPException(status_code=404, detail="Баланс компании не найден")
|
||||
|
||||
# 5. Создать Sale
|
||||
new_sale = Sale(
|
||||
cost=req.cost,
|
||||
crediting=crediting_amount,
|
||||
ref=referral.id,
|
||||
sale_id=req.sale_id,
|
||||
group_sale_id=req.group_sale_id,
|
||||
company_id=company.id,
|
||||
category=sale_category.id,
|
||||
sale_dttm=datetime.utcnow()
|
||||
)
|
||||
db.add(new_sale)
|
||||
# Создать AgentTransaction
|
||||
agent_transaction_status = TransactionStatus.DONE
|
||||
agent_transaction = AgentTransaction(
|
||||
tg_agent_id=tg_agent.id,
|
||||
amount=crediting_amount,
|
||||
status=agent_transaction_status.value,
|
||||
transaction_group=uuid.uuid4()
|
||||
)
|
||||
db.add(agent_transaction)
|
||||
# Обновление балансов для продаж - всегда в замороженный/ожидающий баланс
|
||||
agent_balance.frozen_balance += crediting_amount
|
||||
company_balance.pending_balance -= crediting_amount
|
||||
company_balance.updated_dttm = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
db.refresh(new_sale)
|
||||
db.refresh(agent_balance)
|
||||
db.refresh(company_balance)
|
||||
db.refresh(agent_transaction)
|
||||
|
||||
return {
|
||||
"msg": "Продажа успешно зарегистрирована",
|
||||
"sale_id": new_sale.sale_id,
|
||||
"crediting": new_sale.crediting
|
||||
}
|
||||
|
||||
@app.post("/validationPromo", tags=["integration"], response_model=PromoValidationResponse)
|
||||
async def validate_promocode(
|
||||
req: PromoValidationRequest,
|
||||
company: Company = Depends(get_current_company_from_jwt),
|
||||
db: Session = Depends(get_integration_db)
|
||||
):
|
||||
"""
|
||||
Проверяет валидность промокода для текущей компании.
|
||||
"""
|
||||
referral = db.exec(select(Ref).where(Ref.promocode == req.promocode)).first()
|
||||
if not referral:
|
||||
return {"validation": False}
|
||||
|
||||
tg_agent = db.exec(select(TgAgent).where(TgAgent.id == referral.tg_agent_id, TgAgent.company_id == company.id)).first()
|
||||
if not tg_agent:
|
||||
return {"validation": False}
|
||||
|
||||
return {"validation": True}
|
||||
|
||||
@app.post("/register", tags=["agent-tg"], response_model=RegisterResponse)
|
||||
def register(req: RegisterRequest, db: Session = Depends(get_integration_db)):
|
||||
"""
|
||||
Регистрирует нового Telegram-агента в системе.
|
||||
"""
|
||||
tg_id = req.tg_id
|
||||
chat_id = req.chat_id
|
||||
phone = req.phone
|
||||
name = getattr(req, 'name', None)
|
||||
login = getattr(req, 'login', None)
|
||||
company_key = req.company_key
|
||||
print(f'tg_id: {tg_id}, chat_id: {chat_id}, phone: {phone}, name: {name}, login: {login}, company_key: {company_key}')
|
||||
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
|
||||
if tg_agent:
|
||||
raise HTTPException(status_code=400, detail="tg_id already registered")
|
||||
# Поиск компании по ключу
|
||||
company = db.exec(select(Company).where(Company.key == company_key)).first()
|
||||
if not company:
|
||||
raise HTTPException(status_code=400, detail="Компания с таким ключом не найдена")
|
||||
hash_value = hashlib.sha256(f"{tg_id}sold".encode()).hexdigest()
|
||||
new_tg_agent = TgAgent(
|
||||
tg_id=tg_id,
|
||||
chat_id=chat_id,
|
||||
phone=phone,
|
||||
name=name,
|
||||
login=login,
|
||||
hash=hash_value,
|
||||
company_id=company.id
|
||||
)
|
||||
db.add(new_tg_agent)
|
||||
db.commit()
|
||||
db.refresh(new_tg_agent)
|
||||
return {"msg": "TgAgent registered successfully"}
|
||||
51
integration_models.py
Normal file
51
integration_models.py
Normal 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
|
||||
670
main.py
670
main.py
@@ -1,268 +1,94 @@
|
||||
import uuid
|
||||
from fastapi import FastAPI, Depends, HTTPException, status, Query, Body, Request
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from sqlmodel import SQLModel, Field, create_engine, Session, select
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import (
|
||||
FastAPI,
|
||||
Depends,
|
||||
HTTPException,
|
||||
status,
|
||||
Query,
|
||||
Body,
|
||||
)
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import SQLModel, Session, select, Field
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime, timedelta
|
||||
from bff_models import Token, AccountProfileUpdateRequest, AccountPasswordChangeRequest, AgentTransactionResponse, AutoApproveSettingsRequest, ApproveTransactionsRequest, TransactionStatus, RegisterResponse, DashboardCardsResponse, DashboardChartTotalResponse, DashboardChartAgentResponse, StatAgentsResponse, StatReferralsResponse, StatSalesResponse, BillingCardsResponse, BillingChartStatResponse, BillingChartPieResponse, AccountResponse, CompanyProfileResponse, AccountProfileResponse, AccountProfileUpdateResponse, AccountPasswordChangeResponse, AutoApproveSettingsGetResponse, AutoApproveSettingsUpdateResponse, ApproveTransactionsResult, TgAuthResponse, BillingPayoutsTransactionsResponse
|
||||
from tg_models import RefAddRequest, RefResponse, RegisterRequest, TokenRequest, RefAddResponse, RefStatResponse, StatResponse
|
||||
from uuid import uuid4
|
||||
from fastapi.responses import JSONResponse
|
||||
from datetime import timedelta, datetime
|
||||
from bff_models import (
|
||||
Token,
|
||||
DashboardCardsResponse,
|
||||
DashboardChartTotalResponse,
|
||||
DashboardChartAgentResponse,
|
||||
StatAgentsResponse,
|
||||
StatReferralsResponse,
|
||||
StatSalesResponse,
|
||||
BillingCardsResponse,
|
||||
BillingChartStatResponse,
|
||||
BillingChartPieResponse,
|
||||
AccountResponse,
|
||||
AccountProfileResponse,
|
||||
AccountProfileUpdateResponse,
|
||||
AccountPasswordChangeResponse,
|
||||
AutoApproveSettingsGetResponse,
|
||||
AutoApproveSettingsUpdateResponse,
|
||||
ApproveTransactionsResult,
|
||||
BillingPayoutsTransactionsResponse,
|
||||
AccountProfileUpdateRequest,
|
||||
AccountPasswordChangeRequest,
|
||||
AutoApproveSettingsRequest,
|
||||
ApproveTransactionsRequest,
|
||||
AgentTransactionResponse,
|
||||
TransactionStatus,
|
||||
IntegrationTokenResponse,
|
||||
IntegrationTokenCreateRequest,
|
||||
IntegrationTokenUpdateRequest,
|
||||
SaleCategoryRequest,
|
||||
SaleCategoryResponse
|
||||
)
|
||||
from sql_models import (
|
||||
Company,
|
||||
TgAgent,
|
||||
Ref,
|
||||
Sale,
|
||||
AgentTransaction,
|
||||
PartnerTransaction,
|
||||
AgentBalance,
|
||||
Account,
|
||||
IntegrationToken,
|
||||
SaleCategory
|
||||
)
|
||||
from sqlalchemy import func
|
||||
from hashlib import sha256
|
||||
import jwt
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from enum import Enum
|
||||
|
||||
# Конфигурация
|
||||
AUTH_DATABASE_ADDRESS = "sqlite:///partner.db"
|
||||
|
||||
#SQLModel
|
||||
class Company(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
commission: float # процент комиссии
|
||||
key: str = Field(index=True, unique=True)
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
auto_approve_transactions: bool = Field(default=False) # Новое поле для автоподтверждения
|
||||
|
||||
class TgAgent(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
tg_id: int = Field(index=True, unique=True)
|
||||
chat_id: Optional[int] = None
|
||||
phone: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
login: Optional[str] = None
|
||||
hash: Optional[str] = None
|
||||
company_id: int = Field(foreign_key="company.id")
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class Ref(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
tg_agent_id: int = Field(foreign_key="tgagent.id")
|
||||
ref: str
|
||||
description: Optional[str] = None
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class Sale(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
cost: float
|
||||
crediting: float # сколько начислено за продажу
|
||||
ref: int = Field(foreign_key="ref.id")
|
||||
sale_id: str
|
||||
company_id: int = Field(foreign_key="company.id")
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class 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") # ID агента, связь с TgAgent
|
||||
amount: float # Используем float для DECIMAL(15,2)
|
||||
status: str # 'waiting', 'process', 'done', 'reject', 'error'
|
||||
transaction_group: uuid.UUID = Field(default_factory=uuid.uuid4) # UUID для группировки
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class PartnerTransaction(SQLModel, table=True):
|
||||
__tablename__ = "partner_transactions" # Указываем имя таблицы явно
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
company_id: int = Field(foreign_key="company.id") # ID партнера, связь с Company
|
||||
type: str # 'deposit', 'agent_payout', 'service_fee'
|
||||
amount: float # Используем float для DECIMAL(15,2)
|
||||
status: str # 'process', 'done', 'error'
|
||||
transaction_group: uuid.UUID # UUID для группировки, может быть связан с agent_transactions
|
||||
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)
|
||||
|
||||
class CompanyBalance(SQLModel, table=True):
|
||||
__tablename__ = "company_balances" # Указываем имя таблицы явно
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
company_id: int = Field(foreign_key="company.id", unique=True) # ID компании, уникальный баланс на компанию
|
||||
available_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
|
||||
pending_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
|
||||
updated_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class AgentBalance(SQLModel, table=True):
|
||||
__tablename__ = "agent_balances" # Указываем имя таблицы явно
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
tg_agent_id: int = Field(foreign_key="tgagent.id", unique=True) # ID агента, уникальный баланс на агента
|
||||
available_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
|
||||
frozen_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
|
||||
updated_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class Account(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
login: str = Field(index=True, unique=True)
|
||||
password_hash: str
|
||||
firstName: Optional[str] = None
|
||||
surname: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
company_id: int = Field(foreign_key="company.id")
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class AccountProfileUpdateRequest(BaseModel):
|
||||
firstName: str
|
||||
surname: str
|
||||
email: EmailStr
|
||||
phone: str
|
||||
|
||||
class AccountPasswordChangeRequest(BaseModel):
|
||||
currentPassword: str
|
||||
newPassword: str
|
||||
|
||||
|
||||
class TransactionStatus(str, Enum): # Определяем Enum для статусов
|
||||
WAITING = 'waiting'
|
||||
PROCESS = 'process'
|
||||
DONE = 'done'
|
||||
REJECT = 'reject'
|
||||
ERROR = 'error'
|
||||
NEW = 'new' # Новый статус
|
||||
|
||||
|
||||
# Новая модель ответа для агентских транзакций с именем агента
|
||||
class AgentTransactionResponse(BaseModel):
|
||||
amount: float
|
||||
status: TransactionStatus # Используем Enum
|
||||
transaction_group: uuid.UUID
|
||||
create_dttm: datetime
|
||||
update_dttm: datetime
|
||||
agent_name: Optional[str] = None # Поле для имени агента
|
||||
import hashlib
|
||||
from helpers_bff import (
|
||||
AUTH_DB_ENGINE,
|
||||
get_db,
|
||||
get_current_account,
|
||||
create_access_token,
|
||||
verify_password,
|
||||
get_account_by_login,
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||
pwd_context,
|
||||
)
|
||||
import os
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Создание движка базы данных
|
||||
AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True)
|
||||
SQLModel.metadata.create_all(AUTH_DB_ENGINE)
|
||||
|
||||
|
||||
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
|
||||
|
||||
# FastAPI app
|
||||
app = FastAPI()
|
||||
|
||||
# CRUD
|
||||
|
||||
def get_tg_agent_by_tg_id(db: Session, tg_id: int) -> Optional[TgAgent]:
|
||||
statement = select(TgAgent).where(TgAgent.tg_id == tg_id)
|
||||
return db.exec(statement).first()
|
||||
|
||||
# Dependency
|
||||
|
||||
def get_db():
|
||||
with Session(AUTH_DB_ENGINE) as session:
|
||||
yield session
|
||||
|
||||
def get_current_account(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
login: str = payload.get("sub")
|
||||
if login is None:
|
||||
raise credentials_exception
|
||||
except InvalidTokenError:
|
||||
raise credentials_exception
|
||||
account = get_account_by_login(db, login)
|
||||
if account is None:
|
||||
raise credentials_exception
|
||||
return account
|
||||
|
||||
# Авторизация
|
||||
async def get_current_tg_agent(request: Request, db: Session = Depends(get_db)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
raise credentials_exception
|
||||
hash_value = auth_header.replace("Bearer ", "").strip()
|
||||
tg_agent = db.exec(select(TgAgent).where(TgAgent.hash == hash_value)).first()
|
||||
if tg_agent is None:
|
||||
raise credentials_exception
|
||||
return tg_agent
|
||||
|
||||
# Регистрация
|
||||
|
||||
|
||||
@app.post("/register", tags=["partner-tg"], response_model=RegisterResponse)
|
||||
def register(req: RegisterRequest, db: Session = Depends(get_db)):
|
||||
tg_id = req.tg_id
|
||||
chat_id = req.chat_id
|
||||
phone = req.phone
|
||||
name = getattr(req, 'name', None)
|
||||
login = getattr(req, 'login', None)
|
||||
company_key = req.company_key
|
||||
print(f'tg_id: {tg_id}, chat_id: {chat_id}, phone: {phone}, name: {name}, login: {login}, company_key: {company_key}')
|
||||
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
|
||||
if tg_agent:
|
||||
raise HTTPException(status_code=400, detail="tg_id already registered")
|
||||
# Поиск компании по ключу
|
||||
company = db.exec(select(Company).where(Company.key == company_key)).first()
|
||||
if not company:
|
||||
raise HTTPException(status_code=400, detail="Компания с таким ключом не найдена")
|
||||
hash_value = sha256(f"{tg_id}sold".encode()).hexdigest()
|
||||
new_tg_agent = TgAgent(
|
||||
tg_id=tg_id,
|
||||
chat_id=chat_id,
|
||||
phone=phone,
|
||||
name=name,
|
||||
login=login,
|
||||
hash=hash_value,
|
||||
company_id=company.id
|
||||
)
|
||||
db.add(new_tg_agent)
|
||||
db.commit()
|
||||
db.refresh(new_tg_agent)
|
||||
return {"msg": "TgAgent registered successfully"}
|
||||
|
||||
def authenticate_tg_agent(engine, tg_id: int):
|
||||
with Session(engine) as db:
|
||||
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
|
||||
if not tg_agent:
|
||||
return None
|
||||
return tg_agent
|
||||
|
||||
|
||||
|
||||
# Авторизация
|
||||
|
||||
SECRET_KEY = "supersecretkey" # Лучше вынести в .env
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@app.post("/token", response_model=Token, tags=["bff", "token"])
|
||||
@app.post("/token", response_model=Token, tags=[ "token"])
|
||||
def login_account_for_access_token(
|
||||
# login: str = Body(...),
|
||||
# password: str = Body(...),
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Авторизует аккаунт и возвращает токен доступа.
|
||||
"""
|
||||
# login: str = Body(...),
|
||||
# password: str = Body(...),
|
||||
account = get_account_by_login(db, form_data.username)
|
||||
if not account or not verify_password(form_data.password, account.password_hash):
|
||||
raise HTTPException(
|
||||
@@ -277,62 +103,11 @@ 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)):
|
||||
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)):
|
||||
new_ref = Ref(
|
||||
tg_agent_id=current_tg_agent.id,
|
||||
ref=str(uuid4()),
|
||||
description=req.description
|
||||
)
|
||||
db.add(new_ref)
|
||||
db.commit()
|
||||
db.refresh(new_ref)
|
||||
return {"ref": new_ref.ref}
|
||||
|
||||
@app.get("/ref/stat", tags=["partner-tg"], response_model=RefStatResponse)
|
||||
def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||||
# 1. Получаем все реферальные ссылки пользователя
|
||||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||||
result = []
|
||||
for ref in refs:
|
||||
# 2. Для каждой ссылки считаем продажи и сумму
|
||||
sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all()
|
||||
sales_count = len(sales)
|
||||
income = sum(sale.crediting for sale in sales)
|
||||
result.append({
|
||||
"description": ref.description or "",
|
||||
"sales": sales_count,
|
||||
"income": income
|
||||
})
|
||||
return {"refData": result}
|
||||
|
||||
@app.get("/stat", tags=["partner-tg"], response_model=StatResponse)
|
||||
def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||||
# 1. Получаем все реферальные ссылки пользователя
|
||||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||||
ref_ids = [r.id for r in refs]
|
||||
|
||||
# 2. Считаем totalSales (продажи по всем рефам пользователя)
|
||||
total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
|
||||
totalSales = len(total_sales)
|
||||
totalIncome = sum(sale.crediting for sale in total_sales)
|
||||
# Заменено получение доступного остатка из 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) пользователя, включая общий доход, выплаты, активных рефералов, ожидающие выплаты и общее количество продаж.
|
||||
"""
|
||||
# 1. Общий доход - сумма всех Sale.cost
|
||||
total_revenue = db.exec(select(Sale).where(Sale.company_id == current_account.company_id)).all()
|
||||
totalRevenue = sum(sale.cost for sale in total_revenue)
|
||||
@@ -359,16 +134,19 @@ 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).
|
||||
"""
|
||||
# Группируем продажи по дате (день)
|
||||
result = db.exec(
|
||||
select(
|
||||
func.strftime('%Y-%m-%d', Sale.create_dttm).label('date'),
|
||||
func.strftime('%Y-%m-%d', Sale.sale_dttm).label('date'),
|
||||
func.sum(Sale.cost).label('revenue'),
|
||||
func.count(Sale.id).label('sales')
|
||||
).where(Sale.company_id == current_account.company_id).group_by(func.strftime('%Y-%m-%d', Sale.create_dttm))
|
||||
.order_by(func.strftime('%Y-%m-%d', Sale.create_dttm))
|
||||
).where(Sale.company_id == current_account.company_id).group_by(func.strftime('%Y-%m-%d', Sale.sale_dttm))
|
||||
.order_by(func.strftime('%Y-%m-%d', Sale.sale_dttm))
|
||||
).all()
|
||||
# Преобразуем результат в нужный формат
|
||||
data = [
|
||||
@@ -377,8 +155,11 @@ 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).
|
||||
"""
|
||||
# Получаем всех агентов
|
||||
agents = db.exec(select(TgAgent).where(TgAgent.company_id == current_account.company_id)).all()
|
||||
result = []
|
||||
@@ -404,13 +185,17 @@ 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),
|
||||
date_end: str = Query(None),
|
||||
current_account: Account = Depends(get_current_account),
|
||||
):
|
||||
"""
|
||||
Возвращает статистику по агентам компании, включая количество рефералов, продаж, сумму продаж и начислений.
|
||||
Возможна фильтрация по дате создания агентов.
|
||||
"""
|
||||
agents_query = select(TgAgent).where(TgAgent.company_id == current_account.company_id)
|
||||
if date_start:
|
||||
agents_query = agents_query.where(TgAgent.create_dttm >= date_start)
|
||||
@@ -444,13 +229,17 @@ 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),
|
||||
date_end: str = Query(None),
|
||||
current_account: Account = Depends(get_current_account),
|
||||
):
|
||||
"""
|
||||
Возвращает статистику по реферальным ссылкам компании, включая имя агента, описание, сумму и количество продаж.
|
||||
Возможна фильтрация по дате создания рефералов.
|
||||
"""
|
||||
refs_query = select(Ref).join(TgAgent).where(TgAgent.company_id == current_account.company_id)
|
||||
if date_start:
|
||||
refs_query = refs_query.where(Ref.create_dttm >= date_start)
|
||||
@@ -467,23 +256,28 @@ 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),
|
||||
date_end: str = Query(None),
|
||||
current_account: Account = Depends(get_current_account),
|
||||
):
|
||||
"""
|
||||
Возвращает статистику по продажам компании, включая ID продажи, стоимость, начисления, реферала и имя агента.
|
||||
Возможна фильтрация по дате создания продаж.
|
||||
"""
|
||||
sales_query = select(Sale).where(Sale.company_id == current_account.company_id)
|
||||
if date_start:
|
||||
sales_query = sales_query.where(Sale.create_dttm >= date_start)
|
||||
sales_query = sales_query.where(Sale.sale_dttm >= date_start)
|
||||
if date_end:
|
||||
sales_query = sales_query.where(Sale.create_dttm <= date_end)
|
||||
sales_query = sales_query.where(Sale.sale_dttm <= date_end)
|
||||
sales = db.exec(sales_query).all()
|
||||
ref_ids = list(set(sale.ref for sale in sales))
|
||||
refs = db.exec(select(Ref).where(Ref.id.in_(ref_ids))).all() if ref_ids else []
|
||||
@@ -504,8 +298,11 @@ 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)):
|
||||
"""
|
||||
Возвращает ключевые показатели биллинга для компании, включая общий заработок, общие выплаты и доступные к выводу средства.
|
||||
"""
|
||||
# 1. cost - Общий заработок (сумма всех Sale.cost)
|
||||
sales = db.exec(select(Sale).where(Sale.company_id == current_account.company_id)).all()
|
||||
cost = sum(sale.cost for sale in sales)
|
||||
@@ -524,13 +321,17 @@ 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),
|
||||
date_end: str = Query(None),
|
||||
current_account: Account = Depends(get_current_account),
|
||||
):
|
||||
"""
|
||||
Возвращает список транзакций выплат для компании текущего пользователя.
|
||||
Возможна фильтрация по дате создания.
|
||||
"""
|
||||
# Используем AgentTransaction вместо Transaction
|
||||
# Явно выбираем обе модели для корректной распаковки
|
||||
query = select(AgentTransaction, TgAgent).join(TgAgent).where(TgAgent.company_id == current_account.company_id)
|
||||
@@ -560,8 +361,11 @@ 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)):
|
||||
"""
|
||||
Возвращает статистику выплат по датам и статусам для компании текущего пользователя.
|
||||
"""
|
||||
# Группируем агентские транзакции по дате (день) и статусу
|
||||
result = db.exec(
|
||||
select(
|
||||
@@ -582,8 +386,11 @@ 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)):
|
||||
"""
|
||||
Возвращает круговую диаграмму статистики выплат по статусам для компании текущего пользователя.
|
||||
"""
|
||||
# Группируем агентские транзакции по статусу
|
||||
result = db.exec(
|
||||
select(
|
||||
@@ -599,34 +406,21 @@ 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)):
|
||||
tg_agent = db.exec(select(TgAgent).where(TgAgent.hash == hash)).first()
|
||||
if not tg_agent:
|
||||
raise HTTPException(status_code=401, detail="Hash not found")
|
||||
return {"msg": "Auth success", "tg_id": tg_agent.tg_id}
|
||||
|
||||
# --- Новый функционал для Account ---
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_account_by_login(db: Session, login: str) -> Optional[Account]:
|
||||
statement = select(Account).where(Account.login == login)
|
||||
return db.exec(statement).first()
|
||||
|
||||
|
||||
|
||||
@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)):
|
||||
"""
|
||||
Возвращает базовую информацию об аккаунте текущего пользователя (имя и фамилию).
|
||||
"""
|
||||
return {
|
||||
"firstName": current_account.firstName,
|
||||
"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)):
|
||||
"""
|
||||
Возвращает полную информацию о профиле аккаунта текущего пользователя, включая данные компании.
|
||||
"""
|
||||
company = db.exec(select(Company).where(Company.id == current_account.company_id)).first()
|
||||
if not company:
|
||||
raise HTTPException(status_code=404, detail="Компания не найдена")
|
||||
@@ -639,16 +433,19 @@ 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),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Обновляет информацию профиля текущего пользователя.
|
||||
"""
|
||||
# Проверка, что все поля заполнены (Pydantic уже валидирует email и обязательность)
|
||||
if not req.firstName.strip() or not req.surname.strip() or not req.email or not req.phone.strip():
|
||||
raise HTTPException(status_code=400, detail="Все поля должны быть заполнены")
|
||||
@@ -662,12 +459,15 @@ 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),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Изменяет пароль текущего пользователя.
|
||||
"""
|
||||
# Проверяем текущий пароль
|
||||
if not verify_password(req.currentPassword, current_account.password_hash):
|
||||
raise HTTPException(status_code=400, detail="Текущий пароль неверный")
|
||||
@@ -685,11 +485,11 @@ 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), # Изменено на List[TransactionStatus]
|
||||
date_start: str = Query(None), # Добавлен параметр date_start
|
||||
date_end: str = Query(None), # Добавлен параметр date_end
|
||||
statuses: Optional[List[TransactionStatus]] = Query(None),
|
||||
date_start: str = Query(None),
|
||||
date_end: str = Query(None),
|
||||
current_account: Account = Depends(get_current_account),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -721,25 +521,28 @@ def get_account_agent_transactions(
|
||||
# Формируем список ответов в формате AgentTransactionResponse
|
||||
agent_transactions_response = []
|
||||
for agent_trans, agent in results:
|
||||
try:
|
||||
status_enum = TransactionStatus(agent_trans.status)
|
||||
except ValueError:
|
||||
# Если статус из БД не соответствует Enum, используем статус ERROR
|
||||
status_enum = TransactionStatus.ERROR
|
||||
agent_transactions_response.append(
|
||||
AgentTransactionResponse(
|
||||
amount=agent_trans.amount,
|
||||
status=agent_trans.status,
|
||||
status=status_enum,
|
||||
transaction_group=agent_trans.transaction_group,
|
||||
create_dttm=agent_trans.create_dttm,
|
||||
update_dttm=agent_trans.update_dttm,
|
||||
agent_name=agent.name # Используем имя агента
|
||||
agent_name=agent.name
|
||||
)
|
||||
)
|
||||
|
||||
return agent_transactions_response
|
||||
|
||||
# Модель запроса для POST /account/auto-approve
|
||||
class AutoApproveSettingsRequest(BaseModel):
|
||||
auto_approve: bool
|
||||
apply_to_current: Optional[bool] = False
|
||||
|
||||
@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)
|
||||
@@ -752,14 +555,14 @@ 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),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Обновляет настройку автоматического подтверждения для компании пользователя.
|
||||
Обновляет настройку автоматического подтверждения транзакций для компании пользователя.
|
||||
При необходимости переводит транзакции из 'waiting' в 'new'.
|
||||
"""
|
||||
company = db.exec(select(Company).where(Company.id == current_account.company_id)).first()
|
||||
@@ -787,8 +590,8 @@ def update_auto_approve_settings(
|
||||
# Находим соответствующие партнерские транзакции и обновляем их статус
|
||||
partner_transactions_to_update = db.exec(
|
||||
select(PartnerTransaction)
|
||||
.where(PartnerTransaction.agent_transaction_id == agent_trans.id) # Используем связь по ID
|
||||
.where(PartnerTransaction.status == TransactionStatus.PROCESS) # Предполагаем, что связанные партнерские транзакции в статусе PROCESS
|
||||
.where(PartnerTransaction.agent_transaction_id == agent_trans.id)
|
||||
.where(PartnerTransaction.status == TransactionStatus.PROCESS)
|
||||
).all()
|
||||
for partner_trans in partner_transactions_to_update:
|
||||
partner_trans.status = TransactionStatus.NEW
|
||||
@@ -800,11 +603,9 @@ def update_auto_approve_settings(
|
||||
|
||||
return {"msg": "Настройка автоматического подтверждения обновлена", "auto_approve_transactions": company.auto_approve_transactions}
|
||||
|
||||
# Модель запроса для POST /account/approve-transactions
|
||||
class ApproveTransactionsRequest(BaseModel):
|
||||
transaction_ids: List[uuid.UUID]
|
||||
|
||||
@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),
|
||||
@@ -826,11 +627,11 @@ def approve_agent_transactions(
|
||||
.join(TgAgent)
|
||||
.where(TgAgent.company_id == company_id)
|
||||
.where(AgentTransaction.transaction_group.in_(req.transaction_ids))
|
||||
.where(AgentTransaction.status == TransactionStatus.WAITING) # Утверждаем только транзакции в статусе 'waiting'
|
||||
.where(AgentTransaction.status == TransactionStatus.WAITING)
|
||||
).all()
|
||||
|
||||
for agent_trans in transactions_to_approve:
|
||||
agent_trans.status = TransactionStatus.NEW # Переводим в статус 'new'
|
||||
agent_trans.status = TransactionStatus.NEW
|
||||
agent_trans.update_dttm = datetime.utcnow()
|
||||
db.add(agent_trans)
|
||||
approved_count += 1
|
||||
@@ -838,3 +639,150 @@ def approve_agent_transactions(
|
||||
db.commit()
|
||||
|
||||
return {"msg": f"Переведено в статус NEW {approved_count} транзакций", "approved_count": approved_count}
|
||||
|
||||
# --- Новый функционал для интеграционных токенов ---
|
||||
|
||||
@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)
|
||||
):
|
||||
"""
|
||||
Возвращает список интеграционных токенов для компании текущего пользователя.
|
||||
"""
|
||||
tokens = db.exec(select(IntegrationToken).where(IntegrationToken.company_id == current_account.company_id)).all()
|
||||
return tokens # Позволяем FastAPI самостоятельно сериализовать объекты IntegrationToken в IntegrationTokenResponse
|
||||
|
||||
|
||||
@app.post("/account/integration-tokens", tags=[ "account"], response_model=IntegrationTokenResponse)
|
||||
def create_integration_token(
|
||||
req: IntegrationTokenCreateRequest,
|
||||
current_account: Account = Depends(get_current_account),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Создает новый интеграционный токен для компании текущего пользователя.
|
||||
Возвращает созданный токен (замаскированный).
|
||||
"""
|
||||
new_token_value = str(uuid.uuid4()) # Генерируем уникальный токен
|
||||
token_hash = hashlib.sha256(new_token_value.encode()).hexdigest() # Хешируем токен для хранения
|
||||
|
||||
# Генерируем замаскированный токен
|
||||
masked_token = new_token_value[:5] + "***********************" + new_token_value[-4:]
|
||||
|
||||
new_integration_token = IntegrationToken(
|
||||
description=req.description,
|
||||
token_hash=token_hash,
|
||||
masked_token=masked_token,
|
||||
company_id=current_account.company_id,
|
||||
)
|
||||
|
||||
db.add(new_integration_token)
|
||||
db.commit()
|
||||
db.refresh(new_integration_token)
|
||||
|
||||
# Создаем объект ответа, используя model_validate для извлечения данных из new_integration_token
|
||||
response_token = IntegrationTokenResponse.model_validate(new_integration_token)
|
||||
response_token.rawToken = new_token_value # Добавляем незамаскированный токен
|
||||
|
||||
return response_token
|
||||
|
||||
@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),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Обновляет описание интеграционного токена по его ID.
|
||||
"""
|
||||
token = db.exec(
|
||||
select(IntegrationToken).where(IntegrationToken.id == req.id).where(IntegrationToken.company_id == current_account.company_id)
|
||||
).first()
|
||||
|
||||
if not token:
|
||||
raise HTTPException(status_code=404, detail="Токен не найден")
|
||||
|
||||
token.description = req.description
|
||||
token.update_dttm = datetime.utcnow() # Обновляем дату изменения, если поле существует
|
||||
db.add(token)
|
||||
db.commit()
|
||||
db.refresh(token)
|
||||
return {"msg": "Описание токена обновлено успешно"}
|
||||
|
||||
@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),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Удаляет интеграционный токен по его ID для компании текущего пользователя.
|
||||
"""
|
||||
token = db.exec(
|
||||
select(IntegrationToken).where(IntegrationToken.id == token_id).where(IntegrationToken.company_id == current_account.company_id)
|
||||
).first()
|
||||
|
||||
if not token:
|
||||
raise HTTPException(status_code=404, detail="Токен не найден")
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
179
sql_create.sql
Normal 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)
|
||||
)
|
||||
|
||||
;
|
||||
155
sql_models.py
Normal file
155
sql_models.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from sqlmodel import SQLModel, Field, Relationship
|
||||
from sqlalchemy import Column, String
|
||||
|
||||
class Company(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
commission: float # процент комиссии, который взымается за пользование сервисом
|
||||
key: str = Field(index=True, unique=True)
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
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)
|
||||
tg_id: int = Field(index=True, unique=True)
|
||||
chat_id: Optional[int] = None
|
||||
phone: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
login: Optional[str] = None
|
||||
hash: Optional[str] = None
|
||||
company_id: int = Field(foreign_key="company.id")
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
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, 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)
|
||||
company_id: int = Field(foreign_key="company.id")
|
||||
type: str
|
||||
amount: float
|
||||
status: str
|
||||
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)
|
||||
|
||||
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)
|
||||
company_id: int = Field(foreign_key="company.id", unique=True)
|
||||
available_balance: float = Field(default=0.0)
|
||||
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)
|
||||
tg_agent_id: int = Field(foreign_key="tgagent.id", unique=True)
|
||||
available_balance: float = Field(default=0.0)
|
||||
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)
|
||||
password_hash: str
|
||||
firstName: Optional[str] = None
|
||||
surname: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
company_id: int = Field(foreign_key="company.id")
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
company: "Company" = Relationship(back_populates="accounts")
|
||||
|
||||
# Новая модель для интеграционных токенов
|
||||
class IntegrationToken(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
description: str
|
||||
token_hash: str = Field(sa_column=Column(String, unique=True, index=True))
|
||||
masked_token: str = Field(sa_column=Column(String))
|
||||
company_id: int = Field(foreign_key="company.id")
|
||||
create_dttm: datetime = Field(default_factory=datetime.utcnow, nullable=False)
|
||||
update_dttm: datetime = Field(default_factory=datetime.utcnow, nullable=False)
|
||||
use_dttm: Optional[datetime] = None
|
||||
|
||||
company: "Company" = Relationship(back_populates="integration_tokens")
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user