Compare commits

...

5 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
9 changed files with 298 additions and 69 deletions

View File

@@ -145,7 +145,6 @@ class CompanyProfileResponse(BaseModel):
name: str name: str
key: str key: str
commission: float commission: float
agent_commission: float
class AccountProfileResponse(BaseModel): class AccountProfileResponse(BaseModel):
firstName: Optional[str] = None firstName: Optional[str] = None
@@ -189,3 +188,20 @@ class IntegrationTokenCreateRequest(BaseModel):
class IntegrationTokenUpdateRequest(BaseModel): class IntegrationTokenUpdateRequest(BaseModel):
id: int id: int
description: str 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)

View File

@@ -4,15 +4,17 @@ import json
# Конфигурация API # Конфигурация API
BASE_URL = "http://127.0.0.1:8001" BASE_URL = "http://127.0.0.1:8001"
API_KEY = "672a1437-70e8-461f-9bff-20f5ce4a023d" API_KEY = "de058226-37d3-4d0e-a483-3c2a7fac3573"
REF = "9bd1a6bd-98e1-48f4-a120-3b3d016011c0" REF = "0d9aaa96-80e6-424c-84c9-ff70a6eb915e"
# Данные для запроса на создание продажи # Данные для запроса на создание продажи
# Замените эти значения на актуальные для вашей продажи # Замените эти значения на актуальные для вашей продажи
sale_data = { sale_data = {
"cost": 100.50, # Стоимость продажи "cost": 100, # Стоимость продажи
"ref": REF, # Ваш реферальный код "ref": REF, # Ваш реферальный код
"sale_id": str(uuid.uuid4()) # Уникальный идентификатор продажи для вашей компании "sale_id": str(uuid.uuid4()), # Уникальный идентификатор продажи для вашей компании
"category": "vip", # название категории (например, 'basic', 'premium', 'vip')
"group_sale_id": str(uuid.uuid4()) # уникальный идентификатор группы продаж
} }
# Эндпоинты # Эндпоинты

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,7 +1,7 @@
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, IntegrationToken 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
@@ -81,13 +81,13 @@ 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,
agent_commission=15.0,
key="re-premium-key", key="re-premium-key",
) )
session.add(company) session.add(company)
@@ -110,6 +110,18 @@ def fill_db():
session.add(integration_token) session.add(integration_token)
session.commit() 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):
@@ -182,25 +194,30 @@ 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 * (company.agent_commission / 100.0), 2) sale_category = random.choice(all_categories)
crediting = round(cost * (sale_category.perc / 100.0), 2)
# Генерируем случайную дату и время в пределах последних 7 дней # Генерируем случайную дату и время в пределах последних 7 дней
end_dttm = datetime.utcnow() end_dttm = datetime.utcnow()
start_dttm = end_dttm - timedelta(days=7) start_dttm = end_dttm - timedelta(days=7)
time_diff = end_dttm - start_dttm time_diff = end_dttm - start_dttm
random_seconds = random.uniform(0, time_diff.total_seconds()) random_seconds = random.uniform(0, time_diff.total_seconds())
sale_dttm = start_dttm + timedelta(seconds=random_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,
category=sale_category.id,
sale_dttm=sale_dttm, sale_dttm=sale_dttm,
create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне
update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне

View File

@@ -7,8 +7,8 @@ import uuid
from random import choices from random import choices
import string import string
from sql_models import Company, IntegrationToken, Ref, Sale, AgentTransaction, PartnerTransaction, AgentBalance, TgAgent, CompanyBalance from sql_models import Company, IntegrationToken, Ref, Sale, AgentTransaction, PartnerTransaction, AgentBalance, TgAgent, CompanyBalance, SaleCategory
from integration_models import Token, SaleCreateRequest, SaleCreateResponse, TransactionStatus, WithdrawRequest, WithdrawResponse from integration_models import Token, SaleCreateRequest, SaleCreateResponse, TransactionStatus, WithdrawRequest, WithdrawResponse, PromoValidationRequest, PromoValidationResponse
from bff_models import RegisterResponse, TgAuthResponse from bff_models import RegisterResponse, TgAuthResponse
from tg_models import RefAddRequest, RefResponse, RefAddResponse, RefStatResponse, RegisterRequest, StatResponse 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 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
@@ -189,7 +189,7 @@ async def withdraw_funds(
@app.post("/sale", tags=["integration"], response_model=SaleCreateResponse) @app.post("/sale", tags=["integration"], response_model=SaleCreateResponse)
async def create_sale( async def create_sale(
req: SaleCreateRequest, req: SaleCreateRequest,
company: Company = Depends(get_current_company_from_jwt), # Используем новую зависимость company: Company = Depends(get_current_company_from_jwt),
db: Session = Depends(get_integration_db) db: Session = Depends(get_integration_db)
): ):
""" """
@@ -198,24 +198,35 @@ async def create_sale(
# Устанавливаем уровень изоляции для текущей транзакции # Устанавливаем уровень изоляции для текущей транзакции
db.connection(execution_options={'isolation_level': 'SERIALIZABLE'}) db.connection(execution_options={'isolation_level': 'SERIALIZABLE'})
# 1. Найти Ref по `ref` и `company.id` # Проверка входных данных
# Сначала находим TgAgent, связанный с компанией, затем Ref 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( tg_agent = db.exec(select(TgAgent).where(TgAgent.id == referral.tg_agent_id, TgAgent.company_id == company.id)).first()
select(TgAgent)
.join(Ref)
.where(TgAgent.company_id == company.id)
.where(Ref.ref == req.ref)
).first()
if not tg_agent: if not tg_agent:
raise HTTPException(status_code=404, detail="Реферальная ссылка не найдена или не принадлежит данной компании") raise HTTPException(status_code=404, detail="Реферальная ссылка не принадлежит данной компании")
referral = db.exec(select(Ref).where(Ref.ref == req.ref).where(Ref.tg_agent_id == tg_agent.id)).first()
if not referral:
raise HTTPException(status_code=404, detail="Реферальная ссылка не найдена")
# 2. Проверить, что sale_id уникален для данной компании # 2. Проверить, что sale_id уникален для данной компании
existing_sale = db.exec( existing_sale = db.exec(
@@ -223,20 +234,19 @@ async def create_sale(
.where(Sale.company_id == company.id) .where(Sale.company_id == company.id)
.where(Sale.sale_id == req.sale_id) .where(Sale.sale_id == req.sale_id)
).first() ).first()
if existing_sale: if existing_sale:
raise HTTPException(status_code=400, detail="Продажа с таким sale_id уже существует для данной компании") raise HTTPException(status_code=400, detail="Продажа с таким sale_id уже существует для данной компании")
# 3. Рассчитать crediting # 3. Найти категорию и рассчитать crediting
crediting_amount = req.cost * (company.agent_commission / 100.0) 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 # 4. Проверить и обновить AgentBalance и CompanyBalance
# AgentBalance
agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == tg_agent.id)).first() agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == tg_agent.id)).first()
if not agent_balance: if not agent_balance:
raise HTTPException(status_code=404, detail="Баланс агента не найден") raise HTTPException(status_code=404, detail="Баланс агента не найден")
# CompanyBalance
company_balance = db.exec(select(CompanyBalance).where(CompanyBalance.company_id == company.id)).first() company_balance = db.exec(select(CompanyBalance).where(CompanyBalance.company_id == company.id)).first()
if not company_balance: if not company_balance:
raise HTTPException(status_code=404, detail="Баланс компании не найден") raise HTTPException(status_code=404, detail="Баланс компании не найден")
@@ -247,13 +257,14 @@ async def create_sale(
crediting=crediting_amount, crediting=crediting_amount,
ref=referral.id, ref=referral.id,
sale_id=req.sale_id, sale_id=req.sale_id,
group_sale_id=req.group_sale_id,
company_id=company.id, company_id=company.id,
category=sale_category.id,
sale_dttm=datetime.utcnow() sale_dttm=datetime.utcnow()
) )
db.add(new_sale) db.add(new_sale)
# Создать AgentTransaction # Создать AgentTransaction
agent_transaction_status = TransactionStatus.DONE # auto_approve_transactions отвечает только за апрув агентских транзакций на вывод agent_transaction_status = TransactionStatus.DONE
agent_transaction = AgentTransaction( agent_transaction = AgentTransaction(
tg_agent_id=tg_agent.id, tg_agent_id=tg_agent.id,
amount=crediting_amount, amount=crediting_amount,
@@ -261,7 +272,6 @@ async def create_sale(
transaction_group=uuid.uuid4() transaction_group=uuid.uuid4()
) )
db.add(agent_transaction) db.add(agent_transaction)
# Обновление балансов для продаж - всегда в замороженный/ожидающий баланс # Обновление балансов для продаж - всегда в замороженный/ожидающий баланс
agent_balance.frozen_balance += crediting_amount agent_balance.frozen_balance += crediting_amount
company_balance.pending_balance -= crediting_amount company_balance.pending_balance -= crediting_amount
@@ -279,6 +289,25 @@ async def create_sale(
"crediting": new_sale.crediting "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) @app.post("/register", tags=["agent-tg"], response_model=RegisterResponse)
def register(req: RegisterRequest, db: Session = Depends(get_integration_db)): def register(req: RegisterRequest, db: Session = Depends(get_integration_db)):
""" """

View File

@@ -16,9 +16,12 @@ class IntegrationTokenResponse(BaseModel):
# Models for /sale endpoint # Models for /sale endpoint
class SaleCreateRequest(BaseModel): class SaleCreateRequest(BaseModel):
ref: str ref: Optional[str] = None
promocode: Optional[str] = None
sale_id: str sale_id: str
cost: float cost: float
category: str # название категории продажи
group_sale_id: str # новое поле для группировки продаж
class SaleCreateResponse(BaseModel): class SaleCreateResponse(BaseModel):
msg: str msg: str
@@ -40,3 +43,9 @@ class TransactionStatus(str, Enum):
DONE = "done" DONE = "done"
CANCELED = "canceled" CANCELED = "canceled"
ERROR = "error" ERROR = "error"
class PromoValidationRequest(BaseModel):
promocode: str
class PromoValidationResponse(BaseModel):
validation: bool

112
main.py
View File

@@ -38,7 +38,9 @@ from bff_models import (
TransactionStatus, TransactionStatus,
IntegrationTokenResponse, IntegrationTokenResponse,
IntegrationTokenCreateRequest, IntegrationTokenCreateRequest,
IntegrationTokenUpdateRequest IntegrationTokenUpdateRequest,
SaleCategoryRequest,
SaleCategoryResponse
) )
from sql_models import ( from sql_models import (
Company, Company,
@@ -49,7 +51,8 @@ from sql_models import (
PartnerTransaction, PartnerTransaction,
AgentBalance, AgentBalance,
Account, Account,
IntegrationToken IntegrationToken,
SaleCategory
) )
from sqlalchemy import func from sqlalchemy import func
import hashlib import hashlib
@@ -64,6 +67,7 @@ from helpers_bff import (
pwd_context, pwd_context,
) )
import os import os
from pydantic import BaseModel
# Создание движка базы данных # Создание движка базы данных
@@ -75,7 +79,7 @@ app = FastAPI()
@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)
@@ -99,7 +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=["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)): def get_dashboard_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
""" """
Возвращает данные для карточек на главной панели (dashboard) пользователя, включая общий доход, выплаты, активных рефералов, ожидающие выплаты и общее количество продаж. Возвращает данные для карточек на главной панели (dashboard) пользователя, включая общий доход, выплаты, активных рефералов, ожидающие выплаты и общее количество продаж.
@@ -130,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).
@@ -151,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).
@@ -181,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),
@@ -225,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),
@@ -258,7 +262,7 @@ def get_referrals_stat(
}) })
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),
@@ -294,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)):
""" """
Возвращает ключевые показатели биллинга для компании, включая общий заработок, общие выплаты и доступные к выводу средства. Возвращает ключевые показатели биллинга для компании, включая общий заработок, общие выплаты и доступные к выводу средства.
@@ -317,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),
@@ -357,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)):
""" """
Возвращает статистику выплат по датам и статусам для компании текущего пользователя. Возвращает статистику выплат по датам и статусам для компании текущего пользователя.
@@ -382,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)):
""" """
Возвращает круговую диаграмму статистики выплат по статусам для компании текущего пользователя. Возвращает круговую диаграмму статистики выплат по статусам для компании текущего пользователя.
@@ -402,7 +406,7 @@ def get_billing_chart_pie(current_account: Account = Depends(get_current_account
@app.get("/account", tags=["bff", "account"], response_model=AccountResponse) @app.get("/account", tags=[ "account"], response_model=AccountResponse)
def get_account(current_account: Account = Depends(get_current_account)): def get_account(current_account: Account = Depends(get_current_account)):
""" """
Возвращает базовую информацию об аккаунте текущего пользователя (имя и фамилию). Возвращает базовую информацию об аккаунте текущего пользователя (имя и фамилию).
@@ -412,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)):
""" """
Возвращает полную информацию о профиле аккаунта текущего пользователя, включая данные компании. Возвращает полную информацию о профиле аккаунта текущего пользователя, включая данные компании.
@@ -430,11 +434,10 @@ def get_account_profile(current_account: Account = Depends(get_current_account),
"name": company.name, "name": company.name,
"key": company.key, "key": company.key,
"commission": company.commission, "commission": company.commission,
"agent_commission": company.agent_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),
@@ -456,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),
@@ -482,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),
@@ -539,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)
@@ -552,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),
@@ -602,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),
@@ -639,7 +642,7 @@ def approve_agent_transactions(
# --- Новый функционал для интеграционных токенов --- # --- Новый функционал для интеграционных токенов ---
@app.get("/account/integration-tokens", tags=["bff", "account"], response_model=List[IntegrationTokenResponse]) @app.get("/account/integration-tokens", tags=[ "account"], response_model=List[IntegrationTokenResponse])
def get_integration_tokens( def get_integration_tokens(
current_account: Account = Depends(get_current_account), current_account: Account = Depends(get_current_account),
db: Session = Depends(get_db) db: Session = Depends(get_db)
@@ -651,7 +654,7 @@ def get_integration_tokens(
return tokens # Позволяем FastAPI самостоятельно сериализовать объекты IntegrationToken в IntegrationTokenResponse return tokens # Позволяем FastAPI самостоятельно сериализовать объекты IntegrationToken в IntegrationTokenResponse
@app.post("/account/integration-tokens", tags=["bff", "account"], response_model=IntegrationTokenResponse) @app.post("/account/integration-tokens", tags=[ "account"], response_model=IntegrationTokenResponse)
def create_integration_token( def create_integration_token(
req: IntegrationTokenCreateRequest, req: IntegrationTokenCreateRequest,
current_account: Account = Depends(get_current_account), current_account: Account = Depends(get_current_account),
@@ -684,7 +687,7 @@ def create_integration_token(
return response_token return response_token
@app.post("/account/integration-tokens/update-description", tags=["bff", "account"], response_model=Dict[str, str]) @app.post("/account/integration-tokens/update-description", tags=[ "account"], response_model=Dict[str, str])
def update_integration_token_description( def update_integration_token_description(
req: IntegrationTokenUpdateRequest, req: IntegrationTokenUpdateRequest,
current_account: Account = Depends(get_current_account), current_account: Account = Depends(get_current_account),
@@ -707,7 +710,7 @@ def update_integration_token_description(
db.refresh(token) db.refresh(token)
return {"msg": "Описание токена обновлено успешно"} return {"msg": "Описание токена обновлено успешно"}
@app.delete("/account/integration-tokens/{token_id}", tags=["bff", "account"], response_model=Dict[str, str]) @app.delete("/account/integration-tokens/{token_id}", tags=[ "account"], response_model=Dict[str, str])
def delete_integration_token( def delete_integration_token(
token_id: int, token_id: int,
current_account: Account = Depends(get_current_account), current_account: Account = Depends(get_current_account),
@@ -726,3 +729,60 @@ def delete_integration_token(
db.delete(token) db.delete(token)
db.commit() db.commit()
return {"msg": "Токен удален успешно"} 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

@@ -62,6 +62,21 @@ CREATE TABLE integrationtoken (
; ;
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 ( CREATE TABLE tgagent (
id INTEGER NOT NULL, id INTEGER NOT NULL,
tg_id INTEGER NOT NULL, tg_id INTEGER NOT NULL,
@@ -149,13 +164,16 @@ CREATE TABLE sale (
crediting FLOAT NOT NULL, crediting FLOAT NOT NULL,
ref INTEGER NOT NULL, ref INTEGER NOT NULL,
sale_id VARCHAR NOT NULL, sale_id VARCHAR NOT NULL,
group_sale_id VARCHAR NOT NULL,
company_id INTEGER NOT NULL, company_id INTEGER NOT NULL,
category INTEGER NOT NULL,
sale_dttm DATETIME NOT NULL, sale_dttm DATETIME NOT NULL,
create_dttm DATETIME NOT NULL, create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL, update_dttm DATETIME NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY(ref) REFERENCES ref (id), FOREIGN KEY(ref) REFERENCES ref (id),
FOREIGN KEY(company_id) REFERENCES company (id) FOREIGN KEY(company_id) REFERENCES company (id),
FOREIGN KEY(category) REFERENCES salecategory (id)
) )
; ;

View File

@@ -8,7 +8,6 @@ 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 # процент комиссии, который взымается за пользование сервисом
agent_commission: float = Field(default=0.0) # процент от продаж, который начисляется агенту
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)
@@ -20,6 +19,7 @@ class Company(SQLModel, table=True):
partner_transactions: List["PartnerTransaction"] = Relationship(back_populates="company") partner_transactions: List["PartnerTransaction"] = Relationship(back_populates="company")
company_balance: Optional["CompanyBalance"] = Relationship(back_populates="company") company_balance: Optional["CompanyBalance"] = Relationship(back_populates="company")
accounts: List["Account"] = 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)
@@ -50,19 +50,34 @@ class Ref(SQLModel, table=True):
tg_agent: "TgAgent" = Relationship(back_populates="refs") tg_agent: "TgAgent" = Relationship(back_populates="refs")
sales: List["Sale"] = Relationship(back_populates="ref_obj") 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) 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") ref_obj: "Ref" = Relationship(back_populates="sales")
company: "Company" = 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"