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): class IntegrationTokenUpdateRequest(BaseModel):
id: int id: int
description: str 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 = { sale_data = {
"cost": 100.50, # Стоимость продажи "cost": 100.50, # Стоимость продажи
"ref": REF, # Ваш реферальный код "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 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, IntegrationToken from sql_models import TgAgent, Ref, Sale, Account, Company, AgentTransaction, PartnerTransaction, CompanyBalance, AgentBalance, IntegrationToken, SaleCategory
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
@ -81,6 +81,7 @@ def fill_db():
session.execute(text('DELETE FROM "partner_transactions"')) session.execute(text('DELETE FROM "partner_transactions"'))
session.execute(text('DELETE FROM "company_balances"')) session.execute(text('DELETE FROM "company_balances"'))
session.execute(text('DELETE FROM "agent_balances"')) session.execute(text('DELETE FROM "agent_balances"'))
session.execute(text("DELETE FROM salecategory"))
session.execute(text("DELETE FROM company")) session.execute(text("DELETE FROM company"))
session.commit() session.commit()
# 0. Company # 0. Company
@ -110,6 +111,18 @@ def fill_db():
session.add(integration_token) session.add(integration_token)
session.commit() 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 # 1. Accounts
accounts = [] accounts = []
for i in range(4): for i in range(4):
@ -182,25 +195,26 @@ def fill_db():
for ref in refs: for ref in refs:
session.refresh(ref) session.refresh(ref)
# 4. Sales (минимум 20 на каждый ref) # 4. Sales (минимум 20 на каждый ref)
all_categories = session.query(SaleCategory).filter_by(company_id=company.id).all()
for ref in refs: for ref in refs:
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 * (company.agent_commission / 100.0), 2) sale_category = random.choice(all_categories)
crediting = round(cost * (sale_category.perc / 100.0), 2)
# Генерируем случайную дату и время в пределах последних 7 дней # Генерируем случайную дату и время в пределах последних 7 дней
end_dttm = datetime.utcnow() end_dttm = datetime.utcnow()
start_dttm = end_dttm - timedelta(days=7) start_dttm = end_dttm - timedelta(days=7)
time_diff = end_dttm - start_dttm time_diff = end_dttm - start_dttm
random_seconds = random.uniform(0, time_diff.total_seconds()) random_seconds = random.uniform(0, time_diff.total_seconds())
sale_dttm = start_dttm + timedelta(seconds=random_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,
category=sale_category.id,
sale_dttm=sale_dttm, sale_dttm=sale_dttm,
create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне create_dttm=sale_dttm, # create_dttm также будет случайным в этом диапазоне
update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне update_dttm=sale_dttm # update_dttm также будет случайным в этом диапазоне

View File

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

View File

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

65
main.py
View File

@ -38,7 +38,9 @@ from bff_models import (
TransactionStatus, TransactionStatus,
IntegrationTokenResponse, IntegrationTokenResponse,
IntegrationTokenCreateRequest, IntegrationTokenCreateRequest,
IntegrationTokenUpdateRequest IntegrationTokenUpdateRequest,
SaleCategoryRequest,
SaleCategoryResponse
) )
from sql_models import ( from sql_models import (
Company, Company,
@ -49,7 +51,8 @@ from sql_models import (
PartnerTransaction, PartnerTransaction,
AgentBalance, AgentBalance,
Account, Account,
IntegrationToken IntegrationToken,
SaleCategory
) )
from sqlalchemy import func from sqlalchemy import func
import hashlib import hashlib
@ -64,6 +67,7 @@ from helpers_bff import (
pwd_context, pwd_context,
) )
import os import os
from pydantic import BaseModel
# Создание движка базы данных # Создание движка базы данных
@ -726,3 +730,60 @@ def delete_integration_token(
db.delete(token) db.delete(token)
db.commit() db.commit()
return {"msg": "Токен удален успешно"} 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 ( CREATE TABLE tgagent (
id INTEGER NOT NULL, id INTEGER NOT NULL,
tg_id INTEGER NOT NULL, tg_id INTEGER NOT NULL,
@ -150,12 +165,14 @@ CREATE TABLE sale (
ref INTEGER NOT NULL, ref INTEGER NOT NULL,
sale_id VARCHAR NOT NULL, sale_id VARCHAR NOT NULL,
company_id INTEGER NOT NULL, company_id INTEGER NOT NULL,
category INTEGER NOT NULL,
sale_dttm DATETIME NOT NULL, sale_dttm DATETIME NOT NULL,
create_dttm DATETIME NOT NULL, create_dttm DATETIME NOT NULL,
update_dttm DATETIME NOT NULL, update_dttm DATETIME NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY(ref) REFERENCES ref (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") partner_transactions: List["PartnerTransaction"] = Relationship(back_populates="company")
company_balance: Optional["CompanyBalance"] = Relationship(back_populates="company") company_balance: Optional["CompanyBalance"] = Relationship(back_populates="company")
accounts: List["Account"] = Relationship(back_populates="company") accounts: List["Account"] = Relationship(back_populates="company")
sale_categories: List["SaleCategory"] = 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)
@ -50,6 +51,18 @@ class Ref(SQLModel, table=True):
tg_agent: "TgAgent" = Relationship(back_populates="refs") tg_agent: "TgAgent" = Relationship(back_populates="refs")
sales: List["Sale"] = Relationship(back_populates="ref_obj") 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): class Sale(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
cost: float cost: float
@ -57,12 +70,14 @@ 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")
category: int = Field(foreign_key="salecategory.id") # новая ссылка на категорию
sale_dttm: datetime = Field(default_factory=datetime.utcnow) 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)
ref_obj: "Ref" = Relationship(back_populates="sales") ref_obj: "Ref" = Relationship(back_populates="sales")
company: "Company" = Relationship(back_populates="sales") company: "Company" = Relationship(back_populates="sales")
sale_category: "SaleCategory" = Relationship(back_populates="sales")
class AgentTransaction(SQLModel, table=True): class AgentTransaction(SQLModel, table=True):
__tablename__ = "agent_transactions" __tablename__ = "agent_transactions"