Compare commits

..

14 Commits

Author SHA1 Message Date
Redsandyg
2c8cbe753e Добавлен новый файл call_validation_promo_api.py для валидации промокодов через API. Реализована логика получения JWT токена и проверки валидности промокода. Обновлены модели в integration_models.py для поддержки запросов и ответов на валидацию промокодов. Добавлен новый эндпоинт /validationPromo в integration_api.py для обработки запросов на валидацию промокодов. 2025-06-22 14:39:14 +03:00
Redsandyg
a6308582cb Удалено поле agent_commission из модели CompanyProfileResponse и соответствующих мест в коде. Обновлены значения комиссии в функции fill_db для создания компании. Изменены теги в эндпоинтах API, убраны лишние теги "bff" для упрощения структуры. Улучшена читаемость кода. 2025-06-21 15:02:21 +03:00
Redsandyg
9157570e58 Обновлены модели и функции для поддержки групповых продаж. Внесены изменения в API для передачи идентификаторов групповых продаж. Обновлены SQL-скрипты и модели для учета нового поля group_sale_id. Изменены данные для создания продажи, включая категорию и стоимость. 2025-06-18 11:31:32 +03:00
Redsandyg
bf6a6a8987 Добавлены новые модели SaleCategoryRequest и SaleCategoryResponse в bff_models.py для работы с категориями продаж. Обновлены функции в fill_db.py для заполнения базы данных категориями продаж. Изменены эндпоинты в main.py для создания и получения категорий продаж. Обновлены модели и SQL-скрипты для поддержки новых полей и связей. Улучшена логика обработки продаж с учетом категорий. 2025-06-18 10:48:29 +03:00
Redsandyg
4f366680bf Обновлен эндпоинт создания продажи в integration_api.py: добавлена проверка на наличие ref и promocode, улучшена логика поиска реферальных ссылок и проверка их принадлежности компании. Модель SaleCreateRequest обновлена для поддержки опционального поля promocode. 2025-06-16 11:11:41 +03:00
Redsandyg
d113ae4adb Добавлено новое поле promocode в модель StatReferralsItem и модель Ref. Обновлены функции в fill_db.py для генерации уникальных промокодов при создании реферальных ссылок. Обновлены эндпоинты в integration_api.py для возврата промокодов вместе с реферальными ссылками. Удалены устаревшие функции работы с промокодами из кода. Обновлены SQL-скрипты и модели для учета новых полей. 2025-06-15 17:03:41 +03:00
Redsandyg
92df59ad23 Добавлены новые функции для работы с промокодами в integration_api.py, включая создание и получение промокодов для Telegram-агентов. Обновлены модели и добавлен новый файл generate_sql.py для генерации SQL-скрипта создания таблиц. Обновлены fill_db.py для генерации промокодов при заполнении базы данных. Также обновлены sql_models.py для добавления модели PromoCode и соответствующих связей. Улучшена структура кода и добавлены отладочные сообщения. 2025-06-15 15:20:28 +03:00
Redsandyg
3973d6404d Добавлен новый эндпоинт для запроса на вывод средств для Telegram-агентов в integration_api.py. Обновлены теги для существующих эндпоинтов, изменив их с "partner-tg" на "agent-tg". В integration_models.py добавлены модели WithdrawRequest и WithdrawResponse для обработки запросов на вывод средств. Улучшена логика обработки транзакций и проверок баланса. 2025-06-13 14:12:58 +03:00
Redsandyg
7045d6790a Добавлены новые эндпоинты для работы с реферальными ссылками и статистикой Telegram-агентов в integration_api.py. Реализованы функции для получения списка реферальных ссылок, добавления новых ссылок, получения статистики по ссылкам и общей статистики для агентов. Также добавлена функция авторизации Telegram-агента по хешу и регистрация новых агентов. Удалены устаревшие функции из main.py для улучшения структуры кода. 2025-06-12 16:27:40 +03:00
Redsandyg
16973bbb64 Обновлены модели Sale, AgentTransaction и PartnerTransaction: добавлено уникальное ограничение для поля transaction_group в AgentTransaction, а также исправлены пробелы в определениях полей sale_id и transaction_group. Улучшена читаемость кода. 2025-06-12 16:03:03 +03:00
Redsandyg
736f04bb7e Обновлены идентификаторы API и добавлен генератор уникальных идентификаторов для продаж. В функции создания продажи добавлены проверки на наличие балансов агента и компании, а также улучшена логика обновления балансов. Установлен уровень изоляции для транзакций. Обновлены комментарии для ясности кода. 2025-06-11 11:01:26 +03:00
Redsandyg
1cc18e0364 Добавлены новые поля для агентской комиссии в модели Company и CompanyProfileResponse. Реализованы функции для обработки продаж через интеграционный API, включая создание и регистрацию продаж с учетом агентской комиссии. Обновлены соответствующие эндпоинты и модели для работы с токенами и продажами. Улучшена логика обработки транзакций и обновления балансов компаний и агентов. 2025-06-10 14:15:46 +03:00
Redsandyg
076cdd1828 Добавлены новые модели для интеграционных токенов в bff_models.py и sql_models.py. Реализованы функции для создания, обновления и удаления токенов в main.py, а также обновлено заполнение базы данных в fill_db.py для генерации токенов. Обновлены запросы к базе данных для учета новых полей и логики работы с токенами. 2025-06-09 15:27:50 +03:00
Redsandyg
57188186c0 Добавлен новый файл integration_api.py для интеграционного API, реализованы функции для создания и проверки токенов, а также эндпоинт для загрузки данных о продажах. Обновлены модели и логика работы с датами в fill_db.py и main.py для использования поля sale_date вместо create_dttm. 2025-06-09 12:52:49 +03:00
13 changed files with 1143 additions and 258 deletions

View File

@@ -1,4 +1,4 @@
from pydantic import BaseModel, Field, EmailStr from pydantic import BaseModel, Field, EmailStr, ConfigDict
from typing import Optional, List from typing import Optional, List
from datetime import datetime from datetime import datetime
import uuid import uuid
@@ -89,6 +89,7 @@ class StatReferralsItem(BaseModel):
ref: str ref: str
agent: Optional[str] = None agent: Optional[str] = None
description: str description: str
promocode: str
salesSum: float salesSum: float
salesCount: int salesCount: int
@@ -168,4 +169,39 @@ class AutoApproveSettingsUpdateResponse(BaseModel):
class ApproveTransactionsResult(BaseModel): class ApproveTransactionsResult(BaseModel):
msg: str msg: str
approved_count: int 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
View File

@@ -0,0 +1,63 @@
import uuid
import requests
import json
# Конфигурация API
BASE_URL = "http://127.0.0.1:8001"
API_KEY = "de058226-37d3-4d0e-a483-3c2a7fac3573"
REF = "0d9aaa96-80e6-424c-84c9-ff70a6eb915e"
# Данные для запроса на создание продажи
# Замените эти значения на актуальные для вашей продажи
sale_data = {
"cost": 100, # Стоимость продажи
"ref": REF, # Ваш реферальный код
"sale_id": str(uuid.uuid4()), # Уникальный идентификатор продажи для вашей компании
"category": "vip", # название категории (например, 'basic', 'premium', 'vip')
"group_sale_id": str(uuid.uuid4()) # уникальный идентификатор группы продаж
}
# Эндпоинты
token_endpoint = f"{BASE_URL}/token"
sale_endpoint = f"{BASE_URL}/sale"
# Шаг 1: Получение JWT токена
print(f"Отправка запроса на получение токена на {token_endpoint}")
token_headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json"
}
try:
token_response = requests.post(token_endpoint, headers=token_headers)
token_response.raise_for_status()
token_data = token_response.json()
jwt_token = token_data["access_token"]
print("JWT токен успешно получен.")
except requests.exceptions.RequestException as e:
print(f"Произошла ошибка при получении токена: {e}")
if hasattr(e, 'response') and e.response is not None:
print("Тело ответа с ошибкой:", e.response.json())
exit() # Прерываем выполнение, если не удалось получить токен
# Шаг 2: Вызов эндпоинта /sale с использованием полученного JWT токена
headers_with_jwt = {
"Authorization": f"Bearer {jwt_token}",
"Content-Type": "application/json"
}
print(f"Отправка запроса на {sale_endpoint} с данными: {sale_data}")
try:
sale_response = requests.post(sale_endpoint, headers=headers_with_jwt, data=json.dumps(sale_data))
sale_response.raise_for_status() # Вызовет исключение для ошибок HTTP (4xx или 5xx)
print("Статус ответа:", sale_response.status_code)
print("Тело ответа:", sale_response.json())
except requests.exceptions.RequestException as e:
print(f"Произошла ошибка при вызове API sale: {e}")
if hasattr(e, 'response') and e.response is not None:
print("Тело ответа с ошибкой:", e.response.json())

View File

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

View File

@@ -1,11 +1,12 @@
import random import random
from uuid import uuid4 from uuid import uuid4
from sqlmodel import Session from sqlmodel import Session
from sql_models import 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 sqlalchemy import text
from datetime import datetime, timedelta from datetime import datetime, timedelta
from hashlib import sha256 from hashlib import sha256
from helpers_bff import AUTH_DB_ENGINE, get_password_hash from helpers_bff import AUTH_DB_ENGINE, get_password_hash
import string
# Константа: список user_ids # Константа: список user_ids
@@ -71,6 +72,7 @@ def fill_db():
date_list = get_date_list(7) # 8 дней: от недели назад до сегодня date_list = get_date_list(7) # 8 дней: от недели назад до сегодня
with Session(AUTH_DB_ENGINE) as session: with Session(AUTH_DB_ENGINE) as session:
# Очистка таблиц # Очистка таблиц
# session.execute(text("DELETE FROM promocode"))
session.execute(text("DELETE FROM sale")) session.execute(text("DELETE FROM sale"))
session.execute(text("DELETE FROM ref")) session.execute(text("DELETE FROM ref"))
session.execute(text("DELETE FROM tgagent")) session.execute(text("DELETE FROM tgagent"))
@@ -79,17 +81,47 @@ def fill_db():
session.execute(text('DELETE FROM "partner_transactions"')) session.execute(text('DELETE FROM "partner_transactions"'))
session.execute(text('DELETE FROM "company_balances"')) session.execute(text('DELETE FROM "company_balances"'))
session.execute(text('DELETE FROM "agent_balances"')) session.execute(text('DELETE FROM "agent_balances"'))
session.execute(text("DELETE FROM salecategory"))
session.execute(text("DELETE FROM company")) session.execute(text("DELETE FROM company"))
session.commit() session.commit()
# 0. Company # 0. Company
company = Company( company = Company(
name="RE: Premium", name="RE: Premium",
commission=10.0, commission=0.0,
key="re-premium-key", key="re-premium-key",
) )
session.add(company) session.add(company)
session.commit() session.commit()
session.refresh(company) 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 # 1. Accounts
accounts = [] accounts = []
for i in range(4): for i in range(4):
@@ -131,19 +163,28 @@ def fill_db():
session.commit() session.commit()
for tg_agent in tg_agents: for tg_agent in tg_agents:
session.refresh(tg_agent) session.refresh(tg_agent)
# Отладка: количество агентов
agent_count = session.execute(text("SELECT COUNT(*) FROM tgagent")).scalar()
print(f'Агентов в базе: {agent_count}')
# 3. Refs (минимум 22 на агента) # 3. Refs (минимум 22 на агента)
refs = [] refs = []
desc_count = len(ALL_DESCRIPTIONS) desc_count = len(ALL_DESCRIPTIONS)
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
for tg_agent in tg_agents: for tg_agent in tg_agents:
ref_count = random.randint(22, int(22 * 1.25)) # от 22 до 27 ref_count = random.randint(22, int(22 * 1.25)) # от 22 до 27
for j in range(ref_count): for j in range(ref_count):
ref_val = str(uuid4()) ref_val = str(uuid4())
desc_val = ALL_DESCRIPTIONS[(j % desc_count)] desc_val = ALL_DESCRIPTIONS[(j % desc_count)]
dt = random.choice(date_list) 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( ref = Ref(
tg_agent_id=tg_agent.id, tg_agent_id=tg_agent.id,
ref=ref_val, ref=ref_val,
description=desc_val, description=desc_val,
promocode=promocode,
create_dttm=dt, create_dttm=dt,
update_dttm=dt update_dttm=dt
) )
@@ -153,20 +194,33 @@ def fill_db():
for ref in refs: for ref in refs:
session.refresh(ref) session.refresh(ref)
# 4. Sales (минимум 20 на каждый ref) # 4. Sales (минимум 20 на каждый ref)
all_categories = session.query(SaleCategory).filter_by(company_id=company.id).all()
for ref in refs: for ref in refs:
sale_count = random.randint(20, int(20 * 1.25)) # от 20 до 25 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) cost = round(random.uniform(100, 1000), 2)
crediting = round(cost * random.uniform(0.5, 1.0), 2) sale_category = random.choice(all_categories)
dt = random.choice(date_list) 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( sale = Sale(
cost=cost, cost=cost,
crediting=crediting, crediting=crediting,
ref=ref.id, ref=ref.id,
sale_id=str(uuid4()), sale_id=str(uuid4()),
group_sale_id=group_sale_id,
company_id=company.id, company_id=company.id,
create_dttm=dt, category=sale_category.id,
update_dttm=dt sale_dttm=sale_dttm,
create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне
update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне
) )
session.add(sale) session.add(sale)
session.commit() session.commit()
@@ -203,13 +257,21 @@ def fill_db():
PARTNER_TRANSACTION_TYPES = ['deposit', 'agent_payout', 'service_fee'] PARTNER_TRANSACTION_TYPES = ['deposit', 'agent_payout', 'service_fee']
PARTNER_TRANSACTION_STATUSES = ['process', 'done', 'error', 'new'] PARTNER_TRANSACTION_STATUSES = ['process', 'done', 'error', 'new']
waiting_transactions_to_ensure = 7
waiting_transactions_count = 0
for tg_agent in tg_agents: for tg_agent in tg_agents:
# Генерируем несколько групп транзакций для каждого агента # Генерируем несколько групп транзакций для каждого агента
for _ in range(random.randint(3, 6)): # От 3 до 6 групп на агента for _ in range(random.randint(3, 6)): # От 3 до 6 групп на агента
transaction_group_id = uuid4() transaction_group_id = uuid4()
dt = random.choice(date_list) dt = random.choice(date_list)
agent_trans_amount = round(random.uniform(500, 3000), 2) 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 # Создаем AgentTransaction
agent_transaction = AgentTransaction( agent_transaction = AgentTransaction(

22
generate_sql.py Normal file
View File

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

View File

@@ -3,12 +3,13 @@ from passlib.context import CryptContext
from typing import Optional from typing import Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from bff_models import Token, TransactionStatus from bff_models import Token, TransactionStatus
from sql_models import Company, TgAgent, Account, AgentBalance, AgentTransaction, PartnerTransaction, Sale, Ref from sql_models import Company, TgAgent, Account, AgentBalance, AgentTransaction, PartnerTransaction, Sale, Ref, IntegrationToken, CompanyBalance
from hashlib import sha256 from hashlib import sha256
import jwt import jwt
from jwt.exceptions import InvalidTokenError from jwt.exceptions import InvalidTokenError
from fastapi import HTTPException, status, Depends, Request from fastapi import HTTPException, status, Depends, Request, Header
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
import hashlib
# Конфигурация # Конфигурация
AUTH_DATABASE_ADDRESS = "sqlite:///partner.db" AUTH_DATABASE_ADDRESS = "sqlite:///partner.db"
@@ -18,9 +19,60 @@ SECRET_KEY = "supersecretkey"
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 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") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 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]: def get_tg_agent_by_tg_id(db: Session, tg_id: int) -> Optional[TgAgent]:
statement = select(TgAgent).where(TgAgent.tg_id == tg_id) statement = select(TgAgent).where(TgAgent.tg_id == tg_id)
return db.exec(statement).first() return db.exec(statement).first()

343
integration_api.py Normal file
View 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
View File

@@ -0,0 +1,51 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
import uuid
from enum import Enum
# Models for /token endpoint
class Token(BaseModel):
access_token: str
token_type: str
class IntegrationTokenResponse(BaseModel):
msg: str
company_name: str
company_key: str
# Models for /sale endpoint
class SaleCreateRequest(BaseModel):
ref: Optional[str] = None
promocode: Optional[str] = None
sale_id: str
cost: float
category: str # название категории продажи
group_sale_id: str # новое поле для группировки продаж
class SaleCreateResponse(BaseModel):
msg: str
sale_id: str
crediting: float
class WithdrawRequest(BaseModel):
tg_id: int
amount: float
class WithdrawResponse(BaseModel):
msg: str
transaction_id: uuid.UUID
class TransactionStatus(str, Enum):
NEW = "new"
PROCESS = "process"
WAITING = "waiting"
DONE = "done"
CANCELED = "canceled"
ERROR = "error"
class PromoValidationRequest(BaseModel):
promocode: str
class PromoValidationResponse(BaseModel):
validation: bool

331
main.py
View File

@@ -8,12 +8,11 @@ from fastapi import (
Body, Body,
) )
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import SQLModel, Session, select from sqlmodel import SQLModel, Session, select, Field
from typing import Optional, List, Dict from typing import Optional, List, Dict
from datetime import timedelta, datetime from datetime import timedelta, datetime
from bff_models import ( from bff_models import (
Token, Token,
RegisterResponse,
DashboardCardsResponse, DashboardCardsResponse,
DashboardChartTotalResponse, DashboardChartTotalResponse,
DashboardChartAgentResponse, DashboardChartAgentResponse,
@@ -30,16 +29,19 @@ from bff_models import (
AutoApproveSettingsGetResponse, AutoApproveSettingsGetResponse,
AutoApproveSettingsUpdateResponse, AutoApproveSettingsUpdateResponse,
ApproveTransactionsResult, ApproveTransactionsResult,
TgAuthResponse,
BillingPayoutsTransactionsResponse, BillingPayoutsTransactionsResponse,
AccountProfileUpdateRequest, AccountProfileUpdateRequest,
AccountPasswordChangeRequest, AccountPasswordChangeRequest,
AutoApproveSettingsRequest, AutoApproveSettingsRequest,
ApproveTransactionsRequest, ApproveTransactionsRequest,
AgentTransactionResponse, AgentTransactionResponse,
TransactionStatus TransactionStatus,
IntegrationTokenResponse,
IntegrationTokenCreateRequest,
IntegrationTokenUpdateRequest,
SaleCategoryRequest,
SaleCategoryResponse
) )
from tg_models import RefAddRequest, RefResponse, RegisterRequest, RefAddResponse, RefStatResponse, StatResponse
from sql_models import ( from sql_models import (
Company, Company,
TgAgent, TgAgent,
@@ -48,22 +50,25 @@ from sql_models import (
AgentTransaction, AgentTransaction,
PartnerTransaction, PartnerTransaction,
AgentBalance, AgentBalance,
Account Account,
IntegrationToken,
SaleCategory
) )
from sqlalchemy import func from sqlalchemy import func
import hashlib import hashlib
from helpers_bff import ( from helpers_bff import (
AUTH_DB_ENGINE, AUTH_DB_ENGINE,
get_db, get_db,
get_tg_agent_by_tg_id,
get_current_account, get_current_account,
get_current_tg_agent,
create_access_token, create_access_token,
verify_password, verify_password,
get_account_by_login, get_account_by_login,
ACCESS_TOKEN_EXPIRE_MINUTES, ACCESS_TOKEN_EXPIRE_MINUTES,
pwd_context, pwd_context,
) )
import os
from pydantic import BaseModel
# Создание движка базы данных # Создание движка базы данных
SQLModel.metadata.create_all(AUTH_DB_ENGINE) SQLModel.metadata.create_all(AUTH_DB_ENGINE)
@@ -72,42 +77,9 @@ SQLModel.metadata.create_all(AUTH_DB_ENGINE)
app = FastAPI() app = FastAPI()
@app.post("/register", tags=["partner-tg"], response_model=RegisterResponse)
def register(req: RegisterRequest, db: Session = Depends(get_db)):
"""
Регистрирует нового Telegram-агента в системе.
"""
tg_id = req.tg_id
chat_id = req.chat_id
phone = req.phone
name = getattr(req, 'name', None)
login = getattr(req, 'login', None)
company_key = req.company_key
print(f'tg_id: {tg_id}, chat_id: {chat_id}, phone: {phone}, name: {name}, login: {login}, company_key: {company_key}')
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
if tg_agent:
raise HTTPException(status_code=400, detail="tg_id already registered")
# Поиск компании по ключу
company = db.exec(select(Company).where(Company.key == company_key)).first()
if not company:
raise HTTPException(status_code=400, detail="Компания с таким ключом не найдена")
hash_value = hashlib.sha256(f"{tg_id}sold".encode()).hexdigest()
new_tg_agent = TgAgent(
tg_id=tg_id,
chat_id=chat_id,
phone=phone,
name=name,
login=login,
hash=hash_value,
company_id=company.id
)
db.add(new_tg_agent)
db.commit()
db.refresh(new_tg_agent)
return {"msg": "TgAgent registered successfully"}
@app.post("/token", response_model=Token, tags=["bff", "token"]) @app.post("/token", response_model=Token, tags=[ "token"])
def login_account_for_access_token( def login_account_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(), form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db) db: Session = Depends(get_db)
@@ -131,73 +103,7 @@ def login_account_for_access_token(
return Token(access_token=access_token, token_type="bearer") return Token(access_token=access_token, token_type="bearer")
@app.get("/dashboard/cards", tags=[ "dashboard"], response_model=DashboardCardsResponse)
@app.get("/ref", response_model=List[RefResponse], tags=["partner-tg"])
def get_refs(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
"""
Возвращает список реферальных ссылок текущего Telegram-агента.
"""
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
return [RefResponse(ref=r.ref, description=r.description or "") for r in refs]
@app.post("/ref/add", tags=["partner-tg"], response_model=RefAddResponse)
def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
"""
Добавляет новую реферальную ссылку для текущего Telegram-агента.
"""
new_ref = Ref(
tg_agent_id=current_tg_agent.id,
ref=str(uuid.uuid4()),
description=req.description
)
db.add(new_ref)
db.commit()
db.refresh(new_ref)
return {"ref": new_ref.ref}
@app.get("/ref/stat", tags=["partner-tg"], response_model=RefStatResponse)
def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
"""
Возвращает статистику по реферальным ссылкам текущего Telegram-агента.
"""
# 1. Получаем все реферальные ссылки пользователя
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
result = []
for ref in refs:
# 2. Для каждой ссылки считаем продажи и сумму
sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all()
sales_count = len(sales)
income = sum(sale.crediting for sale in sales)
result.append({
"description": ref.description or "",
"sales": sales_count,
"income": income
})
return {"refData": result}
@app.get("/stat", tags=["partner-tg"], response_model=StatResponse)
def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
"""
Возвращает общую статистику для текущего Telegram-агента.
"""
# 1. Получаем все реферальные ссылки пользователя
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
ref_ids = [r.id for r in refs]
# 2. Считаем totalSales (продажи по всем рефам пользователя)
total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
totalSales = len(total_sales)
totalIncome = sum(sale.crediting for sale in total_sales)
# Заменено получение доступного остатка из AgentBalance
agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == current_tg_agent.id)).first()
availableWithdrawal = agent_balance.available_balance if agent_balance else 0.0
return {
"totalSales": totalSales,
"totalIncome": totalIncome,
"availableWithdrawal": availableWithdrawal
}
@app.get("/dashboard/cards", tags=["bff", "dashboard"], response_model=DashboardCardsResponse)
def get_dashboard_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)): def get_dashboard_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
""" """
Возвращает данные для карточек на главной панели (dashboard) пользователя, включая общий доход, выплаты, активных рефералов, ожидающие выплаты и общее количество продаж. Возвращает данные для карточек на главной панели (dashboard) пользователя, включая общий доход, выплаты, активных рефералов, ожидающие выплаты и общее количество продаж.
@@ -228,7 +134,7 @@ def get_dashboard_cards(current_account: Account = Depends(get_current_account),
"totalSales": totalSales "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)): def get_dashboard_chart_total(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
""" """
Возвращает данные для графика "Общий доход и продажи по датам" на главной панели (dashboard). Возвращает данные для графика "Общий доход и продажи по датам" на главной панели (dashboard).
@@ -236,11 +142,11 @@ def get_dashboard_chart_total(current_account: Account = Depends(get_current_acc
# Группируем продажи по дате (день) # Группируем продажи по дате (день)
result = db.exec( result = db.exec(
select( 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.sum(Sale.cost).label('revenue'),
func.count(Sale.id).label('sales') func.count(Sale.id).label('sales')
).where(Sale.company_id == current_account.company_id).group_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.create_dttm)) .order_by(func.strftime('%Y-%m-%d', Sale.sale_dttm))
).all() ).all()
# Преобразуем результат в нужный формат # Преобразуем результат в нужный формат
data = [ data = [
@@ -249,7 +155,7 @@ def get_dashboard_chart_total(current_account: Account = Depends(get_current_acc
] ]
return DashboardChartTotalResponse(items=data) 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)): def get_dashboard_chart_agent(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
""" """
Возвращает данные для графика "Продажи по агентам" на главной панели (dashboard). Возвращает данные для графика "Продажи по агентам" на главной панели (dashboard).
@@ -279,7 +185,7 @@ def get_dashboard_chart_agent(current_account: Account = Depends(get_current_acc
}) })
return DashboardChartAgentResponse(items=result) 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( def get_agents_stat(
db: Session = Depends(get_db), db: Session = Depends(get_db),
date_start: str = Query(None), date_start: str = Query(None),
@@ -323,7 +229,7 @@ def get_agents_stat(
}) })
return StatAgentsResponse(items=result) 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( def get_referrals_stat(
db: Session = Depends(get_db), db: Session = Depends(get_db),
date_start: str = Query(None), date_start: str = Query(None),
@@ -350,12 +256,13 @@ def get_referrals_stat(
"ref": ref.ref, "ref": ref.ref,
"agent": agent.name if agent and agent.name else f"Агент {ref.tg_agent_id}", "agent": agent.name if agent and agent.name else f"Агент {ref.tg_agent_id}",
"description": ref.description or "", "description": ref.description or "",
"promocode": ref.promocode,
"salesSum": sales_sum, "salesSum": sales_sum,
"salesCount": sales_count "salesCount": sales_count
}) })
return StatReferralsResponse(items=result) 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( def get_sales_stat(
db: Session = Depends(get_db), db: Session = Depends(get_db),
date_start: str = Query(None), date_start: str = Query(None),
@@ -368,9 +275,9 @@ def get_sales_stat(
""" """
sales_query = select(Sale).where(Sale.company_id == current_account.company_id) sales_query = select(Sale).where(Sale.company_id == current_account.company_id)
if date_start: 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: 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() sales = db.exec(sales_query).all()
ref_ids = list(set(sale.ref for sale in sales)) 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 [] refs = db.exec(select(Ref).where(Ref.id.in_(ref_ids))).all() if ref_ids else []
@@ -391,7 +298,7 @@ def get_sales_stat(
}) })
return StatSalesResponse(items=result) 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)): def get_billing_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
""" """
Возвращает ключевые показатели биллинга для компании, включая общий заработок, общие выплаты и доступные к выводу средства. Возвращает ключевые показатели биллинга для компании, включая общий заработок, общие выплаты и доступные к выводу средства.
@@ -414,7 +321,7 @@ def get_billing_cards(current_account: Account = Depends(get_current_account), d
"pendingPayouts": pendingPayouts "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( def get_billing_payouts_transactions(
db: Session = Depends(get_db), db: Session = Depends(get_db),
date_start: str = Query(None), date_start: str = Query(None),
@@ -454,7 +361,7 @@ def get_billing_payouts_transactions(
}) })
return BillingPayoutsTransactionsResponse(items=result) 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)): def get_billing_chart_stat(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
""" """
Возвращает статистику выплат по датам и статусам для компании текущего пользователя. Возвращает статистику выплат по датам и статусам для компании текущего пользователя.
@@ -479,7 +386,7 @@ def get_billing_chart_stat(current_account: Account = Depends(get_current_accoun
] ]
return BillingChartStatResponse(items=data) 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)): def get_billing_chart_pie(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
""" """
Возвращает круговую диаграмму статистики выплат по статусам для компании текущего пользователя. Возвращает круговую диаграмму статистики выплат по статусам для компании текущего пользователя.
@@ -499,20 +406,7 @@ def get_billing_chart_pie(current_account: Account = Depends(get_current_account
@app.post("/tg_auth", tags=["partner-tg"], response_model=TgAuthResponse) @app.get("/account", tags=[ "account"], response_model=AccountResponse)
def tg_auth(hash: str = Body(..., embed=True), db: Session = Depends(get_db)):
"""
Авторизует Telegram-агента по хешу.
"""
tg_agent = db.exec(select(TgAgent).where(TgAgent.hash == hash)).first()
if not tg_agent:
raise HTTPException(status_code=401, detail="Hash not found")
return {"msg": "Auth success", "tg_id": tg_agent.tg_id}
# --- Новый функционал для Account ---
@app.get("/account", tags=["bff", "account"], response_model=AccountResponse)
def get_account(current_account: Account = Depends(get_current_account)): def get_account(current_account: Account = Depends(get_current_account)):
""" """
Возвращает базовую информацию об аккаунте текущего пользователя (имя и фамилию). Возвращает базовую информацию об аккаунте текущего пользователя (имя и фамилию).
@@ -522,7 +416,7 @@ def get_account(current_account: Account = Depends(get_current_account)):
"surname": current_account.surname "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)): def get_account_profile(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
""" """
Возвращает полную информацию о профиле аккаунта текущего пользователя, включая данные компании. Возвращает полную информацию о профиле аккаунта текущего пользователя, включая данные компании.
@@ -539,11 +433,11 @@ def get_account_profile(current_account: Account = Depends(get_current_account),
"company": { "company": {
"name": company.name, "name": company.name,
"key": company.key, "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( def update_account_profile(
req: AccountProfileUpdateRequest, req: AccountProfileUpdateRequest,
current_account: Account = Depends(get_current_account), current_account: Account = Depends(get_current_account),
@@ -565,7 +459,7 @@ def update_account_profile(
db.refresh(current_account) db.refresh(current_account)
return {"msg": "Профиль обновлён успешно"} 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( def change_account_password(
req: AccountPasswordChangeRequest, req: AccountPasswordChangeRequest,
current_account: Account = Depends(get_current_account), current_account: Account = Depends(get_current_account),
@@ -591,7 +485,7 @@ def change_account_password(
# --- Новый функционал для агентских транзакций партнера --- # --- Новый функционал для агентских транзакций партнера ---
@app.get("/account/agent-transaction", response_model=List[AgentTransactionResponse], tags=["bff", "account"]) @app.get("/account/agent-transaction", response_model=List[AgentTransactionResponse], tags=[ "account"])
def get_account_agent_transactions( def get_account_agent_transactions(
statuses: Optional[List[TransactionStatus]] = Query(None), statuses: Optional[List[TransactionStatus]] = Query(None),
date_start: str = Query(None), date_start: str = Query(None),
@@ -648,7 +542,7 @@ def get_account_agent_transactions(
@app.get("/account/auto-approve", tags=["bff", "account"], response_model=AutoApproveSettingsGetResponse) @app.get("/account/auto-approve", tags=[ "account"], response_model=AutoApproveSettingsGetResponse)
def get_auto_approve_settings( def get_auto_approve_settings(
current_account: Account = Depends(get_current_account), current_account: Account = Depends(get_current_account),
db: Session = Depends(get_db) db: Session = Depends(get_db)
@@ -661,7 +555,7 @@ def get_auto_approve_settings(
raise HTTPException(status_code=404, detail="Компания не найдена") raise HTTPException(status_code=404, detail="Компания не найдена")
return {"auto_approve_transactions": company.auto_approve_transactions} 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( def update_auto_approve_settings(
req: AutoApproveSettingsRequest, req: AutoApproveSettingsRequest,
current_account: Account = Depends(get_current_account), current_account: Account = Depends(get_current_account),
@@ -711,7 +605,7 @@ def update_auto_approve_settings(
@app.post("/account/approve-transactions", tags=["bff", "account"], response_model=ApproveTransactionsResult) @app.post("/account/approve-transactions", tags=[ "account"], response_model=ApproveTransactionsResult)
def approve_agent_transactions( def approve_agent_transactions(
req: ApproveTransactionsRequest, req: ApproveTransactionsRequest,
current_account: Account = Depends(get_current_account), current_account: Account = Depends(get_current_account),
@@ -745,3 +639,150 @@ def approve_agent_transactions(
db.commit() db.commit()
return {"msg": f"Переведено в статус NEW {approved_count} транзакций", "approved_count": approved_count} 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)

View File

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

179
sql_create.sql Normal file
View File

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

View File

@@ -1,16 +1,25 @@
from typing import Optional from typing import Optional, List
from datetime import datetime from datetime import datetime
import uuid import uuid
from sqlmodel import SQLModel, Field from sqlmodel import SQLModel, Field, Relationship
from sqlalchemy import Column, String
class Company(SQLModel, table=True): class Company(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
name: str name: str
commission: float # процент комиссии commission: float # процент комиссии, который взымается за пользование сервисом
key: str = Field(index=True, unique=True) key: str = Field(index=True, unique=True)
create_dttm: datetime = Field(default_factory=datetime.utcnow) create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow) update_dttm: datetime = Field(default_factory=datetime.utcnow)
auto_approve_transactions: bool = Field(default=False) auto_approve_transactions: bool = Field(default=False) # Отвечает за автоматическое одобрение агентских транзакций на вывод.
integration_tokens: List["IntegrationToken"] = Relationship(back_populates="company")
tg_agents: List["TgAgent"] = Relationship(back_populates="company")
sales: List["Sale"] = Relationship(back_populates="company")
partner_transactions: List["PartnerTransaction"] = Relationship(back_populates="company")
company_balance: Optional["CompanyBalance"] = Relationship(back_populates="company")
accounts: List["Account"] = Relationship(back_populates="company")
sale_categories: List["SaleCategory"] = Relationship(back_populates="company")
class TgAgent(SQLModel, table=True): class TgAgent(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
@@ -24,34 +33,65 @@ class TgAgent(SQLModel, table=True):
create_dttm: datetime = Field(default_factory=datetime.utcnow) create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_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): class Ref(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
tg_agent_id: int = Field(foreign_key="tgagent.id") tg_agent_id: int = Field(foreign_key="tgagent.id")
ref: str ref: str
description: Optional[str] = None description: Optional[str] = None
promocode: str = Field(index=True, unique=True, max_length=8)
create_dttm: datetime = Field(default_factory=datetime.utcnow) create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_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): class Sale(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
cost: float cost: float
crediting: float # сколько начислено за продажу crediting: float # сколько начислено за продажу
ref: int = Field(foreign_key="ref.id") ref: int = Field(foreign_key="ref.id")
sale_id: str sale_id: str
group_sale_id: str # новое поле для группировки продаж
company_id: int = Field(foreign_key="company.id") 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) create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_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): class AgentTransaction(SQLModel, table=True):
__tablename__ = "agent_transactions" __tablename__ = "agent_transactions"
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
tg_agent_id: int = Field(foreign_key="tgagent.id") tg_agent_id: int = Field(foreign_key="tgagent.id")
amount: float amount: float
status: str status: str
transaction_group: uuid.UUID = Field(default_factory=uuid.uuid4) transaction_group: uuid.UUID = Field(default_factory=uuid.uuid4, unique=True)
create_dttm: datetime = Field(default_factory=datetime.utcnow) create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_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): class PartnerTransaction(SQLModel, table=True):
__tablename__ = "partner_transactions" __tablename__ = "partner_transactions"
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
@@ -59,11 +99,14 @@ class PartnerTransaction(SQLModel, table=True):
type: str type: str
amount: float amount: float
status: str status: str
transaction_group: uuid.UUID transaction_group: uuid.UUID
agent_transaction_id: Optional[int] = Field(default=None, foreign_key="agent_transactions.id") agent_transaction_id: Optional[int] = Field(default=None, foreign_key="agent_transactions.id")
create_dttm: datetime = Field(default_factory=datetime.utcnow) create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_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): class CompanyBalance(SQLModel, table=True):
__tablename__ = "company_balances" __tablename__ = "company_balances"
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
@@ -72,6 +115,8 @@ class CompanyBalance(SQLModel, table=True):
pending_balance: float = Field(default=0.0) pending_balance: float = Field(default=0.0)
updated_dttm: datetime = Field(default_factory=datetime.utcnow) updated_dttm: datetime = Field(default_factory=datetime.utcnow)
company: "Company" = Relationship(back_populates="company_balance")
class AgentBalance(SQLModel, table=True): class AgentBalance(SQLModel, table=True):
__tablename__ = "agent_balances" __tablename__ = "agent_balances"
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
@@ -80,6 +125,8 @@ class AgentBalance(SQLModel, table=True):
frozen_balance: float = Field(default=0.0) frozen_balance: float = Field(default=0.0)
updated_dttm: datetime = Field(default_factory=datetime.utcnow) updated_dttm: datetime = Field(default_factory=datetime.utcnow)
tg_agent: "TgAgent" = Relationship(back_populates="agent_balance")
class Account(SQLModel, table=True): class Account(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
login: str = Field(index=True, unique=True) login: str = Field(index=True, unique=True)
@@ -90,4 +137,19 @@ class Account(SQLModel, table=True):
email: Optional[str] = None email: Optional[str] = None
company_id: int = Field(foreign_key="company.id") company_id: int = Field(foreign_key="company.id")
create_dttm: datetime = Field(default_factory=datetime.utcnow) create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_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")

View File

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