Compare commits
19 Commits
a011b2724b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 09046ee882 | |||
| 69187bc6ec | |||
| 7031ed4d46 | |||
| 04be991865 | |||
| 981db23b6f | |||
| a6f06f3493 | |||
| 608447816e | |||
| d2fb817a44 | |||
| b02511e2b9 | |||
| 8ad3ecd804 | |||
| b30aa59e00 | |||
| fea3ceea87 | |||
| 48600d7802 | |||
| 3ef80048a9 | |||
| 406356c240 | |||
| 1b78f2ba36 | |||
| 104cf71969 | |||
| 8444386885 | |||
| ca276cdd61 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -136,6 +136,7 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
*.env
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
@@ -176,3 +177,5 @@ cython_debug/
|
|||||||
|
|
||||||
# Static / IMG
|
# Static / IMG
|
||||||
static/img
|
static/img
|
||||||
|
static/*
|
||||||
|
.env.prod
|
||||||
42
Dockerfile
Normal file
42
Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
FROM python:3.14-slim AS builder
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install --yes --no-install-recommends build-essential gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip \
|
||||||
|
&& pip wheel --wheel-dir /wheels -r requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
FROM python:3.14-slim AS runtime
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN addgroup --system app && adduser --system --ingroup app app
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
COPY --from=builder /wheels /wheels
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt \
|
||||||
|
&& rm -rf /wheels
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p /app/static/img /app/images \
|
||||||
|
&& chown -R app:app /app
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
@@ -86,7 +86,7 @@ path_separator = os
|
|||||||
# database URL. This is consumed by the user-maintained env.py script only.
|
# database URL. This is consumed by the user-maintained env.py script only.
|
||||||
# other means of configuring database URLs may be customized within the env.py
|
# other means of configuring database URLs may be customized within the env.py
|
||||||
# file.
|
# file.
|
||||||
sqlalchemy.url = postgresql+asyncpg://postgres:HEXDEVFUCKINGSUCKS!@localhost:5432/postgres
|
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/postgres
|
||||||
|
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import os
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
@@ -6,14 +7,17 @@ from sqlalchemy.engine import Connection
|
|||||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
from db.base import Base
|
from db.base import Base
|
||||||
from db.models import * # noqa: F403
|
from db.models import *
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
|
postgres_url = os.getenv("POSTGRES_URL")
|
||||||
|
if postgres_url:
|
||||||
|
config.set_main_option("sqlalchemy.url", postgres_url)
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
# Interpret the config file for Python logging.
|
||||||
# This line sets up loggers basically.
|
# This line sets up loggers basically.
|
||||||
if config.config_file_name is not None:
|
if config.config_file_name is not None:
|
||||||
|
|||||||
32
alembic/versions/6621b149364e_products_is_hidden.py
Normal file
32
alembic/versions/6621b149364e_products_is_hidden.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""+products.is_hidden
|
||||||
|
|
||||||
|
Revision ID: 6621b149364e
|
||||||
|
Revises: 1d2e799ed0f3
|
||||||
|
Create Date: 2026-07-21 21:08:34.258587
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "6621b149364e"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "1d2e799ed0f3"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column("products", sa.Column("is_hidden", sa.Boolean(), nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("products", "is_hidden")
|
||||||
|
# ### end Alembic commands ###
|
||||||
36
alembic/versions/8011a3651239_users.py
Normal file
36
alembic/versions/8011a3651239_users.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""+users
|
||||||
|
|
||||||
|
Revision ID: 8011a3651239
|
||||||
|
Revises: 6621b149364e
|
||||||
|
Create Date: 2026-08-18 13:32:42.638070
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '8011a3651239'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '6621b149364e'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sa.BIGINT(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('users')
|
||||||
|
# ### end Alembic commands ###
|
||||||
12
compose.yml.local
Normal file
12
compose.yml.local
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
internal: false
|
||||||
21
config.py
21
config.py
@@ -1,6 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
@@ -13,7 +14,6 @@ class CardInfo:
|
|||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
### Environment Variables ###
|
### Environment Variables ###
|
||||||
|
|
||||||
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
|
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
|
||||||
@@ -26,9 +26,20 @@ POSTGRES_URL = os.getenv(
|
|||||||
card_info = CardInfo(os.getenv("CARD_NUMBER", ""), os.getenv("CARD_HOLDER", ""))
|
card_info = CardInfo(os.getenv("CARD_NUMBER", ""), os.getenv("CARD_HOLDER", ""))
|
||||||
|
|
||||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/1")
|
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/1")
|
||||||
|
PROXY_URL = os.getenv("PROXY_URL")
|
||||||
|
|
||||||
|
WELCOME_VIDEO_NOTE_FP = os.getenv("WELCOME_VIDEO_NOTE_FP", "static/welcome.mp4")
|
||||||
|
CHANNEL_URL = os.getenv("CHANNEL_URL", "https://t.me/shveitech")
|
||||||
|
CHANNEL_ID = os.getenv("CHANNEL_ID")
|
||||||
|
|
||||||
|
if not CHANNEL_ID:
|
||||||
|
raise Exception("Specify Channel ID in .env")
|
||||||
|
|
||||||
### Constants ###
|
### Constants ###
|
||||||
|
|
||||||
PAGE_SIZE = 8
|
PAGE_SIZE = 8
|
||||||
VERIFIED_ACCOUNTS = [1026030711, 8480400744]
|
VERIFIED_ACCOUNTS: list[int] = [int(el) for el in os.getenv("ADMINS", "").split(",")]
|
||||||
NOTIFICATION_CHANNEL = -1003849564110
|
NOTIFICATION_CHANNEL = os.getenv("NOTIFICATION_CHANNEL")
|
||||||
|
|
||||||
|
if not NOTIFICATION_CHANNEL:
|
||||||
|
raise Exception("Specify Notification Channel in .env")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
from config import POSTGRES_URL
|
from config import POSTGRES_URL
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
from .orders import Order, OrderItem, OrderStatus
|
|
||||||
from .products import Product, Category
|
|
||||||
from .invoices import Invoice, InvoiceStatus
|
from .invoices import Invoice, InvoiceStatus
|
||||||
|
from .orders import Order, OrderItem, OrderStatus
|
||||||
|
from .products import Category, Product
|
||||||
|
from .users import User
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"Category",
|
||||||
|
"Invoice",
|
||||||
|
"InvoiceStatus",
|
||||||
"Order",
|
"Order",
|
||||||
"OrderItem",
|
"OrderItem",
|
||||||
"OrderStatus",
|
"OrderStatus",
|
||||||
"Product",
|
"Product",
|
||||||
"Category",
|
"User",
|
||||||
"Invoice",
|
|
||||||
"InvoiceStatus",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum as E_cls
|
from enum import Enum as E_cls
|
||||||
from sqlalchemy import DateTime, func, Enum
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy import DateTime, Enum, func
|
||||||
from sqlalchemy.dialects.postgresql import BIGINT
|
from sqlalchemy.dialects.postgresql import BIGINT
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from db.base import Base
|
from db.base import Base
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import enum
|
import enum
|
||||||
from typing import List, Optional
|
|
||||||
from sqlalchemy import ForeignKey, BigInteger
|
from sqlalchemy import BigInteger, ForeignKey
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
from sqlalchemy.dialects.postgresql import ENUM
|
from sqlalchemy.dialects.postgresql import ENUM
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from db.base import Base
|
from db.base import Base
|
||||||
from db.models.products import Product
|
from db.models.products import Product
|
||||||
|
|
||||||
@@ -19,10 +20,10 @@ class Order(Base):
|
|||||||
__tablename__ = "orders"
|
__tablename__ = "orders"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
customer: Mapped[Optional[int]] = mapped_column(BigInteger)
|
customer: Mapped[int | None] = mapped_column(BigInteger)
|
||||||
status: Mapped[OrderStatus] = mapped_column(ENUM(OrderStatus, name="order_status"))
|
status: Mapped[OrderStatus] = mapped_column(ENUM(OrderStatus, name="order_status"))
|
||||||
|
|
||||||
items: Mapped[List["OrderItem"]] = relationship("OrderItem", back_populates="order")
|
items: Mapped[list["OrderItem"]] = relationship("OrderItem", back_populates="order")
|
||||||
|
|
||||||
|
|
||||||
class OrderItem(Base):
|
class OrderItem(Base):
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from typing import Optional
|
from sqlalchemy import Boolean, ForeignKey, Index, Text
|
||||||
from sqlalchemy import ForeignKey, Text, Index
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
from sqlalchemy.dialects.postgresql import TSVECTOR
|
from sqlalchemy.dialects.postgresql import TSVECTOR
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from db.base import Base
|
from db.base import Base
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ class Category(Base):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("categories.id"))
|
parent_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"))
|
||||||
|
|
||||||
parent: Mapped["Category"] = relationship("Category", remote_side=[id])
|
parent: Mapped["Category"] = relationship("Category", remote_side=[id])
|
||||||
|
|
||||||
@@ -20,15 +19,14 @@ class Product(Base):
|
|||||||
__tablename__ = "products"
|
__tablename__ = "products"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
category_id: Mapped[Optional[int]] = mapped_column(ForeignKey("categories.id"))
|
category_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"))
|
||||||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
description: Mapped[str | None] = mapped_column(Text)
|
||||||
price: Mapped[int] = mapped_column()
|
price: Mapped[int] = mapped_column()
|
||||||
img_path: Mapped[str] = mapped_column(Text, nullable=True)
|
img_path: Mapped[str] = mapped_column(Text, nullable=True)
|
||||||
file_id: Mapped[str] = mapped_column(Text, nullable=True, unique=True)
|
file_id: Mapped[str] = mapped_column(Text, nullable=True, unique=True)
|
||||||
|
is_hidden: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
search_vector: Mapped[str] = mapped_column(TSVECTOR)
|
search_vector: Mapped[str] = mapped_column(TSVECTOR)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (Index("idx_products_search", "search_vector", postgresql_using="gin"),)
|
||||||
Index("idx_products_search", "search_vector", postgresql_using="gin"),
|
|
||||||
)
|
|
||||||
|
|||||||
10
db/models/users.py
Normal file
10
db/models/users.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from sqlalchemy import BIGINT
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(BIGINT, unique=True, nullable=False, primary_key=True)
|
||||||
66
docker-compose.yml
Normal file
66
docker-compose.yml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
env_file: .env
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: shveitech_postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-postgres}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres}",
|
||||||
|
]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: shveitech_redis
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
bot:
|
||||||
|
env_file: .env
|
||||||
|
build:
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: shveitech_bot
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
BOT_TOKEN: ${BOT_TOKEN}
|
||||||
|
ADMINS: ${ADMINS}
|
||||||
|
POSTGRES_URL: postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-postgres}
|
||||||
|
CARD_NUMBER: ${CARD_NUMBER}
|
||||||
|
CARD_HOLDER: ${CARD_HOLDER}
|
||||||
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
||||||
|
PROXY_URL: ${PROXY_URL}
|
||||||
|
command: ["sh", "-c", "alembic upgrade head && python main.py"]
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./images:/app/images
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- internal
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
from .cart import CartItemDTO
|
from .cart import CartItemDTO
|
||||||
from .control import EditProductContext
|
from .catalogue import CatalogueType, CatalogueView
|
||||||
from .checkout import CheckoutContext
|
from .checkout import CheckoutContext
|
||||||
from .catalogue import CatalogueView, CatalogueType
|
from .control import EditProductContext
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CartItemDTO",
|
"CartItemDTO",
|
||||||
"EditProductContext",
|
|
||||||
"CheckoutContext",
|
|
||||||
"CatalogueView",
|
|
||||||
"CatalogueType",
|
"CatalogueType",
|
||||||
|
"CatalogueView",
|
||||||
|
"CheckoutContext",
|
||||||
|
"EditProductContext",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -18,4 +17,4 @@ class CartItemDTO:
|
|||||||
class CartDTO:
|
class CartDTO:
|
||||||
items: list[CartItemDTO]
|
items: list[CartItemDTO]
|
||||||
total: int
|
total: int
|
||||||
order_id: Optional[int] = None
|
order_id: int | None = None
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
@@ -16,12 +15,13 @@ class CatalogueType(Enum):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CatalogueView:
|
class CatalogueView:
|
||||||
view_type: Optional[CatalogueType] = None
|
view_type: CatalogueType | None = None
|
||||||
category: Optional[Category] = None
|
category: Category | None = None
|
||||||
children: Optional[list[Category]] = None
|
children: list[Category] | None = None
|
||||||
products: Optional[list[Product]] = None
|
products: list[Product] | None = None
|
||||||
page: int = 0
|
page: int = 0
|
||||||
parent_id: Union[int, str, None] = None
|
total: int = 0
|
||||||
|
parent_id: int | str | None = None
|
||||||
has_next: bool = False
|
has_next: bool = False
|
||||||
show_menu: bool = False
|
show_menu: bool = False
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class CatalogueView:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class NewCatalogueElement:
|
class NewCatalogueElement:
|
||||||
msg: Message
|
msg: Message
|
||||||
parent_id: Union[int, str]
|
parent_id: int | str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
@@ -8,6 +7,6 @@ from aiogram.types import Message
|
|||||||
class CheckoutContext:
|
class CheckoutContext:
|
||||||
orig_msg: Message
|
orig_msg: Message
|
||||||
order_id: int
|
order_id: int
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
phone: Optional[str] = None
|
phone: str | None = None
|
||||||
address: Optional[str] = None
|
address: str | None = None
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .client import routers as client_routers
|
|
||||||
from .admins import routers as admins_routers
|
from .admins import routers as admins_routers
|
||||||
|
from .client import routers as client_routers
|
||||||
|
|
||||||
__all__ = ["client_routers", "admins_routers"]
|
__all__ = ["admins_routers", "client_routers"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
from .creation import router as creation_router
|
||||||
from .inline_mode import router as inline_router
|
from .inline_mode import router as inline_router
|
||||||
from .menu import router as menu_router
|
from .menu import router as menu_router
|
||||||
from .product_mgmt import router as product_router
|
from .product_mgmt import router as product_router
|
||||||
from .creation import router as creation_router
|
|
||||||
|
|
||||||
routers = [inline_router, menu_router, product_router, creation_router]
|
routers = [inline_router, menu_router, product_router, creation_router]
|
||||||
|
|||||||
@@ -1,21 +1,40 @@
|
|||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
from aiogram.types import CallbackQuery, Message
|
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from dto.catalogue import CategoryActionContext
|
from dto.catalogue import CategoryActionContext, ProductActionContext
|
||||||
from misc.filters import IsVerified
|
from misc.filters import IsVerified
|
||||||
from misc.kb.client import created_category, get_back_to_catalogue
|
from misc.kb.client import created_category, get_back_to_catalogue
|
||||||
|
from misc.kb.common import return_menu
|
||||||
from misc.mapper import parse_cat_id
|
from misc.mapper import parse_cat_id
|
||||||
from misc.states import AdminControlStorage
|
from misc.states import AdminControlStorage
|
||||||
from misc.kb.common import return_menu
|
|
||||||
from repositories.categories import CategoriesRepository
|
from repositories.categories import CategoriesRepository
|
||||||
|
from repositories.products import ProductRepository
|
||||||
from services.catalogue import CatalogueService
|
from services.catalogue import CatalogueService
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(IsVerified(), F.data.startswith("new:"))
|
def skip_product_description_kb(parent_id: str) -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="Пропустить", callback_data="new_product:skip_description")],
|
||||||
|
[InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def skip_product_photo_kb(parent_id: str) -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="Пропустить", callback_data="new_product:skip_photo")],
|
||||||
|
[InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(IsVerified(), F.data.regexp(r"^new:(category|product):.+$"))
|
||||||
async def creation_init(
|
async def creation_init(
|
||||||
cb: CallbackQuery,
|
cb: CallbackQuery,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
@@ -41,7 +60,12 @@ async def creation_init(
|
|||||||
await state.set_data({"ctx": ctx})
|
await state.set_data({"ctx": ctx})
|
||||||
|
|
||||||
if mode == "product":
|
if mode == "product":
|
||||||
...
|
await cb.message.edit_text(
|
||||||
|
f"{path} <i>-> ...</i>\n\n<b>📦 Введите название товара.</b>",
|
||||||
|
reply_markup=get_back_to_catalogue(cat_id),
|
||||||
|
)
|
||||||
|
await state.set_state(AdminControlStorage.new_product_name)
|
||||||
|
await state.set_data({"ctx": ProductActionContext(cb.message, cat_id)})
|
||||||
|
|
||||||
|
|
||||||
@router.message(AdminControlStorage.new_category)
|
@router.message(AdminControlStorage.new_category)
|
||||||
@@ -67,6 +91,134 @@ async def create_new_category(
|
|||||||
await ctx.msg.edit_text(
|
await ctx.msg.edit_text(
|
||||||
f"✅ Категория {msg.text} создана успешно.",
|
f"✅ Категория {msg.text} создана успешно.",
|
||||||
reply_markup=created_category(
|
reply_markup=created_category(
|
||||||
category.id, f"🛒 {category.name}", category.parent_id
|
category.id, f"🛒 {category.name}", category.parent_id or "root"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(IsVerified(), AdminControlStorage.new_product_name)
|
||||||
|
async def create_product_name(msg: Message, state: FSMContext):
|
||||||
|
if not msg.text or not msg.text.strip():
|
||||||
|
await msg.reply("❌ Название не может быть пустым.")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = await state.get_data()
|
||||||
|
ctx: ProductActionContext | None = data.get("ctx")
|
||||||
|
if not ctx:
|
||||||
|
await state.clear()
|
||||||
|
await msg.reply("❌ Ошибка при создании товара.", reply_markup=return_menu)
|
||||||
|
return
|
||||||
|
|
||||||
|
await msg.delete()
|
||||||
|
await ctx.msg.edit_text(
|
||||||
|
"<b>📜 Введите описание товара.</b>",
|
||||||
|
reply_markup=skip_product_description_kb(str(ctx.parent_id)),
|
||||||
|
)
|
||||||
|
await state.update_data(name=msg.text.strip())
|
||||||
|
await state.set_state(AdminControlStorage.new_product_description)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(IsVerified(), F.data == "new_product:skip_description")
|
||||||
|
async def skip_product_description(cb: CallbackQuery, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
ctx: ProductActionContext | None = data.get("ctx")
|
||||||
|
if not ctx:
|
||||||
|
await state.clear()
|
||||||
|
await cb.answer("Ошибка при создании товара.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await cb.message.edit_text(
|
||||||
|
"<b>💸 Введите цену товара целым числом.</b>",
|
||||||
|
reply_markup=get_back_to_catalogue(ctx.parent_id),
|
||||||
|
)
|
||||||
|
await state.update_data(description=None)
|
||||||
|
await state.set_state(AdminControlStorage.new_product_price)
|
||||||
|
await cb.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(IsVerified(), AdminControlStorage.new_product_description)
|
||||||
|
async def create_product_description(msg: Message, state: FSMContext):
|
||||||
|
if not msg.text or not msg.text.strip():
|
||||||
|
await msg.reply("❌ Описание не может быть пустым.")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = await state.get_data()
|
||||||
|
ctx: ProductActionContext | None = data.get("ctx")
|
||||||
|
if not ctx:
|
||||||
|
await state.clear()
|
||||||
|
await msg.reply("❌ Ошибка при создании товара.", reply_markup=return_menu)
|
||||||
|
return
|
||||||
|
|
||||||
|
await msg.delete()
|
||||||
|
await ctx.msg.edit_text(
|
||||||
|
"<b>💸 Введите цену товара целым числом.</b>",
|
||||||
|
reply_markup=get_back_to_catalogue(ctx.parent_id),
|
||||||
|
)
|
||||||
|
await state.update_data(description=msg.text.strip())
|
||||||
|
await state.set_state(AdminControlStorage.new_product_price)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(IsVerified(), AdminControlStorage.new_product_price)
|
||||||
|
async def create_product_price(
|
||||||
|
msg: Message,
|
||||||
|
state: FSMContext,
|
||||||
|
session: AsyncSession,
|
||||||
|
products_repo: ProductRepository,
|
||||||
|
):
|
||||||
|
if not msg.text or not msg.text.isdigit() or int(msg.text) <= 0:
|
||||||
|
await msg.reply("❌ Цена должна быть положительным целым числом.")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = await state.get_data()
|
||||||
|
ctx: ProductActionContext | None = data.get("ctx")
|
||||||
|
if not ctx:
|
||||||
|
await state.clear()
|
||||||
|
await msg.reply("❌ Ошибка при создании товара.", reply_markup=return_menu)
|
||||||
|
return
|
||||||
|
|
||||||
|
await msg.delete()
|
||||||
|
product = await products_repo.add_product(
|
||||||
|
session,
|
||||||
|
category_id=parse_cat_id(ctx.parent_id),
|
||||||
|
name=data["name"],
|
||||||
|
description=data["description"],
|
||||||
|
price=int(msg.text),
|
||||||
|
)
|
||||||
|
await ctx.msg.edit_text(
|
||||||
|
"✅ Основные данные сохранены.\n\n<b>📷 Отправьте фотографию товара или пропустите этот шаг.</b>",
|
||||||
|
reply_markup=skip_product_photo_kb(str(ctx.parent_id)),
|
||||||
|
)
|
||||||
|
await state.update_data(product_id=product.id)
|
||||||
|
await state.set_state(AdminControlStorage.new_product_photo)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(IsVerified(), F.data == "new_product:skip_photo")
|
||||||
|
async def skip_product_photo(cb: CallbackQuery, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
ctx: ProductActionContext | None = data.get("ctx")
|
||||||
|
await state.clear()
|
||||||
|
parent_id = ctx.parent_id if ctx else "root"
|
||||||
|
await cb.message.edit_text("✅ Товар добавлен.", reply_markup=get_back_to_catalogue(parent_id))
|
||||||
|
await cb.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(IsVerified(), AdminControlStorage.new_product_photo)
|
||||||
|
async def create_product_photo(
|
||||||
|
msg: Message,
|
||||||
|
state: FSMContext,
|
||||||
|
session: AsyncSession,
|
||||||
|
products_repo: ProductRepository,
|
||||||
|
):
|
||||||
|
data = await state.get_data()
|
||||||
|
if not msg.photo or not data.get("product_id"):
|
||||||
|
await msg.delete()
|
||||||
|
await msg.reply("❌ Отправьте именно фотографию или нажмите «Пропустить».")
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
|
await msg.delete()
|
||||||
|
await products_repo.add_product_file_id(
|
||||||
|
session, int(data["product_id"]), file_id=msg.photo[-1].file_id
|
||||||
|
)
|
||||||
|
ctx: ProductActionContext = data["ctx"]
|
||||||
|
await ctx.msg.edit_text("✅ Товар добавлен.", reply_markup=get_back_to_catalogue(ctx.parent_id))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
ChosenInlineResult,
|
ChosenInlineResult,
|
||||||
InlineQuery,
|
InlineQuery,
|
||||||
@@ -26,9 +26,7 @@ async def send_invoice(iq: InlineQuery, amount: int):
|
|||||||
InlineQueryResultArticle(
|
InlineQueryResultArticle(
|
||||||
id=dict_to_b64({"a": amount}),
|
id=dict_to_b64({"a": amount}),
|
||||||
title=f"💸 Счёт на {amount}₽",
|
title=f"💸 Счёт на {amount}₽",
|
||||||
input_message_content=InputTextMessageContent(
|
input_message_content=InputTextMessageContent(message_text="⏳ Создаю счёт..."),
|
||||||
message_text="⏳ Создаю счёт..."
|
|
||||||
),
|
|
||||||
reply_markup=placeholder_kb,
|
reply_markup=placeholder_kb,
|
||||||
description="Нажмите, чтобы создать счёт.",
|
description="Нажмите, чтобы создать счёт.",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
from aiogram.types import Message, CallbackQuery
|
|
||||||
from aiogram.filters import Command
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery, Message
|
||||||
|
|
||||||
from misc.filters import IsVerified
|
from misc.filters import IsVerified
|
||||||
from misc.kb.admins import main_menu
|
from misc.kb.admins import main_menu
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import logging
|
import logging
|
||||||
from aiogram import Router, F
|
|
||||||
from aiogram.types import Message, CallbackQuery
|
from aiogram import F, Router
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from dto.catalogue import CategoryActionContext
|
from dto.catalogue import CategoryActionContext
|
||||||
from dto.control import EditProductContext
|
from dto.control import EditProductContext
|
||||||
from misc.filters import IsVerified
|
from misc.filters import IsVerified
|
||||||
|
from misc.kb import admins
|
||||||
from misc.kb.admins import back_to_product_kb
|
from misc.kb.admins import back_to_product_kb
|
||||||
from misc.kb.client import get_back_to_catalogue
|
from misc.kb.client import get_back_to_catalogue
|
||||||
from misc.mapper import parse_cat_id
|
from misc.mapper import parse_cat_id
|
||||||
from misc.states import AdminControlStorage
|
from misc.states import AdminControlStorage
|
||||||
from misc.texts import product_editing_mapping
|
from misc.texts import get_product_description, product_editing_mapping
|
||||||
from repositories.categories import CategoriesRepository
|
from repositories.categories import CategoriesRepository
|
||||||
from repositories.products import ProductRepository
|
from repositories.products import ProductRepository
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ logger = logging.getLogger(__name__)
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(IsVerified(), F.data.startswith("edit_product:"))
|
@router.callback_query(IsVerified(), F.data.regexp(r"^edit_product:\d+:(name|description|price)$"))
|
||||||
async def edit_product(cb: CallbackQuery, state: FSMContext):
|
async def edit_product(cb: CallbackQuery, state: FSMContext):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
@@ -31,18 +33,14 @@ async def edit_product(cb: CallbackQuery, state: FSMContext):
|
|||||||
await cb.message.delete()
|
await cb.message.delete()
|
||||||
msg = await cb.message.answer(
|
msg = await cb.message.answer(
|
||||||
f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>",
|
f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>",
|
||||||
reply_markup=back_to_product_kb(
|
reply_markup=back_to_product_kb(product_id, cb_factory=lambda p: f"product:{p}"),
|
||||||
product_id, cb_factory=lambda p: f"product:{p}"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
msg = cb.message
|
msg = cb.message
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>",
|
f"<b>✍️ Введите {product_editing_mapping.get(mode)}</b>",
|
||||||
reply_markup=back_to_product_kb(
|
reply_markup=back_to_product_kb(product_id, cb_factory=lambda p: f"product:{p}"),
|
||||||
product_id, cb_factory=lambda p: f"product:{p}"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx = EditProductContext(int(product_id), mode, msg)
|
ctx = EditProductContext(int(product_id), mode, msg)
|
||||||
@@ -68,13 +66,9 @@ async def edit_product_query(
|
|||||||
try:
|
try:
|
||||||
await ctx.msg.edit_text("⏳")
|
await ctx.msg.edit_text("⏳")
|
||||||
if ctx.mode == "name":
|
if ctx.mode == "name":
|
||||||
await products_repo.update_product_name_by_id(
|
await products_repo.update_product_name_by_id(session, ctx.product_id, msg.text)
|
||||||
session, ctx.product_id, msg.text
|
|
||||||
)
|
|
||||||
elif ctx.mode == "description":
|
elif ctx.mode == "description":
|
||||||
await products_repo.update_product_description_by_id(
|
await products_repo.update_product_description_by_id(session, ctx.product_id, msg.text)
|
||||||
session, ctx.product_id, msg.text
|
|
||||||
)
|
|
||||||
elif ctx.mode == "price":
|
elif ctx.mode == "price":
|
||||||
if msg.text.isdigit():
|
if msg.text.isdigit():
|
||||||
await products_repo.update_product_price_by_id(
|
await products_repo.update_product_price_by_id(
|
||||||
@@ -99,6 +93,158 @@ async def edit_product_query(
|
|||||||
await ctx.msg.edit_text(text="‼️ Что-то пошло не так.", reply_markup=kb)
|
await ctx.msg.edit_text(text="‼️ Что-то пошло не так.", reply_markup=kb)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(IsVerified(), F.data.startswith("edit_product:"), F.data.endswith(":hide"))
|
||||||
|
async def toggle_product_visibility(
|
||||||
|
cb: CallbackQuery,
|
||||||
|
state: FSMContext,
|
||||||
|
session: AsyncSession,
|
||||||
|
products_repo: ProductRepository,
|
||||||
|
):
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
product_id = int(cb.data.split(":")[1])
|
||||||
|
product = await products_repo.toggle_product_hidden_by_id(session, product_id)
|
||||||
|
if not product:
|
||||||
|
await cb.answer("Товар не найден.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await cb.answer("Товар скрыт." if product.is_hidden else "Товар снова виден.")
|
||||||
|
|
||||||
|
if cb.message.photo:
|
||||||
|
await cb.message.edit_caption(
|
||||||
|
caption=cb.message.caption,
|
||||||
|
reply_markup=admins.edit_product(product),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await cb.message.edit_reply_markup(reply_markup=admins.edit_product(product))
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(IsVerified(), F.data.startswith("edit_product:"), F.data.endswith(":photo"))
|
||||||
|
async def request_product_photo(cb: CallbackQuery, state: FSMContext):
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
product_id = int(cb.data.split(":")[1])
|
||||||
|
|
||||||
|
if cb.message.photo:
|
||||||
|
await cb.message.delete()
|
||||||
|
msg = await cb.message.answer(
|
||||||
|
"<b>📷 Отправьте фотографию товара.</b>",
|
||||||
|
reply_markup=back_to_product_kb(product_id, cb_factory=lambda p: f"product:{p}"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg = cb.message
|
||||||
|
await cb.message.edit_text(
|
||||||
|
"<b>📷 Отправьте фотографию товара.</b>",
|
||||||
|
reply_markup=back_to_product_kb(product_id, cb_factory=lambda p: f"product:{p}"),
|
||||||
|
)
|
||||||
|
|
||||||
|
await state.set_state(AdminControlStorage.edit_product_photo)
|
||||||
|
await state.set_data({"ctx": EditProductContext(product_id, "photo", msg)})
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(AdminControlStorage.edit_product_photo)
|
||||||
|
async def upload_product_photo(
|
||||||
|
msg: Message,
|
||||||
|
state: FSMContext,
|
||||||
|
session: AsyncSession,
|
||||||
|
products_repo: ProductRepository,
|
||||||
|
):
|
||||||
|
data = await state.get_data()
|
||||||
|
ctx: EditProductContext = data.get("ctx", EditProductContext)
|
||||||
|
kb = back_to_product_kb(ctx.product_id, cb_factory=lambda p: f"product:{p}")
|
||||||
|
|
||||||
|
if not msg.photo:
|
||||||
|
await msg.delete()
|
||||||
|
await ctx.msg.edit_text("❌ Отправьте именно фотографию.", reply_markup=kb)
|
||||||
|
await state.set_state(AdminControlStorage.edit_product_photo)
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
|
await msg.delete()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ctx.msg.edit_text("⏳")
|
||||||
|
await products_repo.add_product_file_id(
|
||||||
|
session, ctx.product_id, file_id=msg.photo[-1].file_id
|
||||||
|
)
|
||||||
|
await ctx.msg.edit_text("✅ Фотография обновлена.", reply_markup=kb)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
await ctx.msg.edit_text("‼️ Что-то пошло не так.", reply_markup=kb)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(
|
||||||
|
IsVerified(), F.data.startswith("edit_product:"), F.data.endswith(":delete_photo")
|
||||||
|
)
|
||||||
|
async def delete_product_photo(
|
||||||
|
cb: CallbackQuery,
|
||||||
|
state: FSMContext,
|
||||||
|
session: AsyncSession,
|
||||||
|
products_repo: ProductRepository,
|
||||||
|
):
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
product_id = int(cb.data.split(":")[1])
|
||||||
|
product = await products_repo.clear_product_photo(session, product_id)
|
||||||
|
if not product:
|
||||||
|
await cb.answer("Товар не найден.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await cb.answer("Фото удалено.")
|
||||||
|
|
||||||
|
text = get_product_description(product)
|
||||||
|
reply_markup = admins.edit_product(product)
|
||||||
|
|
||||||
|
if cb.message.photo:
|
||||||
|
await cb.message.delete()
|
||||||
|
await cb.message.answer(text, reply_markup=reply_markup)
|
||||||
|
return
|
||||||
|
|
||||||
|
if cb.message.caption:
|
||||||
|
await cb.message.edit_text(text, reply_markup=reply_markup)
|
||||||
|
return
|
||||||
|
|
||||||
|
await cb.message.edit_reply_markup(reply_markup=reply_markup)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(IsVerified(), F.data.regexp(r"^edit_product:\d+:delete$"))
|
||||||
|
async def request_product_deletion(cb: CallbackQuery, state: FSMContext):
|
||||||
|
await state.clear()
|
||||||
|
product_id = int(cb.data.split(":")[1])
|
||||||
|
await cb.message.edit_reply_markup(reply_markup=admins.confirm_product_deletion(product_id))
|
||||||
|
await cb.answer("Подтвердите удаление товара.")
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(IsVerified(), F.data.regexp(r"^delete_product:confirm:\d+$"))
|
||||||
|
async def delete_product(
|
||||||
|
cb: CallbackQuery,
|
||||||
|
state: FSMContext,
|
||||||
|
session: AsyncSession,
|
||||||
|
products_repo: ProductRepository,
|
||||||
|
):
|
||||||
|
await state.clear()
|
||||||
|
product_id = int(cb.data.split(":")[2])
|
||||||
|
product = await products_repo.get_product_by_id(session, product_id)
|
||||||
|
if not product:
|
||||||
|
await cb.answer("Товар уже удалён.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
category_id = product.category_id
|
||||||
|
try:
|
||||||
|
await products_repo.delete_product(session, product_id)
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
logger.exception("Failed to delete product %s", product_id)
|
||||||
|
await cb.answer("Не удалось удалить товар: он используется в заказах.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await cb.message.delete()
|
||||||
|
await cb.message.answer("✅ Товар удалён.", reply_markup=get_back_to_catalogue(category_id))
|
||||||
|
await cb.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("edit:"))
|
@router.callback_query(F.data.startswith("edit:"))
|
||||||
async def edit_entry(cb: CallbackQuery, state: FSMContext):
|
async def edit_entry(cb: CallbackQuery, state: FSMContext):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
@@ -143,6 +289,4 @@ async def edit_category(
|
|||||||
session=session, category_id=parse_cat_id(ctx.parent_id), value=msg.text
|
session=session, category_id=parse_cat_id(ctx.parent_id), value=msg.text
|
||||||
)
|
)
|
||||||
|
|
||||||
await ctx.msg.edit_text(
|
await ctx.msg.edit_text("✅ Успешно.", reply_markup=get_back_to_catalogue(cat.parent_id))
|
||||||
"✅ Успешно.", reply_markup=get_back_to_catalogue(cat.parent_id)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
from .cart import router as cart_router
|
||||||
|
from .catalogue import router as catalogue_router
|
||||||
|
from .checkout import router as checkout_router
|
||||||
from .invoices import router as invoice_router
|
from .invoices import router as invoice_router
|
||||||
from .menu import router as menu_router
|
from .menu import router as menu_router
|
||||||
from .catalogue import router as catalogue_router
|
|
||||||
from .products import router as products_router
|
from .products import router as products_router
|
||||||
from .cart import router as cart_router
|
|
||||||
from .checkout import router as checkout_router
|
|
||||||
from .security import router as security_router
|
|
||||||
from .search import router as search_router
|
from .search import router as search_router
|
||||||
|
from .security import router as security_router
|
||||||
|
|
||||||
routers = [
|
routers = [
|
||||||
invoice_router,
|
invoice_router,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.types import CallbackQuery, Message
|
|
||||||
from aiogram.filters import Command
|
from aiogram.filters import Command
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import PAGE_SIZE
|
from config import PAGE_SIZE
|
||||||
@@ -21,15 +21,14 @@ async def cart_init(
|
|||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
cart = await order_service.build_pagination_cart_dto(
|
cart = await order_service.build_pagination_cart_dto(session, customer=cb.from_user.id)
|
||||||
session, customer=cb.from_user.id
|
|
||||||
)
|
|
||||||
if not len(cart.items):
|
if not len(cart.items):
|
||||||
await cb.message.edit_text("🛒 Корзина пуста.", reply_markup=return_menu)
|
await cb.message.edit_text("🛒 Корзина пуста.", reply_markup=return_menu)
|
||||||
return
|
return
|
||||||
|
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
f"total: {cart.total}₽ | {len(cart.items)}",
|
"<b>🌸 Убедитесь в том, что все желаемые позиции есть в заказе ниже!</b>\n\n"
|
||||||
|
f"<b>Сумма:</b> <i>{cart.total}₽</i>",
|
||||||
reply_markup=render_cart(
|
reply_markup=render_cart(
|
||||||
cart.items[:PAGE_SIZE],
|
cart.items[:PAGE_SIZE],
|
||||||
show_next=len(cart.items) > PAGE_SIZE,
|
show_next=len(cart.items) > PAGE_SIZE,
|
||||||
@@ -47,16 +46,15 @@ async def cart_cmd(
|
|||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
cart = await order_service.build_pagination_cart_dto(
|
cart = await order_service.build_pagination_cart_dto(session, customer=msg.from_user.id)
|
||||||
session, customer=msg.from_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
if not len(cart.items):
|
if not len(cart.items):
|
||||||
await msg.answer("🛒 Корзина пуста.", reply_markup=return_menu)
|
await msg.answer("🛒 Корзина пуста.", reply_markup=return_menu)
|
||||||
return
|
return
|
||||||
|
|
||||||
await msg.answer(
|
await msg.answer(
|
||||||
f"total: {cart.total}₽",
|
"<b>🌸 Убедитесь в том, что все желаемые позиции есть в заказе ниже!</b>\n\n"
|
||||||
|
f"<b>Сумма:</b> <i>{cart.total}₽</i>",
|
||||||
reply_markup=render_cart(
|
reply_markup=render_cart(
|
||||||
cart.items[:PAGE_SIZE],
|
cart.items[:PAGE_SIZE],
|
||||||
show_next=len(cart.items) > PAGE_SIZE,
|
show_next=len(cart.items) > PAGE_SIZE,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.types import CallbackQuery
|
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import VERIFIED_ACCOUNTS
|
from config import VERIFIED_ACCOUNTS
|
||||||
@@ -24,7 +24,7 @@ async def subcatalogue(
|
|||||||
cat_id = cb.data.split(":")[1]
|
cat_id = cb.data.split(":")[1]
|
||||||
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
|
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
|
||||||
|
|
||||||
view = await catalogue_service.build_category_view(session, cat_id)
|
view = await catalogue_service.build_category_view(session, cat_id, include_hidden=is_admin)
|
||||||
kb = render_catalogue(view, is_admin=is_admin)
|
kb = render_catalogue(view, is_admin=is_admin)
|
||||||
path = await catalogue_service.get_path(session, cat_id)
|
path = await catalogue_service.get_path(session, cat_id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
|
||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.types import CallbackQuery, Message
|
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import NOTIFICATION_CHANNEL
|
from config import CHANNEL_ID, NOTIFICATION_CHANNEL
|
||||||
from db.models.orders import OrderStatus
|
from db.models.orders import OrderStatus
|
||||||
from dto.checkout import CheckoutContext
|
from dto.checkout import CheckoutContext
|
||||||
from misc.kb.admins import customer_contacts
|
from misc.kb.admins import customer_contacts
|
||||||
from misc.kb.client import order_confirmation, order_specs_confirmation
|
from misc.kb.client import order_confirmation, order_specs_confirmation, subscription
|
||||||
from misc.kb.common import return_menu
|
from misc.kb.common import return_menu
|
||||||
from misc.states import CheckoutStorage
|
from misc.states import CheckoutStorage
|
||||||
from misc.texts import get_order_item_list
|
from misc.texts import get_order_item_list
|
||||||
from misc.utils import is_valid_phone
|
from misc.utils import is_member, is_valid_phone
|
||||||
from repositories.orders import OrderRepository
|
from repositories.orders import OrderRepository
|
||||||
from services.orders import OrderService
|
from services.orders import OrderService
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ async def checkout_phone(msg: Message, state: FSMContext):
|
|||||||
|
|
||||||
ctx.phone = msg.text
|
ctx.phone = msg.text
|
||||||
await ctx.orig_msg.edit_text(
|
await ctx.orig_msg.edit_text(
|
||||||
f"<i>👤 Имя: {ctx.name}</i>\n<i>📱 Номер телефона: {ctx.phone}</i>\n\n<b>📍 Укажите ваш адрес (город, улица):</b>",
|
f"<i>👤 Имя: {ctx.name}</i>\n<i>📱 Номер телефона: {ctx.phone}</i>\n\n<b>📍 Укажите адрес ближайшего ПВЗ Ozon (город, улица):</b>",
|
||||||
reply_markup=return_menu,
|
reply_markup=return_menu,
|
||||||
)
|
)
|
||||||
await state.set_state(CheckoutStorage.address)
|
await state.set_state(CheckoutStorage.address)
|
||||||
@@ -119,7 +119,7 @@ async def checkout_address(msg: Message, state: FSMContext):
|
|||||||
"━━━━━━━━━━━━━━\n\n"
|
"━━━━━━━━━━━━━━\n\n"
|
||||||
f"<i>👤 Имя: {ctx.name}</i>\n"
|
f"<i>👤 Имя: {ctx.name}</i>\n"
|
||||||
f"<i>📱 Номер телефона: {ctx.phone}</i>\n"
|
f"<i>📱 Номер телефона: {ctx.phone}</i>\n"
|
||||||
f"<i>📍 Ваш адрес: {ctx.address}</i>\n\n"
|
f"<i>📍 Адрес ближайшего ПВЗ Ozon: {ctx.address}</i>\n\n"
|
||||||
"━━━━━━━━━━━━━━",
|
"━━━━━━━━━━━━━━",
|
||||||
reply_markup=order_specs_confirmation(ctx.order_id),
|
reply_markup=order_specs_confirmation(ctx.order_id),
|
||||||
)
|
)
|
||||||
@@ -138,7 +138,7 @@ async def payment_create(
|
|||||||
order_repo: OrderRepository,
|
order_repo: OrderRepository,
|
||||||
):
|
):
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
ctx: Optional[CheckoutContext] = data.get("ctx")
|
ctx: CheckoutContext | None = data.get("ctx")
|
||||||
if not ctx:
|
if not ctx:
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
"🍃 Что-то пошло не так, повторите попытку позже...",
|
"🍃 Что-то пошло не так, повторите попытку позже...",
|
||||||
@@ -157,16 +157,14 @@ async def payment_create(
|
|||||||
"━━━━━━━━━━━━━━\n\n"
|
"━━━━━━━━━━━━━━\n\n"
|
||||||
f"<i>👤 Имя: {ctx.name}</i>\n"
|
f"<i>👤 Имя: {ctx.name}</i>\n"
|
||||||
f"<i>📱 Номер телефона: {ctx.phone}</i>\n"
|
f"<i>📱 Номер телефона: {ctx.phone}</i>\n"
|
||||||
f"<i>📍 Адрес: {ctx.address}</i>\n\n"
|
f"<i>📍 Адрес ближайшего ПВЗ Ozon: {ctx.address}</i>\n\n"
|
||||||
"━━━━━━━━━━━━━━\n"
|
"━━━━━━━━━━━━━━\n"
|
||||||
f"{get_order_item_list(cart)}",
|
f"{get_order_item_list(cart)}",
|
||||||
reply_markup=customer_contacts(
|
reply_markup=customer_contacts(
|
||||||
cb.from_user.id, bool(cb.from_user.username), cb.from_user.username
|
cb.from_user.id, bool(cb.from_user.username), cb.from_user.username
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await order_repo.update_order_status(
|
await order_repo.update_order_status(session, cart.order_id, OrderStatus.CREATED)
|
||||||
session, cart.order_id, OrderStatus.CREATED
|
|
||||||
)
|
|
||||||
|
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
"<b>🟢 Ваш заказ успешно отправлен!</b>\n"
|
"<b>🟢 Ваш заказ успешно отправлен!</b>\n"
|
||||||
@@ -182,3 +180,30 @@ async def payment_create(
|
|||||||
)
|
)
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("precheck_order:"))
|
||||||
|
async def precheck_order(
|
||||||
|
cb: CallbackQuery, state: FSMContext, session: AsyncSession, order_repo: OrderRepository
|
||||||
|
):
|
||||||
|
await state.clear()
|
||||||
|
order_id = cb.data.split(":")[1]
|
||||||
|
if not order_id.isdigit():
|
||||||
|
await cb.message.edit_text("🍃 Что-то пошло не так.", reply_markup=return_menu)
|
||||||
|
return
|
||||||
|
|
||||||
|
member = await cb.bot.get_chat_member(CHANNEL_ID, cb.from_user.id)
|
||||||
|
|
||||||
|
if not is_member(member):
|
||||||
|
await cb.message.edit_text(
|
||||||
|
"<b>📚 Мы заметили, что вы не подписаны на канал.</b>",
|
||||||
|
reply_markup=subscription(order_id),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await order_confirm(
|
||||||
|
cb=cb,
|
||||||
|
state=state,
|
||||||
|
session=session,
|
||||||
|
order_repo=order_repo,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.filters import CommandObject, CommandStart
|
from aiogram.filters import CommandObject, CommandStart
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
@@ -9,12 +9,12 @@ from aiogram.types import (
|
|||||||
)
|
)
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from config import card_info
|
||||||
from db.models.invoices import InvoiceStatus
|
from db.models.invoices import InvoiceStatus
|
||||||
from misc.kb.admins import verify_payment
|
from misc.kb.admins import verify_payment
|
||||||
from misc.kb.client import confirm_payment
|
from misc.kb.client import confirm_payment
|
||||||
from misc.kb.common import return_menu
|
from misc.kb.common import return_menu
|
||||||
from repositories.invoices import InvoiceRepository
|
from repositories.invoices import InvoiceRepository
|
||||||
from config import card_info
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -33,9 +33,7 @@ async def activate_invoice(
|
|||||||
invoice = await invoice_repo.get_invoice_by_id(session, invoice_id)
|
invoice = await invoice_repo.get_invoice_by_id(session, invoice_id)
|
||||||
|
|
||||||
if not invoice:
|
if not invoice:
|
||||||
await msg.answer(
|
await msg.answer("⏳ Счёт истёк. Повторите попытку позже.", reply_markup=return_menu)
|
||||||
"⏳ Счёт истёк. Повторите попытку позже.", reply_markup=return_menu
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if invoice.status != InvoiceStatus.PENDING:
|
if invoice.status != InvoiceStatus.PENDING:
|
||||||
@@ -43,9 +41,7 @@ async def activate_invoice(
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not (card_info.number and card_info.holder):
|
if not (card_info.number and card_info.holder):
|
||||||
await msg.answer(
|
await msg.answer("🍃 Оплата недоступна, повторите попытку позже.", reply_markup=return_menu)
|
||||||
"🍃 Оплата недоступна, повторите попытку позже.", reply_markup=return_menu
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
await msg.answer(
|
await msg.answer(
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
from aiogram import Router, F
|
import contextlib
|
||||||
from aiogram.types import CallbackQuery, Message
|
import os
|
||||||
|
|
||||||
|
from aiogram import F, Router
|
||||||
from aiogram.filters import Command
|
from aiogram.filters import Command
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery, FSInputFile, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from misc.kb import main_menu_kb
|
from config import WELCOME_VIDEO_NOTE_FP
|
||||||
|
from misc.kb.client import main_menu_kb
|
||||||
from repositories.order_items import OrderItemRepository
|
from repositories.order_items import OrderItemRepository
|
||||||
|
from repositories.users import create_user, get_user
|
||||||
from services.orders import OrderService
|
from services.orders import OrderService
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
@@ -20,12 +25,17 @@ async def user_main_menu(
|
|||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
cart_items = await order_items_repo.get_items_count_by_customer(
|
if not await get_user(session, msg.from_user.id):
|
||||||
session, msg.from_user.id
|
if os.path.isfile(WELCOME_VIDEO_NOTE_FP):
|
||||||
)
|
await msg.bot.send_chat_action(msg.from_user.id, "record_video")
|
||||||
|
await msg.answer_video_note(FSInputFile(WELCOME_VIDEO_NOTE_FP))
|
||||||
|
await create_user(session, msg.from_user.id)
|
||||||
|
|
||||||
|
cart_items = await order_items_repo.get_items_count_by_customer(session, msg.from_user.id)
|
||||||
await msg.answer(
|
await msg.answer(
|
||||||
"hii!", reply_markup=main_menu_kb(cart_items)
|
"<b>Добрый день!</b>\n" 'Нажмите "Каталог" чтобы выбрать товар или воспользуйтесь поиском.',
|
||||||
) # TODO: Write a welcome message
|
reply_markup=main_menu_kb(cart_items),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("menu:"))
|
@router.callback_query(F.data.startswith("menu:"))
|
||||||
@@ -37,14 +47,11 @@ async def user_main_menu_cb(
|
|||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
await cb.message.reply_to_message.delete()
|
await cb.message.reply_to_message.delete()
|
||||||
except Exception:
|
|
||||||
...
|
|
||||||
|
|
||||||
cart_items = await order_items_repo.get_items_count_by_customer(
|
cart_items = await order_items_repo.get_items_count_by_customer(session, cb.from_user.id)
|
||||||
session, cb.from_user.id
|
|
||||||
)
|
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
"hii!", reply_markup=main_menu_kb(cart_items)
|
"<b>Добрый день!</b>\n" 'Нажмите "Каталог" чтобы выбрать товар или воспользуйтесь поиском.',
|
||||||
) # TODO: Write a welcome message
|
reply_markup=main_menu_kb(cart_items),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import os
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from re import Match
|
from re import Match
|
||||||
from aiogram import Router, F
|
|
||||||
from aiogram.types import CallbackQuery, FSInputFile
|
from aiogram import F, Router
|
||||||
from aiogram.exceptions import TelegramBadRequest
|
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery, FSInputFile
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import VERIFIED_ACCOUNTS
|
from config import VERIFIED_ACCOUNTS
|
||||||
@@ -23,9 +24,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@router.callback_query(
|
@router.callback_query(
|
||||||
F.data.regexp(r"products:(?P<category_id>.*):(?P<page_id>\d+)").as_(
|
F.data.regexp(r"products:(?P<category_id>.*):(?P<page_id>\d+)").as_("pagination_match")
|
||||||
"pagination_match"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
async def products_pagination(
|
async def products_pagination(
|
||||||
cb: CallbackQuery,
|
cb: CallbackQuery,
|
||||||
@@ -39,11 +38,11 @@ async def products_pagination(
|
|||||||
pagination_data = pagination_match.groupdict()
|
pagination_data = pagination_match.groupdict()
|
||||||
category_id = int(pagination_data.get("category_id"))
|
category_id = int(pagination_data.get("category_id"))
|
||||||
page_id = int(pagination_data.get("page_id", 0))
|
page_id = int(pagination_data.get("page_id", 0))
|
||||||
|
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
|
||||||
|
|
||||||
view = await catalogue_service.build_category_view(
|
view = await catalogue_service.build_category_view(
|
||||||
session, category_id, page=page_id
|
session, category_id, page=page_id, include_hidden=is_admin
|
||||||
)
|
)
|
||||||
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
|
|
||||||
|
|
||||||
kb = render_catalogue(view, is_admin=is_admin)
|
kb = render_catalogue(view, is_admin=is_admin)
|
||||||
|
|
||||||
@@ -61,10 +60,15 @@ async def product_card(
|
|||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
product_id = int(cb.data.split(":")[1])
|
product_id = int(cb.data.split(":")[1])
|
||||||
product = await products_repo.get_product_by_id(session, product_id)
|
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
|
||||||
|
product = await products_repo.get_product_by_id(session, product_id, include_hidden=is_admin)
|
||||||
|
|
||||||
if cb.from_user.id in VERIFIED_ACCOUNTS:
|
if not product:
|
||||||
kb = admins.edit_product(product_id=product.id, category_id=product.category_id)
|
await cb.answer("<b>❌ Товар недоступен.</b>", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_admin:
|
||||||
|
kb = admins.edit_product(product)
|
||||||
else:
|
else:
|
||||||
order_item = await order_service.get_cart_product_amount(
|
order_item = await order_service.get_cart_product_amount(
|
||||||
session, customer=cb.from_user.id, product_id=product_id
|
session, customer=cb.from_user.id, product_id=product_id
|
||||||
@@ -76,9 +80,7 @@ async def product_card(
|
|||||||
cart_amount=order_item,
|
cart_amount=order_item,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not product.file_id and not (
|
if not product.file_id and not (product.img_path and os.path.isfile(product.img_path)):
|
||||||
product.img_path and os.path.isfile(product.img_path)
|
|
||||||
):
|
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
get_product_description(product),
|
get_product_description(product),
|
||||||
reply_markup=kb,
|
reply_markup=kb,
|
||||||
@@ -95,10 +97,8 @@ async def product_card(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
await cb.message.edit_text("<b>⏳ Загружаем карточку товара...</b>")
|
await cb.message.edit_text("<b>⏳ Загружаем карточку товара...</b>")
|
||||||
except TelegramBadRequest:
|
|
||||||
...
|
|
||||||
|
|
||||||
msg = await cb.message.answer_photo(
|
msg = await cb.message.answer_photo(
|
||||||
FSInputFile(product.img_path),
|
FSInputFile(product.img_path),
|
||||||
@@ -108,11 +108,9 @@ async def product_card(
|
|||||||
|
|
||||||
if not msg.photo:
|
if not msg.photo:
|
||||||
logger.warning("didnt get a photo back wahhh :(")
|
logger.warning("didnt get a photo back wahhh :(")
|
||||||
return # TODO: Add logging
|
return
|
||||||
|
|
||||||
await products_repo.add_product_file_id(
|
await products_repo.add_product_file_id(session, product_id, file_id=msg.photo[-1].file_id)
|
||||||
session, product_id, file_id=msg.photo[-1].file_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("cart_action:"))
|
@router.callback_query(F.data.startswith("cart_action:"))
|
||||||
@@ -127,7 +125,13 @@ async def cart_action(
|
|||||||
|
|
||||||
action = cb.data.split(":")[1]
|
action = cb.data.split(":")[1]
|
||||||
product_id = int(cb.data.split(":")[2])
|
product_id = int(cb.data.split(":")[2])
|
||||||
product = await products_repo.get_product_by_id(session, product_id)
|
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
|
||||||
|
product = await products_repo.get_product_by_id(session, product_id, include_hidden=is_admin)
|
||||||
|
|
||||||
|
if not product:
|
||||||
|
await cb.answer("❌ Товар недоступен.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
show_cart = True
|
show_cart = True
|
||||||
|
|
||||||
if action == "add":
|
if action == "add":
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from typing import Optional
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import PAGE_SIZE
|
from config import PAGE_SIZE, VERIFIED_ACCOUNTS
|
||||||
from misc.kb.client import render_products
|
from misc.kb.client import render_products
|
||||||
from misc.kb.common import return_menu
|
from misc.kb.common import return_menu
|
||||||
from misc.redis import RedisClient
|
from misc.redis import RedisClient
|
||||||
@@ -20,7 +19,7 @@ router = Router()
|
|||||||
async def search_trigger(cb: CallbackQuery, state: FSMContext):
|
async def search_trigger(cb: CallbackQuery, state: FSMContext):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await cb.message.edit_text("🔍 Введите запрос:", reply_markup=return_menu)
|
await cb.message.edit_text("<b>🔍 Введите запрос:</b>", reply_markup=return_menu)
|
||||||
await state.set_state(SearchStorage.query)
|
await state.set_state(SearchStorage.query)
|
||||||
await state.set_data({"msg": cb.message})
|
await state.set_data({"msg": cb.message})
|
||||||
|
|
||||||
@@ -37,11 +36,13 @@ async def searching(
|
|||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await msg.delete()
|
await msg.delete()
|
||||||
orig_msg: Optional[Message] = data.get("msg") or await msg.answer("⏳")
|
orig_msg: Message | None = data.get("msg") or await msg.answer("⏳")
|
||||||
await orig_msg.edit_text("⏳ Поиск...")
|
await orig_msg.edit_text("⏳ Поиск...")
|
||||||
|
is_admin = msg.from_user.id in VERIFIED_ACCOUNTS
|
||||||
results = await products_repo.search(
|
results = await products_repo.search(
|
||||||
session,
|
session,
|
||||||
msg.text,
|
msg.text,
|
||||||
|
include_hidden=is_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
next_cb = None
|
next_cb = None
|
||||||
@@ -50,13 +51,19 @@ async def searching(
|
|||||||
await redis_client.set_search_query(query_hash, msg.text)
|
await redis_client.set_search_query(query_hash, msg.text)
|
||||||
next_cb = f"search:{query_hash}:1"
|
next_cb = f"search:{query_hash}:1"
|
||||||
|
|
||||||
|
total = await products_repo.count_search(session, msg.text, include_hidden=is_admin)
|
||||||
|
total_pages = (total + PAGE_SIZE - 1) // PAGE_SIZE if total else None
|
||||||
|
|
||||||
await orig_msg.edit_text(
|
await orig_msg.edit_text(
|
||||||
"here",
|
"<b>🗒 Результаты поиска:</b>",
|
||||||
reply_markup=render_products(
|
reply_markup=render_products(
|
||||||
results[:PAGE_SIZE],
|
results[:PAGE_SIZE],
|
||||||
product_cb_factory=lambda p: f"product:{p.id}",
|
product_cb_factory=lambda p: f"product:{p.id}",
|
||||||
back_cb="menu:main",
|
back_cb="menu:main",
|
||||||
next_cb=next_cb,
|
next_cb=next_cb,
|
||||||
|
is_admin=is_admin,
|
||||||
|
page=0,
|
||||||
|
total_pages=total_pages,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,7 +86,16 @@ async def search_pagination(
|
|||||||
if not query:
|
if not query:
|
||||||
await cb.message.edit_text("🍃 Попробуйте ещё раз...")
|
await cb.message.edit_text("🍃 Попробуйте ещё раз...")
|
||||||
|
|
||||||
results = await products_repo.search(session, query, offset=PAGE_SIZE * page)
|
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
|
||||||
|
results = await products_repo.search(
|
||||||
|
session,
|
||||||
|
query,
|
||||||
|
offset=PAGE_SIZE * page,
|
||||||
|
include_hidden=is_admin,
|
||||||
|
)
|
||||||
|
|
||||||
|
total = await products_repo.count_search(session, query, include_hidden=is_admin)
|
||||||
|
total_pages = (total + PAGE_SIZE - 1) // PAGE_SIZE if total else None
|
||||||
|
|
||||||
await cb.message.edit_reply_markup(
|
await cb.message.edit_reply_markup(
|
||||||
reply_markup=render_products(
|
reply_markup=render_products(
|
||||||
@@ -88,5 +104,8 @@ async def search_pagination(
|
|||||||
back_cb="menu:main",
|
back_cb="menu:main",
|
||||||
next_cb=f"search:{q_hash}:{page + 1}" if len(results) > PAGE_SIZE else None,
|
next_cb=f"search:{q_hash}:{page + 1}" if len(results) > PAGE_SIZE else None,
|
||||||
prev_cb=f"search:{q_hash}:{page - 1}" if page > 0 else None,
|
prev_cb=f"search:{q_hash}:{page - 1}" if page > 0 else None,
|
||||||
|
is_admin=is_admin,
|
||||||
|
page=page,
|
||||||
|
total_pages=total_pages,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.enums import MessageOriginType
|
from aiogram.enums import MessageOriginType
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
|||||||
12
main.py
12
main.py
@@ -1,14 +1,15 @@
|
|||||||
import os
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
from redis.asyncio import Redis
|
|
||||||
from aiogram.client.session.aiohttp import AiohttpSession
|
from aiogram.client.session.aiohttp import AiohttpSession
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
from config import BOT_TOKEN, REDIS_URL
|
from config import BOT_TOKEN, PROXY_URL, REDIS_URL
|
||||||
|
from db.base import async_session
|
||||||
from handlers import admins_routers, client_routers
|
from handlers import admins_routers, client_routers
|
||||||
|
|
||||||
from middlewares.di import DIMiddleware
|
from middlewares.di import DIMiddleware
|
||||||
from middlewares.repository import RepositoryMiddleware
|
from middlewares.repository import RepositoryMiddleware
|
||||||
from middlewares.session import DBSessionMiddleware
|
from middlewares.session import DBSessionMiddleware
|
||||||
@@ -19,7 +20,6 @@ from repositories.order_items import OrderItemRepository
|
|||||||
from repositories.orders import OrderRepository
|
from repositories.orders import OrderRepository
|
||||||
from repositories.products import ProductRepository
|
from repositories.products import ProductRepository
|
||||||
from services import OrderService
|
from services import OrderService
|
||||||
from db.base import async_session
|
|
||||||
from services.catalogue import CatalogueService
|
from services.catalogue import CatalogueService
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
@@ -32,7 +32,7 @@ os.makedirs("static/img", exist_ok=True)
|
|||||||
async def main():
|
async def main():
|
||||||
dp = Dispatcher()
|
dp = Dispatcher()
|
||||||
|
|
||||||
aiohttp_session = AiohttpSession(proxy="http://127.0.0.1:2080")
|
aiohttp_session = AiohttpSession(proxy=PROXY_URL)
|
||||||
bot = Bot(
|
bot = Bot(
|
||||||
token=BOT_TOKEN,
|
token=BOT_TOKEN,
|
||||||
default=DefaultBotProperties(parse_mode="HTML"),
|
default=DefaultBotProperties(parse_mode="HTML"),
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from .di import DIMiddleware
|
|||||||
from .repository import RepositoryMiddleware
|
from .repository import RepositoryMiddleware
|
||||||
from .session import DBSessionMiddleware
|
from .session import DBSessionMiddleware
|
||||||
|
|
||||||
__all__ = ["DIMiddleware", "RepositoryMiddleware", "DBSessionMiddleware"]
|
__all__ = ["DBSessionMiddleware", "DIMiddleware", "RepositoryMiddleware"]
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from aiogram import BaseMiddleware
|
from collections.abc import Callable
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
from services.orders import OrderService
|
|
||||||
from services.catalogue import CatalogueService
|
from services.catalogue import CatalogueService
|
||||||
|
from services.orders import OrderService
|
||||||
|
|
||||||
|
|
||||||
class DIMiddleware(BaseMiddleware):
|
class DIMiddleware(BaseMiddleware):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from aiogram import BaseMiddleware
|
from aiogram import BaseMiddleware
|
||||||
|
|
||||||
from repositories import OrderRepository, CategoriesRepository
|
from repositories import CategoriesRepository, OrderRepository
|
||||||
from repositories.invoices import InvoiceRepository
|
from repositories.invoices import InvoiceRepository
|
||||||
from repositories.order_items import OrderItemRepository
|
from repositories.order_items import OrderItemRepository
|
||||||
from repositories.products import ProductRepository
|
from repositories.products import ProductRepository
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
from misc.kb.admins import *
|
|
||||||
from misc.kb.client import *
|
|
||||||
|
|
||||||
__all__ = ["admins", "client"]
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from typing import Optional, Callable, Union
|
from collections.abc import Callable
|
||||||
|
|
||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
from db.models import Product
|
||||||
|
|
||||||
main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
||||||
[
|
[
|
||||||
[InlineKeyboardButton(text="📦 Изменить каталог", callback_data="cat:root")],
|
[InlineKeyboardButton(text="📦 Изменить каталог", callback_data="cat:root")],
|
||||||
@@ -10,22 +13,18 @@ main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
|||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
def back_to_product_kb(
|
def back_to_product_kb(product_id: int | str, *, cb_factory: Callable[[int | str], str]):
|
||||||
product_id: Union[int, str], *, cb_factory: Callable[[int | str], str]
|
|
||||||
):
|
|
||||||
return InlineKeyboardBuilder(
|
return InlineKeyboardBuilder(
|
||||||
[[InlineKeyboardButton(text="⬅️", callback_data=cb_factory(product_id))]]
|
[[InlineKeyboardButton(text="⬅️", callback_data=cb_factory(product_id))]]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
def customer_contacts(
|
def customer_contacts(
|
||||||
user_id: int, has_mention: bool, mention: Optional[str] = None
|
user_id: int, has_mention: bool, mention: str | None = None
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
btns: list[list[InlineKeyboardButton]] = []
|
btns: list[list[InlineKeyboardButton]] = []
|
||||||
(
|
(
|
||||||
btns.append(
|
btns.append([InlineKeyboardButton(text="👤 Перейти", url=f"https://t.me/{mention}")])
|
||||||
[InlineKeyboardButton(text="👤 Перейти", url=f"https://t.me/{mention}")]
|
|
||||||
)
|
|
||||||
if has_mention
|
if has_mention
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
@@ -40,9 +39,7 @@ def customer_contacts(
|
|||||||
|
|
||||||
|
|
||||||
def payment_link(url: str) -> InlineKeyboardMarkup:
|
def payment_link(url: str) -> InlineKeyboardMarkup:
|
||||||
return InlineKeyboardBuilder(
|
return InlineKeyboardBuilder([[InlineKeyboardButton(text="💸 Оплатить", url=url)]]).as_markup()
|
||||||
[[InlineKeyboardButton(text="💸 Оплатить", url=url)]]
|
|
||||||
).as_markup()
|
|
||||||
|
|
||||||
|
|
||||||
def verify_payment(invoice_id: int):
|
def verify_payment(invoice_id: int):
|
||||||
@@ -57,24 +54,41 @@ def verify_payment(invoice_id: int):
|
|||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
def edit_product(product_id: int, category_id: int):
|
def edit_product(product: Product):
|
||||||
|
product_id = product.id
|
||||||
prefix = f"edit_product:{product_id}"
|
prefix = f"edit_product:{product_id}"
|
||||||
|
visibility_btn = "🙈 Скрыт" if product.is_hidden else "👁️ Видим"
|
||||||
|
photo_btn = "🗑️ Удалить фото" if product.file_id or product.img_path else "📷 Добавить фото"
|
||||||
|
photo_action = "delete_photo" if product.file_id or product.img_path else "photo"
|
||||||
|
|
||||||
return InlineKeyboardBuilder(
|
return InlineKeyboardBuilder(
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(text="✍️ Название", callback_data=f"{prefix}:name"),
|
||||||
text="✍️ Название", callback_data=f"{prefix}:name"
|
|
||||||
),
|
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text="✍️ Описание",
|
text="✍️ Описание",
|
||||||
callback_data=f"{prefix}:description",
|
callback_data=f"{prefix}:description",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
[InlineKeyboardButton(text="💸 Цена", callback_data=f"{prefix}:price")],
|
[InlineKeyboardButton(text="💸 Цена", callback_data=f"{prefix}:price")],
|
||||||
|
[InlineKeyboardButton(text=photo_btn, callback_data=f"{prefix}:{photo_action}")],
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(text="👁️", callback_data=f"{prefix}:hide"),
|
InlineKeyboardButton(text=visibility_btn, callback_data=f"{prefix}:hide"),
|
||||||
InlineKeyboardButton(text="❌", callback_data=f"{prefix}:delete"),
|
InlineKeyboardButton(text="❌", callback_data=f"{prefix}:delete"),
|
||||||
],
|
],
|
||||||
[InlineKeyboardButton(text="⬅️", callback_data=f"cat:{category_id}")],
|
[InlineKeyboardButton(text="⬅️", callback_data=f"cat:{product.category_id}")],
|
||||||
|
]
|
||||||
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_product_deletion(product_id: int) -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardBuilder(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="✅ Да, удалить", callback_data=f"delete_product:confirm:{product_id}"
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(text="❌ Отмена", callback_data=f"product:{product_id}"),
|
||||||
|
]
|
||||||
]
|
]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
from typing import Optional, Callable, Union
|
from collections.abc import Callable
|
||||||
|
|
||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
import config
|
||||||
from config import PAGE_SIZE
|
from config import PAGE_SIZE
|
||||||
from db.models import Category, Product
|
from db.models import Category, Product
|
||||||
from dto.cart import CartItemDTO
|
from dto.cart import CartItemDTO
|
||||||
from dto.catalogue import CatalogueView, CatalogueType
|
from dto.catalogue import CatalogueType, CatalogueView
|
||||||
from misc.mapper import serialize_cat_id
|
from misc.mapper import serialize_cat_id
|
||||||
|
|
||||||
|
|
||||||
def pagination_row(
|
def pagination_row(
|
||||||
*,
|
*,
|
||||||
page: int,
|
page: int,
|
||||||
prev_cb: Optional[str] = None,
|
prev_cb: str | None = None,
|
||||||
next_cb: Optional[str] = None,
|
next_cb: str | None = None,
|
||||||
) -> list[InlineKeyboardButton]:
|
) -> list[InlineKeyboardButton]:
|
||||||
row = []
|
row = []
|
||||||
|
|
||||||
@@ -39,22 +40,18 @@ def cart_btn(cart_items: int) -> InlineKeyboardButton:
|
|||||||
def get_back_to_catalogue(
|
def get_back_to_catalogue(
|
||||||
parent_id: int,
|
parent_id: int,
|
||||||
*,
|
*,
|
||||||
text: Optional[str] = "⬅️ Назад",
|
text: str | None = "⬅️ Назад",
|
||||||
show_controls: bool = False,
|
show_controls: bool = False,
|
||||||
cat_id: Optional[Union[str, int]] = None,
|
cat_id: str | int | None = None,
|
||||||
fallback_cb: Optional[str] = "menu:main",
|
fallback_cb: str | None = "menu:main",
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
|
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
if show_controls and cat_id:
|
if show_controls and cat_id:
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(text="➕ Категория", callback_data=f"new:category:{cat_id}"),
|
||||||
text="➕ Категория", callback_data=f"new:category:{cat_id}"
|
InlineKeyboardButton(text="➕ Товар", callback_data=f"new:product:{cat_id}"),
|
||||||
),
|
|
||||||
InlineKeyboardButton(
|
|
||||||
text="➕ Товар", callback_data=f"new:product:{cat_id}"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
builder.row(
|
builder.row(
|
||||||
@@ -82,19 +79,20 @@ def main_menu_kb(cart_items: int) -> InlineKeyboardMarkup:
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(text="📦 Каталог", callback_data="cat:root"),
|
InlineKeyboardButton(text="📦 Каталог", callback_data="cat:root"),
|
||||||
InlineKeyboardButton(text="🔍", callback_data="find"),
|
InlineKeyboardButton(text="🔍 Поиск", callback_data="find"),
|
||||||
],
|
],
|
||||||
[cart_btn(cart_items)],
|
[cart_btn(cart_items)],
|
||||||
|
[InlineKeyboardButton(text="🌸 Подписаться на канал", url=config.CHANNEL_URL)],
|
||||||
]
|
]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
def render_category(
|
def render_category(
|
||||||
children: list[Category],
|
children: list[Category],
|
||||||
parent_id: Optional[Union[str, int]] = None,
|
parent_id: str | int | None = None,
|
||||||
show_menu: bool = False,
|
show_menu: bool = False,
|
||||||
*,
|
*,
|
||||||
cat_id: Optional[Union[str, int]] = None,
|
cat_id: str | int | None = None,
|
||||||
category_cb_factory: Callable[[Category], str],
|
category_cb_factory: Callable[[Category], str],
|
||||||
is_admin: bool = False,
|
is_admin: bool = False,
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
@@ -116,9 +114,7 @@ def render_category(
|
|||||||
text="➕ Категория",
|
text="➕ Категория",
|
||||||
callback_data=f"new:category:{cat_id}",
|
callback_data=f"new:category:{cat_id}",
|
||||||
),
|
),
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(text="✍️", callback_data=f"edit:category:{cat_id}"),
|
||||||
text="✍️", callback_data=f"edit:category:{cat_id}"
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,7 +122,10 @@ def render_category(
|
|||||||
nav.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="menu:main")])
|
nav.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="menu:main")])
|
||||||
elif parent_id is not None:
|
elif parent_id is not None:
|
||||||
nav.append(
|
nav.append(
|
||||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}")]
|
[
|
||||||
|
InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}"),
|
||||||
|
InlineKeyboardButton(text="🏠", callback_data="menu:main"),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return InlineKeyboardBuilder(
|
return InlineKeyboardBuilder(
|
||||||
@@ -138,22 +137,27 @@ def render_products(
|
|||||||
products: list[Product],
|
products: list[Product],
|
||||||
*,
|
*,
|
||||||
product_cb_factory: Callable[[Product], str],
|
product_cb_factory: Callable[[Product], str],
|
||||||
back_cb: Optional[str] = None,
|
back_cb: str | None = None,
|
||||||
prev_cb: Optional[str] = None,
|
prev_cb: str | None = None,
|
||||||
next_cb: Optional[str] = None,
|
next_cb: str | None = None,
|
||||||
columns: int = 2,
|
columns: int = 2,
|
||||||
is_admin: bool = False,
|
is_admin: bool = False,
|
||||||
cat_id: Optional[int] = None,
|
cat_id: int | None = None,
|
||||||
|
page: int = 0,
|
||||||
|
total_pages: int | None = None,
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
if back_cb:
|
if back_cb:
|
||||||
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=back_cb))
|
builder.row(
|
||||||
|
InlineKeyboardButton(text="⬅️ Назад", callback_data=back_cb),
|
||||||
|
InlineKeyboardButton(text="🏠", callback_data="menu:main"),
|
||||||
|
)
|
||||||
|
|
||||||
for i in range(0, len(products), columns):
|
for i in range(0, len(products), columns):
|
||||||
row = [
|
row = [
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=p.name,
|
text=f"{p.name} {'🙈' if is_admin and p.is_hidden else ''}".strip(),
|
||||||
callback_data=product_cb_factory(p),
|
callback_data=product_cb_factory(p),
|
||||||
)
|
)
|
||||||
for p in products[i : i + columns]
|
for p in products[i : i + columns]
|
||||||
@@ -161,14 +165,13 @@ def render_products(
|
|||||||
builder.row(*row)
|
builder.row(*row)
|
||||||
|
|
||||||
if is_admin:
|
if is_admin:
|
||||||
builder.row(
|
builder.row(InlineKeyboardButton(text="➕ Товар", callback_data=f"new:product:{cat_id}"))
|
||||||
InlineKeyboardButton(text="➕ Товар", callback_data=f"new:product:{cat_id}")
|
|
||||||
)
|
|
||||||
|
|
||||||
nav_buttons = []
|
nav_buttons = []
|
||||||
if prev_cb:
|
if prev_cb:
|
||||||
nav_buttons.append(InlineKeyboardButton(text="◀️", callback_data=prev_cb))
|
nav_buttons.append(InlineKeyboardButton(text="◀️", callback_data=prev_cb))
|
||||||
nav_buttons.append(InlineKeyboardButton(text="🔍", callback_data="find"))
|
page_text = f"Стр. {page + 1}/{total_pages}" if total_pages is not None else f"Стр. {page + 1}"
|
||||||
|
nav_buttons.append(InlineKeyboardButton(text=page_text, callback_data="..."))
|
||||||
if next_cb:
|
if next_cb:
|
||||||
nav_buttons.append(InlineKeyboardButton(text="▶️", callback_data=next_cb))
|
nav_buttons.append(InlineKeyboardButton(text="▶️", callback_data=next_cb))
|
||||||
|
|
||||||
@@ -189,18 +192,17 @@ def render_catalogue(view: CatalogueView, *, is_admin: bool):
|
|||||||
cat_id=view.category,
|
cat_id=view.category,
|
||||||
)
|
)
|
||||||
if view.view_type == CatalogueType.PRODUCTS:
|
if view.view_type == CatalogueType.PRODUCTS:
|
||||||
|
total_pages = (view.total + PAGE_SIZE - 1) // PAGE_SIZE if view.total else None
|
||||||
return render_products(
|
return render_products(
|
||||||
products=view.products[:PAGE_SIZE], # type: ignore
|
products=view.products[:PAGE_SIZE], # type: ignore
|
||||||
product_cb_factory=lambda p: f"product:{p.id}",
|
product_cb_factory=lambda p: f"product:{p.id}",
|
||||||
back_cb=f"cat:{view.parent_id}",
|
back_cb=f"cat:{view.parent_id}",
|
||||||
prev_cb=(
|
prev_cb=(f"products:{view.category}:{view.page - 1}" if view.page > 0 else None),
|
||||||
f"products:{view.category}:{view.page - 1}" if view.page > 0 else None
|
next_cb=(f"products:{view.category}:{view.page + 1}" if view.has_next else None),
|
||||||
),
|
|
||||||
next_cb=(
|
|
||||||
f"products:{view.category}:{view.page + 1}" if view.has_next else None
|
|
||||||
),
|
|
||||||
is_admin=is_admin,
|
is_admin=is_admin,
|
||||||
cat_id=view.category,
|
cat_id=view.category,
|
||||||
|
page=view.page,
|
||||||
|
total_pages=total_pages,
|
||||||
)
|
)
|
||||||
|
|
||||||
return get_back_to_catalogue(
|
return get_back_to_catalogue(
|
||||||
@@ -216,13 +218,9 @@ def render_product_interactions(
|
|||||||
if show_cart:
|
if show_cart:
|
||||||
btns.append(
|
btns.append(
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(text="➖", callback_data=f"cart_action:remove:{product_id}"),
|
||||||
text="➖", callback_data=f"cart_action:remove:{product_id}"
|
|
||||||
),
|
|
||||||
InlineKeyboardButton(text=f"🛒 {cart_amount}", callback_data="..."),
|
InlineKeyboardButton(text=f"🛒 {cart_amount}", callback_data="..."),
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(text="➕", callback_data=f"cart_action:add:{product_id}"),
|
||||||
text="➕", callback_data=f"cart_action:add:{product_id}"
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
btns.append(
|
btns.append(
|
||||||
@@ -243,7 +241,12 @@ def render_product_interactions(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
btns.append([InlineKeyboardButton(text="⬅️", callback_data=f"cat:{category_id}")])
|
btns.append(
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="⬅️", callback_data=f"cat:{category_id}"),
|
||||||
|
InlineKeyboardButton(text="🏠", callback_data="menu:main"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return InlineKeyboardBuilder(btns).as_markup()
|
return InlineKeyboardBuilder(btns).as_markup()
|
||||||
|
|
||||||
@@ -252,9 +255,9 @@ def render_cart(
|
|||||||
cart_items: list[CartItemDTO],
|
cart_items: list[CartItemDTO],
|
||||||
*,
|
*,
|
||||||
page: int = 0,
|
page: int = 0,
|
||||||
show_prev: Optional[bool] = False,
|
show_prev: bool | None = False,
|
||||||
show_next: Optional[bool] = False,
|
show_next: bool | None = False,
|
||||||
show_purchase: Optional[bool] = False,
|
show_purchase: bool | None = False,
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
btns = [
|
btns = [
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
@@ -284,9 +287,7 @@ def render_cart(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if show_purchase:
|
if show_purchase:
|
||||||
markup.append(
|
markup.append([InlineKeyboardButton(text="🔷 Оформить заказ", callback_data="checkout")])
|
||||||
[InlineKeyboardButton(text="🔷 Оформить заказ", callback_data="checkout")]
|
|
||||||
)
|
|
||||||
|
|
||||||
return InlineKeyboardBuilder(markup).as_markup()
|
return InlineKeyboardBuilder(markup).as_markup()
|
||||||
|
|
||||||
@@ -297,7 +298,7 @@ def order_confirmation(order_id: int) -> InlineKeyboardMarkup:
|
|||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text="🟢 Подтвердить заказ",
|
text="🟢 Подтвердить заказ",
|
||||||
callback_data=f"confirm_order:{order_id}",
|
callback_data=f"precheck_order:{order_id}",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[InlineKeyboardButton(text="❌ Отменить", callback_data="menu:main")],
|
[InlineKeyboardButton(text="❌ Отменить", callback_data="menu:main")],
|
||||||
@@ -305,6 +306,20 @@ def order_confirmation(order_id: int) -> InlineKeyboardMarkup:
|
|||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def subscription(order_id: int) -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardBuilder(
|
||||||
|
[
|
||||||
|
[InlineKeyboardButton(text="🌸 Подписаться на канал", url=config.CHANNEL_URL)],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="▶️ Продолжить оформление", callback_data=f"confirm_order:{order_id}"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[InlineKeyboardButton(text="🏠", callback_data="menu:main")],
|
||||||
|
]
|
||||||
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
def order_specs_confirmation(order_id: int) -> InlineKeyboardMarkup:
|
def order_specs_confirmation(order_id: int) -> InlineKeyboardMarkup:
|
||||||
return InlineKeyboardBuilder(
|
return InlineKeyboardBuilder(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ class RedisClient:
|
|||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_search_query(self, query_hash: str) -> Optional[str]:
|
async def get_search_query(self, query_hash: str) -> str | None:
|
||||||
try:
|
try:
|
||||||
res = await self.r.get(f"search:{query_hash}")
|
res = await self.r.get(f"search:{query_hash}")
|
||||||
return res.decode()
|
return res.decode()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from aiogram.fsm.state import StatesGroup, State
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
|
||||||
|
|
||||||
class CheckoutStorage(StatesGroup):
|
class CheckoutStorage(StatesGroup):
|
||||||
@@ -18,5 +18,10 @@ class SearchStorage(StatesGroup):
|
|||||||
|
|
||||||
class AdminControlStorage(StatesGroup):
|
class AdminControlStorage(StatesGroup):
|
||||||
edit_product = State()
|
edit_product = State()
|
||||||
|
edit_product_photo = State()
|
||||||
edit_category = State()
|
edit_category = State()
|
||||||
new_category = State()
|
new_category = State()
|
||||||
|
new_product_name = State()
|
||||||
|
new_product_description = State()
|
||||||
|
new_product_price = State()
|
||||||
|
new_product_photo = State()
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
|
from aiogram.types import (
|
||||||
|
ChatMemberAdministrator,
|
||||||
|
ChatMemberMember,
|
||||||
|
ChatMemberOwner,
|
||||||
|
ResultChatMemberUnion,
|
||||||
|
)
|
||||||
from phonenumbers import NumberParseException
|
from phonenumbers import NumberParseException
|
||||||
|
|
||||||
|
|
||||||
@@ -25,3 +32,13 @@ def b64_to_dict(payload: str) -> dict["str", Any]:
|
|||||||
payload += padding
|
payload += padding
|
||||||
json_bytes = base64.urlsafe_b64decode(payload)
|
json_bytes = base64.urlsafe_b64decode(payload)
|
||||||
return json.loads(json_bytes.decode("utf-8"))
|
return json.loads(json_bytes.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def is_member(chatmember: ResultChatMemberUnion):
|
||||||
|
return any(
|
||||||
|
[
|
||||||
|
isinstance(chatmember, ChatMemberOwner),
|
||||||
|
isinstance(chatmember, ChatMemberAdministrator),
|
||||||
|
isinstance(chatmember, ChatMemberMember),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,11 +1,56 @@
|
|||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 88
|
line-length = 100
|
||||||
target-version = ["py314"]
|
target-version = ['py313']
|
||||||
|
include = '\.pyi?$'
|
||||||
|
extend-exclude = '''
|
||||||
|
/(
|
||||||
|
\.git
|
||||||
|
| venv
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
| alembic
|
||||||
|
)/
|
||||||
|
'''
|
||||||
|
|
||||||
skip-string-normalization = false
|
[tool.ruff]
|
||||||
skip-magic-trailing-comma = false
|
line-length = 100
|
||||||
|
target-version = "py313"
|
||||||
|
|
||||||
preview = false
|
exclude = [
|
||||||
|
".git",
|
||||||
|
"venv",
|
||||||
|
"build",
|
||||||
|
"dist",
|
||||||
|
"alembic",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
ignore=["F405", "F403"]
|
select = [
|
||||||
|
"E",
|
||||||
|
"W",
|
||||||
|
"F",
|
||||||
|
"I",
|
||||||
|
"N",
|
||||||
|
"UP",
|
||||||
|
"B",
|
||||||
|
"SIM",
|
||||||
|
"PL",
|
||||||
|
"RUF",
|
||||||
|
"TID",
|
||||||
|
"PT",
|
||||||
|
]
|
||||||
|
|
||||||
|
ignore = [
|
||||||
|
"E501",
|
||||||
|
"D100",
|
||||||
|
"D104",
|
||||||
|
"G004",
|
||||||
|
"PLR0913",
|
||||||
|
"RUF001",
|
||||||
|
"RUF002",
|
||||||
|
"RUF003",
|
||||||
|
"B008"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
combine-as-imports = true
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
from .orders import OrderRepository
|
|
||||||
from .categories import CategoriesRepository
|
from .categories import CategoriesRepository
|
||||||
from .products import ProductRepository
|
|
||||||
from .invoices import InvoiceRepository
|
from .invoices import InvoiceRepository
|
||||||
|
from .orders import OrderRepository
|
||||||
|
from .products import ProductRepository
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"OrderRepository",
|
|
||||||
"CategoriesRepository",
|
"CategoriesRepository",
|
||||||
"ProductRepository",
|
|
||||||
"InvoiceRepository",
|
"InvoiceRepository",
|
||||||
|
"OrderRepository",
|
||||||
|
"ProductRepository",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
from typing import Optional
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
|
|
||||||
from db.models import Category
|
from db.models import Category
|
||||||
|
|
||||||
|
|
||||||
class CategoriesRepository:
|
class CategoriesRepository:
|
||||||
async def get_category_by_id(
|
async def get_category_by_id(self, session: AsyncSession, category_id: int) -> Category | None:
|
||||||
self, session: AsyncSession, category_id: int
|
|
||||||
) -> Optional[Category]:
|
|
||||||
stmt = select(Category).where(Category.id == category_id)
|
stmt = select(Category).where(Category.id == category_id)
|
||||||
return await session.scalar(stmt)
|
return await session.scalar(stmt)
|
||||||
|
|
||||||
async def get_categories_by_parent_id(
|
async def get_categories_by_parent_id(
|
||||||
self, session: AsyncSession, parent_id: Optional[int] = None
|
self, session: AsyncSession, parent_id: int | None = None
|
||||||
) -> list[Category]:
|
) -> list[Category]:
|
||||||
stmt = select(Category).where(Category.parent_id == parent_id)
|
stmt = select(Category).where(Category.parent_id == parent_id)
|
||||||
result = await session.scalars(stmt)
|
result = await session.scalars(stmt)
|
||||||
@@ -31,9 +28,7 @@ class CategoriesRepository:
|
|||||||
parent = aliased(Category)
|
parent = aliased(Category)
|
||||||
|
|
||||||
cte = cte.union_all(
|
cte = cte.union_all(
|
||||||
select(parent.id, parent.parent_id, parent.name).join(
|
select(parent.id, parent.parent_id, parent.name).join(cte, cte.c.parent_id == parent.id)
|
||||||
cte, cte.c.parent_id == parent.id
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
stmt = select(cte)
|
stmt = select(cte)
|
||||||
@@ -45,7 +40,7 @@ class CategoriesRepository:
|
|||||||
return list(reversed(rows))
|
return list(reversed(rows))
|
||||||
|
|
||||||
async def add_category(
|
async def add_category(
|
||||||
self, session: AsyncSession, *, name: str, parent_id: Optional[int]
|
self, session: AsyncSession, *, name: str, parent_id: int | None
|
||||||
) -> Category:
|
) -> Category:
|
||||||
category = Category(name=name, parent_id=parent_id)
|
category = Category(name=name, parent_id=parent_id)
|
||||||
|
|
||||||
@@ -54,9 +49,7 @@ class CategoriesRepository:
|
|||||||
|
|
||||||
return category
|
return category
|
||||||
|
|
||||||
async def update_category_name(
|
async def update_category_name(self, session: AsyncSession, category_id: int, value: str):
|
||||||
self, session: AsyncSession, category_id: int, value: str
|
|
||||||
):
|
|
||||||
category = await self.get_category_by_id(session, category_id)
|
category = await self.get_category_by_id(session, category_id)
|
||||||
category.name = value
|
category.name = value
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from db.models import Invoice
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from db.models import Invoice
|
||||||
from db.models.invoices import InvoiceStatus
|
from db.models.invoices import InvoiceStatus
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +12,7 @@ class InvoiceRepository:
|
|||||||
*,
|
*,
|
||||||
amount: int,
|
amount: int,
|
||||||
creator_id: int,
|
creator_id: int,
|
||||||
inline_message_id: Optional[int] = None,
|
inline_message_id: int | None = None,
|
||||||
status: InvoiceStatus = InvoiceStatus.PENDING,
|
status: InvoiceStatus = InvoiceStatus.PENDING,
|
||||||
) -> Invoice:
|
) -> Invoice:
|
||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
@@ -28,9 +26,7 @@ class InvoiceRepository:
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
async def get_invoice_by_id(
|
async def get_invoice_by_id(self, session: AsyncSession, invoice_id: int) -> Invoice | None:
|
||||||
self, session: AsyncSession, invoice_id: int
|
|
||||||
) -> Optional[Invoice]:
|
|
||||||
stmt = select(Invoice).where(Invoice.id == invoice_id)
|
stmt = select(Invoice).where(Invoice.id == invoice_id)
|
||||||
return await session.scalar(stmt)
|
return await session.scalar(stmt)
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy import delete, func, select
|
from sqlalchemy import delete, func, select
|
||||||
from typing import Optional
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from db.models.orders import Order, OrderItem, OrderStatus
|
from db.models.orders import Order, OrderItem, OrderStatus
|
||||||
|
|
||||||
|
|
||||||
class OrderItemRepository:
|
class OrderItemRepository:
|
||||||
async def get_item_by_id(
|
async def get_item_by_id(self, session: AsyncSession, item_id: int) -> OrderItem | None:
|
||||||
self, session: AsyncSession, item_id: int
|
|
||||||
) -> Optional[OrderItem]:
|
|
||||||
stmt = select(OrderItem).where(OrderItem.id == item_id)
|
stmt = select(OrderItem).where(OrderItem.id == item_id)
|
||||||
return await session.scalar(stmt)
|
return await session.scalar(stmt)
|
||||||
|
|
||||||
async def get_items_by_order(
|
async def get_items_by_order(
|
||||||
self, session: AsyncSession, *, order_id: int, limit: int, offset: int = 0
|
self, session: AsyncSession, *, order_id: int, limit: int, offset: int = 0
|
||||||
) -> list[OrderItem]:
|
) -> list[OrderItem]:
|
||||||
stmt = (
|
stmt = select(OrderItem).where(OrderItem.order_id == order_id).limit(limit).offset(offset)
|
||||||
select(OrderItem)
|
|
||||||
.where(OrderItem.order_id == order_id)
|
|
||||||
.limit(limit)
|
|
||||||
.offset(offset)
|
|
||||||
)
|
|
||||||
result = await session.scalars(stmt)
|
result = await session.scalars(stmt)
|
||||||
|
|
||||||
return list(result)
|
return list(result)
|
||||||
@@ -29,9 +21,7 @@ class OrderItemRepository:
|
|||||||
stmt = select(func.count(OrderItem.id)).where(OrderItem.order_id == order_id)
|
stmt = select(func.count(OrderItem.id)).where(OrderItem.order_id == order_id)
|
||||||
return await session.scalar(stmt) or 0
|
return await session.scalar(stmt) or 0
|
||||||
|
|
||||||
async def get_items_count_by_customer(
|
async def get_items_count_by_customer(self, session: AsyncSession, customer: int) -> int:
|
||||||
self, session: AsyncSession, customer: int
|
|
||||||
) -> int:
|
|
||||||
count = await session.scalar(
|
count = await session.scalar(
|
||||||
select(func.count(OrderItem.id))
|
select(func.count(OrderItem.id))
|
||||||
.join(Order)
|
.join(Order)
|
||||||
@@ -44,7 +34,7 @@ class OrderItemRepository:
|
|||||||
|
|
||||||
async def get_item_by_order_and_product(
|
async def get_item_by_order_and_product(
|
||||||
self, session: AsyncSession, *, order_id: int, product_id: int
|
self, session: AsyncSession, *, order_id: int, product_id: int
|
||||||
) -> Optional[OrderItem]:
|
) -> OrderItem | None:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(OrderItem)
|
select(OrderItem)
|
||||||
.where(OrderItem.order_id == order_id)
|
.where(OrderItem.order_id == order_id)
|
||||||
@@ -68,9 +58,7 @@ class OrderItemRepository:
|
|||||||
product_id: int,
|
product_id: int,
|
||||||
quantity: int = 1,
|
quantity: int = 1,
|
||||||
) -> OrderItem:
|
) -> OrderItem:
|
||||||
order_item = OrderItem(
|
order_item = OrderItem(order_id=order_id, product_id=product_id, quantity=quantity)
|
||||||
order_id=order_id, product_id=product_id, quantity=quantity
|
|
||||||
)
|
|
||||||
|
|
||||||
session.add(order_item)
|
session.add(order_item)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|||||||
@@ -1,30 +1,22 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy import delete, select
|
from sqlalchemy import delete, select
|
||||||
from typing import Optional
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from db.models import Order, OrderStatus, OrderItem
|
from db.models import Order, OrderItem, OrderStatus
|
||||||
|
|
||||||
|
|
||||||
class OrderRepository:
|
class OrderRepository:
|
||||||
async def get_order_by_id(
|
async def get_order_by_id(self, session: AsyncSession, order_id: int) -> Order | None:
|
||||||
self, session: AsyncSession, order_id: int
|
|
||||||
) -> Optional[Order]:
|
|
||||||
stmt = select(Order).where(Order.id == order_id)
|
stmt = select(Order).where(Order.id == order_id)
|
||||||
return await session.scalar(stmt)
|
return await session.scalar(stmt)
|
||||||
|
|
||||||
async def get_orders_by_user(
|
async def get_orders_by_user(self, session: AsyncSession, customer: int) -> list[Order]:
|
||||||
self, session: AsyncSession, customer: int
|
|
||||||
) -> list[Order]:
|
|
||||||
stmt = select(Order).where(Order.customer == customer)
|
stmt = select(Order).where(Order.customer == customer)
|
||||||
result = await session.scalars(stmt)
|
result = await session.scalars(stmt)
|
||||||
|
|
||||||
return list(result)
|
return list(result)
|
||||||
|
|
||||||
async def get_draft_order_by_user(
|
async def get_draft_order_by_user(self, session: AsyncSession, customer: int) -> Order | None:
|
||||||
self, session: AsyncSession, customer: int
|
|
||||||
) -> Optional[Order]:
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Order)
|
select(Order)
|
||||||
.where(Order.customer == customer)
|
.where(Order.customer == customer)
|
||||||
@@ -59,9 +51,7 @@ class OrderRepository:
|
|||||||
await session.execute(stmt)
|
await session.execute(stmt)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
async def update_order_status(
|
async def update_order_status(self, session: AsyncSession, order_id: int, status: OrderStatus):
|
||||||
self, session: AsyncSession, order_id: int, status: OrderStatus
|
|
||||||
):
|
|
||||||
order = await self.get_order_by_id(session, order_id)
|
order = await self.get_order_by_id(session, order_id)
|
||||||
order.status = status
|
order.status = status
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,64 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.sql.expression import func
|
from sqlalchemy.sql.expression import func
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from config import PAGE_SIZE
|
from config import PAGE_SIZE
|
||||||
from db.models import Product
|
from db.models import Product
|
||||||
|
|
||||||
|
|
||||||
class ProductRepository:
|
class ProductRepository:
|
||||||
|
async def delete_product(self, session: AsyncSession, product_id: int) -> Product | None:
|
||||||
|
product = await self.get_product_by_id(session, product_id)
|
||||||
|
if not product:
|
||||||
|
return None
|
||||||
|
|
||||||
|
await session.delete(product)
|
||||||
|
await session.commit()
|
||||||
|
return product
|
||||||
|
|
||||||
|
async def add_product(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
*,
|
||||||
|
category_id: int | None,
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
price: int,
|
||||||
|
) -> Product:
|
||||||
|
product = Product(
|
||||||
|
category_id=category_id,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
price=price,
|
||||||
|
)
|
||||||
|
session.add(product)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(product)
|
||||||
|
return product
|
||||||
|
|
||||||
async def get_product_by_id(
|
async def get_product_by_id(
|
||||||
self, session: AsyncSession, product_id: int
|
self, session: AsyncSession, product_id: int, *, include_hidden: bool = True
|
||||||
) -> Optional[Product]:
|
) -> Product | None:
|
||||||
stmt = select(Product).where(Product.id == product_id)
|
stmt = select(Product).where(Product.id == product_id)
|
||||||
|
if not include_hidden:
|
||||||
|
stmt = stmt.where(Product.is_hidden.is_(False))
|
||||||
return await session.scalar(stmt)
|
return await session.scalar(stmt)
|
||||||
|
|
||||||
async def get_product_by_category(
|
async def get_product_by_category(
|
||||||
self, session: AsyncSession, category_id: int, *, limit: int, offset: int
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
category_id: int,
|
||||||
|
*,
|
||||||
|
limit: int,
|
||||||
|
offset: int,
|
||||||
|
include_hidden: bool = False,
|
||||||
) -> list[Product]:
|
) -> list[Product]:
|
||||||
stmt = (
|
stmt = select(Product).where(Product.category_id == category_id)
|
||||||
select(Product)
|
|
||||||
.where(Product.category_id == category_id)
|
if not include_hidden:
|
||||||
.order_by(Product.id)
|
stmt = stmt.where(Product.is_hidden.is_(False))
|
||||||
.offset(offset)
|
|
||||||
.limit(limit)
|
stmt = stmt.order_by(Product.id).offset(offset).limit(limit)
|
||||||
)
|
|
||||||
|
|
||||||
result = await session.scalars(stmt)
|
result = await session.scalars(stmt)
|
||||||
|
|
||||||
@@ -37,6 +72,51 @@ class ProductRepository:
|
|||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
async def clear_product_photo(self, session: AsyncSession, product_id: int) -> Product | None:
|
||||||
|
product = await self.get_product_by_id(session, product_id)
|
||||||
|
if not product:
|
||||||
|
return None
|
||||||
|
|
||||||
|
product.file_id = None
|
||||||
|
product.img_path = None
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
return product
|
||||||
|
|
||||||
|
async def count_products_by_category(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
category_id: int,
|
||||||
|
*,
|
||||||
|
include_hidden: bool = False,
|
||||||
|
) -> int:
|
||||||
|
stmt = select(func.count()).select_from(Product).where(Product.category_id == category_id)
|
||||||
|
|
||||||
|
if not include_hidden:
|
||||||
|
stmt = stmt.where(Product.is_hidden.is_(False))
|
||||||
|
|
||||||
|
return await session.scalar(stmt)
|
||||||
|
|
||||||
|
async def count_search(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
include_hidden: bool = False,
|
||||||
|
) -> int:
|
||||||
|
ts_query = func.plainto_tsquery("simple", query)
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(func.count())
|
||||||
|
.select_from(Product)
|
||||||
|
.where(Product.search_vector.op("@@")(ts_query))
|
||||||
|
)
|
||||||
|
|
||||||
|
if not include_hidden:
|
||||||
|
stmt = stmt.where(Product.is_hidden.is_(False))
|
||||||
|
|
||||||
|
return await session.scalar(stmt)
|
||||||
|
|
||||||
async def search(
|
async def search(
|
||||||
self,
|
self,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
@@ -44,13 +124,17 @@ class ProductRepository:
|
|||||||
*,
|
*,
|
||||||
limit: int = PAGE_SIZE + 1,
|
limit: int = PAGE_SIZE + 1,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[Optional[Product]]:
|
include_hidden: bool = False,
|
||||||
|
) -> list[Product | None]:
|
||||||
ts_query = func.plainto_tsquery("simple", query)
|
ts_query = func.plainto_tsquery("simple", query)
|
||||||
|
|
||||||
|
stmt = select(Product).where(Product.search_vector.op("@@")(ts_query))
|
||||||
|
|
||||||
|
if not include_hidden:
|
||||||
|
stmt = stmt.where(Product.is_hidden.is_(False))
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Product)
|
stmt.order_by(func.ts_rank(Product.search_vector, ts_query).desc())
|
||||||
.where(Product.search_vector.op("@@")(ts_query))
|
|
||||||
.order_by(func.ts_rank(Product.search_vector, ts_query).desc())
|
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
)
|
)
|
||||||
@@ -59,9 +143,7 @@ class ProductRepository:
|
|||||||
|
|
||||||
return list(res)
|
return list(res)
|
||||||
|
|
||||||
async def update_product_name_by_id(
|
async def update_product_name_by_id(self, session: AsyncSession, product_id: int, name: str):
|
||||||
self, session: AsyncSession, product_id: int, name: str
|
|
||||||
):
|
|
||||||
product = await self.get_product_by_id(session, product_id=product_id)
|
product = await self.get_product_by_id(session, product_id=product_id)
|
||||||
product.name = name
|
product.name = name
|
||||||
|
|
||||||
@@ -77,11 +159,21 @@ class ProductRepository:
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
return product
|
return product
|
||||||
|
|
||||||
async def update_product_price_by_id(
|
async def update_product_price_by_id(self, session: AsyncSession, product_id: int, price: str):
|
||||||
self, session: AsyncSession, product_id: int, price: str
|
|
||||||
):
|
|
||||||
product = await self.get_product_by_id(session, product_id=product_id)
|
product = await self.get_product_by_id(session, product_id=product_id)
|
||||||
product.price = price
|
product.price = price
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return product
|
return product
|
||||||
|
|
||||||
|
async def toggle_product_hidden_by_id(
|
||||||
|
self, session: AsyncSession, product_id: int
|
||||||
|
) -> Product | None:
|
||||||
|
product = await self.get_product_by_id(session, product_id=product_id)
|
||||||
|
if not product:
|
||||||
|
return None
|
||||||
|
|
||||||
|
product.is_hidden = not product.is_hidden
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
return product
|
||||||
|
|||||||
19
repositories/users.py
Normal file
19
repositories/users.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from db.models.users import User
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user(session: AsyncSession, user_id: int) -> User | None:
|
||||||
|
stmt = select(User).where(User.id == user_id)
|
||||||
|
r = await session.execute(stmt)
|
||||||
|
|
||||||
|
return r.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def create_user(session: AsyncSession, user_id: int) -> User:
|
||||||
|
user = User(id=user_id)
|
||||||
|
session.add(user)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return user
|
||||||
@@ -4,7 +4,6 @@ python-dotenv
|
|||||||
black
|
black
|
||||||
asyncpg
|
asyncpg
|
||||||
sqlalchemy[asyncio]
|
sqlalchemy[asyncio]
|
||||||
psycopg2
|
|
||||||
phonenumbers
|
phonenumbers
|
||||||
ruff
|
ruff
|
||||||
redis
|
redis
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from typing import Union, Optional
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from config import PAGE_SIZE
|
||||||
from dto.catalogue import CatalogueType, CatalogueView
|
from dto.catalogue import CatalogueType, CatalogueView
|
||||||
from misc.mapper import parse_cat_id, serialize_cat_id
|
from misc.mapper import parse_cat_id, serialize_cat_id
|
||||||
from misc.texts import get_breadcrumps_path
|
from misc.texts import get_breadcrumps_path
|
||||||
from repositories import ProductRepository, CategoriesRepository
|
from repositories import CategoriesRepository, ProductRepository
|
||||||
from config import PAGE_SIZE
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogueService:
|
class CatalogueService:
|
||||||
@@ -16,14 +15,17 @@ class CatalogueService:
|
|||||||
self.categories_repo = categories_repo
|
self.categories_repo = categories_repo
|
||||||
|
|
||||||
async def build_category_view(
|
async def build_category_view(
|
||||||
self, session: AsyncSession, cat_id: Optional[Union[int, str]], page: int = 0
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
cat_id: int | str | None,
|
||||||
|
page: int = 0,
|
||||||
|
*,
|
||||||
|
include_hidden: bool = False,
|
||||||
) -> CatalogueView:
|
) -> CatalogueView:
|
||||||
cat_id = parse_cat_id(cat_id)
|
cat_id = parse_cat_id(cat_id)
|
||||||
|
|
||||||
view = CatalogueView(page=page)
|
view = CatalogueView(page=page)
|
||||||
children = await self.categories_repo.get_categories_by_parent_id(
|
children = await self.categories_repo.get_categories_by_parent_id(session, cat_id)
|
||||||
session, cat_id
|
|
||||||
)
|
|
||||||
category = await self.categories_repo.get_category_by_id(session, cat_id)
|
category = await self.categories_repo.get_category_by_id(session, cat_id)
|
||||||
|
|
||||||
if not category: # Root category
|
if not category: # Root category
|
||||||
@@ -35,12 +37,19 @@ class CatalogueService:
|
|||||||
|
|
||||||
if not children: # Products
|
if not children: # Products
|
||||||
products = await self.product_repo.get_product_by_category(
|
products = await self.product_repo.get_product_by_category(
|
||||||
session, cat_id, limit=PAGE_SIZE + 1, offset=view.page * PAGE_SIZE
|
session,
|
||||||
|
cat_id,
|
||||||
|
limit=PAGE_SIZE + 1,
|
||||||
|
offset=view.page * PAGE_SIZE,
|
||||||
|
include_hidden=include_hidden,
|
||||||
)
|
)
|
||||||
if products:
|
if products:
|
||||||
view.view_type = CatalogueType.PRODUCTS
|
view.view_type = CatalogueType.PRODUCTS
|
||||||
view.products = products[:PAGE_SIZE]
|
view.products = products[:PAGE_SIZE]
|
||||||
view.has_next = len(products) > PAGE_SIZE
|
view.has_next = len(products) > PAGE_SIZE
|
||||||
|
view.total = await self.product_repo.count_products_by_category(
|
||||||
|
session, cat_id, include_hidden=include_hidden
|
||||||
|
)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
view.view_type = CatalogueType.EMPTY
|
view.view_type = CatalogueType.EMPTY
|
||||||
@@ -51,7 +60,7 @@ class CatalogueService:
|
|||||||
|
|
||||||
return view
|
return view
|
||||||
|
|
||||||
async def get_path(self, session: AsyncSession, cat_id: Union[int, str]):
|
async def get_path(self, session: AsyncSession, cat_id: int | str):
|
||||||
category = int(cat_id) if isinstance(cat_id, int) or cat_id.isdigit() else None
|
category = int(cat_id) if isinstance(cat_id, int) or cat_id.isdigit() else None
|
||||||
products = await self.categories_repo.get_category_path(session, category)
|
products = await self.categories_repo.get_category_path(session, category)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import PAGE_SIZE
|
from config import PAGE_SIZE
|
||||||
from db.models.orders import OrderStatus
|
from db.models.orders import OrderStatus
|
||||||
from dto.cart import CartDTO, CartItemDTO
|
from dto.cart import CartDTO, CartItemDTO
|
||||||
@@ -9,13 +11,11 @@ from repositories.orders import OrderRepository
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OrderServiceException(Exception): ...
|
class OrderServiceError(Exception): ...
|
||||||
|
|
||||||
|
|
||||||
class OrderService:
|
class OrderService:
|
||||||
def __init__(
|
def __init__(self, order_repo: OrderRepository, order_items_repo: OrderItemRepository):
|
||||||
self, order_repo: OrderRepository, order_items_repo: OrderItemRepository
|
|
||||||
):
|
|
||||||
self.order_repo = order_repo
|
self.order_repo = order_repo
|
||||||
self.order_items_repo = order_items_repo
|
self.order_items_repo = order_items_repo
|
||||||
|
|
||||||
@@ -34,9 +34,7 @@ class OrderService:
|
|||||||
order = await self.order_repo.get_draft_order_by_user(session, customer)
|
order = await self.order_repo.get_draft_order_by_user(session, customer)
|
||||||
|
|
||||||
if not order:
|
if not order:
|
||||||
order = await self.order_repo.create_order(
|
order = await self.order_repo.create_order(session, customer, OrderStatus.DRAFT)
|
||||||
session, customer, OrderStatus.DRAFT
|
|
||||||
)
|
|
||||||
|
|
||||||
order_item = await self.order_items_repo.get_item_by_order_and_product(
|
order_item = await self.order_items_repo.get_item_by_order_and_product(
|
||||||
session, order_id=order.id, product_id=product_id
|
session, order_id=order.id, product_id=product_id
|
||||||
@@ -65,7 +63,7 @@ class OrderService:
|
|||||||
order = await self.order_repo.get_draft_order_by_user(session, customer)
|
order = await self.order_repo.get_draft_order_by_user(session, customer)
|
||||||
|
|
||||||
if not order:
|
if not order:
|
||||||
raise OrderServiceException(
|
raise OrderServiceError(
|
||||||
"attempt of removing product from non-existing cart, potential callback_query exploit, ignoring..."
|
"attempt of removing product from non-existing cart, potential callback_query exploit, ignoring..."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ class OrderService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not order_item:
|
if not order_item:
|
||||||
raise OrderServiceException(
|
raise OrderServiceError(
|
||||||
"attempt of decreasing quantity of non-existing order_item, potential callback_query exploit, ignoring..."
|
"attempt of decreasing quantity of non-existing order_item, potential callback_query exploit, ignoring..."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,13 +86,11 @@ class OrderService:
|
|||||||
|
|
||||||
return new_quantity
|
return new_quantity
|
||||||
|
|
||||||
async def clear_from_cart(
|
async def clear_from_cart(self, session: AsyncSession, *, customer: int, product_id: int):
|
||||||
self, session: AsyncSession, *, customer: int, product_id: int
|
|
||||||
):
|
|
||||||
order = await self.order_repo.get_draft_order_by_user(session, customer)
|
order = await self.order_repo.get_draft_order_by_user(session, customer)
|
||||||
|
|
||||||
if not order:
|
if not order:
|
||||||
raise OrderServiceException(
|
raise OrderServiceError(
|
||||||
"attempt of removing product from non-existing cart, potential callback_query exploit, ignoring..."
|
"attempt of removing product from non-existing cart, potential callback_query exploit, ignoring..."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,7 +99,7 @@ class OrderService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not order_item:
|
if not order_item:
|
||||||
raise OrderServiceException(
|
raise OrderServiceError(
|
||||||
"attempt of decreasing quantity of non-existing order_item, potential callback_query exploit, ignoring..."
|
"attempt of decreasing quantity of non-existing order_item, potential callback_query exploit, ignoring..."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -162,9 +158,7 @@ class OrderService:
|
|||||||
|
|
||||||
return CartDTO(items=cart_items, total=total, order_id=order.id)
|
return CartDTO(items=cart_items, total=total, order_id=order.id)
|
||||||
|
|
||||||
async def build_full_cart_dto(
|
async def build_full_cart_dto(self, session: AsyncSession, customer: int) -> CartDTO:
|
||||||
self, session: AsyncSession, customer: int
|
|
||||||
) -> CartDTO:
|
|
||||||
order = await self.order_repo.get_draft_order_by_user(session, customer)
|
order = await self.order_repo.get_draft_order_by_user(session, customer)
|
||||||
|
|
||||||
cart_items = []
|
cart_items = []
|
||||||
|
|||||||
BIN
static/welcome.mp4
Normal file
BIN
static/welcome.mp4
Normal file
Binary file not shown.
Reference in New Issue
Block a user