From bf6a6a898713bd6e06d7024d1edad7e0cbfd9674 Mon Sep 17 00:00:00 2001 From: Redsandyg Date: Wed, 18 Jun 2025 10:48:29 +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=20SaleCategoryRequest=20=D0=B8=20SaleC?= =?UTF-8?q?ategoryResponse=20=D0=B2=20bff=5Fmodels.py=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D1=8F=D0=BC=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B4=D0=B0=D0=B6.=20=D0=9E=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B2=20fill=5Fdb.py=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B1=D0=B0=D0=B7=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D1=8F=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B4=D0=B0=D0=B6.=20=D0=98=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D1=8B=20=D1=8D=D0=BD=D0=B4=D0=BF?= =?UTF-8?q?=D0=BE=D0=B8=D0=BD=D1=82=D1=8B=20=D0=B2=20main.py=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B4=D0=B0=D0=B6.=20=D0=9E=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BC=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=B8=20SQL-=D1=81=D0=BA=D1=80=D0=B8=D0=BF?= =?UTF-8?q?=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=BA=D0=B8=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=B8=20=D1=81=D0=B2=D1=8F?= =?UTF-8?q?=D0=B7=D0=B5=D0=B9.=20=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=D0=B6=20=D1=81=20=D1=83=D1=87=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bff_models.py | 17 +++++++++++ call_sale_api.py | 3 +- fill_db.py | 22 ++++++++++++--- integration_api.py | 12 ++++++-- integration_models.py | 1 + main.py | 65 +++++++++++++++++++++++++++++++++++++++++-- sql_create.sql | 19 ++++++++++++- sql_models.py | 15 ++++++++++ 8 files changed, 143 insertions(+), 11 deletions(-) diff --git a/bff_models.py b/bff_models.py index d70e983..2f353fa 100644 --- a/bff_models.py +++ b/bff_models.py @@ -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) diff --git a/call_sale_api.py b/call_sale_api.py index 6f4f5db..c19c74d 100644 --- a/call_sale_api.py +++ b/call_sale_api.py @@ -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) } # Эндпоинты diff --git a/fill_db.py b/fill_db.py index e43c3a3..755019f 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, 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 также будет случайным в этом диапазоне diff --git a/integration_api.py b/integration_api.py index fd54734..17d49db 100644 --- a/integration_api.py +++ b/integration_api.py @@ -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 @@ -201,6 +201,8 @@ async def create_sale( # Проверка входных данных 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 @@ -235,8 +237,11 @@ async def create_sale( 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 agent_balance = db.exec(select(AgentBalance).where(AgentBalance.tg_agent_id == tg_agent.id)).first() @@ -253,6 +258,7 @@ 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) diff --git a/integration_models.py b/integration_models.py index 401d56c..bb39284 100644 --- a/integration_models.py +++ b/integration_models.py @@ -20,6 +20,7 @@ class SaleCreateRequest(BaseModel): promocode: Optional[str] = None sale_id: str cost: float + category: int # id категории продажи class SaleCreateResponse(BaseModel): msg: str diff --git a/main.py b/main.py index 59acf24..b2cf8f1 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/sql_create.sql b/sql_create.sql index 4fe118a..24e80b0 100644 --- a/sql_create.sql +++ b/sql_create.sql @@ -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) ) ; \ No newline at end of file diff --git a/sql_models.py b/sql_models.py index cc38eb1..d528bc5 100644 --- a/sql_models.py +++ b/sql_models.py @@ -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"