invoices, order handling, db updates
This commit is contained in:
44
alembic/versions/536f700c6d2b_invoice_id_text_fs.py
Normal file
44
alembic/versions/536f700c6d2b_invoice_id_text_fs.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""invoice id -> text fs
|
||||||
|
|
||||||
|
Revision ID: 536f700c6d2b
|
||||||
|
Revises: f7372a4db67d
|
||||||
|
Create Date: 2026-02-14 22:54:39.201321
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "536f700c6d2b"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "f7372a4db67d"
|
||||||
|
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.alter_column(
|
||||||
|
"invoices",
|
||||||
|
"inline_message_id",
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
type_=sa.String(),
|
||||||
|
existing_nullable=True,
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column(
|
||||||
|
"invoices",
|
||||||
|
"inline_message_id",
|
||||||
|
existing_type=sa.String(),
|
||||||
|
type_=sa.INTEGER(),
|
||||||
|
existing_nullable=True,
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
47
alembic/versions/ac19c319304a_invoices_creation.py
Normal file
47
alembic/versions/ac19c319304a_invoices_creation.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""invoices creation
|
||||||
|
|
||||||
|
Revision ID: ac19c319304a
|
||||||
|
Revises: 2c9553f99e03
|
||||||
|
Create Date: 2026-02-14 22:20:47.308942
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "ac19c319304a"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "2c9553f99e03"
|
||||||
|
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(
|
||||||
|
"invoices",
|
||||||
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column("creator_id", sa.BIGINT(), nullable=False),
|
||||||
|
sa.Column("amount", sa.BIGINT(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"status",
|
||||||
|
sa.Enum("PENDING", "PAID", "EXPIRED", name="invoicestatus"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("inline_message_id", sa.BIGINT(), nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table("invoices")
|
||||||
|
# ### end Alembic commands ###
|
||||||
44
alembic/versions/f7372a4db67d_invoice_id_text.py
Normal file
44
alembic/versions/f7372a4db67d_invoice_id_text.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""invoice id -> text
|
||||||
|
|
||||||
|
Revision ID: f7372a4db67d
|
||||||
|
Revises: ac19c319304a
|
||||||
|
Create Date: 2026-02-14 22:54:03.047715
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "f7372a4db67d"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "ac19c319304a"
|
||||||
|
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.alter_column(
|
||||||
|
"invoices",
|
||||||
|
"inline_message_id",
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=True,
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column(
|
||||||
|
"invoices",
|
||||||
|
"inline_message_id",
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=True,
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -14,7 +14,11 @@ POSTGRES_URL = os.getenv(
|
|||||||
"postgresql+asyncpg://postgres:HEXDEVFUCKINGSUCKS!@localhost:5432/postgres",
|
"postgresql+asyncpg://postgres:HEXDEVFUCKINGSUCKS!@localhost:5432/postgres",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
POST_TOKEN = os.getenv("POST_TOKEN", "")
|
||||||
|
POST_AUTH_KEY = os.getenv("POST_AUTH_KEY", "")
|
||||||
|
|
||||||
### Constants ###
|
### Constants ###
|
||||||
|
|
||||||
PAGE_SIZE = 4
|
PAGE_SIZE = 4
|
||||||
VERIFIED_ACCOUNTS = [1026030711]
|
VERIFIED_ACCOUNTS = [1026030711, 8480400744]
|
||||||
|
NOTIFICATION_CHANNEL = -1003849564110
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
from .orders import Order, OrderItem, OrderStatus
|
from .orders import Order, OrderItem, OrderStatus
|
||||||
from .products import Product, Category
|
from .products import Product, Category
|
||||||
|
from .invoices import Invoice, InvoiceStatus
|
||||||
|
|
||||||
__all__ = ["Order", "OrderItem", "OrderStatus", "Product", "Category"]
|
__all__ = [
|
||||||
|
"Order",
|
||||||
|
"OrderItem",
|
||||||
|
"OrderStatus",
|
||||||
|
"Product",
|
||||||
|
"Category",
|
||||||
|
"Invoice",
|
||||||
|
"InvoiceStatus",
|
||||||
|
]
|
||||||
|
|||||||
24
db/models/invoices.py
Normal file
24
db/models/invoices.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum as E_cls
|
||||||
|
from sqlalchemy import DateTime, func, Enum
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from sqlalchemy.dialects.postgresql import BIGINT
|
||||||
|
|
||||||
|
from db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceStatus(E_cls):
|
||||||
|
PENDING = "PENDING"
|
||||||
|
PAID = "PAID"
|
||||||
|
EXPIRED = "EXPIRED"
|
||||||
|
|
||||||
|
|
||||||
|
class Invoice(Base):
|
||||||
|
__tablename__ = "invoices"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True)
|
||||||
|
creator_id: Mapped[int] = mapped_column(BIGINT)
|
||||||
|
amount: Mapped[int] = mapped_column(BIGINT)
|
||||||
|
status: Mapped[InvoiceStatus] = mapped_column(Enum(InvoiceStatus))
|
||||||
|
inline_message_id: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
@@ -11,8 +11,3 @@ class CheckoutContext:
|
|||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
address: Optional[str] = None
|
address: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PaymentContext:
|
|
||||||
notification_msg: Message
|
|
||||||
|
|||||||
@@ -2,10 +2,21 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
echo "checking venv..."
|
||||||
|
if [[ "$VIRTUAL_ENV" != "" ]]
|
||||||
|
then
|
||||||
|
echo "process is in venv, skipping."
|
||||||
|
else
|
||||||
|
echo "entering venv."
|
||||||
|
source $(pwd)/venv/bin/activate
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
echo "formatting..."
|
echo "formatting..."
|
||||||
ruff check . --fix
|
ruff check . --fix
|
||||||
black .
|
black .
|
||||||
|
|
||||||
|
echo
|
||||||
echo "checking psql..."
|
echo "checking psql..."
|
||||||
if ! pg_isready -h localhost -p 5432; then
|
if ! pg_isready -h localhost -p 5432; then
|
||||||
echo "psql is down, try again" >&2
|
echo "psql is down, try again" >&2
|
||||||
@@ -13,8 +24,10 @@ if ! pg_isready -h localhost -p 5432; then
|
|||||||
fi
|
fi
|
||||||
echo "psql is up and is recieving connections."
|
echo "psql is up and is recieving connections."
|
||||||
|
|
||||||
|
echo
|
||||||
echo "running migrations..."
|
echo "running migrations..."
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
|
|
||||||
|
echo
|
||||||
echo "starting the bot..."
|
echo "starting the bot..."
|
||||||
exec python main.py
|
exec python main.py
|
||||||
@@ -1 +1,3 @@
|
|||||||
routers = []
|
from .inline_mode import router as inline_router
|
||||||
|
|
||||||
|
routers = [inline_router]
|
||||||
|
|||||||
71
handlers/admins/inline_mode.py
Normal file
71
handlers/admins/inline_mode.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.types import (
|
||||||
|
ChosenInlineResult,
|
||||||
|
InlineQuery,
|
||||||
|
InlineQueryResultArticle,
|
||||||
|
InputTextMessageContent,
|
||||||
|
)
|
||||||
|
from aiogram.utils.deep_linking import create_start_link
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from misc.filters import IsVerified
|
||||||
|
from misc.kb.admins import payment_link, placeholder_kb
|
||||||
|
from misc.utils import b64_to_dict, dict_to_b64
|
||||||
|
from repositories.invoices import InvoiceRepository
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.inline_query(IsVerified(), F.query.cast(int).as_("amount"))
|
||||||
|
async def send_invoice(iq: InlineQuery, amount: int):
|
||||||
|
results = [
|
||||||
|
InlineQueryResultArticle(
|
||||||
|
id=dict_to_b64({"a": amount}),
|
||||||
|
title=f"💸 Счёт на {amount}₽",
|
||||||
|
input_message_content=InputTextMessageContent(
|
||||||
|
message_text="⏳ Создаю счёт..."
|
||||||
|
),
|
||||||
|
reply_markup=placeholder_kb,
|
||||||
|
description="Нажмите, чтобы создать счёт.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
await iq.answer(results, is_personal=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.chosen_inline_result()
|
||||||
|
async def chosen_inline(
|
||||||
|
ev: ChosenInlineResult, session: AsyncSession, invoice_repo: InvoiceRepository
|
||||||
|
):
|
||||||
|
data = b64_to_dict(ev.result_id)
|
||||||
|
amount = data.get("a")
|
||||||
|
if not amount:
|
||||||
|
await ev.bot.edit_message_text(
|
||||||
|
inline_message_id=ev.inline_message_id, text="🍃 Что-то пошло не так..."
|
||||||
|
)
|
||||||
|
logger.critical("amount is null during the invoice creation.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
amount = int(amount)
|
||||||
|
except Exception as e:
|
||||||
|
await ev.bot.edit_message_text(
|
||||||
|
inline_message_id=ev.inline_message_id, text="🍃 Что-то пошло не так..."
|
||||||
|
)
|
||||||
|
logger.exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
invoice = await invoice_repo.create_invoice(
|
||||||
|
session,
|
||||||
|
amount=amount,
|
||||||
|
creator_id=ev.from_user.id,
|
||||||
|
inline_message_id=ev.inline_message_id,
|
||||||
|
)
|
||||||
|
invoice_link = await create_start_link(ev.bot, f"invoice:{invoice.id}", encode=True)
|
||||||
|
await ev.bot.edit_message_text(
|
||||||
|
inline_message_id=ev.inline_message_id,
|
||||||
|
text=f"<b>💸 Счёт на {amount}₽.</b>",
|
||||||
|
reply_markup=payment_link(invoice_link),
|
||||||
|
)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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 .catalogue import router as catalogue_router
|
||||||
from .products import router as products_router
|
from .products import router as products_router
|
||||||
@@ -6,6 +7,7 @@ from .checkout import router as checkout_router
|
|||||||
from .security import router as security_router
|
from .security import router as security_router
|
||||||
|
|
||||||
routers = [
|
routers = [
|
||||||
|
invoice_router,
|
||||||
menu_router,
|
menu_router,
|
||||||
catalogue_router,
|
catalogue_router,
|
||||||
products_router,
|
products_router,
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from config import NOTIFICATION_CHANNEL
|
||||||
from dto.checkout import CheckoutContext
|
from dto.checkout import CheckoutContext
|
||||||
|
from misc.kb.admins import customer_contacts
|
||||||
from misc.kb.client import (
|
from misc.kb.client import (
|
||||||
order_confirmation,
|
order_confirmation,
|
||||||
order_specs_confirmation,
|
order_specs_confirmation,
|
||||||
@@ -16,6 +20,7 @@ from repositories.orders import OrderRepository
|
|||||||
from services.orders import OrderService
|
from services.orders import OrderService
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "checkout")
|
@router.callback_query(F.data == "checkout")
|
||||||
@@ -119,4 +124,55 @@ async def checkout_address(msg: Message, state: FSMContext):
|
|||||||
f"<i>📍 Ваш адрес: {ctx.address}</i>\n\n"
|
f"<i>📍 Ваш адрес: {ctx.address}</i>\n\n"
|
||||||
"━━━━━━━━━━━━━━",
|
"━━━━━━━━━━━━━━",
|
||||||
reply_markup=order_specs_confirmation(ctx.order_id),
|
reply_markup=order_specs_confirmation(ctx.order_id),
|
||||||
) # TODO: State-driven Payment System.
|
)
|
||||||
|
|
||||||
|
await state.set_state(CheckoutStorage.confirmation)
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(CheckoutStorage.confirmation)
|
||||||
|
@router.callback_query(F.data.startswith("payment:"))
|
||||||
|
async def payment_create(
|
||||||
|
cb: CallbackQuery,
|
||||||
|
state: FSMContext,
|
||||||
|
session: AsyncSession,
|
||||||
|
order_service: OrderService,
|
||||||
|
):
|
||||||
|
data = await state.get_data()
|
||||||
|
ctx: Optional[CheckoutContext] = data.get("ctx")
|
||||||
|
if not ctx:
|
||||||
|
await cb.message.edit_text(
|
||||||
|
"🍃 Что-то пошло не так, повторите попытку позже...",
|
||||||
|
reply_markup=return_menu,
|
||||||
|
)
|
||||||
|
logger.warning("ctx not found on confirmation screen")
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
await cb.message.edit_text(
|
||||||
|
"<b>🟢 Ваш заказ успешно отправлен!</b>\n"
|
||||||
|
"<i>📦 Мы свяжемся с вами для уточнения деталей в ближайшее время.</i>\n\n"
|
||||||
|
"<b>‼️ Чтобы убедиться, что вы общаетесь с доверенным контактом ШВЕЙТЕХЦЕНТР, перешлите любое сообщение человека в этот чат.</b>",
|
||||||
|
reply_markup=return_menu,
|
||||||
|
)
|
||||||
|
|
||||||
|
cart = await order_service.build_full_cart_dto(session, cb.from_user.id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await cb.bot.send_message(
|
||||||
|
NOTIFICATION_CHANNEL,
|
||||||
|
"🟢 Новый заказ.\n\n"
|
||||||
|
"━━━━━━━━━━━━━━\n\n"
|
||||||
|
f"<i>👤 Имя: {ctx.name}</i>\n"
|
||||||
|
f"<i>📱 Номер телефона: {ctx.phone}</i>\n"
|
||||||
|
f"<i>📍 Адрес: {ctx.address}</i>\n\n"
|
||||||
|
"━━━━━━━━━━━━━━\n"
|
||||||
|
f"{get_order_item_list(cart)}",
|
||||||
|
reply_markup=customer_contacts(
|
||||||
|
cb.from_user.id, bool(cb.from_user.username), cb.from_user.username
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return
|
||||||
|
|||||||
29
handlers/client/invoices.py
Normal file
29
handlers/client/invoices.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from aiogram import Router
|
||||||
|
from aiogram.filters import CommandObject, CommandStart
|
||||||
|
from aiogram.types import (
|
||||||
|
Message,
|
||||||
|
)
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from repositories.invoices import InvoiceRepository
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(CommandStart(deep_link=True, deep_link_encoded=True))
|
||||||
|
async def activate_invoice(
|
||||||
|
msg: Message,
|
||||||
|
command: CommandObject,
|
||||||
|
session: AsyncSession,
|
||||||
|
invoice_repo: InvoiceRepository,
|
||||||
|
):
|
||||||
|
payload = command.args
|
||||||
|
|
||||||
|
invoice_id = int(payload.split(":")[1])
|
||||||
|
invoice = await invoice_repo.get_invoice_by_id(session, invoice_id)
|
||||||
|
await msg.answer(
|
||||||
|
f"<b>🟢 Получен счёт на {invoice.amount}₽</b>"
|
||||||
|
) # TODO: Process payment
|
||||||
@@ -5,6 +5,7 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from misc.kb import main_menu_kb
|
from misc.kb import main_menu_kb
|
||||||
|
from repositories.order_items import OrderItemRepository
|
||||||
from services.orders import OrderService
|
from services.orders import OrderService
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
@@ -12,11 +13,16 @@ router = Router()
|
|||||||
|
|
||||||
@router.message(Command("start"))
|
@router.message(Command("start"))
|
||||||
async def main_menu(
|
async def main_menu(
|
||||||
msg: Message, state: FSMContext, session: AsyncSession, order_service: OrderService
|
msg: Message,
|
||||||
|
state: FSMContext,
|
||||||
|
session: AsyncSession,
|
||||||
|
order_items_repo: OrderItemRepository,
|
||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
cart_items = await order_service.get_cart_items_count(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)
|
"hii!", reply_markup=main_menu_kb(cart_items)
|
||||||
) # TODO: Write a welcome message
|
) # TODO: Write a welcome message
|
||||||
@@ -27,11 +33,13 @@ async def main_menu_cb(
|
|||||||
cb: CallbackQuery,
|
cb: CallbackQuery,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
order_service: OrderService,
|
order_items_repo: OrderService,
|
||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
cart_items = await order_service.get_cart_items_count(session, cb.from_user.id)
|
cart_items = await order_items_repo.get_items_count_by_customer(
|
||||||
|
session, cb.from_user.id
|
||||||
|
)
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
"hii!", reply_markup=main_menu_kb(cart_items)
|
"hii!", reply_markup=main_menu_kb(cart_items)
|
||||||
) # TODO: Write a welcome message
|
) # TODO: Write a welcome message
|
||||||
|
|||||||
21
main.py
21
main.py
@@ -11,6 +11,7 @@ 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
|
||||||
from repositories.categories import CategoriesRepository
|
from repositories.categories import CategoriesRepository
|
||||||
|
from repositories.invoices import InvoiceRepository
|
||||||
from repositories.order_items import OrderItemRepository
|
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
|
||||||
@@ -38,26 +39,18 @@ async def main():
|
|||||||
|
|
||||||
product_repository = ProductRepository()
|
product_repository = ProductRepository()
|
||||||
|
|
||||||
# DI Middleware Init
|
invoice_repository = InvoiceRepository()
|
||||||
dp.message.middleware(DIMiddleware(order_service))
|
|
||||||
dp.message.middleware(DBSessionMiddleware(async_session))
|
|
||||||
dp.message.middleware(
|
|
||||||
RepositoryMiddleware(
|
|
||||||
order_repository,
|
|
||||||
category_repository,
|
|
||||||
product_repository,
|
|
||||||
order_items_repository,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dp.callback_query.middleware(DIMiddleware(order_service))
|
# DI Middleware Init
|
||||||
dp.callback_query.middleware(DBSessionMiddleware(async_session))
|
dp.update.middleware(DIMiddleware(order_service))
|
||||||
dp.callback_query.middleware(
|
dp.update.middleware(DBSessionMiddleware(async_session))
|
||||||
|
dp.update.middleware(
|
||||||
RepositoryMiddleware(
|
RepositoryMiddleware(
|
||||||
order_repository,
|
order_repository,
|
||||||
category_repository,
|
category_repository,
|
||||||
product_repository,
|
product_repository,
|
||||||
order_items_repository,
|
order_items_repository,
|
||||||
|
invoice_repository,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from aiogram import BaseMiddleware
|
from aiogram import BaseMiddleware
|
||||||
|
|
||||||
from repositories import OrderRepository, CategoriesRepository
|
from repositories import OrderRepository, CategoriesRepository
|
||||||
|
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
|
||||||
|
|
||||||
@@ -12,15 +13,19 @@ class RepositoryMiddleware(BaseMiddleware):
|
|||||||
categories_repo: CategoriesRepository,
|
categories_repo: CategoriesRepository,
|
||||||
products_repo: ProductRepository,
|
products_repo: ProductRepository,
|
||||||
order_items_repo: OrderItemRepository,
|
order_items_repo: OrderItemRepository,
|
||||||
|
invoice_repo: InvoiceRepository,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._order_repo = order_repo
|
self._order_repo = order_repo
|
||||||
self._categories_repo = categories_repo
|
self._categories_repo = categories_repo
|
||||||
self._products_repo = products_repo
|
self._products_repo = products_repo
|
||||||
self._order_items_repo = order_items_repo
|
self._order_items_repo = order_items_repo
|
||||||
|
self._invoice_repo = invoice_repo
|
||||||
|
|
||||||
async def __call__(self, handler, event, data):
|
async def __call__(self, handler, event, data):
|
||||||
data["order_repo"] = self._order_repo
|
data["order_repo"] = self._order_repo
|
||||||
data["categories_repo"] = self._categories_repo
|
data["categories_repo"] = self._categories_repo
|
||||||
data["products_repo"] = self._products_repo
|
data["products_repo"] = self._products_repo
|
||||||
data["order_items_repo"] = self._order_items_repo
|
data["order_items_repo"] = self._order_items_repo
|
||||||
|
data["invoice_repo"] = self._invoice_repo
|
||||||
|
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
|
|||||||
9
misc/filters.py
Normal file
9
misc/filters.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from aiogram.filters import BaseFilter
|
||||||
|
from aiogram.types import InlineQuery
|
||||||
|
|
||||||
|
from config import VERIFIED_ACCOUNTS
|
||||||
|
|
||||||
|
|
||||||
|
class IsVerified(BaseFilter):
|
||||||
|
async def __call__(self, inline_query: InlineQuery) -> bool:
|
||||||
|
return inline_query.from_user.id in VERIFIED_ACCOUNTS
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
placeholder_kb = InlineKeyboardBuilder(
|
||||||
|
[[InlineKeyboardButton(text="🍃", callback_data="...")]]
|
||||||
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def customer_contacts(
|
||||||
|
user_id: int, has_mention: bool, mention: Optional[str] = None
|
||||||
|
) -> InlineKeyboardMarkup:
|
||||||
|
btns: list[list[InlineKeyboardButton]] = []
|
||||||
|
(
|
||||||
|
btns.append(
|
||||||
|
[InlineKeyboardButton(text="👤 Перейти", url=f"https://t.me/{mention}")]
|
||||||
|
)
|
||||||
|
if has_mention
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
btns.append(
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="✍️", url=f"tg://openmessage?user_id={user_id}"),
|
||||||
|
InlineKeyboardButton(text="✍️", url=f"tg://user?id={user_id}"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return InlineKeyboardBuilder(btns).as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def payment_link(url: str) -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardBuilder(
|
||||||
|
[[InlineKeyboardButton(text="💸 Оплатить", url=url)]]
|
||||||
|
).as_markup()
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ def order_specs_confirmation(order_id: int) -> InlineKeyboardMarkup:
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text="🟢 Перейти к оплате",
|
text="🟢 Подтвердить",
|
||||||
callback_data=f"payment:{order_id}",
|
callback_data=f"payment:{order_id}",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,3 +5,8 @@ class CheckoutStorage(StatesGroup):
|
|||||||
name = State()
|
name = State()
|
||||||
phone = State()
|
phone = State()
|
||||||
address = State()
|
address = State()
|
||||||
|
confirmation = State()
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentStorage(StatesGroup):
|
||||||
|
refresh = State()
|
||||||
|
|||||||
@@ -18,8 +18,3 @@ def get_order_item_list(cart: CartDTO):
|
|||||||
text += f"\n━━━━━━━━━━━━━━\n💰 Итого: {cart.total}₽"
|
text += f"\n━━━━━━━━━━━━━━\n💰 Итого: {cart.total}₽"
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
# def get_order_notification(cart: CartDTO):
|
|
||||||
# text
|
|
||||||
# FIXME
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
from phonenumbers import NumberParseException
|
from phonenumbers import NumberParseException
|
||||||
|
|
||||||
@@ -8,3 +11,17 @@ def is_valid_phone(phone: str, region="RU") -> bool:
|
|||||||
return phonenumbers.is_valid_number(parsed)
|
return phonenumbers.is_valid_number(parsed)
|
||||||
except NumberParseException:
|
except NumberParseException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def dict_to_b64(payload: dict) -> str:
|
||||||
|
json_str = json.dumps(payload, separators=(",", ":"))
|
||||||
|
json_bytes = json_str.encode("utf-8")
|
||||||
|
b64 = base64.urlsafe_b64encode(json_bytes).decode("ascii")
|
||||||
|
return b64.rstrip("=")
|
||||||
|
|
||||||
|
|
||||||
|
def b64_to_dict(payload: str) -> dict["str", Any]:
|
||||||
|
padding = "=" * (-len(payload) % 4)
|
||||||
|
payload += padding
|
||||||
|
json_bytes = base64.urlsafe_b64decode(payload)
|
||||||
|
return json.loads(json_bytes.decode("utf-8"))
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
docker run --name test-postgres -e POSTGRES_PASSWORD=HEXDEVFUCKINGSUCKS! -p 5432:5432 -d postgres
|
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
from .orders import OrderRepository
|
from .orders import OrderRepository
|
||||||
from .categories import CategoriesRepository
|
from .categories import CategoriesRepository
|
||||||
from .products import ProductRepository
|
from .products import ProductRepository
|
||||||
|
from .invoices import InvoiceRepository
|
||||||
|
|
||||||
__all__ = ["OrderRepository", "CategoriesRepository", "ProductRepository"]
|
__all__ = [
|
||||||
|
"OrderRepository",
|
||||||
|
"CategoriesRepository",
|
||||||
|
"ProductRepository",
|
||||||
|
"InvoiceRepository",
|
||||||
|
]
|
||||||
|
|||||||
45
repositories/invoices.py
Normal file
45
repositories/invoices.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
from db.models import Invoice
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from db.models.invoices import InvoiceStatus
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceRepository:
|
||||||
|
async def create_invoice(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
*,
|
||||||
|
amount: int,
|
||||||
|
creator_id: int,
|
||||||
|
inline_message_id: Optional[int] = None,
|
||||||
|
status: InvoiceStatus = InvoiceStatus.PENDING,
|
||||||
|
) -> Invoice:
|
||||||
|
invoice = Invoice(
|
||||||
|
creator_id=creator_id,
|
||||||
|
amount=amount,
|
||||||
|
status=status,
|
||||||
|
inline_message_id=inline_message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(invoice)
|
||||||
|
await session.commit()
|
||||||
|
return invoice
|
||||||
|
|
||||||
|
async def get_invoice_by_id(
|
||||||
|
self, session: AsyncSession, invoice_id: int
|
||||||
|
) -> Optional[Invoice]:
|
||||||
|
stmt = select(Invoice).where(Invoice.id == invoice_id)
|
||||||
|
return await session.scalar(stmt)
|
||||||
|
|
||||||
|
async def update_invoice_msg(
|
||||||
|
self, session: AsyncSession, *, invoice_id: int, inline_message_id: int
|
||||||
|
):
|
||||||
|
stmt = (
|
||||||
|
update(Invoice.__table__)
|
||||||
|
.where(Invoice.id == invoice_id)
|
||||||
|
.values(inline_message_id=inline_message_id)
|
||||||
|
)
|
||||||
|
await session.execute(stmt)
|
||||||
@@ -2,7 +2,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy import delete, func, select, update
|
from sqlalchemy import delete, func, select, update
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from db.models.orders import OrderItem
|
from db.models.orders import Order, OrderItem, OrderStatus
|
||||||
|
|
||||||
|
|
||||||
class OrderItemRepository:
|
class OrderItemRepository:
|
||||||
@@ -29,6 +29,19 @@ 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(
|
||||||
|
self, session: AsyncSession, customer: int
|
||||||
|
) -> int:
|
||||||
|
count = await session.scalar(
|
||||||
|
select(func.count(OrderItem.id))
|
||||||
|
.join(Order)
|
||||||
|
.where(
|
||||||
|
Order.customer == customer,
|
||||||
|
Order.status == OrderStatus.DRAFT,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return int(count)
|
||||||
|
|
||||||
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]:
|
) -> Optional[OrderItem]:
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ black
|
|||||||
asyncpg
|
asyncpg
|
||||||
sqlalchemy[asyncio]
|
sqlalchemy[asyncio]
|
||||||
psycopg2
|
psycopg2
|
||||||
|
phonenumbers
|
||||||
|
ruff
|
||||||
@@ -149,13 +149,6 @@ class OrderService:
|
|||||||
session, order_item_id=order_item.id, quantity=0
|
session, order_item_id=order_item.id, quantity=0
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_cart_items_count(self, session: AsyncSession, customer: int) -> int:
|
|
||||||
order = await self.order_repo.get_draft_order_by_user(session, customer)
|
|
||||||
if not order:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
return await self.order_items_repo.get_items_count(session, order_id=order.id)
|
|
||||||
|
|
||||||
async def build_pagination_cart_dto(
|
async def build_pagination_cart_dto(
|
||||||
self,
|
self,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
|
|||||||
Reference in New Issue
Block a user