From 6e804953c094f051747c5254180474e5ffc54d4d Mon Sep 17 00:00:00 2001 From: Redsandyg Date: Sun, 8 Jun 2025 21:23:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D1=80=D1=8B=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= =?UTF-8?q?:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=B8=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=B1=D0=B0=D0=B7=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB=20helpers?= =?UTF-8?q?=5Fbff.py.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20=D0=B2=20?= =?UTF-8?q?fill=5Fdb.py=20=D0=B8=20main.py=20=D0=B4=D0=BB=D1=8F=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=B9.=20=D0=A3=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D1=88=D0=B8=D0=B5=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D1=8B=20=D0=B8=D0=B7?= =?UTF-8?q?=20main.py,=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BE=D1=80=D0=B3=D0=B0=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=BE=D0=B4=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fill_db.py | 9 +- helpers_bff.py | 90 ++++++++++++++ main.py | 324 +++++++++++++------------------------------------ sql_models.py | 93 ++++++++++++++ 4 files changed, 267 insertions(+), 249 deletions(-) create mode 100644 helpers_bff.py create mode 100644 sql_models.py diff --git a/fill_db.py b/fill_db.py index bac4e2f..5e70beb 100644 --- a/fill_db.py +++ b/fill_db.py @@ -1,11 +1,11 @@ import random from uuid import uuid4 from sqlmodel import Session -from main import AUTH_DB_ENGINE, TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance +from sql_models import TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance from sqlalchemy import text from datetime import datetime, timedelta from hashlib import sha256 -from passlib.context import CryptContext +from helpers_bff import AUTH_DB_ENGINE, get_password_hash # Константа: список user_ids @@ -63,11 +63,6 @@ LOGINS = [ ALL_DESCRIPTIONS = DESCRIPTIONS # --- -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - -def get_password_hash(password): - return pwd_context.hash(password) - def get_date_list(days=7): today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) return [today - timedelta(days=i) for i in range(days, -1, -1)] diff --git a/helpers_bff.py b/helpers_bff.py new file mode 100644 index 0000000..7717faa --- /dev/null +++ b/helpers_bff.py @@ -0,0 +1,90 @@ +from sqlmodel import Session, select, create_engine +from passlib.context import CryptContext +from typing import Optional +from datetime import datetime, timedelta +from bff_models import Token, TransactionStatus +from sql_models import Company, TgAgent, Account, AgentBalance, AgentTransaction, PartnerTransaction, Sale, Ref +from hashlib import sha256 +import jwt +from jwt.exceptions import InvalidTokenError +from fastapi import HTTPException, status, Depends, Request +from fastapi.security import OAuth2PasswordBearer + +# Конфигурация +AUTH_DATABASE_ADDRESS = "sqlite:///partner.db" +AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True) + +SECRET_KEY = "supersecretkey" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +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() + +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 + +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 + +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 + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +def get_account_by_login(db: Session, login: str) -> Optional[Account]: + statement = select(Account).where(Account.login == login) + return db.exec(statement).first() \ No newline at end of file diff --git a/main.py b/main.py index a41de6b..f33dc48 100644 --- a/main.py +++ b/main.py @@ -1,204 +1,76 @@ 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 fastapi import ( + FastAPI, + Depends, + HTTPException, + status, + Query, + Body, +) +from fastapi.security import OAuth2PasswordRequestForm +from sqlmodel import SQLModel, Session, select 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 datetime import timedelta, datetime +from bff_models import ( + Token, + RegisterResponse, + DashboardCardsResponse, + DashboardChartTotalResponse, + DashboardChartAgentResponse, + StatAgentsResponse, + StatReferralsResponse, + StatSalesResponse, + BillingCardsResponse, + BillingChartStatResponse, + BillingChartPieResponse, + AccountResponse, + AccountProfileResponse, + AccountProfileUpdateResponse, + AccountPasswordChangeResponse, + AutoApproveSettingsGetResponse, + AutoApproveSettingsUpdateResponse, + ApproveTransactionsResult, + TgAuthResponse, + BillingPayoutsTransactionsResponse, + AccountProfileUpdateRequest, + AccountPasswordChangeRequest, + AutoApproveSettingsRequest, + ApproveTransactionsRequest, + AgentTransactionResponse, + TransactionStatus +) +from tg_models import RefAddRequest, RefResponse, RegisterRequest, RefAddResponse, RefStatResponse, StatResponse +from sql_models import ( + Company, + TgAgent, + Ref, + Sale, + AgentTransaction, + PartnerTransaction, + AgentBalance, + Account +) 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 # Поле для имени агента - +import hashlib +from helpers_bff import ( + AUTH_DB_ENGINE, + get_db, + get_tg_agent_by_tg_id, + get_current_account, + get_current_tg_agent, + create_access_token, + verify_password, + get_account_by_login, + ACCESS_TOKEN_EXPIRE_MINUTES, + pwd_context, +) # Создание движка базы данных -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)): @@ -219,7 +91,7 @@ def register(req: RegisterRequest, db: Session = Depends(get_db)): 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() + hash_value = hashlib.sha256(f"{tg_id}sold".encode()).hexdigest() new_tg_agent = TgAgent( tg_id=tg_id, chat_id=chat_id, @@ -234,30 +106,6 @@ def register(req: RegisterRequest, db: Session = Depends(get_db)): 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( @@ -299,7 +147,7 @@ def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_ """ new_ref = Ref( tg_agent_id=current_tg_agent.id, - ref=str(uuid4()), + ref=str(uuid.uuid4()), description=req.description ) db.add(new_ref) @@ -662,15 +510,6 @@ def tg_auth(hash: str = Body(..., embed=True), db: Session = Depends(get_db)): 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) @@ -754,9 +593,9 @@ def change_account_password( @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 + statuses: Optional[List[TransactionStatus]] = Query(None), + date_start: str = Query(None), + date_end: str = Query(None), current_account: Account = Depends(get_current_account), db: Session = Depends(get_db) ): @@ -788,23 +627,26 @@ def get_account_agent_transactions( # Формируем список ответов в формате AgentTransactionResponse agent_transactions_response = [] for agent_trans, agent in results: + try: + status_enum = TransactionStatus(agent_trans.status) + except ValueError: + # Если статус из БД не соответствует Enum, используем статус ERROR + status_enum = TransactionStatus.ERROR agent_transactions_response.append( AgentTransactionResponse( amount=agent_trans.amount, - status=agent_trans.status, + status=status_enum, transaction_group=agent_trans.transaction_group, create_dttm=agent_trans.create_dttm, update_dttm=agent_trans.update_dttm, - agent_name=agent.name # Используем имя агента + 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( @@ -854,8 +696,8 @@ def update_auto_approve_settings( # Находим соответствующие партнерские транзакции и обновляем их статус partner_transactions_to_update = db.exec( select(PartnerTransaction) - .where(PartnerTransaction.agent_transaction_id == agent_trans.id) # Используем связь по ID - .where(PartnerTransaction.status == TransactionStatus.PROCESS) # Предполагаем, что связанные партнерские транзакции в статусе PROCESS + .where(PartnerTransaction.agent_transaction_id == agent_trans.id) + .where(PartnerTransaction.status == TransactionStatus.PROCESS) ).all() for partner_trans in partner_transactions_to_update: partner_trans.status = TransactionStatus.NEW @@ -867,9 +709,7 @@ def update_auto_approve_settings( 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( @@ -893,11 +733,11 @@ def approve_agent_transactions( .join(TgAgent) .where(TgAgent.company_id == company_id) .where(AgentTransaction.transaction_group.in_(req.transaction_ids)) - .where(AgentTransaction.status == TransactionStatus.WAITING) # Утверждаем только транзакции в статусе 'waiting' + .where(AgentTransaction.status == TransactionStatus.WAITING) ).all() for agent_trans in transactions_to_approve: - agent_trans.status = TransactionStatus.NEW # Переводим в статус 'new' + agent_trans.status = TransactionStatus.NEW agent_trans.update_dttm = datetime.utcnow() db.add(agent_trans) approved_count += 1 diff --git a/sql_models.py b/sql_models.py new file mode 100644 index 0000000..f9da77a --- /dev/null +++ b/sql_models.py @@ -0,0 +1,93 @@ +from typing import Optional +from datetime import datetime +import uuid +from sqlmodel import SQLModel, Field + +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") + amount: float + status: str + transaction_group: uuid.UUID = Field(default_factory=uuid.uuid4) + 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") + type: str + amount: float + status: str + transaction_group: uuid.UUID + 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) + available_balance: float = Field(default=0.0) + pending_balance: float = Field(default=0.0) + 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) + available_balance: float = Field(default=0.0) + frozen_balance: float = Field(default=0.0) + 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) \ No newline at end of file