From 37c855c601431d2c91841ee62309be0de0fb05da Mon Sep 17 00:00:00 2001 From: Redsandyg Date: Mon, 2 Jun 2025 13:15:42 +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=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D1=8B:=20.gitignore=20=D0=B4=D0=BB=D1=8F=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=BE=D0=B2,=20fill=5Fdb.py=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B1=D0=B0=D0=B7=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=BC=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8,=20main.py=20?= =?UTF-8?q?=D1=81=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=BD=D1=8B=D0=BC=20?= =?UTF-8?q?=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D0=BE=D0=BC=20FastAPI,=20models.py=20=D1=81=20API=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D1=8F=D0=BC=D0=B8=20=D0=B8=20requireme?= =?UTF-8?q?nts.txt=20=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B5=D0=B9=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 121 +++++++++++++ fill_db.py | 161 +++++++++++++++++ main.py | 448 +++++++++++++++++++++++++++++++++++++++++++++++ models.py | 31 ++++ requirements.txt | 146 +++++++++++++++ 5 files changed, 907 insertions(+) create mode 100644 .gitignore create mode 100644 fill_db.py create mode 100644 main.py create mode 100644 models.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80ff257 --- /dev/null +++ b/.gitignore @@ -0,0 +1,121 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# poetry +poetry.lock + +# mypy +.mypy_cache/ +.dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VS Code +.vscode/ + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ + +# SQLite database +*.db + +# MacOS +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# Other +*.swp +*.swo \ No newline at end of file diff --git a/fill_db.py b/fill_db.py new file mode 100644 index 0000000..280a92b --- /dev/null +++ b/fill_db.py @@ -0,0 +1,161 @@ +import random +from uuid import uuid4 +from sqlmodel import Session +from main import AUTH_DB_ENGINE, TgAgent, Ref, Sale, Transaction +from sqlalchemy import text +from datetime import datetime, timedelta + + +# Константа: список user_ids +USER_IDS = [1001, 256844410, 426261192, 564746427] + +# Примеры телефонов +PHONES = [ + "+79991234567", + "+79997654321", + "+79993456789", + "+79992345678", + "+79994561234" +] + +# Примеры описаний +DESCRIPTIONS = [ + "Партнёр по рекламе", + "Блогер", + "Тестовая ссылка", + "Промо акция", + "VIP клиент", + "Реклама в Telegram от 01.05.2025", + "Пост в Instagram от 15.04.2025", + "Рассылка в ВК от 24.02.2025", + "Промо в Facebook от 10.03.2025", + "Пост в Twitter от 05.03.2025", + "Реклама в YouTube от 12.04.2025", + "Промо в TikTok от 20.04.2025", + "Пост в LinkedIn от 18.03.2025", + "Реклама в Одноклассниках от 22.03.2025", + "Промо в Pinterest от 30.03.2025", + "Пост в Reddit от 02.04.2025", + "Реклама в Dzen от 11.04.2025", + "Промо в Medium от 08.04.2025", + "Пост в WhatsApp от 14.04.2025", + "Реклама в Discord от 21.04.2025" +] + +# Примеры имен и логинов +NAMES = [ + "Иван Иванов", + "Петр Петров", + "Сергей Сергеев", + "Анна Смирнова" +] +LOGINS = [ + "ivanov1001", + "petrov256", + "serg426", + "anna564" +] + +# --- Загрузка mock-данных --- + +ALL_DESCRIPTIONS = DESCRIPTIONS + +# --- +def get_date_list(days=7): + today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + return [today - timedelta(days=i) for i in range(days, -1, -1)] + +def fill_db(): + date_list = get_date_list(7) # 8 дней: от недели назад до сегодня + with Session(AUTH_DB_ENGINE) as session: + # Очистка таблиц + session.execute(text("DELETE FROM sale")) + session.execute(text("DELETE FROM ref")) + session.execute(text("DELETE FROM tgagent")) + session.commit() + # 1. TgAgents + tg_agents = [] + for i, tg_agent_id in enumerate(USER_IDS): + dt = random.choice(date_list) + tg_agent = TgAgent( + tg_id=tg_agent_id, + chat_id=tg_agent_id, # chat_id совпадает с tg_id + phone=PHONES[i % len(PHONES)], + name=NAMES[i % len(NAMES)], + login=LOGINS[i % len(LOGINS)], + create_dttm=dt, + update_dttm=dt + ) + session.add(tg_agent) + tg_agents.append(tg_agent) + session.commit() + for tg_agent in tg_agents: + session.refresh(tg_agent) + + # 2. Refs (минимум 22 на агента) + refs = [] + desc_count = len(ALL_DESCRIPTIONS) + for tg_agent in tg_agents: + ref_count = random.randint(22, int(22 * 1.25)) # от 22 до 27 + for j in range(ref_count): + ref_val = str(uuid4()) + desc_val = ALL_DESCRIPTIONS[(j % desc_count)] + dt = random.choice(date_list) + ref = Ref( + tg_agent_id=tg_agent.id, + ref=ref_val, + description=desc_val, + create_dttm=dt, + update_dttm=dt + ) + session.add(ref) + refs.append(ref) + session.commit() + for ref in refs: + session.refresh(ref) + + # 3. Sales (минимум 20 на каждый ref) + 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 * random.uniform(0.5, 1.0), 2) + dt = random.choice(date_list) + sale = Sale( + cost=cost, + crediting=crediting, + ref=ref.id, + sale_id=str(uuid4()), + create_dttm=dt, + update_dttm=dt + ) + session.add(sale) + session.commit() + + # 4. Transactions (только withdrawal на агента) + TRANSACTION_STATUSES = ['process', 'done', 'error', 'waiting'] + for tg_agent in tg_agents: + withdrawal_count = random.randint(5, int(5 * 1.25)) # от 5 до 6 + used_statuses = set() + for i in range(withdrawal_count): + dt = random.choice(date_list) + # Гарантируем, что каждый статус будет использован хотя бы раз + if len(used_statuses) < len(TRANSACTION_STATUSES): + status = TRANSACTION_STATUSES[len(used_statuses)] + used_statuses.add(status) + else: + status = random.choice(TRANSACTION_STATUSES) + transaction = Transaction( + transaction_id=str(uuid4()), + sum=round(random.uniform(200, 3000), 2), + tg_agent_id=tg_agent.id, + status=status, + create_dttm=dt, + update_dttm=dt + ) + session.add(transaction) + session.commit() + print("База успешно заполнена!") + +if __name__ == "__main__": + fill_db() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..acf4788 --- /dev/null +++ b/main.py @@ -0,0 +1,448 @@ +from fastapi import FastAPI, Depends, HTTPException, status, Query +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from sqlmodel import SQLModel, Field, create_engine, Session, select +from passlib.context import CryptContext +from typing import Optional, List, Dict +from datetime import datetime, timedelta +from models import RefAddRequest, RefResponse, RegisterRequest, Token, TokenRequest +from uuid import uuid4 +from fastapi.responses import JSONResponse +from sqlalchemy import func + +# Конфигурация +AUTH_DATABASE_ADDRESS = "sqlite:///partner.db" + +#SQLModel +class TgAgent(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + tg_id: int = Field(index=True, unique=True) + chat_id: Optional[int] = None + phone: Optional[str] = None + name: Optional[str] = None + login: Optional[str] = None + create_dttm: datetime = Field(default_factory=datetime.utcnow) + update_dttm: datetime = Field(default_factory=datetime.utcnow) + +class Ref(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + tg_agent_id: int = Field(foreign_key="tgagent.id") + ref: str + description: Optional[str] = None + create_dttm: datetime = Field(default_factory=datetime.utcnow) + update_dttm: datetime = Field(default_factory=datetime.utcnow) + +class Sale(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + cost: float + crediting: float # сколько начислено за продажу + ref: int = Field(foreign_key="ref.id") + sale_id: str + create_dttm: datetime = Field(default_factory=datetime.utcnow) + update_dttm: datetime = Field(default_factory=datetime.utcnow) + +class Transaction(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + transaction_id: str = Field(default_factory=lambda: str(uuid4()), index=True, unique=True) + sum: float + tg_agent_id: int = Field(foreign_key="tgagent.id") + status: str # 'process' || 'done' || 'error' || 'waiting' + create_dttm: datetime = Field(default_factory=datetime.utcnow) + update_dttm: datetime = Field(default_factory=datetime.utcnow) + +# Создание движка базы данных +AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True) +SQLModel.metadata.create_all(AUTH_DB_ENGINE) + + + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") + +# FastAPI app +app = FastAPI() + +# CRUD + +def get_tg_agent_by_tg_id(db: Session, tg_id: int) -> Optional[TgAgent]: + statement = select(TgAgent).where(TgAgent.tg_id == tg_id) + return db.exec(statement).first() + +# Dependency + +def get_db(): + with Session(AUTH_DB_ENGINE) as session: + yield session + +# Авторизация +async def get_current_tg_agent(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + # Ожидаем токен вида 'session_for_{tg_id}' + if not token.startswith("session_for_"): + raise credentials_exception + try: + tg_id = int(token.replace("session_for_", "")) + except Exception: + raise credentials_exception + tg_agent = get_tg_agent_by_tg_id(db, tg_id) + if tg_agent is None: + raise credentials_exception + return tg_agent + +# Регистрация + + +@app.post("/register", tags=["partner-tg"]) +def register(req: RegisterRequest, db: Session = Depends(get_db)): + tg_id = req.tg_id + chat_id = req.chat_id + phone = req.phone + name = getattr(req, 'name', None) + login = getattr(req, 'login', None) + print(f'tg_id: {tg_id}, chat_id: {chat_id}, phone: {phone}, name: {name}, login: {login}') + tg_agent = get_tg_agent_by_tg_id(db, tg_id) + if tg_agent: + raise HTTPException(status_code=400, detail="tg_id already registered") + new_tg_agent = TgAgent(tg_id=tg_id, chat_id=chat_id, phone=phone, name=name, login=login) + db.add(new_tg_agent) + db.commit() + db.refresh(new_tg_agent) + return {"msg": "TgAgent registered successfully"} + +def authenticate_tg_agent(engine, tg_id: int): + with Session(engine) as db: + tg_agent = get_tg_agent_by_tg_id(db, tg_id) + if not tg_agent: + return None + return tg_agent + +# Защищённый эндпоинт +@app.get("/protected", tags=["partner-tg"]) +def protected_route(current_tg_agent: TgAgent = Depends(get_current_tg_agent)): + return {"msg": f"Hello, {current_tg_agent.tg_id}! This is a protected route."} + +# Авторизация + + +@app.post("/token", response_model=Token, tags=["partner-tg"]) +async def login_for_access_token(req: TokenRequest): + tg_id = req.tg_id + tg_agent = authenticate_tg_agent(AUTH_DB_ENGINE, tg_id) + if not tg_agent: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect tg_id", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token = f"session_for_{tg_agent.tg_id}" + return Token(access_token=access_token, token_type="bearer") + + + +@app.get("/ref", response_model=list[RefResponse], tags=["partner-tg"]) +def get_refs(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): + refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all() + return [RefResponse(ref=r.ref, description=r.description or "") for r in refs] + +@app.post("/ref/add", tags=["partner-tg"]) +def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): + new_ref = Ref( + tg_agent_id=current_tg_agent.id, + ref=str(uuid4()), + description=req.description + ) + db.add(new_ref) + db.commit() + db.refresh(new_ref) + return {"ref": new_ref.ref} + +@app.get("/ref/stat", tags=["partner-tg"]) +def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): + # 1. Получаем все реферальные ссылки пользователя + refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all() + result = [] + for ref in refs: + # 2. Для каждой ссылки считаем продажи и сумму + sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all() + sales_count = len(sales) + income = sum(sale.crediting for sale in sales) + result.append({ + "description": ref.description or "", + "sales": sales_count, + "income": income + }) + return {"refData": result} + +@app.get("/stat", tags=["partner-tg"]) +def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)): + # 1. Получаем все реферальные ссылки пользователя + refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all() + ref_ids = [r.id for r in refs] + + # 2. Считаем totalSales (продажи по всем рефам пользователя) + total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all() + totalSales = len(total_sales) + totalIncome = sum(sale.crediting for sale in total_sales) + withdrawals = db.exec( + select(Transaction).where( + Transaction.tg_agent_id == current_tg_agent.id + ) + ).all() + availableWithdrawal = totalIncome - sum(t.sum for t in withdrawals) + return { + "totalSales": totalSales, + "totalIncome": totalIncome, + "availableWithdrawal": availableWithdrawal + } + +@app.get("/dashboard/cards", tags=["bff"]) +def get_dashboard_cards(db: Session = Depends(get_db)): + # 1. Общий доход - сумма всех Sale.cost + total_revenue = db.exec(select(Sale)).all() + totalRevenue = sum(sale.cost for sale in total_revenue) + + # 2. Общие выплаты - сумма всех Sale.crediting + totalPayouts = sum(sale.crediting for sale in total_revenue) + + # 3. Активные рефералы - количество уникальных TgAgent.tg_id + unique_agents = db.exec(select(TgAgent.tg_id)).all() + activeReferrals = len(set(unique_agents)) + + # 4. Ожидающие выплаты - разница между суммой всех Sale.crediting и суммой всех Transaction.sum + all_transactions = db.exec(select(Transaction)).all() + totalTransactions = sum(t.sum for t in all_transactions) + pendingPayouts = totalPayouts - totalTransactions + + # 5. Количество продаж + totalSales = len(total_revenue) + + return { + "totalRevenue": totalRevenue, + "totalPayouts": totalPayouts, + "activeReferrals": activeReferrals, + "pendingPayouts": pendingPayouts, + "totalSales": totalSales + } + +@app.get("/dashboard/chart/total", tags=["bff"]) +def get_dashboard_chart_total(db: Session = Depends(get_db)): + # Группируем продажи по дате (день) + result = db.exec( + select( + func.strftime('%Y-%m-%d', Sale.create_dttm).label('date'), + func.sum(Sale.cost).label('revenue'), + func.count(Sale.id).label('sales') + ).group_by(func.strftime('%Y-%m-%d', Sale.create_dttm)) + .order_by(func.strftime('%Y-%m-%d', Sale.create_dttm)) + ).all() + # Преобразуем результат в нужный формат + data = [ + {"date": row.date, "revenue": row.revenue or 0, "sales": row.sales or 0} + for row in result + ] + return JSONResponse(content=data) + +@app.get("/dashboard/chart/agent", tags=["bff"]) +def get_dashboard_chart_agent(db: Session = Depends(get_db)): + # Получаем всех агентов + agents = db.exec(select(TgAgent)).all() + result = [] + for agent in agents: + # Получаем все рефы этого агента + refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all() + ref_ids = [r.id for r in refs] + if not ref_ids: + result.append({ + "name": agent.name or f"Агент {agent.id}", + "count": 0, + "sum": 0.0 + }) + continue + # Получаем все продажи по этим рефам + sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all() + sales_count = len(sales) + sales_sum = sum(sale.cost for sale in sales) + result.append({ + "name": agent.name or f"Агент {agent.id}", + "count": sales_count, + "sum": sales_sum + }) + return JSONResponse(content=result) + +@app.get("/stat/agents", tags=["bff"]) +def get_agents_stat( + db: Session = Depends(get_db), + date_start: str = Query(None), + date_end: str = Query(None), +): + agents_query = select(TgAgent) + if date_start: + agents_query = agents_query.where(TgAgent.create_dttm >= date_start) + if date_end: + agents_query = agents_query.where(TgAgent.create_dttm <= date_end) + agents = db.exec(agents_query).all() + result = [] + for agent in agents: + refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all() + ref_ids = [r.id for r in refs] + ref_count = len(ref_ids) + if not ref_ids: + result.append({ + "name": agent.name or f"Агент {agent.id}", + "refCount": 0, + "salesCount": 0, + "salesSum": 0.0, + "crediting": 0.0 + }) + continue + sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all() + sales_count = len(sales) + sales_sum = sum(sale.cost for sale in sales) + crediting_sum = sum(sale.crediting for sale in sales) + result.append({ + "name": agent.name or f"Агент {agent.id}", + "refCount": ref_count, + "salesCount": sales_count, + "salesSum": sales_sum, + "crediting": crediting_sum + }) + return JSONResponse(content=result) + +@app.get("/stat/referrals", tags=["bff"]) +def get_referrals_stat( + db: Session = Depends(get_db), + date_start: str = Query(None), + date_end: str = Query(None), +): + refs_query = select(Ref) + if date_start: + refs_query = refs_query.where(Ref.create_dttm >= date_start) + if date_end: + refs_query = refs_query.where(Ref.create_dttm <= date_end) + refs = db.exec(refs_query).all() + result = [] + for ref in refs: + agent = db.exec(select(TgAgent).where(TgAgent.id == ref.tg_agent_id)).first() + sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all() + sales_count = len(sales) + sales_sum = sum(sale.cost for sale in sales) + result.append({ + "ref": ref.ref, + "agent": agent.name if agent and agent.name else f"Агент {ref.tg_agent_id}", + "description": ref.description or "", + "salesSum": sales_sum, + "salesCount": sales_count + }) + return JSONResponse(content=result) + +@app.get("/stat/sales", tags=["bff"]) +def get_sales_stat( + db: Session = Depends(get_db), + date_start: str = Query(None), + date_end: str = Query(None), +): + sales_query = select(Sale) + if date_start: + sales_query = sales_query.where(Sale.create_dttm >= date_start) + if date_end: + sales_query = sales_query.where(Sale.create_dttm <= date_end) + sales = db.exec(sales_query).all() + 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 [] + ref_map = {ref.id: ref for ref in refs} + agent_ids = list(set(ref.tg_agent_id for ref in refs)) if refs else [] + agents = db.exec(select(TgAgent).where(TgAgent.id.in_(agent_ids))).all() if agent_ids else [] + agent_map = {agent.id: agent for agent in agents} + result = [] + for sale in sales: + ref_obj = ref_map.get(sale.ref) + agent_obj = agent_map.get(ref_obj.tg_agent_id) if ref_obj else None + result.append({ + "saleId": sale.sale_id, + "cost": sale.cost, + "crediting": sale.crediting, + "ref": ref_obj.ref if ref_obj else None, + "name": agent_obj.name if agent_obj else None + }) + return JSONResponse(content=result) + +@app.get("/billing/cards", tags=["bff"]) +def get_billing_cards(db: Session = Depends(get_db)): + # 1. cost - сумма всех Sale.cost + sales = db.exec(select(Sale)).all() + cost = sum(sale.cost for sale in sales) + # 2. crediting - сумма всех Sale.crediting + crediting = sum(sale.crediting for sale in sales) + # 3. pendingPayouts - разница между crediting и суммой всех Transaction.sum + transactions = db.exec(select(Transaction)).all() + total_transactions = sum(t.sum for t in transactions) + pendingPayouts = crediting - total_transactions + return { + "cost": cost, + "crediting": crediting, + "pendingPayouts": pendingPayouts + } + +@app.get("/billing/payouts/transactions", tags=["bff"]) +def get_billing_payouts_transactions( + db: Session = Depends(get_db), + date_start: str = Query(None), + date_end: str = Query(None), +): + query = select(Transaction) + if date_start: + query = query.where(Transaction.create_dttm >= date_start) + if date_end: + query = query.where(Transaction.create_dttm <= date_end) + transactions = db.exec(query).all() + result = [] + for t in transactions: + agent = db.exec(select(TgAgent).where(TgAgent.id == t.tg_agent_id)).first() + result.append({ + "id": t.transaction_id, + "sum": t.sum, + "agent": agent.name if agent else None, + "status": t.status, + "create_dttm": t.create_dttm, + "update_dttm": t.update_dttm, + }) + return result + +@app.get("/billing/chart/stat", tags=["bff"]) +def get_billing_chart_stat(db: Session = Depends(get_db)): + # Группируем транзакции по дате (день) и статусу + result = db.exec( + select( + func.strftime('%Y-%m-%d', Transaction.create_dttm).label('date'), + Transaction.status.label('status'), + func.count(Transaction.id).label('count') + ).group_by( + func.strftime('%Y-%m-%d', Transaction.create_dttm), + Transaction.status + ).order_by( + func.strftime('%Y-%m-%d', Transaction.create_dttm), + Transaction.status + ) + ).all() + data = [ + {"date": row.date, "status": row.status, "count": row.count} + for row in result + ] + return JSONResponse(content=data) + +@app.get("/billing/chart/pie", tags=["bff"]) +def get_billing_chart_pie(db: Session = Depends(get_db)): + result = db.exec( + select( + Transaction.status.label('status'), + func.count(Transaction.id).label('count') + ).group_by(Transaction.status) + ).all() + data = [ + {"status": row.status, "count": row.count} + for row in result + ] + return JSONResponse(content=data) diff --git a/models.py b/models.py new file mode 100644 index 0000000..c04bd76 --- /dev/null +++ b/models.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel, Field +from typing import Optional + +from sqlmodel import SQLModel + + + + + + +#API models +class Token(BaseModel): + access_token: str + token_type: str + +class RefResponse(BaseModel): + ref: str + description: str + +class RefAddRequest(BaseModel): + description: str + +class TokenRequest(BaseModel): + tg_id: int + +class RegisterRequest(BaseModel): + tg_id: int + chat_id: Optional[int] = None + phone: Optional[str] = None + name: Optional[str] = None + login: Optional[str] = None \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6c178a2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,146 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml -o requirements.txt +annotated-types==0.7.0 + # via pydantic +anyio==4.8.0 + # via + # httpx + # starlette + # watchfiles +bcrypt==4.2.1 + # via passlib +behave==1.2.6 + # via epai-auth (pyproject.toml) +certifi==2024.12.14 + # via + # httpcore + # httpx + # requests +cffi==1.17.1 + # via cryptography +charset-normalizer==3.4.2 + # via requests +click==8.1.8 + # via + # rich-toolkit + # typer + # uvicorn +cryptography==44.0.0 + # via epai-auth (pyproject.toml) +dnspython==2.7.0 + # via email-validator +email-validator==2.2.0 + # via fastapi +fastapi==0.115.6 + # via epai-auth (pyproject.toml) +fastapi-cli==0.0.7 + # via fastapi +greenlet==3.1.1 + # via sqlalchemy +h11==0.14.0 + # via + # httpcore + # uvicorn +httpcore==1.0.7 + # via httpx +httptools==0.6.4 + # via uvicorn +httpx==0.28.1 + # via fastapi +idna==3.10 + # via + # anyio + # email-validator + # httpx + # requests +jinja2==3.1.5 + # via fastapi +markdown-it-py==3.0.0 + # via rich +markupsafe==3.0.2 + # via jinja2 +mdurl==0.1.2 + # via markdown-it-py +parse==1.20.2 + # via + # behave + # parse-type +parse-type==0.6.4 + # via behave +passlib==1.7.4 + # via epai-auth (pyproject.toml) +psycopg2==2.9.10 + # via epai-auth (pyproject.toml) +pycparser==2.22 + # via cffi +pydantic==2.10.5 + # via + # fastapi + # pydantic-settings + # sqlmodel +pydantic-core==2.27.2 + # via pydantic +pydantic-settings==2.9.1 + # via epai-auth (pyproject.toml) +pygments==2.19.1 + # via rich +pyjwt==2.10.1 + # via epai-auth (pyproject.toml) +python-dotenv==1.0.1 + # via + # pydantic-settings + # uvicorn +python-multipart==0.0.20 + # via fastapi +pyyaml==6.0.2 + # via uvicorn +requests==2.32.3 + # via epai-auth (pyproject.toml) +rich==13.9.4 + # via + # rich-toolkit + # typer +rich-toolkit==0.12.0 + # via fastapi-cli +ruff==0.9.1 + # via epai-auth (pyproject.toml) +shellingham==1.5.4 + # via typer +six==1.17.0 + # via + # behave + # parse-type +sniffio==1.3.1 + # via anyio +sqlalchemy==2.0.37 + # via sqlmodel +sqlmodel==0.0.22 + # via epai-auth (pyproject.toml) +starlette==0.41.3 + # via fastapi +typer==0.15.1 + # via fastapi-cli +typing-extensions==4.12.2 + # via + # anyio + # fastapi + # pydantic + # pydantic-core + # rich-toolkit + # sqlalchemy + # typer + # typing-inspection +typing-inspection==0.4.0 + # via pydantic-settings +urllib3==2.4.0 + # via requests +uvicorn==0.34.0 + # via + # fastapi + # fastapi-cli +#uvloop==0.21.0 + # via uvicorn +watchfiles==1.0.4 + # via uvicorn +websockets==14.1 + # via uvicorn