partner-core/main.py

835 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 models import RefAddRequest, RefResponse, RegisterRequest, Token, TokenRequest
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"])
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"])
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"])
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"])
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"])
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"])
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)).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)).all()
activeReferrals = len(set(unique_agents))
# 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)
return {
"totalRevenue": totalRevenue,
"totalPayouts": totalPayouts,
"activeReferrals": activeReferrals,
"pendingPayouts": pendingPayouts,
"totalSales": totalSales
}
@app.get("/dashboard/chart/total", tags=["bff"])
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')
).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 JSONResponse(content=data)
@app.get("/dashboard/chart/agent", tags=["bff"])
def get_dashboard_chart_agent(current_account: Account = Depends(get_current_account), db: Session = Depends(get_db)):
# Получаем всех агентов
agents = db.exec(select(TgAgent)).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 JSONResponse(content=result)
@app.get("/stat/agents", tags=["bff"])
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)
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 JSONResponse(content=result)
@app.get("/stat/referrals", tags=["bff"])
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)
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 JSONResponse(content=result)
@app.get("/stat/sales", tags=["bff"])
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)
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 JSONResponse(content=result)
@app.get("/billing/cards", tags=["bff"])
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)).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')).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,
"pendingPayouts": pendingPayouts
}
@app.get("/billing/payouts/transactions", tags=["bff"])
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)
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:
result.append({
"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(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')
).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 JSONResponse(content=data)
@app.get("/billing/chart/pie", tags=["bff"])
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')
).group_by(AgentTransaction.status)
).all()
data = [
{"status": row.status, "count": row.count}
for row in result
]
return JSONResponse(content=data)
@app.post("/tg_auth", tags=["partner-tg"])
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"])
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"])
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"])
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"])
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"])
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"])
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"])
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"])
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}