Compare commits

..

2 Commits

5 changed files with 256 additions and 18 deletions

View File

@ -1,4 +1,4 @@
from pydantic import BaseModel, Field, EmailStr from pydantic import BaseModel, Field, EmailStr, ConfigDict
from typing import Optional, List from typing import Optional, List
from datetime import datetime from datetime import datetime
import uuid import uuid
@ -169,3 +169,21 @@ class AutoApproveSettingsUpdateResponse(BaseModel):
class ApproveTransactionsResult(BaseModel): class ApproveTransactionsResult(BaseModel):
msg: str msg: str
approved_count: int 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

View File

@ -1,7 +1,7 @@
import random import random
from uuid import uuid4 from uuid import uuid4
from sqlmodel import Session 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 sqlalchemy import text
from datetime import datetime, timedelta from datetime import datetime, timedelta
from hashlib import sha256 from hashlib import sha256
@ -90,6 +90,23 @@ def fill_db():
session.add(company) session.add(company)
session.commit() session.commit()
session.refresh(company) 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 # 1. Accounts
accounts = [] accounts = []
for i in range(4): for i in range(4):
@ -158,15 +175,23 @@ def fill_db():
for _ in range(sale_count): for _ in range(sale_count):
cost = round(random.uniform(100, 1000), 2) cost = round(random.uniform(100, 1000), 2)
crediting = round(cost * random.uniform(0.5, 1.0), 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( sale = Sale(
cost=cost, cost=cost,
crediting=crediting, crediting=crediting,
ref=ref.id, ref=ref.id,
sale_id=str(uuid4()), sale_id=str(uuid4()),
company_id=company.id, company_id=company.id,
create_dttm=dt, sale_dttm=sale_dttm,
update_dttm=dt create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне
update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне
) )
session.add(sale) session.add(sale)
session.commit() session.commit()
@ -203,12 +228,20 @@ def fill_db():
PARTNER_TRANSACTION_TYPES = ['deposit', 'agent_payout', 'service_fee'] PARTNER_TRANSACTION_TYPES = ['deposit', 'agent_payout', 'service_fee']
PARTNER_TRANSACTION_STATUSES = ['process', 'done', 'error', 'new'] PARTNER_TRANSACTION_STATUSES = ['process', 'done', 'error', 'new']
waiting_transactions_to_ensure = 7
waiting_transactions_count = 0
for tg_agent in tg_agents: for tg_agent in tg_agents:
# Генерируем несколько групп транзакций для каждого агента # Генерируем несколько групп транзакций для каждого агента
for _ in range(random.randint(3, 6)): # От 3 до 6 групп на агента for _ in range(random.randint(3, 6)): # От 3 до 6 групп на агента
transaction_group_id = uuid4() transaction_group_id = uuid4()
dt = random.choice(date_list) dt = random.choice(date_list)
agent_trans_amount = round(random.uniform(500, 3000), 2) agent_trans_amount = round(random.uniform(500, 3000), 2)
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) agent_trans_status = random.choice(AGENT_TRANSACTION_STATUSES)
# Создаем AgentTransaction # Создаем AgentTransaction

75
integration_api.py Normal file
View File

@ -0,0 +1,75 @@
from fastapi import FastAPI, HTTPException, status, Depends, Request
from sqlmodel import Session, select
from typing import Optional
from datetime import datetime, timedelta
import jwt
from jwt.exceptions import InvalidTokenError
from pydantic import BaseModel
from sql_models import Sale
from helpers_bff import get_db, AUTH_DB_ENGINE # Assuming these are the correct imports
app = FastAPI()
# Конфигурация для интеграционного API токена
INTEGRATION_SECRET_KEY = "your-integration-super-secret-key" # Смените это на безопасный ключ!
INTEGRATION_ALGORITHM = "HS256"
INTEGRATION_TOKEN_EXPIRE_MINUTES = 60 * 24 # 24 часа
class IntegrationTokenData(BaseModel):
client_id: str
class SaleCreate(BaseModel):
cost: float
crediting: float
ref_id: int
sale_id: str
company_id: int
sale_date: datetime = datetime.utcnow()
def create_integration_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=INTEGRATION_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, INTEGRATION_SECRET_KEY, algorithm=INTEGRATION_ALGORITHM)
return encoded_jwt
async def verify_integration_token(request: Request):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate integration credentials",
headers={"WWW-Authenticate": "Bearer"},
)
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise credentials_exception
token = auth_header.replace("Bearer ", "").strip()
try:
payload = jwt.decode(token, INTEGRATION_SECRET_KEY, algorithms=[INTEGRATION_ALGORITHM])
client_id: str = payload.get("client_id")
if client_id is None:
raise credentials_exception
# Здесь вы можете добавить логику для проверки client_id, например, из базы данных
except InvalidTokenError:
raise credentials_exception
return True # Токен действителен
@app.post("/sale", status_code=status.HTTP_201_CREATED)
async def upload_sale(sale_data: SaleCreate, db: Session = Depends(get_db), verified: bool = Depends(verify_integration_token)):
if not verified:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authorized")
db_sale = Sale(cost=sale_data.cost, crediting=sale_data.crediting, ref=sale_data.ref_id, sale_id=sale_data.sale_id, company_id=sale_data.company_id, sale_date=sale_data.sale_date)
db.add(db_sale)
db.commit()
db.refresh(db_sale)
return {"message": "Sale uploaded successfully", "sale_id": db_sale.id}
@app.get("/generate-integration-token")
async def generate_token_endpoint(client_id: str):
token_data = {"client_id": client_id}
token = create_integration_access_token(token_data)
return {"access_token": token, "token_type": "bearer"}

111
main.py
View File

@ -8,7 +8,7 @@ from fastapi import (
Body, Body,
) )
from fastapi.security import OAuth2PasswordRequestForm 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 typing import Optional, List, Dict
from datetime import timedelta, datetime from datetime import timedelta, datetime
from bff_models import ( from bff_models import (
@ -37,7 +37,10 @@ from bff_models import (
AutoApproveSettingsRequest, AutoApproveSettingsRequest,
ApproveTransactionsRequest, ApproveTransactionsRequest,
AgentTransactionResponse, AgentTransactionResponse,
TransactionStatus TransactionStatus,
IntegrationTokenResponse,
IntegrationTokenCreateRequest,
IntegrationTokenUpdateRequest
) )
from tg_models import RefAddRequest, RefResponse, RegisterRequest, RefAddResponse, RefStatResponse, StatResponse from tg_models import RefAddRequest, RefResponse, RegisterRequest, RefAddResponse, RefStatResponse, StatResponse
from sql_models import ( from sql_models import (
@ -48,7 +51,8 @@ from sql_models import (
AgentTransaction, AgentTransaction,
PartnerTransaction, PartnerTransaction,
AgentBalance, AgentBalance,
Account Account,
IntegrationToken
) )
from sqlalchemy import func from sqlalchemy import func
import hashlib import hashlib
@ -65,6 +69,7 @@ from helpers_bff import (
pwd_context, pwd_context,
) )
# Создание движка базы данных # Создание движка базы данных
SQLModel.metadata.create_all(AUTH_DB_ENGINE) 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( result = db.exec(
select( select(
func.strftime('%Y-%m-%d', Sale.create_dttm).label('date'), func.strftime('%Y-%m-%d', Sale.sale_dttm).label('date'),
func.sum(Sale.cost).label('revenue'), func.sum(Sale.cost).label('revenue'),
func.count(Sale.id).label('sales') func.count(Sale.id).label('sales')
).where(Sale.company_id == current_account.company_id).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.sale_dttm))
.order_by(func.strftime('%Y-%m-%d', Sale.create_dttm)) .order_by(func.strftime('%Y-%m-%d', Sale.sale_dttm))
).all() ).all()
# Преобразуем результат в нужный формат # Преобразуем результат в нужный формат
data = [ data = [
@ -368,9 +373,9 @@ def get_sales_stat(
""" """
sales_query = select(Sale).where(Sale.company_id == current_account.company_id) sales_query = select(Sale).where(Sale.company_id == current_account.company_id)
if date_start: if date_start:
sales_query = sales_query.where(Sale.create_dttm >= date_start) sales_query = sales_query.where(Sale.sale_dttm >= date_start)
if date_end: if date_end:
sales_query = sales_query.where(Sale.create_dttm <= date_end) sales_query = sales_query.where(Sale.sale_dttm <= date_end)
sales = db.exec(sales_query).all() sales = db.exec(sales_query).all()
ref_ids = list(set(sale.ref for sale in sales)) 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 [] 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() db.commit()
return {"msg": f"Переведено в статус NEW {approved_count} транзакций", "approved_count": approved_count} 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": "Токен удален успешно"}

View File

@ -1,7 +1,8 @@
from typing import Optional from typing import Optional, List
from datetime import datetime from datetime import datetime
import uuid import uuid
from sqlmodel import SQLModel, Field from sqlmodel import SQLModel, Field, Relationship
from sqlalchemy import Column, String
class Company(SQLModel, table=True): class Company(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=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) update_dttm: datetime = Field(default_factory=datetime.utcnow)
auto_approve_transactions: bool = Field(default=False) auto_approve_transactions: bool = Field(default=False)
integration_tokens: List["IntegrationToken"] = Relationship(back_populates="company")
class TgAgent(SQLModel, table=True): class TgAgent(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
tg_id: int = Field(index=True, unique=True) tg_id: int = Field(index=True, unique=True)
@ -39,6 +42,7 @@ class Sale(SQLModel, table=True):
ref: int = Field(foreign_key="ref.id") ref: int = Field(foreign_key="ref.id")
sale_id: str sale_id: str
company_id: int = Field(foreign_key="company.id") company_id: int = Field(foreign_key="company.id")
sale_dttm: datetime = Field(default_factory=datetime.utcnow)
create_dttm: datetime = Field(default_factory=datetime.utcnow) create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow) update_dttm: datetime = Field(default_factory=datetime.utcnow)
@ -91,3 +95,16 @@ class Account(SQLModel, table=True):
company_id: int = Field(foreign_key="company.id") company_id: int = Field(foreign_key="company.id")
create_dttm: datetime = Field(default_factory=datetime.utcnow) create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow) 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")