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

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

208
main.py
View File

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