Добавлены новые модели для агентских и партнерских транзакций, а также балансов компаний и агентов. Обновлено заполнение базы данных с учетом новых таблиц и логики транзакций. Изменены функции для работы с транзакциями, включая фильтрацию по статусам и датам. Улучшены комментарии для ясности кода.

This commit is contained in:
Redsandyg 2025-06-06 14:17:47 +03:00
parent 161e0b3ec4
commit 8c6fadb180
2 changed files with 252 additions and 70 deletions

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 main import AUTH_DB_ENGINE, TgAgent, Ref, Sale, Transaction, Account, Company from main import AUTH_DB_ENGINE, TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance
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
@ -80,7 +80,10 @@ def fill_db():
session.execute(text("DELETE FROM ref")) session.execute(text("DELETE FROM ref"))
session.execute(text("DELETE FROM tgagent")) session.execute(text("DELETE FROM tgagent"))
session.execute(text("DELETE FROM account")) session.execute(text("DELETE FROM account"))
session.execute(text('DELETE FROM "transaction"')) session.execute(text('DELETE FROM "agent_transactions"'))
session.execute(text('DELETE FROM "partner_transactions"'))
session.execute(text('DELETE FROM "company_balances"'))
session.execute(text('DELETE FROM "agent_balances"'))
session.execute(text("DELETE FROM company")) session.execute(text("DELETE FROM company"))
session.commit() session.commit()
# 0. Company # 0. Company
@ -172,29 +175,102 @@ def fill_db():
) )
session.add(sale) session.add(sale)
session.commit() session.commit()
# 5. Transactions (только withdrawal на агента) # 5. Заполнение новых таблиц
TRANSACTION_STATUSES = ['process', 'done', 'error', 'waiting'] # 5.1 CompanyBalance
company_balance = CompanyBalance(
company_id=company.id,
available_balance=round(random.uniform(10000, 50000), 2),
pending_balance=round(random.uniform(1000, 10000), 2),
updated_dttm=datetime.utcnow()
)
session.add(company_balance)
session.commit()
session.refresh(company_balance)
# 5.2 AgentBalances
agent_balances = []
for tg_agent in tg_agents: for tg_agent in tg_agents:
withdrawal_count = random.randint(5, int(5 * 1.25)) # от 5 до 6 dt = random.choice(date_list)
used_statuses = set() agent_balance = AgentBalance(
for i in range(withdrawal_count): tg_agent_id=tg_agent.id,
available_balance=round(random.uniform(100, 5000), 2),
frozen_balance=round(random.uniform(0, 1000), 2),
updated_dttm=dt
)
session.add(agent_balance)
agent_balances.append(agent_balance)
session.commit()
for balance in agent_balances:
session.refresh(balance)
# 5.3 AgentTransactions and PartnerTransactions
AGENT_TRANSACTION_STATUSES = ['waiting', 'process', 'done', 'reject', 'error']
PARTNER_TRANSACTION_TYPES = ['deposit', 'agent_payout', 'service_fee']
PARTNER_TRANSACTION_STATUSES = ['process', 'done', 'error']
for tg_agent in tg_agents:
# Генерируем несколько групп транзакций для каждого агента
for _ in range(random.randint(3, 6)): # От 3 до 6 групп на агента
transaction_group_id = uuid4()
dt = random.choice(date_list) dt = random.choice(date_list)
# Гарантируем, что каждый статус будет использован хотя бы раз agent_trans_amount = round(random.uniform(500, 3000), 2)
if len(used_statuses) < len(TRANSACTION_STATUSES): agent_trans_status = random.choice(AGENT_TRANSACTION_STATUSES)
status = TRANSACTION_STATUSES[len(used_statuses)]
used_statuses.add(status) # Создаем AgentTransaction
else: agent_transaction = AgentTransaction(
status = random.choice(TRANSACTION_STATUSES)
transaction = Transaction(
transaction_id=str(uuid4()),
sum=round(random.uniform(200, 3000), 2),
tg_agent_id=tg_agent.id, tg_agent_id=tg_agent.id,
status=status, amount=agent_trans_amount,
company_id=company.id, status=agent_trans_status,
transaction_group=transaction_group_id,
create_dttm=dt, create_dttm=dt,
update_dttm=dt update_dttm=dt
) )
session.add(transaction) session.add(agent_transaction)
session.commit()
session.refresh(agent_transaction)
# Создаем соответствующие PartnerTransactions
# Для каждой AgentTransaction создаем PartnerTransaction типа 'agent_payout'
if agent_trans_status != 'waiting': # Создаем партнерскую транзакцию только если агентская не в статусе 'waiting'
# Добавляем PartnerTransaction для выплаты агенту
partner_payout = PartnerTransaction(
company_id=company.id,
type='agent_payout',
amount=agent_trans_amount,
status=random.choice([s for s in PARTNER_TRANSACTION_STATUSES if s != 'process']) if agent_trans_status in ['done', 'error', 'reject'] else 'process', # Статус зависит от статуса агентской
transaction_group=transaction_group_id,
agent_transaction_id=agent_transaction.id,
create_dttm=dt,
update_dttm=dt
)
session.add(partner_payout)
# Добавляем другие типы PartnerTransactions для разнообразия
if random.random() < 0.5: # 50% шанс добавить депозит
partner_deposit = PartnerTransaction(
company_id=company.id,
type='deposit',
amount=round(random.uniform(1000, 10000), 2),
status=random.choice(PARTNER_TRANSACTION_STATUSES),
transaction_group=uuid4(), # Новая группа для независимых транзакций
create_dttm=dt,
update_dttm=dt
)
session.add(partner_deposit)
if random.random() < 0.3: # 30% шанс добавить комиссию
partner_fee = PartnerTransaction(
company_id=company.id,
type='service_fee',
amount=round(random.uniform(50, 500), 2),
status=random.choice(PARTNER_TRANSACTION_STATUSES),
transaction_group=uuid4(), # Новая группа
create_dttm=dt,
update_dttm=dt
)
session.add(partner_fee)
session.commit() session.commit()
print("База успешно заполнена!") print("База успешно заполнена!")

208
main.py
View File

@ -1,3 +1,4 @@
import uuid
from fastapi import FastAPI, Depends, HTTPException, status, Query, Body, Request from fastapi import FastAPI, Depends, HTTPException, status, Query, Body, Request
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlmodel import SQLModel, Field, create_engine, Session, select from sqlmodel import SQLModel, Field, create_engine, Session, select
@ -12,6 +13,7 @@ from hashlib import sha256
import jwt import jwt
from jwt.exceptions import InvalidTokenError from jwt.exceptions import InvalidTokenError
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from enum import Enum
# Конфигурация # Конфигурация
AUTH_DATABASE_ADDRESS = "sqlite:///partner.db" AUTH_DATABASE_ADDRESS = "sqlite:///partner.db"
@ -55,20 +57,48 @@ class Sale(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)
class Transaction(SQLModel, table=True): class AgentTransaction(SQLModel, table=True):
__tablename__ = "agent_transactions" # Указываем имя таблицы явно
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
transaction_id: str = Field(default_factory=lambda: str(uuid4()), index=True, unique=True) tg_agent_id: int = Field(foreign_key="tgagent.id") # ID агента, связь с TgAgent
sum: float amount: float # Используем float для DECIMAL(15,2)
tg_agent_id: int = Field(foreign_key="tgagent.id") status: str # 'waiting', 'process', 'done', 'reject', 'error'
status: str # 'process' || 'done' || 'error' || 'waiting' transaction_group: uuid.UUID = Field(default_factory=uuid.uuid4) # UUID для группировки
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)
class PartnerTransaction(SQLModel, table=True):
__tablename__ = "partner_transactions" # Указываем имя таблицы явно
id: Optional[int] = Field(default=None, primary_key=True)
company_id: int = Field(foreign_key="company.id") # ID партнера, связь с Company
type: str # 'deposit', 'agent_payout', 'service_fee'
amount: float # Используем float для DECIMAL(15,2)
status: str # 'process', 'done', 'error'
transaction_group: uuid.UUID # UUID для группировки, может быть связан с agent_transactions
agent_transaction_id: Optional[int] = Field(default=None, foreign_key="agent_transactions.id") # Связь с агентской транзакцией
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
class CompanyBalance(SQLModel, table=True):
__tablename__ = "company_balances" # Указываем имя таблицы явно
id: Optional[int] = Field(default=None, primary_key=True)
company_id: int = Field(foreign_key="company.id", unique=True) # ID компании, уникальный баланс на компанию
available_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
pending_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
updated_dttm: datetime = Field(default_factory=datetime.utcnow)
class AgentBalance(SQLModel, table=True):
__tablename__ = "agent_balances" # Указываем имя таблицы явно
id: Optional[int] = Field(default=None, primary_key=True)
tg_agent_id: int = Field(foreign_key="tgagent.id", unique=True) # ID агента, уникальный баланс на агента
available_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
frozen_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
updated_dttm: datetime = Field(default_factory=datetime.utcnow)
class Account(SQLModel, table=True): 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)
password_hash: str # теперь хранится hash пароля password_hash: str
firstName: Optional[str] = None firstName: Optional[str] = None
surname: Optional[str] = None surname: Optional[str] = None
phone: Optional[str] = None phone: Optional[str] = None
@ -87,6 +117,25 @@ class AccountPasswordChangeRequest(BaseModel):
currentPassword: str currentPassword: str
newPassword: str newPassword: str
class TransactionStatus(str, Enum): # Определяем Enum для статусов
WAITING = 'waiting'
PROCESS = 'process'
DONE = 'done'
REJECT = 'reject'
ERROR = 'error'
# Новая модель ответа для агентских транзакций с именем агента
class AgentTransactionResponse(BaseModel):
amount: float
status: TransactionStatus # Используем Enum
transaction_group: uuid.UUID
create_dttm: datetime
update_dttm: datetime
agent_name: Optional[str] = None # Поле для имени агента
# Создание движка базы данных # Создание движка базы данных
AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True) AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True)
SQLModel.metadata.create_all(AUTH_DB_ENGINE) SQLModel.metadata.create_all(AUTH_DB_ENGINE)
@ -188,12 +237,13 @@ def create_access_token(data: dict, expires_delta: timedelta = None):
@app.post("/token", response_model=Token, tags=["bff"]) @app.post("/token", response_model=Token, tags=["bff"])
def login_account_for_access_token( def login_account_for_access_token(
login: str = Body(...), # login: str = Body(...),
password: str = Body(...), # password: str = Body(...),
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
account = get_account_by_login(db, login) account = get_account_by_login(db, form_data.username)
if not account or not verify_password(password, account.password_hash): if not account or not verify_password(form_data.password, account.password_hash):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect login or password", detail="Incorrect login or password",
@ -251,12 +301,9 @@ def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Sess
total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all() total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
totalSales = len(total_sales) totalSales = len(total_sales)
totalIncome = sum(sale.crediting for sale in total_sales) totalIncome = sum(sale.crediting for sale in total_sales)
withdrawals = db.exec( # Заменено получение доступного остатка из AgentBalance
select(Transaction).where( agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == current_tg_agent.id)).first()
Transaction.tg_agent_id == current_tg_agent.id availableWithdrawal = agent_balance.available_balance if agent_balance else 0.0
)
).all()
availableWithdrawal = totalIncome - sum(t.sum for t in withdrawals)
return { return {
"totalSales": totalSales, "totalSales": totalSales,
"totalIncome": totalIncome, "totalIncome": totalIncome,
@ -276,10 +323,9 @@ def get_dashboard_cards(db: Session = Depends(get_db)):
unique_agents = db.exec(select(TgAgent.tg_id)).all() unique_agents = db.exec(select(TgAgent.tg_id)).all()
activeReferrals = len(set(unique_agents)) activeReferrals = len(set(unique_agents))
# 4. Ожидающие выплаты - разница между суммой всех Sale.crediting и суммой всех Transaction.sum # 4. Ожидающие выплаты - сумма AgentTransaction со статусом 'waiting'
all_transactions = db.exec(select(Transaction)).all() pending_agent_transactions = db.exec(select(AgentTransaction).where(AgentTransaction.status == 'waiting')).all()
totalTransactions = sum(t.sum for t in all_transactions) pendingPayouts = sum(t.amount for t in pending_agent_transactions)
pendingPayouts = totalPayouts - totalTransactions
# 5. Количество продаж # 5. Количество продаж
totalSales = len(total_revenue) totalSales = len(total_revenue)
@ -436,15 +482,18 @@ def get_sales_stat(
@app.get("/billing/cards", tags=["bff"]) @app.get("/billing/cards", tags=["bff"])
def get_billing_cards(db: Session = Depends(get_db)): def get_billing_cards(db: Session = Depends(get_db)):
# 1. cost - сумма всех Sale.cost # 1. cost - Общий заработок (сумма всех Sale.cost)
sales = db.exec(select(Sale)).all() sales = db.exec(select(Sale)).all()
cost = sum(sale.cost for sale in sales) cost = sum(sale.cost for sale in sales)
# 2. crediting - сумма всех Sale.crediting
crediting = sum(sale.crediting for sale in sales) # 2. crediting - Общие выплаты (сумма PartnerTransaction типа 'agent_payout' со статусом 'done')
# 3. pendingPayouts - разница между crediting и суммой всех Transaction.sum completed_payouts = db.exec(select(PartnerTransaction).where(PartnerTransaction.type == 'agent_payout').where(PartnerTransaction.status == 'done')).all()
transactions = db.exec(select(Transaction)).all() crediting = sum(t.amount for t in completed_payouts)
total_transactions = sum(t.sum for t in transactions)
pendingPayouts = crediting - total_transactions # 3. pendingPayouts - Доступно к выводу всеми партнерами (сумма всех доступных балансов агентов)
agent_balances = db.exec(select(AgentBalance)).all()
pendingPayouts = sum(balance.available_balance for balance in agent_balances)
return { return {
"cost": cost, "cost": cost,
"crediting": crediting, "crediting": crediting,
@ -457,39 +506,44 @@ def get_billing_payouts_transactions(
date_start: str = Query(None), date_start: str = Query(None),
date_end: str = Query(None), date_end: str = Query(None),
): ):
query = select(Transaction) # Используем AgentTransaction вместо Transaction
# Явно выбираем обе модели для корректной распаковки
query = select(AgentTransaction, TgAgent).join(TgAgent)
if date_start: if date_start:
query = query.where(Transaction.create_dttm >= date_start) query = query.where(AgentTransaction.create_dttm >= date_start)
if date_end: if date_end:
query = query.where(Transaction.create_dttm <= date_end) query = query.where(AgentTransaction.create_dttm <= date_end)
transactions = db.exec(query).all() # Заказываем по дате создания
query = query.order_by(AgentTransaction.create_dttm.desc())
# Выполняем запрос и формируем результат
results = db.exec(query).all()
result = [] result = []
for t in transactions: for agent_trans, agent in results:
agent = db.exec(select(TgAgent).where(TgAgent.id == t.tg_agent_id)).first()
result.append({ result.append({
"id": t.transaction_id, "id": agent_trans.transaction_group, # Используем transaction_group как ID транзакции группы
"sum": t.sum, "amount": agent_trans.amount,
"agent": agent.name if agent else None, "agent": agent.name if agent else None, # Имя агента из join
"status": t.status, "status": agent_trans.status,
"create_dttm": t.create_dttm, "create_dttm": agent_trans.create_dttm,
"update_dttm": t.update_dttm, "update_dttm": agent_trans.update_dttm,
}) })
return result return result
@app.get("/billing/chart/stat", tags=["bff"]) @app.get("/billing/chart/stat", tags=["bff"])
def get_billing_chart_stat(db: Session = Depends(get_db)): def get_billing_chart_stat(db: Session = Depends(get_db)):
# Группируем транзакции по дате (день) и статусу # Группируем агентские транзакции по дате (день) и статусу
result = db.exec( result = db.exec(
select( select(
func.strftime('%Y-%m-%d', Transaction.create_dttm).label('date'), func.strftime('%Y-%m-%d', AgentTransaction.create_dttm).label('date'),
Transaction.status.label('status'), AgentTransaction.status.label('status'),
func.count(Transaction.id).label('count') func.count(AgentTransaction.id).label('count')
).group_by( ).group_by(
func.strftime('%Y-%m-%d', Transaction.create_dttm), func.strftime('%Y-%m-%d', AgentTransaction.create_dttm),
Transaction.status AgentTransaction.status
).order_by( ).order_by(
func.strftime('%Y-%m-%d', Transaction.create_dttm), func.strftime('%Y-%m-%d', AgentTransaction.create_dttm),
Transaction.status AgentTransaction.status
) )
).all() ).all()
data = [ data = [
@ -500,11 +554,12 @@ def get_billing_chart_stat(db: Session = Depends(get_db)):
@app.get("/billing/chart/pie", tags=["bff"]) @app.get("/billing/chart/pie", tags=["bff"])
def get_billing_chart_pie(db: Session = Depends(get_db)): def get_billing_chart_pie(db: Session = Depends(get_db)):
# Группируем агентские транзакции по статусу
result = db.exec( result = db.exec(
select( select(
Transaction.status.label('status'), AgentTransaction.status.label('status'),
func.count(Transaction.id).label('count') func.count(AgentTransaction.id).label('count')
).group_by(Transaction.status) ).group_by(AgentTransaction.status)
).all() ).all()
data = [ data = [
{"status": row.status, "count": row.count} {"status": row.status, "count": row.count}
@ -613,3 +668,54 @@ def change_account_password(
db.commit() db.commit()
db.refresh(current_account) db.refresh(current_account)
return {"msg": "Пароль успешно изменён"} return {"msg": "Пароль успешно изменён"}
# --- Новый функционал для агентских транзакций партнера ---
@app.get("/account/agent-transaction", response_model=List[AgentTransactionResponse], tags=["bff"])
def get_account_agent_transactions(
statuses: Optional[List[TransactionStatus]] = Query(None), # Изменено на List[TransactionStatus]
date_start: str = Query(None), # Добавлен параметр date_start
date_end: str = Query(None), # Добавлен параметр date_end
current_account: Account = Depends(get_current_account),
db: Session = Depends(get_db)
):
"""
Возвращает список агентских транзакций для компании текущего пользователя,
с возможностью фильтрации по статусу и дате создания.
"""
# Получаем ID компании текущего аккаунта
company_id = current_account.company_id
# Строим базовый запрос: выбрать AgentTransaction и TgAgent, связанные с агентами этой компании
query = select(AgentTransaction, TgAgent).join(TgAgent).where(TgAgent.company_id == company_id)
# Если переданы статусы, добавляем фильтрацию по статусам
if statuses:
query = query.where(AgentTransaction.status.in_(statuses))
# Если передана дата начала, добавляем фильтрацию по дате создания >= date_start
if date_start:
query = query.where(AgentTransaction.create_dttm >= date_start)
# Если передана дата окончания, добавляем фильтрацию по дате создания <= date_end
if date_end:
query = query.where(AgentTransaction.create_dttm <= date_end)
# Выполняем запрос
results = db.exec(query).all()
# Формируем список ответов в формате AgentTransactionResponse
agent_transactions_response = []
for agent_trans, agent in results:
agent_transactions_response.append(
AgentTransactionResponse(
amount=agent_trans.amount,
status=agent_trans.status,
transaction_group=agent_trans.transaction_group,
create_dttm=agent_trans.create_dttm,
update_dttm=agent_trans.update_dttm,
agent_name=agent.name # Используем имя агента
)
)
return agent_transactions_response