Добавлены новые поля для агентской комиссии в модели Company и CompanyProfileResponse. Реализованы функции для обработки продаж через интеграционный API, включая создание и регистрацию продаж с учетом агентской комиссии. Обновлены соответствующие эндпоинты и модели для работы с токенами и продажами. Улучшена логика обработки транзакций и обновления балансов компаний и агентов.

This commit is contained in:
Redsandyg 2025-06-10 14:15:46 +03:00
parent 076cdd1828
commit 1cc18e0364
9 changed files with 312 additions and 160 deletions

View File

@ -144,6 +144,7 @@ class CompanyProfileResponse(BaseModel):
name: str name: str
key: str key: str
commission: float commission: float
agent_commission: float
class AccountProfileResponse(BaseModel): class AccountProfileResponse(BaseModel):
firstName: Optional[str] = None firstName: Optional[str] = None

60
call_sale_api.py Normal file
View File

@ -0,0 +1,60 @@
import requests
import json
# Конфигурация API
BASE_URL = "http://127.0.0.1:8001"
API_KEY = "7c06945b-f4b8-4929-8350-b9841405a609"
REF = "8c514fcb-7a79-4ed1-9c8a-0af2fcab88c0"
# Данные для запроса на создание продажи
# Замените эти значения на актуальные для вашей продажи
sale_data = {
"cost": 100.50, # Стоимость продажи
"ref": REF, # Ваш реферальный код
"sale_id": "UNIQUE_SALE_ID_12345" # Уникальный идентификатор продажи для вашей компании
}
# Эндпоинты
token_endpoint = f"{BASE_URL}/token"
sale_endpoint = f"{BASE_URL}/sale"
# Шаг 1: Получение JWT токена
print(f"Отправка запроса на получение токена на {token_endpoint}")
token_headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json"
}
try:
token_response = requests.post(token_endpoint, headers=token_headers)
token_response.raise_for_status()
token_data = token_response.json()
jwt_token = token_data["access_token"]
print("JWT токен успешно получен.")
except requests.exceptions.RequestException as e:
print(f"Произошла ошибка при получении токена: {e}")
if hasattr(e, 'response') and e.response is not None:
print("Тело ответа с ошибкой:", e.response.json())
exit() # Прерываем выполнение, если не удалось получить токен
# Шаг 2: Вызов эндпоинта /sale с использованием полученного JWT токена
headers_with_jwt = {
"Authorization": f"Bearer {jwt_token}",
"Content-Type": "application/json"
}
print(f"Отправка запроса на {sale_endpoint} с данными: {sale_data}")
try:
sale_response = requests.post(sale_endpoint, headers=headers_with_jwt, data=json.dumps(sale_data))
sale_response.raise_for_status() # Вызовет исключение для ошибок HTTP (4xx или 5xx)
print("Статус ответа:", sale_response.status_code)
print("Тело ответа:", sale_response.json())
except requests.exceptions.RequestException as e:
print(f"Произошла ошибка при вызове API sale: {e}")
if hasattr(e, 'response') and e.response is not None:
print("Тело ответа с ошибкой:", e.response.json())

View File

@ -85,6 +85,7 @@ def fill_db():
company = Company( company = Company(
name="RE: Premium", name="RE: Premium",
commission=10.0, commission=10.0,
agent_commission=15.0,
key="re-premium-key", key="re-premium-key",
) )
session.add(company) session.add(company)
@ -174,7 +175,7 @@ def fill_db():
sale_count = random.randint(20, int(20 * 1.25)) # от 20 до 25 sale_count = random.randint(20, int(20 * 1.25)) # от 20 до 25
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 * (company.agent_commission / 100.0), 2)
# Генерируем случайную дату и время в пределах последних 7 дней # Генерируем случайную дату и время в пределах последних 7 дней
end_dttm = datetime.utcnow() end_dttm = datetime.utcnow()

View File

@ -3,12 +3,13 @@ from passlib.context import CryptContext
from typing import Optional from typing import Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from bff_models import Token, TransactionStatus from bff_models import Token, TransactionStatus
from sql_models import Company, TgAgent, Account, AgentBalance, AgentTransaction, PartnerTransaction, Sale, Ref from sql_models import Company, TgAgent, Account, AgentBalance, AgentTransaction, PartnerTransaction, Sale, Ref, IntegrationToken, CompanyBalance
from hashlib import sha256 from hashlib import sha256
import jwt import jwt
from jwt.exceptions import InvalidTokenError from jwt.exceptions import InvalidTokenError
from fastapi import HTTPException, status, Depends, Request from fastapi import HTTPException, status, Depends, Request, Header
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
import hashlib
# Конфигурация # Конфигурация
AUTH_DATABASE_ADDRESS = "sqlite:///partner.db" AUTH_DATABASE_ADDRESS = "sqlite:///partner.db"
@ -18,9 +19,60 @@ SECRET_KEY = "supersecretkey"
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 ACCESS_TOKEN_EXPIRE_MINUTES = 60
# JWT Configuration for Integration API
INTEGRATION_SECRET_KEY = "your-super-secret-jwt-key" # TODO: Замените на реальный секретный ключ из переменных окружения
INTEGRATION_ALGORITHM = "HS256"
INTEGRATION_ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # Токен действителен 7 дней
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_integration_db():
with Session(AUTH_DB_ENGINE) as session:
yield session
def create_integration_jwt_token(company_id: int):
expires = datetime.utcnow() + timedelta(minutes=INTEGRATION_ACCESS_TOKEN_EXPIRE_MINUTES)
payload = {
"sub": str(company_id),
"exp": expires,
"type": "access"
}
return jwt.encode(payload, INTEGRATION_SECRET_KEY, algorithm=INTEGRATION_ALGORITHM)
async def get_current_company_from_jwt(
token: str = Depends(OAuth2PasswordBearer(tokenUrl="/token")),
db: Session = Depends(get_integration_db)
):
"""
Зависимость для получения текущей компании на основе JWT токена для Integration API.
"""
try:
payload = jwt.decode(token, INTEGRATION_SECRET_KEY, algorithms=[INTEGRATION_ALGORITHM])
company_id: int = int(payload.get("sub"))
if company_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Недействительная полезная нагрузка токена",
headers={"WWW-Authenticate": "Bearer"},
)
company = db.exec(select(Company).where(Company.id == company_id)).first()
if not company:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Компания не найдена")
return company
except InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Недействительный токен",
headers={"WWW-Authenticate": "Bearer"},
)
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Срок действия токена истек",
headers={"WWW-Authenticate": "Bearer"},
)
def get_tg_agent_by_tg_id(db: Session, tg_id: int) -> Optional[TgAgent]: def get_tg_agent_by_tg_id(db: Session, tg_id: int) -> Optional[TgAgent]:
statement = select(TgAgent).where(TgAgent.tg_id == tg_id) statement = select(TgAgent).where(TgAgent.tg_id == tg_id)
return db.exec(statement).first() return db.exec(statement).first()

View File

@ -1,75 +1,169 @@
from fastapi import FastAPI, HTTPException, status, Depends, Request from fastapi import FastAPI, Depends, HTTPException, status, Header
from fastapi.security import OAuth2PasswordBearer
from sqlmodel import Session, select from sqlmodel import Session, select
from typing import Optional from typing import Optional
from datetime import datetime, timedelta from datetime import datetime
import jwt import hashlib
from jwt.exceptions import InvalidTokenError import uuid
from pydantic import BaseModel
from sql_models import Sale from sql_models import Company, IntegrationToken, Ref, Sale, AgentTransaction, PartnerTransaction, AgentBalance, TgAgent, CompanyBalance
from helpers_bff import get_db, AUTH_DB_ENGINE # Assuming these are the correct imports from integration_models import Token, SaleCreateRequest, SaleCreateResponse, TransactionStatus
from helpers_bff import AUTH_DB_ENGINE, get_integration_db, create_integration_jwt_token, get_current_company_from_jwt
app = FastAPI() app = FastAPI()
# Конфигурация для интеграционного API токена #7c06945b-f4b8-4929-8350-b9841405a609
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): @app.post("/token", tags=["integration"], response_model=Token)
cost: float async def get_token_for_api_key(
crediting: float x_api_key: str = Header(..., alias="X-API-Key"),
ref_id: int db: Session = Depends(get_integration_db)
sale_id: str ):
company_id: int """
sale_date: datetime = datetime.utcnow() Обменивает API-ключ на JWT токен.
"""
def create_integration_access_token(data: dict, expires_delta: timedelta = None): if not x_api_key:
to_encode = data.copy() raise HTTPException(
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, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate integration credentials", detail="API-ключ не предоставлен",
headers={"WWW-Authenticate": "Bearer"}, 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) api_key_hash = hashlib.sha256(x_api_key.encode()).hexdigest()
async def upload_sale(sale_data: SaleCreate, db: Session = Depends(get_db), verified: bool = Depends(verify_integration_token)): integration_token_db = db.exec(
if not verified: select(IntegrationToken).where(IntegrationToken.token_hash == api_key_hash)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authorized") ).first()
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) if not integration_token_db:
db.add(db_sale) raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверный API-ключ",
headers={"WWW-Authenticate": "Bearer"},
)
# Обновляем use_dttm токена
integration_token_db.use_dttm = datetime.utcnow()
db.add(integration_token_db)
db.commit() db.commit()
db.refresh(db_sale) db.refresh(integration_token_db)
return {"message": "Sale uploaded successfully", "sale_id": db_sale.id}
@app.get("/generate-integration-token") jwt_token = create_integration_jwt_token(integration_token_db.company_id)
async def generate_token_endpoint(client_id: str): return {"access_token": jwt_token, "token_type": "bearer"}
token_data = {"client_id": client_id}
token = create_integration_access_token(token_data) @app.post("/sale", tags=["integration"], response_model=SaleCreateResponse)
return {"access_token": token, "token_type": "bearer"} async def create_sale(
req: SaleCreateRequest,
company: Company = Depends(get_current_company_from_jwt), # Используем новую зависимость
db: Session = Depends(get_integration_db)
):
"""
Регистрирует новую продажу в системе.
"""
# 1. Найти Ref по `ref` и `company.id`
# Сначала находим TgAgent, связанный с компанией, затем Ref
tg_agent = db.exec(
select(TgAgent)
.join(Ref)
.where(TgAgent.company_id == company.id)
.where(Ref.ref == req.ref)
).first()
if not tg_agent:
raise HTTPException(status_code=404, detail="Реферальная ссылка не найдена или не принадлежит данной компании")
referral = db.exec(select(Ref).where(Ref.ref == req.ref).where(Ref.tg_agent_id == tg_agent.id)).first()
if not referral:
raise HTTPException(status_code=404, detail="Реферальная ссылка не найдена")
# 2. Проверить, что sale_id уникален для данной компании
existing_sale = db.exec(
select(Sale)
.where(Sale.company_id == company.id)
.where(Sale.sale_id == req.sale_id)
).first()
if existing_sale:
raise HTTPException(status_code=400, detail="Продажа с таким sale_id уже существует для данной компании")
# 3. Рассчитать crediting
crediting_amount = req.cost * (company.agent_commission / 100.0)
# 4. Создать Sale
new_sale = Sale(
cost=req.cost,
crediting=crediting_amount,
ref=referral.id,
sale_id=req.sale_id,
company_id=company.id,
sale_dttm=datetime.utcnow()
)
db.add(new_sale)
db.commit()
db.refresh(new_sale)
# 5. Обновить AgentBalance и CompanyBalance
# AgentBalance
agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == tg_agent.id)).first()
if not agent_balance:
agent_balance = AgentBalance(tg_agent_id=tg_agent.id, available_balance=0.0, frozen_balance=0.0)
db.add(agent_balance)
db.commit()
db.refresh(agent_balance)
# CompanyBalance
company_balance = db.exec(select(CompanyBalance).where(CompanyBalance.company_id == company.id)).first()
if not company_balance:
company_balance = CompanyBalance(company_id=company.id, available_balance=0.0, pending_balance=0.0)
db.add(company_balance)
db.commit()
db.refresh(company_balance)
# Создать AgentTransaction
agent_transaction_status = TransactionStatus.NEW if company.auto_approve_transactions else TransactionStatus.WAITING
agent_transaction = AgentTransaction(
tg_agent_id=tg_agent.id,
amount=crediting_amount,
status=agent_transaction_status.value,
transaction_group=uuid.uuid4()
)
db.add(agent_transaction)
db.commit()
db.refresh(agent_transaction)
# Создать PartnerTransaction
partner_transaction_status = TransactionStatus.NEW if company.auto_approve_transactions else TransactionStatus.PROCESS
partner_transaction = PartnerTransaction(
company_id=company.id,
type="sale_crediting",
amount=crediting_amount,
status=partner_transaction_status.value,
transaction_group=agent_transaction.transaction_group,
agent_transaction_id=agent_transaction.id
)
db.add(partner_transaction)
db.commit()
db.refresh(partner_transaction)
# Обновление балансов в зависимости от auto_approve_transactions
if company.auto_approve_transactions:
agent_balance.available_balance += crediting_amount
company_balance.available_balance -= crediting_amount
company_balance.updated_dttm = datetime.utcnow()
else:
agent_balance.frozen_balance += crediting_amount
company_balance.pending_balance -= crediting_amount
company_balance.updated_dttm = datetime.utcnow()
db.add(agent_balance)
db.add(company_balance)
db.commit()
db.refresh(agent_balance)
db.refresh(company_balance)
return {
"msg": "Продажа успешно зарегистрирована",
"sale_id": new_sale.sale_id,
"crediting": new_sale.crediting
}

34
integration_models.py Normal file
View File

@ -0,0 +1,34 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
import uuid
from enum import Enum
# Models for /token endpoint
class Token(BaseModel):
access_token: str
token_type: str
class IntegrationTokenResponse(BaseModel):
msg: str
company_name: str
company_key: str
# Models for /sale endpoint
class SaleCreateRequest(BaseModel):
ref: str
sale_id: str
cost: float
class SaleCreateResponse(BaseModel):
msg: str
sale_id: str
crediting: float
class TransactionStatus(str, Enum):
NEW = "new"
PROCESS = "process"
WAITING = "waiting"
DONE = "done"
CANCELED = "canceled"
ERROR = "error"

View File

@ -544,7 +544,8 @@ def get_account_profile(current_account: Account = Depends(get_current_account),
"company": { "company": {
"name": company.name, "name": company.name,
"key": company.key, "key": company.key,
"commission": company.commission "commission": company.commission,
"agent_commission": company.agent_commission
} }
} }

View File

@ -1,146 +1,54 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt
annotated-types==0.7.0 annotated-types==0.7.0
# via pydantic
anyio==4.8.0 anyio==4.8.0
# via
# httpx
# starlette
# watchfiles
bcrypt==4.2.1 bcrypt==4.2.1
# via passlib
behave==1.2.6 behave==1.2.6
# via epai-auth (pyproject.toml)
certifi==2024.12.14 certifi==2024.12.14
# via
# httpcore
# httpx
# requests
cffi==1.17.1 cffi==1.17.1
# via cryptography
charset-normalizer==3.4.2 charset-normalizer==3.4.2
# via requests
click==8.1.8 click==8.1.8
# via colorama==0.4.6
# rich-toolkit
# typer
# uvicorn
cryptography==44.0.0 cryptography==44.0.0
# via epai-auth (pyproject.toml)
dnspython==2.7.0 dnspython==2.7.0
# via email-validator
email-validator==2.2.0 email-validator==2.2.0
# via fastapi
fastapi==0.115.6 fastapi==0.115.6
# via epai-auth (pyproject.toml)
fastapi-cli==0.0.7 fastapi-cli==0.0.7
# via fastapi
greenlet==3.1.1 greenlet==3.1.1
# via sqlalchemy
h11==0.14.0 h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==1.0.7 httpcore==1.0.7
# via httpx
httptools==0.6.4 httptools==0.6.4
# via uvicorn
httpx==0.28.1 httpx==0.28.1
# via fastapi
idna==3.10 idna==3.10
# via
# anyio
# email-validator
# httpx
# requests
jinja2==3.1.5 jinja2==3.1.5
# via fastapi
markdown-it-py==3.0.0 markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2 markupsafe==3.0.2
# via jinja2
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py
parse==1.20.2 parse==1.20.2
# via
# behave
# parse-type
parse-type==0.6.4 parse-type==0.6.4
# via behave
passlib==1.7.4 passlib==1.7.4
# via epai-auth (pyproject.toml)
psycopg2==2.9.10 psycopg2==2.9.10
# via epai-auth (pyproject.toml)
pycparser==2.22 pycparser==2.22
# via cffi
pydantic==2.10.5 pydantic==2.10.5
# via
# fastapi
# pydantic-settings
# sqlmodel
pydantic-core==2.27.2 pydantic-core==2.27.2
# via pydantic
pydantic-settings==2.9.1 pydantic-settings==2.9.1
# via epai-auth (pyproject.toml)
pygments==2.19.1 pygments==2.19.1
# via rich
pyjwt==2.10.1 pyjwt==2.10.1
# via epai-auth (pyproject.toml)
python-dotenv==1.0.1 python-dotenv==1.0.1
# via
# pydantic-settings
# uvicorn
python-multipart==0.0.20 python-multipart==0.0.20
# via fastapi
pyyaml==6.0.2 pyyaml==6.0.2
# via uvicorn
requests==2.32.3 requests==2.32.3
# via epai-auth (pyproject.toml)
rich==13.9.4 rich==13.9.4
# via
# rich-toolkit
# typer
rich-toolkit==0.12.0 rich-toolkit==0.12.0
# via fastapi-cli
ruff==0.9.1 ruff==0.9.1
# via epai-auth (pyproject.toml)
shellingham==1.5.4 shellingham==1.5.4
# via typer
six==1.17.0 six==1.17.0
# via
# behave
# parse-type
sniffio==1.3.1 sniffio==1.3.1
# via anyio
sqlalchemy==2.0.37 sqlalchemy==2.0.37
# via sqlmodel
sqlmodel==0.0.22 sqlmodel==0.0.22
# via epai-auth (pyproject.toml)
starlette==0.41.3 starlette==0.41.3
# via fastapi
typer==0.15.1 typer==0.15.1
# via fastapi-cli
typing-extensions==4.12.2 typing-extensions==4.12.2
# via
# anyio
# fastapi
# pydantic
# pydantic-core
# rich-toolkit
# sqlalchemy
# typer
# typing-inspection
typing-inspection==0.4.0 typing-inspection==0.4.0
# via pydantic-settings
urllib3==2.4.0 urllib3==2.4.0
# via requests
uvicorn==0.34.0 uvicorn==0.34.0
# via
# fastapi
# fastapi-cli
#uvloop==0.21.0
# via uvicorn
watchfiles==1.0.4 watchfiles==1.0.4
# via uvicorn
websockets==14.1 websockets==14.1
# via uvicorn

View File

@ -7,7 +7,8 @@ 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)
name: str name: str
commission: float # процент комиссии commission: float # процент комиссии, который взымается за пользование сервисом
agent_commission: float = Field(default=0.0) # процент от продаж, который начисляется агенту
key: str = Field(index=True, unique=True) key: str = Field(index=True, unique=True)
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)