841 lines
39 KiB
Python
841 lines
39 KiB
Python
import uuid
|
||
from fastapi import FastAPI, Depends, HTTPException, status, Query, Body, Request
|
||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||
from sqlmodel import SQLModel, Field, create_engine, Session, select
|
||
from passlib.context import CryptContext
|
||
from typing import Optional, List, Dict
|
||
from datetime import datetime, timedelta
|
||
from bff_models import Token, AccountProfileUpdateRequest, AccountPasswordChangeRequest, AgentTransactionResponse, AutoApproveSettingsRequest, ApproveTransactionsRequest, TransactionStatus, RegisterResponse, DashboardCardsResponse, DashboardChartTotalResponse, DashboardChartAgentResponse, StatAgentsResponse, StatReferralsResponse, StatSalesResponse, BillingCardsResponse, BillingChartStatResponse, BillingChartPieResponse, AccountResponse, CompanyProfileResponse, AccountProfileResponse, AccountProfileUpdateResponse, AccountPasswordChangeResponse, AutoApproveSettingsGetResponse, AutoApproveSettingsUpdateResponse, ApproveTransactionsResult, TgAuthResponse, BillingPayoutsTransactionsResponse
|
||
from tg_models import RefAddRequest, RefResponse, RegisterRequest, TokenRequest, RefAddResponse, RefStatResponse, StatResponse
|
||
from uuid import uuid4
|
||
from fastapi.responses import JSONResponse
|
||
from sqlalchemy import func
|
||
from hashlib import sha256
|
||
import jwt
|
||
from jwt.exceptions import InvalidTokenError
|
||
from pydantic import BaseModel, EmailStr
|
||
from enum import Enum
|
||
|
||
# Конфигурация
|
||
AUTH_DATABASE_ADDRESS = "sqlite:///partner.db"
|
||
|
||
#SQLModel
|
||
class Company(SQLModel, table=True):
|
||
id: Optional[int] = Field(default=None, primary_key=True)
|
||
name: str
|
||
commission: float # процент комиссии
|
||
key: str = Field(index=True, unique=True)
|
||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
auto_approve_transactions: bool = Field(default=False) # Новое поле для автоподтверждения
|
||
|
||
class TgAgent(SQLModel, table=True):
|
||
id: Optional[int] = Field(default=None, primary_key=True)
|
||
tg_id: int = Field(index=True, unique=True)
|
||
chat_id: Optional[int] = None
|
||
phone: Optional[str] = None
|
||
name: Optional[str] = None
|
||
login: Optional[str] = None
|
||
hash: Optional[str] = None
|
||
company_id: int = Field(foreign_key="company.id")
|
||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
|
||
class Ref(SQLModel, table=True):
|
||
id: Optional[int] = Field(default=None, primary_key=True)
|
||
tg_agent_id: int = Field(foreign_key="tgagent.id")
|
||
ref: str
|
||
description: Optional[str] = None
|
||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
|
||
class Sale(SQLModel, table=True):
|
||
id: Optional[int] = Field(default=None, primary_key=True)
|
||
cost: float
|
||
crediting: float # сколько начислено за продажу
|
||
ref: int = Field(foreign_key="ref.id")
|
||
sale_id: str
|
||
company_id: int = Field(foreign_key="company.id")
|
||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
|
||
class AgentTransaction(SQLModel, table=True):
|
||
__tablename__ = "agent_transactions" # Указываем имя таблицы явно
|
||
id: Optional[int] = Field(default=None, primary_key=True)
|
||
tg_agent_id: int = Field(foreign_key="tgagent.id") # ID агента, связь с TgAgent
|
||
amount: float # Используем float для DECIMAL(15,2)
|
||
status: str # 'waiting', 'process', 'done', 'reject', 'error'
|
||
transaction_group: uuid.UUID = Field(default_factory=uuid.uuid4) # UUID для группировки
|
||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
|
||
class PartnerTransaction(SQLModel, table=True):
|
||
__tablename__ = "partner_transactions" # Указываем имя таблицы явно
|
||
id: Optional[int] = Field(default=None, primary_key=True)
|
||
company_id: int = Field(foreign_key="company.id") # ID партнера, связь с Company
|
||
type: str # 'deposit', 'agent_payout', 'service_fee'
|
||
amount: float # Используем float для DECIMAL(15,2)
|
||
status: str # 'process', 'done', 'error'
|
||
transaction_group: uuid.UUID # UUID для группировки, может быть связан с agent_transactions
|
||
agent_transaction_id: Optional[int] = Field(default=None, foreign_key="agent_transactions.id") # Связь с агентской транзакцией
|
||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
|
||
class CompanyBalance(SQLModel, table=True):
|
||
__tablename__ = "company_balances" # Указываем имя таблицы явно
|
||
id: Optional[int] = Field(default=None, primary_key=True)
|
||
company_id: int = Field(foreign_key="company.id", unique=True) # ID компании, уникальный баланс на компанию
|
||
available_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
|
||
pending_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
|
||
updated_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
|
||
class AgentBalance(SQLModel, table=True):
|
||
__tablename__ = "agent_balances" # Указываем имя таблицы явно
|
||
id: Optional[int] = Field(default=None, primary_key=True)
|
||
tg_agent_id: int = Field(foreign_key="tgagent.id", unique=True) # ID агента, уникальный баланс на агента
|
||
available_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
|
||
frozen_balance: float = Field(default=0.0) # Используем float для DECIMAL(15,2)
|
||
updated_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
|
||
class Account(SQLModel, table=True):
|
||
id: Optional[int] = Field(default=None, primary_key=True)
|
||
login: str = Field(index=True, unique=True)
|
||
password_hash: str
|
||
firstName: Optional[str] = None
|
||
surname: Optional[str] = None
|
||
phone: Optional[str] = None
|
||
email: Optional[str] = None
|
||
company_id: int = Field(foreign_key="company.id")
|
||
create_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
update_dttm: datetime = Field(default_factory=datetime.utcnow)
|
||
|
||
class AccountProfileUpdateRequest(BaseModel):
|
||
firstName: str
|
||
surname: str
|
||
email: EmailStr
|
||
phone: str
|
||
|
||
class AccountPasswordChangeRequest(BaseModel):
|
||
currentPassword: str
|
||
newPassword: str
|
||
|
||
|
||
class TransactionStatus(str, Enum): # Определяем Enum для статусов
|
||
WAITING = 'waiting'
|
||
PROCESS = 'process'
|
||
DONE = 'done'
|
||
REJECT = 'reject'
|
||
ERROR = 'error'
|
||
NEW = 'new' # Новый статус
|
||
|
||
|
||
# Новая модель ответа для агентских транзакций с именем агента
|
||
class AgentTransactionResponse(BaseModel):
|
||
amount: float
|
||
status: TransactionStatus # Используем Enum
|
||
transaction_group: uuid.UUID
|
||
create_dttm: datetime
|
||
update_dttm: datetime
|
||
agent_name: Optional[str] = None # Поле для имени агента
|
||
|
||
|
||
# Создание движка базы данных
|
||
AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True)
|
||
SQLModel.metadata.create_all(AUTH_DB_ENGINE)
|
||
|
||
|
||
|
||
|
||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
|
||
|
||
# FastAPI app
|
||
app = FastAPI()
|
||
|
||
# CRUD
|
||
|
||
def get_tg_agent_by_tg_id(db: Session, tg_id: int) -> Optional[TgAgent]:
|
||
statement = select(TgAgent).where(TgAgent.tg_id == tg_id)
|
||
return db.exec(statement).first()
|
||
|
||
# Dependency
|
||
|
||
def get_db():
|
||
with Session(AUTH_DB_ENGINE) as session:
|
||
yield session
|
||
|
||
def get_current_account(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
||
credentials_exception = HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Could not validate credentials",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
try:
|
||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||
login: str = payload.get("sub")
|
||
if login is None:
|
||
raise credentials_exception
|
||
except InvalidTokenError:
|
||
raise credentials_exception
|
||
account = get_account_by_login(db, login)
|
||
if account is None:
|
||
raise credentials_exception
|
||
return account
|
||
|
||
# Авторизация
|
||
async def get_current_tg_agent(request: Request, db: Session = Depends(get_db)):
|
||
credentials_exception = HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Could not validate credentials",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
auth_header = request.headers.get("Authorization")
|
||
if not auth_header or not auth_header.startswith("Bearer "):
|
||
raise credentials_exception
|
||
hash_value = auth_header.replace("Bearer ", "").strip()
|
||
tg_agent = db.exec(select(TgAgent).where(TgAgent.hash == hash_value)).first()
|
||
if tg_agent is None:
|
||
raise credentials_exception
|
||
return tg_agent
|
||
|
||
# Регистрация
|
||
|
||
|
||
@app.post("/register", tags=["partner-tg"], response_model=RegisterResponse)
|
||
def register(req: RegisterRequest, db: Session = Depends(get_db)):
|
||
tg_id = req.tg_id
|
||
chat_id = req.chat_id
|
||
phone = req.phone
|
||
name = getattr(req, 'name', None)
|
||
login = getattr(req, 'login', None)
|
||
company_key = req.company_key
|
||
print(f'tg_id: {tg_id}, chat_id: {chat_id}, phone: {phone}, name: {name}, login: {login}, company_key: {company_key}')
|
||
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
|
||
if tg_agent:
|
||
raise HTTPException(status_code=400, detail="tg_id already registered")
|
||
# Поиск компании по ключу
|
||
company = db.exec(select(Company).where(Company.key == company_key)).first()
|
||
if not company:
|
||
raise HTTPException(status_code=400, detail="Компания с таким ключом не найдена")
|
||
hash_value = sha256(f"{tg_id}sold".encode()).hexdigest()
|
||
new_tg_agent = TgAgent(
|
||
tg_id=tg_id,
|
||
chat_id=chat_id,
|
||
phone=phone,
|
||
name=name,
|
||
login=login,
|
||
hash=hash_value,
|
||
company_id=company.id
|
||
)
|
||
db.add(new_tg_agent)
|
||
db.commit()
|
||
db.refresh(new_tg_agent)
|
||
return {"msg": "TgAgent registered successfully"}
|
||
|
||
def authenticate_tg_agent(engine, tg_id: int):
|
||
with Session(engine) as db:
|
||
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
|
||
if not tg_agent:
|
||
return None
|
||
return tg_agent
|
||
|
||
|
||
|
||
# Авторизация
|
||
|
||
SECRET_KEY = "supersecretkey" # Лучше вынести в .env
|
||
ALGORITHM = "HS256"
|
||
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
||
|
||
def create_access_token(data: dict, expires_delta: timedelta = None):
|
||
to_encode = data.copy()
|
||
if expires_delta:
|
||
expire = datetime.utcnow() + expires_delta
|
||
else:
|
||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||
to_encode.update({"exp": expire})
|
||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||
return encoded_jwt
|
||
|
||
@app.post("/token", response_model=Token, tags=["bff", "token"])
|
||
def login_account_for_access_token(
|
||
# login: str = Body(...),
|
||
# password: str = Body(...),
|
||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
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",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
access_token = create_access_token(
|
||
data={"sub": account.login},
|
||
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||
)
|
||
return Token(access_token=access_token, token_type="bearer")
|
||
|
||
|
||
|
||
@app.get("/ref", response_model=List[RefResponse], tags=["partner-tg"])
|
||
def get_refs(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||
return [RefResponse(ref=r.ref, description=r.description or "") for r in refs]
|
||
|
||
@app.post("/ref/add", tags=["partner-tg"], response_model=RefAddResponse)
|
||
def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||
new_ref = Ref(
|
||
tg_agent_id=current_tg_agent.id,
|
||
ref=str(uuid4()),
|
||
description=req.description
|
||
)
|
||
db.add(new_ref)
|
||
db.commit()
|
||
db.refresh(new_ref)
|
||
return {"ref": new_ref.ref}
|
||
|
||
@app.get("/ref/stat", tags=["partner-tg"], response_model=RefStatResponse)
|
||
def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||
# 1. Получаем все реферальные ссылки пользователя
|
||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||
result = []
|
||
for ref in refs:
|
||
# 2. Для каждой ссылки считаем продажи и сумму
|
||
sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all()
|
||
sales_count = len(sales)
|
||
income = sum(sale.crediting for sale in sales)
|
||
result.append({
|
||
"description": ref.description or "",
|
||
"sales": sales_count,
|
||
"income": income
|
||
})
|
||
return {"refData": result}
|
||
|
||
@app.get("/stat", tags=["partner-tg"], response_model=StatResponse)
|
||
def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
|
||
# 1. Получаем все реферальные ссылки пользователя
|
||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
|
||
ref_ids = [r.id for r in refs]
|
||
|
||
# 2. Считаем totalSales (продажи по всем рефам пользователя)
|
||
total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
|
||
totalSales = len(total_sales)
|
||
totalIncome = sum(sale.crediting for sale in total_sales)
|
||
# Заменено получение доступного остатка из AgentBalance
|
||
agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == current_tg_agent.id)).first()
|
||
availableWithdrawal = agent_balance.available_balance if agent_balance else 0.0
|
||
return {
|
||
"totalSales": totalSales,
|
||
"totalIncome": totalIncome,
|
||
"availableWithdrawal": availableWithdrawal
|
||
}
|
||
|
||
@app.get("/dashboard/cards", tags=["bff", "dashboard"], response_model=DashboardCardsResponse)
|
||
def get_dashboard_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
|
||
# 1. Общий доход - сумма всех Sale.cost
|
||
total_revenue = db.exec(select(Sale).where(Sale.company_id == current_account.company_id)).all()
|
||
totalRevenue = sum(sale.cost for sale in total_revenue)
|
||
|
||
# 2. Общие выплаты - сумма всех Sale.crediting
|
||
totalPayouts = sum(sale.crediting for sale in total_revenue)
|
||
|
||
# 3. Активные рефералы - количество уникальных TgAgent.tg_id
|
||
unique_agents = db.exec(select(TgAgent.tg_id).where(TgAgent.company_id == current_account.company_id)).all()
|
||
activeReferrals = len(set(unique_agents))
|
||
|
||
# 4. Ожидающие выплаты - сумма AgentTransaction со статусом 'waiting'
|
||
pending_agent_transactions = db.exec(select(AgentTransaction).join(TgAgent).where(TgAgent.company_id == current_account.company_id).where(AgentTransaction.status == 'waiting')).all()
|
||
pendingPayouts = sum(t.amount for t in pending_agent_transactions)
|
||
|
||
# 5. Количество продаж
|
||
totalSales = len(total_revenue)
|
||
|
||
return {
|
||
"totalRevenue": totalRevenue,
|
||
"totalPayouts": totalPayouts,
|
||
"activeReferrals": activeReferrals,
|
||
"pendingPayouts": pendingPayouts,
|
||
"totalSales": totalSales
|
||
}
|
||
|
||
@app.get("/dashboard/chart/total", tags=["bff", "dashboard"], response_model=DashboardChartTotalResponse)
|
||
def get_dashboard_chart_total(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
|
||
# Группируем продажи по дате (день)
|
||
result = db.exec(
|
||
select(
|
||
func.strftime('%Y-%m-%d', Sale.create_dttm).label('date'),
|
||
func.sum(Sale.cost).label('revenue'),
|
||
func.count(Sale.id).label('sales')
|
||
).where(Sale.company_id == current_account.company_id).group_by(func.strftime('%Y-%m-%d', Sale.create_dttm))
|
||
.order_by(func.strftime('%Y-%m-%d', Sale.create_dttm))
|
||
).all()
|
||
# Преобразуем результат в нужный формат
|
||
data = [
|
||
{"date": row.date, "revenue": row.revenue or 0, "sales": row.sales or 0}
|
||
for row in result
|
||
]
|
||
return DashboardChartTotalResponse(items=data)
|
||
|
||
@app.get("/dashboard/chart/agent", tags=["bff", "dashboard"], response_model=DashboardChartAgentResponse)
|
||
def get_dashboard_chart_agent(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
|
||
# Получаем всех агентов
|
||
agents = db.exec(select(TgAgent).where(TgAgent.company_id == current_account.company_id)).all()
|
||
result = []
|
||
for agent in agents:
|
||
# Получаем все рефы этого агента
|
||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all()
|
||
ref_ids = [r.id for r in refs]
|
||
if not ref_ids:
|
||
result.append({
|
||
"name": agent.name or f"Агент {agent.id}",
|
||
"count": 0,
|
||
"sum": 0.0
|
||
})
|
||
continue
|
||
# Получаем все продажи по этим рефам
|
||
sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
|
||
sales_count = len(sales)
|
||
sales_sum = sum(sale.cost for sale in sales)
|
||
result.append({
|
||
"name": agent.name or f"Агент {agent.id}",
|
||
"count": sales_count,
|
||
"sum": sales_sum
|
||
})
|
||
return DashboardChartAgentResponse(items=result)
|
||
|
||
@app.get("/stat/agents", tags=["bff", "stat"], response_model=StatAgentsResponse)
|
||
def get_agents_stat(
|
||
db: Session = Depends(get_db),
|
||
date_start: str = Query(None),
|
||
date_end: str = Query(None),
|
||
current_account: Account = Depends(get_current_account),
|
||
):
|
||
agents_query = select(TgAgent).where(TgAgent.company_id == current_account.company_id)
|
||
if date_start:
|
||
agents_query = agents_query.where(TgAgent.create_dttm >= date_start)
|
||
if date_end:
|
||
agents_query = agents_query.where(TgAgent.create_dttm <= date_end)
|
||
agents = db.exec(agents_query).all()
|
||
result = []
|
||
for agent in agents:
|
||
refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all()
|
||
ref_ids = [r.id for r in refs]
|
||
ref_count = len(ref_ids)
|
||
if not ref_ids:
|
||
result.append({
|
||
"name": agent.name or f"Агент {agent.id}",
|
||
"refCount": 0,
|
||
"salesCount": 0,
|
||
"salesSum": 0.0,
|
||
"crediting": 0.0
|
||
})
|
||
continue
|
||
sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
|
||
sales_count = len(sales)
|
||
sales_sum = sum(sale.cost for sale in sales)
|
||
crediting_sum = sum(sale.crediting for sale in sales)
|
||
result.append({
|
||
"name": agent.name or f"Агент {agent.id}",
|
||
"refCount": ref_count,
|
||
"salesCount": sales_count,
|
||
"salesSum": sales_sum,
|
||
"crediting": crediting_sum
|
||
})
|
||
return StatAgentsResponse(items=result)
|
||
|
||
@app.get("/stat/referrals", tags=["bff", "stat"], response_model=StatReferralsResponse)
|
||
def get_referrals_stat(
|
||
db: Session = Depends(get_db),
|
||
date_start: str = Query(None),
|
||
date_end: str = Query(None),
|
||
current_account: Account = Depends(get_current_account),
|
||
):
|
||
refs_query = select(Ref).join(TgAgent).where(TgAgent.company_id == current_account.company_id)
|
||
if date_start:
|
||
refs_query = refs_query.where(Ref.create_dttm >= date_start)
|
||
if date_end:
|
||
refs_query = refs_query.where(Ref.create_dttm <= date_end)
|
||
refs = db.exec(refs_query).all()
|
||
result = []
|
||
for ref in refs:
|
||
agent = db.exec(select(TgAgent).where(TgAgent.id == ref.tg_agent_id)).first()
|
||
sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all()
|
||
sales_count = len(sales)
|
||
sales_sum = sum(sale.cost for sale in sales)
|
||
result.append({
|
||
"ref": ref.ref,
|
||
"agent": agent.name if agent and agent.name else f"Агент {ref.tg_agent_id}",
|
||
"description": ref.description or "",
|
||
"salesSum": sales_sum,
|
||
"salesCount": sales_count
|
||
})
|
||
return StatReferralsResponse(items=result)
|
||
|
||
@app.get("/stat/sales", tags=["bff", "stat"], response_model=StatSalesResponse)
|
||
def get_sales_stat(
|
||
db: Session = Depends(get_db),
|
||
date_start: str = Query(None),
|
||
date_end: str = Query(None),
|
||
current_account: Account = Depends(get_current_account),
|
||
):
|
||
sales_query = select(Sale).where(Sale.company_id == current_account.company_id)
|
||
if date_start:
|
||
sales_query = sales_query.where(Sale.create_dttm >= date_start)
|
||
if date_end:
|
||
sales_query = sales_query.where(Sale.create_dttm <= date_end)
|
||
sales = db.exec(sales_query).all()
|
||
ref_ids = list(set(sale.ref for sale in sales))
|
||
refs = db.exec(select(Ref).where(Ref.id.in_(ref_ids))).all() if ref_ids else []
|
||
ref_map = {ref.id: ref for ref in refs}
|
||
agent_ids = list(set(ref.tg_agent_id for ref in refs)) if refs else []
|
||
agents = db.exec(select(TgAgent).where(TgAgent.id.in_(agent_ids))).all() if agent_ids else []
|
||
agent_map = {agent.id: agent for agent in agents}
|
||
result = []
|
||
for sale in sales:
|
||
ref_obj = ref_map.get(sale.ref)
|
||
agent_obj = agent_map.get(ref_obj.tg_agent_id) if ref_obj else None
|
||
result.append({
|
||
"saleId": sale.sale_id,
|
||
"cost": sale.cost,
|
||
"crediting": sale.crediting,
|
||
"ref": ref_obj.ref if ref_obj else None,
|
||
"name": agent_obj.name if agent_obj else None
|
||
})
|
||
return StatSalesResponse(items=result)
|
||
|
||
@app.get("/billing/cards", tags=["bff", "billing"], response_model=BillingCardsResponse)
|
||
def get_billing_cards(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
|
||
# 1. cost - Общий заработок (сумма всех Sale.cost)
|
||
sales = db.exec(select(Sale).where(Sale.company_id == current_account.company_id)).all()
|
||
cost = sum(sale.cost for sale in sales)
|
||
|
||
# 2. crediting - Общие выплаты (сумма PartnerTransaction типа 'agent_payout' со статусом 'done')
|
||
completed_payouts = db.exec(select(PartnerTransaction).where(PartnerTransaction.type == 'agent_payout').where(PartnerTransaction.status == 'done').where(PartnerTransaction.company_id == current_account.company_id)).all()
|
||
crediting = sum(t.amount for t in completed_payouts)
|
||
|
||
# 3. pendingPayouts - Доступно к выводу всеми партнерами (сумма всех доступных балансов агентов)
|
||
agent_balances = db.exec(select(AgentBalance).join(TgAgent).where(TgAgent.company_id == current_account.company_id)).all()
|
||
pendingPayouts = sum(balance.available_balance for balance in agent_balances)
|
||
|
||
return {
|
||
"cost": cost,
|
||
"crediting": crediting,
|
||
"pendingPayouts": pendingPayouts
|
||
}
|
||
|
||
@app.get("/billing/payouts/transactions", tags=["bff", "billing"], response_model=BillingPayoutsTransactionsResponse)
|
||
def get_billing_payouts_transactions(
|
||
db: Session = Depends(get_db),
|
||
date_start: str = Query(None),
|
||
date_end: str = Query(None),
|
||
current_account: Account = Depends(get_current_account),
|
||
):
|
||
# Используем AgentTransaction вместо Transaction
|
||
# Явно выбираем обе модели для корректной распаковки
|
||
query = select(AgentTransaction, TgAgent).join(TgAgent).where(TgAgent.company_id == current_account.company_id)
|
||
if date_start:
|
||
query = query.where(AgentTransaction.create_dttm >= date_start)
|
||
if date_end:
|
||
query = query.where(AgentTransaction.create_dttm <= date_end)
|
||
# Заказываем по дате создания
|
||
query = query.order_by(AgentTransaction.create_dttm.desc())
|
||
|
||
# Выполняем запрос и формируем результат
|
||
results = db.exec(query).all()
|
||
result = []
|
||
for agent_trans, agent in results:
|
||
try:
|
||
status_enum = TransactionStatus(agent_trans.status)
|
||
except ValueError:
|
||
# Если статус из БД не соответствует Enum, используем статус ERROR
|
||
status_enum = TransactionStatus.ERROR
|
||
result.append({
|
||
"id": agent_trans.transaction_group, # Используем id, как и запрошено
|
||
"amount": agent_trans.amount,
|
||
"agent": agent.name if agent else None,
|
||
"status": status_enum,
|
||
"create_dttm": agent_trans.create_dttm,
|
||
"update_dttm": agent_trans.update_dttm,
|
||
})
|
||
return BillingPayoutsTransactionsResponse(items=result)
|
||
|
||
@app.get("/billing/chart/stat", tags=["bff", "billing"], response_model=BillingChartStatResponse)
|
||
def get_billing_chart_stat(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
|
||
# Группируем агентские транзакции по дате (день) и статусу
|
||
result = db.exec(
|
||
select(
|
||
func.strftime('%Y-%m-%d', AgentTransaction.create_dttm).label('date'),
|
||
AgentTransaction.status.label('status'),
|
||
func.count(AgentTransaction.id).label('count')
|
||
).join(TgAgent).where(TgAgent.company_id == current_account.company_id).group_by(
|
||
func.strftime('%Y-%m-%d', AgentTransaction.create_dttm),
|
||
AgentTransaction.status
|
||
).order_by(
|
||
func.strftime('%Y-%m-%d', AgentTransaction.create_dttm),
|
||
AgentTransaction.status
|
||
)
|
||
).all()
|
||
data = [
|
||
{"date": row.date, "status": row.status, "count": row.count}
|
||
for row in result
|
||
]
|
||
return BillingChartStatResponse(items=data)
|
||
|
||
@app.get("/billing/chart/pie", tags=["bff", "billing"], response_model=BillingChartPieResponse)
|
||
def get_billing_chart_pie(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
|
||
# Группируем агентские транзакции по статусу
|
||
result = db.exec(
|
||
select(
|
||
AgentTransaction.status.label('status'),
|
||
func.count(AgentTransaction.id).label('count')
|
||
).join(TgAgent).where(TgAgent.company_id == current_account.company_id).group_by(AgentTransaction.status)
|
||
).all()
|
||
data = [
|
||
{"status": row.status, "count": row.count}
|
||
for row in result
|
||
]
|
||
return BillingChartPieResponse(items=data)
|
||
|
||
|
||
|
||
@app.post("/tg_auth", tags=["partner-tg"], response_model=TgAuthResponse)
|
||
def tg_auth(hash: str = Body(..., embed=True), db: Session = Depends(get_db)):
|
||
tg_agent = db.exec(select(TgAgent).where(TgAgent.hash == hash)).first()
|
||
if not tg_agent:
|
||
raise HTTPException(status_code=401, detail="Hash not found")
|
||
return {"msg": "Auth success", "tg_id": tg_agent.tg_id}
|
||
|
||
# --- Новый функционал для Account ---
|
||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||
|
||
def verify_password(plain_password, hashed_password):
|
||
return pwd_context.verify(plain_password, hashed_password)
|
||
|
||
def get_account_by_login(db: Session, login: str) -> Optional[Account]:
|
||
statement = select(Account).where(Account.login == login)
|
||
return db.exec(statement).first()
|
||
|
||
|
||
|
||
@app.get("/account", tags=["bff", "account"], response_model=AccountResponse)
|
||
def get_account(current_account: Account = Depends(get_current_account)):
|
||
return {
|
||
"firstName": current_account.firstName,
|
||
"surname": current_account.surname
|
||
}
|
||
|
||
@app.get("/account/profile", tags=["bff", "account"], response_model=AccountProfileResponse)
|
||
def get_account_profile(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
|
||
company = db.exec(select(Company).where(Company.id == current_account.company_id)).first()
|
||
if not company:
|
||
raise HTTPException(status_code=404, detail="Компания не найдена")
|
||
return {
|
||
"firstName": current_account.firstName,
|
||
"surname": current_account.surname,
|
||
"phone": current_account.phone,
|
||
"email": current_account.email,
|
||
"create_dttm": current_account.create_dttm,
|
||
"company": {
|
||
"name": company.name,
|
||
"key": company.key,
|
||
"commission": company.commission
|
||
}
|
||
}
|
||
|
||
@app.post("/account/profile", tags=["bff", "account"], response_model=AccountProfileUpdateResponse)
|
||
def update_account_profile(
|
||
req: AccountProfileUpdateRequest,
|
||
current_account: Account = Depends(get_current_account),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
# Проверка, что все поля заполнены (Pydantic уже валидирует email и обязательность)
|
||
if not req.firstName.strip() or not req.surname.strip() or not req.email or not req.phone.strip():
|
||
raise HTTPException(status_code=400, detail="Все поля должны быть заполнены")
|
||
# Обновляем поля
|
||
current_account.firstName = req.firstName.strip()
|
||
current_account.surname = req.surname.strip()
|
||
current_account.email = req.email
|
||
current_account.phone = req.phone.strip()
|
||
db.add(current_account)
|
||
db.commit()
|
||
db.refresh(current_account)
|
||
return {"msg": "Профиль обновлён успешно"}
|
||
|
||
@app.post("/account/password", tags=["bff", "account"], response_model=AccountPasswordChangeResponse)
|
||
def change_account_password(
|
||
req: AccountPasswordChangeRequest,
|
||
current_account: Account = Depends(get_current_account),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
# Проверяем текущий пароль
|
||
if not verify_password(req.currentPassword, current_account.password_hash):
|
||
raise HTTPException(status_code=400, detail="Текущий пароль неверный")
|
||
# Проверяем, что новый пароль не пустой и отличается от текущего
|
||
if not req.newPassword.strip():
|
||
raise HTTPException(status_code=400, detail="Новый пароль не может быть пустым")
|
||
if verify_password(req.newPassword, current_account.password_hash):
|
||
raise HTTPException(status_code=400, detail="Новый пароль не должен совпадать с текущим")
|
||
# Хешируем и сохраняем новый пароль
|
||
current_account.password_hash = pwd_context.hash(req.newPassword)
|
||
db.add(current_account)
|
||
db.commit()
|
||
db.refresh(current_account)
|
||
return {"msg": "Пароль успешно изменён"}
|
||
|
||
# --- Новый функционал для агентских транзакций партнера ---
|
||
|
||
@app.get("/account/agent-transaction", response_model=List[AgentTransactionResponse], tags=["bff", "account"])
|
||
def get_account_agent_transactions(
|
||
statuses: Optional[List[TransactionStatus]] = Query(None), # Изменено на List[TransactionStatus]
|
||
date_start: str = Query(None), # Добавлен параметр date_start
|
||
date_end: str = Query(None), # Добавлен параметр date_end
|
||
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
|
||
|
||
# Модель запроса для POST /account/auto-approve
|
||
class AutoApproveSettingsRequest(BaseModel):
|
||
auto_approve: bool
|
||
apply_to_current: Optional[bool] = False
|
||
|
||
@app.get("/account/auto-approve", tags=["bff", "account"], response_model=AutoApproveSettingsGetResponse)
|
||
def get_auto_approve_settings(
|
||
current_account: Account = Depends(get_current_account),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""
|
||
Возвращает текущую настройку автоматического подтверждения для компании пользователя.
|
||
"""
|
||
company = db.exec(select(Company).where(Company.id == current_account.company_id)).first()
|
||
if not company:
|
||
raise HTTPException(status_code=404, detail="Компания не найдена")
|
||
return {"auto_approve_transactions": company.auto_approve_transactions}
|
||
|
||
@app.post("/account/auto-approve", tags=["bff", "account"], response_model=AutoApproveSettingsUpdateResponse)
|
||
def update_auto_approve_settings(
|
||
req: AutoApproveSettingsRequest,
|
||
current_account: Account = Depends(get_current_account),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""
|
||
Обновляет настройку автоматического подтверждения для компании пользователя.
|
||
При необходимости переводит транзакции из 'waiting' в 'new'.
|
||
"""
|
||
company = db.exec(select(Company).where(Company.id == current_account.company_id)).first()
|
||
if not company:
|
||
raise HTTPException(status_code=404, detail="Компания не найдена")
|
||
|
||
company.auto_approve_transactions = req.auto_approve
|
||
company.update_dttm = datetime.utcnow()
|
||
db.add(company)
|
||
|
||
if req.apply_to_current and req.auto_approve: # Применяем только если авто-аппрув включается и запрошено применение к текущим
|
||
# Находим все агентские транзакции компании в статусе 'waiting'
|
||
agent_transactions_to_update = db.exec(
|
||
select(AgentTransaction)
|
||
.join(TgAgent)
|
||
.where(TgAgent.company_id == company.id)
|
||
.where(AgentTransaction.status == TransactionStatus.WAITING)
|
||
).all()
|
||
|
||
for agent_trans in agent_transactions_to_update:
|
||
agent_trans.status = TransactionStatus.NEW
|
||
agent_trans.update_dttm = datetime.utcnow()
|
||
db.add(agent_trans)
|
||
|
||
# Находим соответствующие партнерские транзакции и обновляем их статус
|
||
partner_transactions_to_update = db.exec(
|
||
select(PartnerTransaction)
|
||
.where(PartnerTransaction.agent_transaction_id == agent_trans.id) # Используем связь по ID
|
||
.where(PartnerTransaction.status == TransactionStatus.PROCESS) # Предполагаем, что связанные партнерские транзакции в статусе PROCESS
|
||
).all()
|
||
for partner_trans in partner_transactions_to_update:
|
||
partner_trans.status = TransactionStatus.NEW
|
||
partner_trans.update_dttm = datetime.utcnow()
|
||
db.add(partner_trans)
|
||
|
||
db.commit()
|
||
db.refresh(company)
|
||
|
||
return {"msg": "Настройка автоматического подтверждения обновлена", "auto_approve_transactions": company.auto_approve_transactions}
|
||
|
||
# Модель запроса для POST /account/approve-transactions
|
||
class ApproveTransactionsRequest(BaseModel):
|
||
transaction_ids: List[uuid.UUID]
|
||
|
||
@app.post("/account/approve-transactions", tags=["bff", "account"], response_model=ApproveTransactionsResult)
|
||
def approve_agent_transactions(
|
||
req: ApproveTransactionsRequest,
|
||
current_account: Account = Depends(get_current_account),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""
|
||
Утверждение выбранных агентских транзакций для компании текущего пользователя.
|
||
Переводит транзакции из статуса 'waiting' в 'new'.
|
||
"""
|
||
company_id = current_account.company_id
|
||
approved_count = 0
|
||
|
||
if not req.transaction_ids:
|
||
return {"msg": "Нет транзакций для утверждения", "approved_count": 0}
|
||
|
||
# Find transactions belonging to the company and with specified IDs and statuses
|
||
transactions_to_approve = db.exec(
|
||
select(AgentTransaction)
|
||
.join(TgAgent)
|
||
.where(TgAgent.company_id == company_id)
|
||
.where(AgentTransaction.transaction_group.in_(req.transaction_ids))
|
||
.where(AgentTransaction.status == TransactionStatus.WAITING) # Утверждаем только транзакции в статусе 'waiting'
|
||
).all()
|
||
|
||
for agent_trans in transactions_to_approve:
|
||
agent_trans.status = TransactionStatus.NEW # Переводим в статус 'new'
|
||
agent_trans.update_dttm = datetime.utcnow()
|
||
db.add(agent_trans)
|
||
approved_count += 1
|
||
|
||
db.commit()
|
||
|
||
return {"msg": f"Переведено в статус NEW {approved_count} транзакций", "approved_count": approved_count}
|