Compare commits

..

2 Commits

8 changed files with 171 additions and 35 deletions

View File

@ -189,3 +189,20 @@ class IntegrationTokenCreateRequest(BaseModel):
class IntegrationTokenUpdateRequest(BaseModel):
id: int
description: str
# New models for sale categories
class SaleCategoryRequest(BaseModel):
id: int | None = None
category: str
description: str | None = None
perc: float
class SaleCategoryResponse(BaseModel):
id: int
category: str
description: str | None = None
perc: float
create_dttm: datetime
update_dttm: datetime
model_config = ConfigDict(from_attributes=True)

View File

@ -12,7 +12,8 @@ REF = "9bd1a6bd-98e1-48f4-a120-3b3d016011c0"
sale_data = {
"cost": 100.50, # Стоимость продажи
"ref": REF, # Ваш реферальный код
"sale_id": str(uuid.uuid4()) # Уникальный идентификатор продажи для вашей компании
"sale_id": str(uuid.uuid4()), # Уникальный идентификатор продажи для вашей компании
"category": 1 # id категории (например, 1 - basic)
}
# Эндпоинты

View File

@ -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, IntegrationToken
from sql_models import TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance, IntegrationToken, SaleCategory
from sqlalchemy import text
from datetime import datetime, timedelta
from hashlib import sha256
@ -81,6 +81,7 @@ def fill_db():
session.execute(text('DELETE FROM "partner_transactions"'))
session.execute(text('DELETE FROM "company_balances"'))
session.execute(text('DELETE FROM "agent_balances"'))
session.execute(text("DELETE FROM salecategory"))
session.execute(text("DELETE FROM company"))
session.commit()
# 0. Company
@ -110,6 +111,18 @@ def fill_db():
session.add(integration_token)
session.commit()
# 0.2 SaleCategory
sale_categories = [
SaleCategory(category="basic", description="Базовая продажа", perc=10.0, company_id=company.id),
SaleCategory(category="premium", description="Премиум продажа", perc=20.0, company_id=company.id),
SaleCategory(category="vip", description="VIP продажа", perc=30.0, company_id=company.id),
]
for cat in sale_categories:
session.add(cat)
session.commit()
for cat in sale_categories:
session.refresh(cat)
# 1. Accounts
accounts = []
for i in range(4):
@ -182,25 +195,26 @@ def fill_db():
for ref in refs:
session.refresh(ref)
# 4. Sales (минимум 20 на каждый ref)
all_categories = session.query(SaleCategory).filter_by(company_id=company.id).all()
for ref in refs:
sale_count = random.randint(20, int(20 * 1.25)) # от 20 до 25
for _ in range(sale_count):
cost = round(random.uniform(100, 1000), 2)
crediting = round(cost * (company.agent_commission / 100.0), 2)
sale_category = random.choice(all_categories)
crediting = round(cost * (sale_category.perc / 100.0), 2)
# Генерируем случайную дату и время в пределах последних 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,
category=sale_category.id,
sale_dttm=sale_dttm,
create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне
update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне

View File

@ -7,7 +7,7 @@ import uuid
from random import choices
import string
from sql_models import Company, IntegrationToken, Ref, Sale, AgentTransaction, PartnerTransaction, AgentBalance, TgAgent, CompanyBalance
from sql_models import Company, IntegrationToken, Ref, Sale, AgentTransaction, PartnerTransaction, AgentBalance, TgAgent, CompanyBalance, SaleCategory
from integration_models import Token, SaleCreateRequest, SaleCreateResponse, TransactionStatus, WithdrawRequest, WithdrawResponse
from bff_models import RegisterResponse, TgAuthResponse
from tg_models import RefAddRequest, RefResponse, RefAddResponse, RefStatResponse, RegisterRequest, StatResponse
@ -189,7 +189,7 @@ async def withdraw_funds(
@app.post("/sale", tags=["integration"], response_model=SaleCreateResponse)
async def create_sale(
req: SaleCreateRequest,
company: Company = Depends(get_current_company_from_jwt), # Используем новую зависимость
company: Company = Depends(get_current_company_from_jwt),
db: Session = Depends(get_integration_db)
):
"""
@ -198,24 +198,35 @@ async def create_sale(
# Устанавливаем уровень изоляции для текущей транзакции
db.connection(execution_options={'isolation_level': 'SERIALIZABLE'})
# 1. Найти Ref по `ref` и `company.id`
# Сначала находим TgAgent, связанный с компанией, затем Ref
# Проверка входных данных
if not req.ref and not req.promocode:
raise HTTPException(status_code=400, detail="Необходимо передать либо ref, либо promocode")
if not req.category:
raise HTTPException(status_code=400, detail="Необходимо передать category (id категории)")
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()
# 1. Найти Ref по ref и/или promocode
referral = None
if req.ref and req.promocode:
referral_by_ref = db.exec(select(Ref).where(Ref.ref == req.ref)).first()
referral_by_code = db.exec(select(Ref).where(Ref.promocode == req.promocode)).first()
if not referral_by_ref or not referral_by_code:
raise HTTPException(status_code=404, detail="Реферальная ссылка или промокод не найдены")
if referral_by_ref.id != referral_by_code.id:
raise HTTPException(status_code=400, detail="ref и promocode не соответствуют одной ссылке")
referral = referral_by_ref
elif req.ref:
referral = db.exec(select(Ref).where(Ref.ref == req.ref)).first()
if not referral:
raise HTTPException(status_code=404, detail="Реферальная ссылка не найдена")
elif req.promocode:
referral = db.exec(select(Ref).where(Ref.promocode == req.promocode)).first()
if not referral:
raise HTTPException(status_code=404, detail="Промокод не найден")
# Проверяем, что реф действительно принадлежит компании
tg_agent = db.exec(select(TgAgent).where(TgAgent.id == referral.tg_agent_id, TgAgent.company_id == company.id)).first()
if not tg_agent:
raise HTTPException(status_code=404, detail="Реферальная ссылка не принадлежит данной компании")
# 2. Проверить, что sale_id уникален для данной компании
existing_sale = db.exec(
@ -223,20 +234,19 @@ async def create_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)
# 3. Найти категорию и рассчитать crediting
sale_category = db.exec(select(SaleCategory).where(SaleCategory.category == req.category, SaleCategory.company_id == company.id)).first()
if not sale_category:
raise HTTPException(status_code=404, detail="Категория продажи не найдена")
crediting_amount = req.cost * (sale_category.perc / 100.0)
# 4. Проверить и обновить AgentBalance и CompanyBalance
# AgentBalance
agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == tg_agent.id)).first()
if not agent_balance:
raise HTTPException(status_code=404, detail="Баланс агента не найден")
# CompanyBalance
company_balance = db.exec(select(CompanyBalance).where(CompanyBalance.company_id == company.id)).first()
if not company_balance:
raise HTTPException(status_code=404, detail="Баланс компании не найден")
@ -248,12 +258,12 @@ async def create_sale(
ref=referral.id,
sale_id=req.sale_id,
company_id=company.id,
category=sale_category.id,
sale_dttm=datetime.utcnow()
)
db.add(new_sale)
# Создать AgentTransaction
agent_transaction_status = TransactionStatus.DONE # auto_approve_transactions отвечает только за апрув агентских транзакций на вывод
agent_transaction_status = TransactionStatus.DONE
agent_transaction = AgentTransaction(
tg_agent_id=tg_agent.id,
amount=crediting_amount,
@ -261,7 +271,6 @@ async def create_sale(
transaction_group=uuid.uuid4()
)
db.add(agent_transaction)
# Обновление балансов для продаж - всегда в замороженный/ожидающий баланс
agent_balance.frozen_balance += crediting_amount
company_balance.pending_balance -= crediting_amount

View File

@ -16,9 +16,11 @@ class IntegrationTokenResponse(BaseModel):
# Models for /sale endpoint
class SaleCreateRequest(BaseModel):
ref: str
ref: Optional[str] = None
promocode: Optional[str] = None
sale_id: str
cost: float
category: int # id категории продажи
class SaleCreateResponse(BaseModel):
msg: str

65
main.py
View File

@ -38,7 +38,9 @@ from bff_models import (
TransactionStatus,
IntegrationTokenResponse,
IntegrationTokenCreateRequest,
IntegrationTokenUpdateRequest
IntegrationTokenUpdateRequest,
SaleCategoryRequest,
SaleCategoryResponse
)
from sql_models import (
Company,
@ -49,7 +51,8 @@ from sql_models import (
PartnerTransaction,
AgentBalance,
Account,
IntegrationToken
IntegrationToken,
SaleCategory
)
from sqlalchemy import func
import hashlib
@ -64,6 +67,7 @@ from helpers_bff import (
pwd_context,
)
import os
from pydantic import BaseModel
# Создание движка базы данных
@ -726,3 +730,60 @@ def delete_integration_token(
db.delete(token)
db.commit()
return {"msg": "Токен удален успешно"}
# --- Категории продаж ---
@app.get("/account/category", tags=["bff", "account"], response_model=List[SaleCategoryResponse])
def get_sale_categories(
current_account: Account = Depends(get_current_account),
db: Session = Depends(get_db)
):
"""
Возвращает список всех категорий продаж компании пользователя.
"""
categories = db.exec(select(SaleCategory).where(SaleCategory.company_id == current_account.company_id)).all()
return categories
@app.post("/account/category", tags=["bff", "account"], response_model=SaleCategoryResponse)
def create_or_update_sale_category(
req: SaleCategoryRequest,
current_account: Account = Depends(get_current_account),
db: Session = Depends(get_db)
):
"""
Создает новую или обновляет существующую категорию продаж для компании пользователя.
"""
# Проверка уникальности category для компании
existing = db.exec(
select(SaleCategory)
.where(SaleCategory.category == req.category)
.where(SaleCategory.company_id == current_account.company_id)
).first()
if req.id is not None:
category = db.exec(select(SaleCategory).where(SaleCategory.id == req.id, SaleCategory.company_id == current_account.company_id)).first()
if not category:
raise HTTPException(status_code=404, detail="Категория не найдена")
# Если меняем имя, оно не должно совпадать с другой категорией
if existing and existing.id != req.id:
raise HTTPException(status_code=400, detail="Категория с таким именем уже существует")
category.category = req.category
category.description = req.description
category.perc = req.perc
category.update_dttm = datetime.utcnow()
db.add(category)
else:
if existing:
raise HTTPException(status_code=400, detail="Категория с таким именем уже существует")
now = datetime.utcnow()
category = SaleCategory(
category=req.category,
description=req.description,
perc=req.perc,
company_id=current_account.company_id,
create_dttm=now,
update_dttm=now
)
db.add(category)
db.commit()
db.refresh(category)
return SaleCategoryResponse.model_validate(category)

View File

@ -62,6 +62,21 @@ CREATE TABLE integrationtoken (
;
CREATE TABLE salecategory (
id INTEGER NOT NULL,
category VARCHAR NOT NULL,
description VARCHAR,
perc FLOAT NOT NULL,
company_id INTEGER NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(company_id) REFERENCES company (id)
)
;
CREATE TABLE tgagent (
id INTEGER NOT NULL,
tg_id INTEGER NOT NULL,
@ -150,12 +165,14 @@ CREATE TABLE sale (
ref INTEGER NOT NULL,
sale_id VARCHAR NOT NULL,
company_id INTEGER NOT NULL,
category INTEGER NOT NULL,
sale_dttm DATETIME NOT NULL,
create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(ref) REFERENCES ref (id),
FOREIGN KEY(company_id) REFERENCES company (id)
FOREIGN KEY(company_id) REFERENCES company (id),
FOREIGN KEY(category) REFERENCES salecategory (id)
)
;

View File

@ -20,6 +20,7 @@ class Company(SQLModel, table=True):
partner_transactions: List["PartnerTransaction"] = Relationship(back_populates="company")
company_balance: Optional["CompanyBalance"] = Relationship(back_populates="company")
accounts: List["Account"] = Relationship(back_populates="company")
sale_categories: List["SaleCategory"] = Relationship(back_populates="company")
class TgAgent(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
@ -50,6 +51,18 @@ class Ref(SQLModel, table=True):
tg_agent: "TgAgent" = Relationship(back_populates="refs")
sales: List["Sale"] = Relationship(back_populates="ref_obj")
class SaleCategory(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
category: str
description: Optional[str] = None
perc: float # процент начисления партнеру
company_id: int = Field(foreign_key="company.id")
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
company: "Company" = Relationship(back_populates="sale_categories")
sales: List["Sale"] = Relationship(back_populates="sale_category")
class Sale(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
cost: float
@ -57,12 +70,14 @@ class Sale(SQLModel, table=True):
ref: int = Field(foreign_key="ref.id")
sale_id: str
company_id: int = Field(foreign_key="company.id")
category: int = Field(foreign_key="salecategory.id") # новая ссылка на категорию
sale_dttm: datetime = Field(default_factory=datetime.utcnow)
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
ref_obj: "Ref" = Relationship(back_populates="sales")
company: "Company" = Relationship(back_populates="sales")
sale_category: "SaleCategory" = Relationship(back_populates="sales")
class AgentTransaction(SQLModel, table=True):
__tablename__ = "agent_transactions"