332 lines
13 KiB
Python
332 lines
13 KiB
Python
# - *- coding: utf- 8 - *-
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Optional, Union
|
|
|
|
from sqlalchemy import BigInteger, Float, Integer, String, Text, text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from tgbot.database.core import Base, session_factory
|
|
from tgbot.database.entities import Purchase
|
|
from tgbot.database.repository import BaseRepository
|
|
from tgbot.utils.const_functions import gen_id, get_unix
|
|
|
|
TELEGRAM_LIMIT = 4000
|
|
|
|
|
|
@dataclass
|
|
class PurchaseResult:
|
|
receipt: int
|
|
items: List[List[str]]
|
|
purchase_count: int
|
|
purchase_unix: int
|
|
purchase_price: float
|
|
new_balance: float
|
|
position_name: str
|
|
|
|
|
|
class PurchaseModel(Base):
|
|
__tablename__ = "storage_purchases"
|
|
|
|
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
|
user_balance_before: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
|
user_balance_after: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
|
purchase_receipt: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
|
purchase_data: Mapped[str] = mapped_column(Text, nullable=False)
|
|
purchase_count: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
purchase_price: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
|
purchase_price_one: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
|
purchase_position_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
|
purchase_position_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
purchase_category_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
|
purchase_category_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
purchase_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
|
|
|
|
|
ModelBase = Purchase
|
|
BaseModel = Purchase
|
|
|
|
|
|
class Purchasesx(BaseRepository[PurchaseModel, Purchase]):
|
|
# Подключение модели покупок
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.table_model = PurchaseModel
|
|
self.entity_model = Purchase
|
|
self.storage_name = PurchaseModel.__tablename__
|
|
|
|
# Добавление записи покупки
|
|
async def add(
|
|
self,
|
|
user_id: int,
|
|
user_balance_before: float,
|
|
user_balance_after: float,
|
|
purchase_receipt: int,
|
|
purchase_data: str,
|
|
purchase_count: int,
|
|
purchase_price: float,
|
|
purchase_price_one: float,
|
|
purchase_position_id: int,
|
|
purchase_position_name: str,
|
|
purchase_category_id: int,
|
|
purchase_category_name: str,
|
|
) -> Purchase:
|
|
return await self._insert(
|
|
user_id=user_id,
|
|
user_balance_before=user_balance_before,
|
|
user_balance_after=user_balance_after,
|
|
purchase_receipt=purchase_receipt,
|
|
purchase_data=purchase_data,
|
|
purchase_count=purchase_count,
|
|
purchase_price=purchase_price,
|
|
purchase_price_one=purchase_price_one,
|
|
purchase_position_id=purchase_position_id,
|
|
purchase_position_name=purchase_position_name,
|
|
purchase_category_id=purchase_category_id,
|
|
purchase_category_name=purchase_category_name,
|
|
purchase_unix=get_unix(),
|
|
)
|
|
|
|
# Обновление покупки по чеку или фильтру
|
|
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
|
if isinstance(where, int):
|
|
where = {"purchase_receipt": where}
|
|
|
|
return await self._update(where=where, **kwargs)
|
|
|
|
# Атомарная покупка товаров
|
|
@staticmethod
|
|
async def buy(*, user_id: int, position_id: int, requested_count: int) -> Union[PurchaseResult, str]:
|
|
async with session_factory() as session:
|
|
try:
|
|
await session.execute(text("BEGIN IMMEDIATE"))
|
|
|
|
user_result = await session.execute(
|
|
text(
|
|
"""
|
|
SELECT user_balance
|
|
FROM storage_users
|
|
WHERE user_id = :user_id
|
|
"""
|
|
),
|
|
{"user_id": user_id},
|
|
)
|
|
get_user = user_result.mappings().first()
|
|
|
|
if get_user is None:
|
|
await session.rollback()
|
|
return "USER_NOT_FOUND"
|
|
|
|
position_result = await session.execute(
|
|
text(
|
|
"""
|
|
SELECT position_price, position_name, category_id
|
|
FROM storage_position
|
|
WHERE position_id = :position_id
|
|
"""
|
|
),
|
|
{"position_id": position_id},
|
|
)
|
|
get_position = position_result.mappings().first()
|
|
|
|
if get_position is None:
|
|
await session.rollback()
|
|
return "POSITION_NOT_FOUND"
|
|
|
|
items_result = await session.execute(
|
|
text(
|
|
"""
|
|
SELECT increment, item_id, item_data
|
|
FROM storage_item
|
|
WHERE position_id = :position_id
|
|
ORDER BY increment
|
|
LIMIT :requested_count
|
|
"""
|
|
),
|
|
{"position_id": position_id, "requested_count": requested_count},
|
|
)
|
|
items = items_result.mappings().all()
|
|
|
|
if len(items) < requested_count:
|
|
await session.rollback()
|
|
return "NOT_ENOUGH_ITEMS"
|
|
|
|
purchase_price = round(float(get_position["position_price"]) * requested_count, 2)
|
|
|
|
if float(get_user["user_balance"]) < purchase_price:
|
|
await session.rollback()
|
|
return "NOT_ENOUGH_BALANCE"
|
|
|
|
item_ids = [row["item_id"] for row in items]
|
|
delete_params = {f"item_id_{index}": item_id for index, item_id in enumerate(item_ids)}
|
|
delete_marks = ", ".join(f":item_id_{index}" for index in range(len(item_ids)))
|
|
await session.execute(
|
|
text(f"DELETE FROM storage_item WHERE item_id IN ({delete_marks})"),
|
|
delete_params,
|
|
)
|
|
|
|
new_balance = round(float(get_user["user_balance"]) - purchase_price, 2)
|
|
|
|
await session.execute(
|
|
text(
|
|
"""
|
|
UPDATE storage_users
|
|
SET user_balance = :new_balance
|
|
WHERE user_id = :user_id
|
|
"""
|
|
),
|
|
{"new_balance": new_balance, "user_id": user_id},
|
|
)
|
|
|
|
receipt = gen_id()
|
|
purchase_data = "\n".join([row["item_data"] for row in items])
|
|
purchase_unix = get_unix()
|
|
|
|
category_result = await session.execute(
|
|
text("SELECT category_name FROM storage_category WHERE category_id = :category_id"),
|
|
{"category_id": get_position["category_id"]},
|
|
)
|
|
category = category_result.mappings().first()
|
|
category_name = category["category_name"] if category else "Unknown"
|
|
|
|
await session.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO storage_purchases (user_id,
|
|
user_balance_before,
|
|
user_balance_after,
|
|
purchase_receipt,
|
|
purchase_data,
|
|
purchase_count,
|
|
purchase_price,
|
|
purchase_price_one,
|
|
purchase_position_id,
|
|
purchase_position_name,
|
|
purchase_category_id,
|
|
purchase_category_name,
|
|
purchase_unix)
|
|
VALUES (:user_id,
|
|
:balance_before,
|
|
:balance_after,
|
|
:receipt,
|
|
:purchase_data,
|
|
:purchase_count,
|
|
:purchase_price,
|
|
:purchase_price_one,
|
|
:position_id,
|
|
:position_name,
|
|
:category_id,
|
|
:category_name,
|
|
:purchase_unix)
|
|
"""
|
|
),
|
|
{
|
|
"user_id": user_id,
|
|
"balance_before": float(get_user["user_balance"]),
|
|
"balance_after": new_balance,
|
|
"receipt": receipt,
|
|
"purchase_data": purchase_data,
|
|
"purchase_count": requested_count,
|
|
"purchase_price": purchase_price,
|
|
"purchase_price_one": float(get_position["position_price"]),
|
|
"position_id": position_id,
|
|
"position_name": get_position["position_name"],
|
|
"category_id": get_position["category_id"],
|
|
"category_name": category_name,
|
|
"purchase_unix": purchase_unix,
|
|
},
|
|
)
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
raise
|
|
|
|
purchase_items = [row["item_data"] for row in items]
|
|
purchase_items_parts = chunk_items_by_len(purchase_items)
|
|
|
|
return PurchaseResult(
|
|
receipt=receipt,
|
|
items=purchase_items_parts,
|
|
purchase_count=requested_count,
|
|
purchase_unix=purchase_unix,
|
|
purchase_price=purchase_price,
|
|
position_name=get_position["position_name"],
|
|
new_balance=new_balance,
|
|
)
|
|
|
|
|
|
# Разделение товаров на сообщения под лимит телеграма
|
|
def chunk_items_by_len(items: List[str], limit: int = TELEGRAM_LIMIT, separator: str = "\n\n") -> List[List[str]]:
|
|
chunks: List[List[str]] = []
|
|
current_chunk: List[str] = []
|
|
current_len = 0
|
|
sep_len = len(separator)
|
|
|
|
for raw_item in items:
|
|
item = str(raw_item)
|
|
|
|
if len(item) > limit:
|
|
if current_chunk:
|
|
chunks.append(current_chunk)
|
|
current_chunk = []
|
|
current_len = 0
|
|
|
|
for part in split_long_item(item, limit):
|
|
chunks.append([part])
|
|
|
|
continue
|
|
|
|
add_len = len(item) if not current_chunk else sep_len + len(item)
|
|
|
|
if current_len + add_len <= limit:
|
|
current_chunk.append(item)
|
|
current_len += add_len
|
|
else:
|
|
chunks.append(current_chunk)
|
|
current_chunk = [item]
|
|
current_len = len(item)
|
|
|
|
if current_chunk:
|
|
chunks.append(current_chunk)
|
|
|
|
return chunks
|
|
|
|
|
|
# Разделение длинного товара без разрыва строк
|
|
def split_long_item(item: str, limit: int = TELEGRAM_LIMIT) -> List[str]:
|
|
chunks: List[str] = []
|
|
current_lines: List[str] = []
|
|
current_len = 0
|
|
|
|
for line in item.splitlines():
|
|
line_len = len(line)
|
|
add_len = line_len if not current_lines else 1 + line_len
|
|
|
|
if current_lines and current_len + add_len > limit:
|
|
chunks.append("\n".join(current_lines))
|
|
current_lines = []
|
|
current_len = 0
|
|
add_len = line_len
|
|
|
|
if line_len > limit:
|
|
if current_lines:
|
|
chunks.append("\n".join(current_lines))
|
|
current_lines = []
|
|
current_len = 0
|
|
|
|
start = 0
|
|
|
|
while start < line_len:
|
|
chunks.append(line[start:start + limit])
|
|
start += limit
|
|
|
|
continue
|
|
|
|
current_lines.append(line)
|
|
current_len += add_len
|
|
|
|
if current_lines:
|
|
chunks.append("\n".join(current_lines))
|
|
|
|
return chunks
|