Compare commits

...

3 Commits

4 changed files with 268 additions and 83 deletions

171
bff_models.py Normal file
View File

@ -0,0 +1,171 @@
from pydantic import BaseModel, Field, EmailStr
from typing import Optional, List
from datetime import datetime
import uuid
from enum import Enum
# BFF Models
class Token(BaseModel):
access_token: str
token_type: str
class AccountProfileUpdateRequest(BaseModel):
firstName: str
surname: str
email: EmailStr
phone: str
class AccountPasswordChangeRequest(BaseModel):
currentPassword: str
newPassword: str
class TransactionStatus(str, Enum):
WAITING = 'waiting'
PROCESS = 'process'
DONE = 'done'
REJECT = 'reject'
ERROR = 'error'
NEW = 'new'
class AgentTransactionResponse(BaseModel):
amount: float
status: TransactionStatus
transaction_group: uuid.UUID
create_dttm: datetime
update_dttm: datetime
agent_name: Optional[str] = None
class AutoApproveSettingsRequest(BaseModel):
auto_approve: bool
apply_to_current: Optional[bool] = False
class ApproveTransactionsRequest(BaseModel):
transaction_ids: List[uuid.UUID]
# New Response Models for BFF APIs
class RegisterResponse(BaseModel):
msg: str
class TgAuthResponse(BaseModel):
msg: str
tg_id: int
class DashboardCardsResponse(BaseModel):
totalRevenue: float
totalPayouts: float
activeReferrals: int
pendingPayouts: float
totalSales: int
class DashboardChartTotalItem(BaseModel):
date: str
revenue: float
sales: int
class DashboardChartTotalResponse(BaseModel):
items: List[DashboardChartTotalItem]
class DashboardChartAgentItem(BaseModel):
name: str
count: int
sum: float
class DashboardChartAgentResponse(BaseModel):
items: List[DashboardChartAgentItem]
class StatAgentsItem(BaseModel):
name: str
refCount: int
salesCount: int
salesSum: float
crediting: float
class StatAgentsResponse(BaseModel):
items: List[StatAgentsItem]
class StatReferralsItem(BaseModel):
ref: str
agent: Optional[str] = None
description: str
salesSum: float
salesCount: int
class StatReferralsResponse(BaseModel):
items: List[StatReferralsItem]
class StatSalesItem(BaseModel):
saleId: str
cost: float
crediting: float
ref: Optional[str] = None
name: Optional[str] = None
class StatSalesResponse(BaseModel):
items: List[StatSalesItem]
class BillingCardsResponse(BaseModel):
cost: float
crediting: float
pendingPayouts: float
class BillingChartStatItem(BaseModel):
date: str
status: str
count: int
class BillingChartStatResponse(BaseModel):
items: List[BillingChartStatItem]
class BillingChartPieItem(BaseModel):
status: str
count: int
class BillingChartPieResponse(BaseModel):
items: List[BillingChartPieItem]
class BillingPayoutsTransactionsItem(BaseModel):
id: uuid.UUID
amount: float
agent: Optional[str] = None
status: TransactionStatus
create_dttm: datetime
update_dttm: datetime
class BillingPayoutsTransactionsResponse(BaseModel):
items: List[BillingPayoutsTransactionsItem]
class AccountResponse(BaseModel):
firstName: Optional[str] = None
surname: Optional[str] = None
class CompanyProfileResponse(BaseModel):
name: str
key: str
commission: float
class AccountProfileResponse(BaseModel):
firstName: Optional[str] = None
surname: Optional[str] = None
phone: Optional[str] = None
email: Optional[EmailStr] = None
create_dttm: datetime
company: CompanyProfileResponse
class AccountProfileUpdateResponse(BaseModel):
msg: str
class AccountPasswordChangeResponse(BaseModel):
msg: str
class AutoApproveSettingsGetResponse(BaseModel):
auto_approve_transactions: bool
class AutoApproveSettingsUpdateResponse(BaseModel):
msg: str
auto_approve_transactions: bool
class ApproveTransactionsResult(BaseModel):
msg: str
approved_count: int

108
main.py
View File

@ -5,7 +5,8 @@ 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 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
@ -199,7 +200,7 @@ async def get_current_tg_agent(request: Request, db: Session = Depends(get_db)):
# Регистрация
@app.post("/register", tags=["partner-tg"])
@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
@ -255,7 +256,7 @@ def create_access_token(data: dict, expires_delta: timedelta = None):
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@app.post("/token", response_model=Token, tags=["bff"])
@app.post("/token", response_model=Token, tags=["bff", "token"])
def login_account_for_access_token(
# login: str = Body(...),
# password: str = Body(...),
@ -277,12 +278,12 @@ def login_account_for_access_token(
@app.get("/ref", response_model=list[RefResponse], tags=["partner-tg"])
@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"])
@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,
@ -294,7 +295,7 @@ def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_
db.refresh(new_ref)
return {"ref": new_ref.ref}
@app.get("/ref/stat", tags=["partner-tg"])
@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()
@ -311,7 +312,7 @@ def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db:
})
return {"refData": result}
@app.get("/stat", tags=["partner-tg"])
@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()
@ -330,21 +331,21 @@ def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Sess
"availableWithdrawal": availableWithdrawal
}
@app.get("/dashboard/cards", tags=["bff"])
@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)).all()
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)).all()
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).where(AgentTransaction.status == 'waiting')).all()
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. Количество продаж
@ -358,7 +359,7 @@ def get_dashboard_cards(current_account: Account = Depends(get_current_account),
"totalSales": totalSales
}
@app.get("/dashboard/chart/total", tags=["bff"])
@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(
@ -366,7 +367,7 @@ def get_dashboard_chart_total(current_account: Account = Depends(get_current_acc
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))
).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()
# Преобразуем результат в нужный формат
@ -374,12 +375,12 @@ def get_dashboard_chart_total(current_account: Account = Depends(get_current_acc
{"date": row.date, "revenue": row.revenue or 0, "sales": row.sales or 0}
for row in result
]
return JSONResponse(content=data)
return DashboardChartTotalResponse(items=data)
@app.get("/dashboard/chart/agent", tags=["bff"])
@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)).all()
agents = db.exec(select(TgAgent).where(TgAgent.company_id == current_account.company_id)).all()
result = []
for agent in agents:
# Получаем все рефы этого агента
@ -401,16 +402,16 @@ def get_dashboard_chart_agent(current_account: Account = Depends(get_current_acc
"count": sales_count,
"sum": sales_sum
})
return JSONResponse(content=result)
return DashboardChartAgentResponse(items=result)
@app.get("/stat/agents", tags=["bff"])
@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)
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:
@ -441,16 +442,16 @@ def get_agents_stat(
"salesSum": sales_sum,
"crediting": crediting_sum
})
return JSONResponse(content=result)
return StatAgentsResponse(items=result)
@app.get("/stat/referrals", tags=["bff"])
@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)
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:
@ -469,16 +470,16 @@ def get_referrals_stat(
"salesSum": sales_sum,
"salesCount": sales_count
})
return JSONResponse(content=result)
return StatReferralsResponse(items=result)
@app.get("/stat/sales", tags=["bff"])
@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)
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:
@ -501,20 +502,20 @@ def get_sales_stat(
"ref": ref_obj.ref if ref_obj else None,
"name": agent_obj.name if agent_obj else None
})
return JSONResponse(content=result)
return StatSalesResponse(items=result)
@app.get("/billing/cards", tags=["bff"])
@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)).all()
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')).all()
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)).all()
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 {
@ -523,7 +524,7 @@ def get_billing_cards(current_account: Account = Depends(get_current_account), d
"pendingPayouts": pendingPayouts
}
@app.get("/billing/payouts/transactions", tags=["bff"])
@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),
@ -532,7 +533,7 @@ def get_billing_payouts_transactions(
):
# Используем AgentTransaction вместо Transaction
# Явно выбираем обе модели для корректной распаковки
query = select(AgentTransaction, TgAgent).join(TgAgent)
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:
@ -544,17 +545,22 @@ def get_billing_payouts_transactions(
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, # Используем transaction_group как ID транзакции группы
"id": agent_trans.transaction_group, # Используем id, как и запрошено
"amount": agent_trans.amount,
"agent": agent.name if agent else None, # Имя агента из join
"status": agent_trans.status,
"agent": agent.name if agent else None,
"status": status_enum,
"create_dttm": agent_trans.create_dttm,
"update_dttm": agent_trans.update_dttm,
})
return result
return BillingPayoutsTransactionsResponse(items=result)
@app.get("/billing/chart/stat", tags=["bff"])
@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(
@ -562,7 +568,7 @@ def get_billing_chart_stat(current_account: Account = Depends(get_current_accoun
func.strftime('%Y-%m-%d', AgentTransaction.create_dttm).label('date'),
AgentTransaction.status.label('status'),
func.count(AgentTransaction.id).label('count')
).group_by(
).join(TgAgent).where(TgAgent.company_id == current_account.company_id).group_by(
func.strftime('%Y-%m-%d', AgentTransaction.create_dttm),
AgentTransaction.status
).order_by(
@ -574,26 +580,26 @@ def get_billing_chart_stat(current_account: Account = Depends(get_current_accoun
{"date": row.date, "status": row.status, "count": row.count}
for row in result
]
return JSONResponse(content=data)
return BillingChartStatResponse(items=data)
@app.get("/billing/chart/pie", tags=["bff"])
@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')
).group_by(AgentTransaction.status)
).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 JSONResponse(content=data)
return BillingChartPieResponse(items=data)
@app.post("/tg_auth", tags=["partner-tg"])
@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:
@ -612,14 +618,14 @@ def get_account_by_login(db: Session, login: str) -> Optional[Account]:
@app.get("/account", tags=["bff"])
@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"])
@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:
@ -637,7 +643,7 @@ def get_account_profile(current_account: Account = Depends(get_current_account),
}
}
@app.post("/account/profile", tags=["bff"])
@app.post("/account/profile", tags=["bff", "account"], response_model=AccountProfileUpdateResponse)
def update_account_profile(
req: AccountProfileUpdateRequest,
current_account: Account = Depends(get_current_account),
@ -656,7 +662,7 @@ def update_account_profile(
db.refresh(current_account)
return {"msg": "Профиль обновлён успешно"}
@app.post("/account/password", tags=["bff"])
@app.post("/account/password", tags=["bff", "account"], response_model=AccountPasswordChangeResponse)
def change_account_password(
req: AccountPasswordChangeRequest,
current_account: Account = Depends(get_current_account),
@ -679,7 +685,7 @@ def change_account_password(
# --- Новый функционал для агентских транзакций партнера ---
@app.get("/account/agent-transaction", response_model=List[AgentTransactionResponse], tags=["bff"])
@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
@ -733,7 +739,7 @@ class AutoApproveSettingsRequest(BaseModel):
auto_approve: bool
apply_to_current: Optional[bool] = False
@app.get("/account/auto-approve", tags=["bff"])
@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)
@ -746,7 +752,7 @@ def get_auto_approve_settings(
raise HTTPException(status_code=404, detail="Компания не найдена")
return {"auto_approve_transactions": company.auto_approve_transactions}
@app.post("/account/auto-approve", tags=["bff"])
@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),
@ -798,7 +804,7 @@ def update_auto_approve_settings(
class ApproveTransactionsRequest(BaseModel):
transaction_ids: List[uuid.UUID]
@app.post("/account/approve-transactions", tags=["bff"])
@app.post("/account/approve-transactions", tags=["bff", "account"], response_model=ApproveTransactionsResult)
def approve_agent_transactions(
req: ApproveTransactionsRequest,
current_account: Account = Depends(get_current_account),

View File

@ -1,32 +0,0 @@
from pydantic import BaseModel, Field
from typing import Optional
from sqlmodel import SQLModel
#API models
class Token(BaseModel):
access_token: str
token_type: str
class RefResponse(BaseModel):
ref: str
description: str
class RefAddRequest(BaseModel):
description: str
class TokenRequest(BaseModel):
tg_id: int
class RegisterRequest(BaseModel):
tg_id: int
chat_id: Optional[int] = None
phone: Optional[str] = None
name: Optional[str] = None
login: Optional[str] = None
company_key: str

40
tg_models.py Normal file
View File

@ -0,0 +1,40 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from uuid import UUID
# TG Models
class RefResponse(BaseModel):
ref: str
description: str
class RefAddRequest(BaseModel):
description: str
class TokenRequest(BaseModel):
tg_id: int
class RegisterRequest(BaseModel):
tg_id: int
chat_id: Optional[int] = None
phone: Optional[str] = None
name: Optional[str] = None
login: Optional[str] = None
company_key: str
# New Response Models for TG APIs
class RefAddResponse(BaseModel):
ref: str
class RefStatItem(BaseModel):
description: str
sales: int
income: float
class RefStatResponse(BaseModel):
refData: List[RefStatItem]
class StatResponse(BaseModel):
totalSales: int
totalIncome: float
availableWithdrawal: float