From 076cdd18281bb2130aebcb31477dcd9b87de752b Mon Sep 17 00:00:00 2001 From: Redsandyg Date: Mon, 9 Jun 2025 15:27:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B2=20bff=5Fmodels.py=20=D0=B8=20sql=5Fmodels.py.=20=D0=A0?= =?UTF-8?q?=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20?= =?UTF-8?q?=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F,=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8?= =?UTF-8?q?=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82?= =?UTF-8?q?=D0=BE=D0=BA=D0=B5=D0=BD=D0=BE=D0=B2=20=D0=B2=20main.py,=20?= =?UTF-8?q?=D0=B0=20=D1=82=D0=B0=D0=BA=D0=B6=D0=B5=20=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=B7=D0=B0=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B7=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2=20fill=5Fdb.py?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B=20=D0=BA=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=B7=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=83=D1=87=D0=B5=D1=82=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D1=85=20=D0=BF=D0=BE=D0=BB=D0=B5=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=D1=82=D0=BE=D0=BA=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=D0=BC=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bff_models.py | 22 +++++++++- fill_db.py | 44 +++++++++++++++++--- main.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++---- sql_models.py | 22 ++++++++-- 4 files changed, 180 insertions(+), 19 deletions(-) diff --git a/bff_models.py b/bff_models.py index 46fa612..0e4d3d8 100644 --- a/bff_models.py +++ b/bff_models.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field, EmailStr +from pydantic import BaseModel, Field, EmailStr, ConfigDict from typing import Optional, List from datetime import datetime import uuid @@ -168,4 +168,22 @@ class AutoApproveSettingsUpdateResponse(BaseModel): class ApproveTransactionsResult(BaseModel): msg: str - approved_count: int \ No newline at end of file + approved_count: int + +# New models for integration tokens +class IntegrationTokenResponse(BaseModel): + id: int + description: str + masked_token: str + rawToken: Optional[str] = None + create_dttm: datetime + use_dttm: Optional[datetime] = None + + model_config = ConfigDict(from_attributes=True) + +class IntegrationTokenCreateRequest(BaseModel): + description: str + +class IntegrationTokenUpdateRequest(BaseModel): + id: int + description: str \ No newline at end of file diff --git a/fill_db.py b/fill_db.py index 7669157..93ea8bd 100644 --- a/fill_db.py +++ b/fill_db.py @@ -1,7 +1,7 @@ import random from uuid import uuid4 from sqlmodel import Session -from sql_models import TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance +from sql_models import TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance, IntegrationToken from sqlalchemy import text from datetime import datetime, timedelta from hashlib import sha256 @@ -90,6 +90,23 @@ def fill_db(): session.add(company) session.commit() session.refresh(company) + + # 0.1 IntegrationTokens + for _ in range(3): # Создаем 3 токена для каждой компании + new_token_value = str(uuid4()) # Генерируем уникальный токен + token_hash = sha256(new_token_value.encode()).hexdigest() # Хешируем токен для хранения + masked_token = new_token_value[:5] + "***********************" + new_token_value[-4:] # Генерируем замаскированный токен + + integration_token = IntegrationToken( + description=random.choice(DESCRIPTIONS), # Используем существующие описания + token_hash=token_hash, + masked_token=masked_token, + company_id=company.id, + use_dttm=random.choice(date_list) if random.random() < 0.7 else None # Пример: 70% токенов будут иметь дату использования + ) + session.add(integration_token) + session.commit() + # 1. Accounts accounts = [] for i in range(4): @@ -158,16 +175,23 @@ def fill_db(): for _ in range(sale_count): cost = round(random.uniform(100, 1000), 2) crediting = round(cost * random.uniform(0.5, 1.0), 2) - dt = random.choice(date_list) + + # Генерируем случайную дату и время в пределах последних 7 дней + end_dttm = datetime.utcnow() + start_dttm = end_dttm - timedelta(days=7) + time_diff = end_dttm - start_dttm + random_seconds = random.uniform(0, time_diff.total_seconds()) + sale_dttm = start_dttm + timedelta(seconds=random_seconds) + sale = Sale( cost=cost, crediting=crediting, ref=ref.id, sale_id=str(uuid4()), company_id=company.id, - sale_date=dt, - create_dttm=dt, - update_dttm=dt + sale_dttm=sale_dttm, + create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне + update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне ) session.add(sale) session.commit() @@ -204,13 +228,21 @@ def fill_db(): PARTNER_TRANSACTION_TYPES = ['deposit', 'agent_payout', 'service_fee'] PARTNER_TRANSACTION_STATUSES = ['process', 'done', 'error', 'new'] + waiting_transactions_to_ensure = 7 + waiting_transactions_count = 0 + for tg_agent in tg_agents: # Генерируем несколько групп транзакций для каждого агента for _ in range(random.randint(3, 6)): # От 3 до 6 групп на агента transaction_group_id = uuid4() dt = random.choice(date_list) agent_trans_amount = round(random.uniform(500, 3000), 2) - agent_trans_status = random.choice(AGENT_TRANSACTION_STATUSES) + + if waiting_transactions_count < waiting_transactions_to_ensure: + agent_trans_status = 'waiting' + waiting_transactions_count += 1 + else: + agent_trans_status = random.choice(AGENT_TRANSACTION_STATUSES) # Создаем AgentTransaction agent_transaction = AgentTransaction( diff --git a/main.py b/main.py index 9818a57..5c69819 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ from fastapi import ( Body, ) from fastapi.security import OAuth2PasswordRequestForm -from sqlmodel import SQLModel, Session, select +from sqlmodel import SQLModel, Session, select, Field from typing import Optional, List, Dict from datetime import timedelta, datetime from bff_models import ( @@ -37,7 +37,10 @@ from bff_models import ( AutoApproveSettingsRequest, ApproveTransactionsRequest, AgentTransactionResponse, - TransactionStatus + TransactionStatus, + IntegrationTokenResponse, + IntegrationTokenCreateRequest, + IntegrationTokenUpdateRequest ) from tg_models import RefAddRequest, RefResponse, RegisterRequest, RefAddResponse, RefStatResponse, StatResponse from sql_models import ( @@ -48,7 +51,8 @@ from sql_models import ( AgentTransaction, PartnerTransaction, AgentBalance, - Account + Account, + IntegrationToken ) from sqlalchemy import func import hashlib @@ -65,6 +69,7 @@ from helpers_bff import ( pwd_context, ) + # Создание движка базы данных SQLModel.metadata.create_all(AUTH_DB_ENGINE) @@ -236,11 +241,11 @@ def get_dashboard_chart_total(current_account: Account = Depends(get_current_acc # Группируем продажи по дате (день) result = db.exec( select( - func.strftime('%Y-%m-%d', Sale.sale_date).label('date'), + func.strftime('%Y-%m-%d', Sale.sale_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.sale_date)) - .order_by(func.strftime('%Y-%m-%d', Sale.sale_date)) + ).where(Sale.company_id == current_account.company_id).group_by(func.strftime('%Y-%m-%d', Sale.sale_dttm)) + .order_by(func.strftime('%Y-%m-%d', Sale.sale_dttm)) ).all() # Преобразуем результат в нужный формат data = [ @@ -368,9 +373,9 @@ def get_sales_stat( """ sales_query = select(Sale).where(Sale.company_id == current_account.company_id) if date_start: - sales_query = sales_query.where(Sale.sale_date >= date_start) + sales_query = sales_query.where(Sale.sale_dttm >= date_start) if date_end: - sales_query = sales_query.where(Sale.sale_date <= date_end) + sales_query = sales_query.where(Sale.sale_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 [] @@ -745,3 +750,93 @@ def approve_agent_transactions( db.commit() return {"msg": f"Переведено в статус NEW {approved_count} транзакций", "approved_count": approved_count} + +# --- Новый функционал для интеграционных токенов --- + +@app.get("/account/integration-tokens", tags=["bff", "account"], response_model=List[IntegrationTokenResponse]) +def get_integration_tokens( + current_account: Account = Depends(get_current_account), + db: Session = Depends(get_db) +): + """ + Возвращает список интеграционных токенов для компании текущего пользователя. + """ + tokens = db.exec(select(IntegrationToken).where(IntegrationToken.company_id == current_account.company_id)).all() + return tokens # Позволяем FastAPI самостоятельно сериализовать объекты IntegrationToken в IntegrationTokenResponse + + +@app.post("/account/integration-tokens", tags=["bff", "account"], response_model=IntegrationTokenResponse) +def create_integration_token( + req: IntegrationTokenCreateRequest, + current_account: Account = Depends(get_current_account), + db: Session = Depends(get_db) +): + """ + Создает новый интеграционный токен для компании текущего пользователя. + Возвращает созданный токен (замаскированный). + """ + new_token_value = str(uuid.uuid4()) # Генерируем уникальный токен + token_hash = hashlib.sha256(new_token_value.encode()).hexdigest() # Хешируем токен для хранения + + # Генерируем замаскированный токен + masked_token = new_token_value[:5] + "***********************" + new_token_value[-4:] + + new_integration_token = IntegrationToken( + description=req.description, + token_hash=token_hash, + masked_token=masked_token, + company_id=current_account.company_id, + ) + + db.add(new_integration_token) + db.commit() + db.refresh(new_integration_token) + + # Создаем объект ответа, используя model_validate для извлечения данных из new_integration_token + response_token = IntegrationTokenResponse.model_validate(new_integration_token) + response_token.rawToken = new_token_value # Добавляем незамаскированный токен + + return response_token + +@app.post("/account/integration-tokens/update-description", tags=["bff", "account"], response_model=Dict[str, str]) +def update_integration_token_description( + req: IntegrationTokenUpdateRequest, + current_account: Account = Depends(get_current_account), + db: Session = Depends(get_db) +): + """ + Обновляет описание интеграционного токена по его ID. + """ + token = db.exec( + select(IntegrationToken).where(IntegrationToken.id == req.id).where(IntegrationToken.company_id == current_account.company_id) + ).first() + + if not token: + raise HTTPException(status_code=404, detail="Токен не найден") + + token.description = req.description + token.update_dttm = datetime.utcnow() # Обновляем дату изменения, если поле существует + db.add(token) + db.commit() + db.refresh(token) + return {"msg": "Описание токена обновлено успешно"} + +@app.delete("/account/integration-tokens/{token_id}", tags=["bff", "account"], response_model=Dict[str, str]) +def delete_integration_token( + token_id: int, + current_account: Account = Depends(get_current_account), + db: Session = Depends(get_db) +): + """ + Удаляет интеграционный токен по его ID для компании текущего пользователя. + """ + token = db.exec( + select(IntegrationToken).where(IntegrationToken.id == token_id).where(IntegrationToken.company_id == current_account.company_id) + ).first() + + if not token: + raise HTTPException(status_code=404, detail="Токен не найден") + + db.delete(token) + db.commit() + return {"msg": "Токен удален успешно"} diff --git a/sql_models.py b/sql_models.py index e4ba7e6..1a1dc04 100644 --- a/sql_models.py +++ b/sql_models.py @@ -1,7 +1,8 @@ -from typing import Optional +from typing import Optional, List from datetime import datetime import uuid -from sqlmodel import SQLModel, Field +from sqlmodel import SQLModel, Field, Relationship +from sqlalchemy import Column, String class Company(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) @@ -12,6 +13,8 @@ class Company(SQLModel, table=True): update_dttm: datetime = Field(default_factory=datetime.utcnow) auto_approve_transactions: bool = Field(default=False) + integration_tokens: List["IntegrationToken"] = Relationship(back_populates="company") + class TgAgent(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) tg_id: int = Field(index=True, unique=True) @@ -91,4 +94,17 @@ class Account(SQLModel, table=True): 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 + update_dttm: datetime = Field(default_factory=datetime.utcnow) + +# Новая модель для интеграционных токенов +class IntegrationToken(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + description: str + token_hash: str = Field(sa_column=Column(String, unique=True, index=True)) + masked_token: str = Field(sa_column=Column(String)) + company_id: int = Field(foreign_key="company.id") + create_dttm: datetime = Field(default_factory=datetime.utcnow, nullable=False) + update_dttm: datetime = Field(default_factory=datetime.utcnow, nullable=False) + use_dttm: Optional[datetime] = None + + company: Company = Relationship(back_populates="integration_tokens") \ No newline at end of file