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",
|
||||
)
|
||||
|
||||
POST_TOKEN = os.getenv("POST_TOKEN", "")
|
||||
POST_AUTH_KEY = os.getenv("POST_AUTH_KEY", "")
|
||||
|
||||
### Constants ###
|
||||
|
||||
PAGE_SIZE = 4
|
||||
VERIFIED_ACCOUNTS = [1026030711]
|
||||
VERIFIED_ACCOUNTS = [1026030711, 8480400744]
|
||||
NOTIFICATION_CHANNEL = -1003849564110
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
from .orders import Order, OrderItem, OrderStatus
|
||||
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
|
||||
phone: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaymentContext:
|
||||
notification_msg: Message
|
||||
|
||||
@@ -2,10 +2,21 @@
|
||||
|
||||
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..."
|
||||
ruff check . --fix
|
||||
black .
|
||||
|
||||
echo
|
||||
echo "checking psql..."
|
||||
if ! pg_isready -h localhost -p 5432; then
|
||||
echo "psql is down, try again" >&2
|
||||
@@ -13,8 +24,10 @@ if ! pg_isready -h localhost -p 5432; then
|
||||
fi
|
||||
echo "psql is up and is recieving connections."
|
||||
|
||||
echo
|
||||
echo "running migrations..."
|
||||
alembic upgrade head
|
||||
|
||||
echo
|
||||
echo "starting the bot..."
|
||||
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 .catalogue import router as catalogue_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
|
||||
|
||||
routers = [
|
||||
invoice_router,
|
||||
menu_router,
|
||||
catalogue_router,
|
||||
products_router,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import NOTIFICATION_CHANNEL
|
||||
from dto.checkout import CheckoutContext
|
||||
from misc.kb.admins import customer_contacts
|
||||
from misc.kb.client import (
|
||||
order_confirmation,
|
||||
order_specs_confirmation,
|
||||
@@ -16,6 +20,7 @@ from repositories.orders import OrderRepository
|
||||
from services.orders import OrderService
|
||||
|
||||
router = Router()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@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"
|
||||
"━━━━━━━━━━━━━━",
|
||||
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 misc.kb import main_menu_kb
|
||||
from repositories.order_items import OrderItemRepository
|
||||
from services.orders import OrderService
|
||||
|
||||
router = Router()
|
||||
@@ -12,11 +13,16 @@ router = Router()
|
||||
|
||||
@router.message(Command("start"))
|
||||
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()
|
||||
|
||||
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(
|
||||
"hii!", reply_markup=main_menu_kb(cart_items)
|
||||
) # TODO: Write a welcome message
|
||||
@@ -27,11 +33,13 @@ async def main_menu_cb(
|
||||
cb: CallbackQuery,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
order_service: OrderService,
|
||||
order_items_repo: OrderService,
|
||||
):
|
||||
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(
|
||||
"hii!", reply_markup=main_menu_kb(cart_items)
|
||||
) # 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.session import DBSessionMiddleware
|
||||
from repositories.categories import CategoriesRepository
|
||||
from repositories.invoices import InvoiceRepository
|
||||
from repositories.order_items import OrderItemRepository
|
||||
from repositories.orders import OrderRepository
|
||||
from repositories.products import ProductRepository
|
||||
@@ -38,26 +39,18 @@ async def main():
|
||||
|
||||
product_repository = ProductRepository()
|
||||
|
||||
# DI Middleware Init
|
||||
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,
|
||||
)
|
||||
)
|
||||
invoice_repository = InvoiceRepository()
|
||||
|
||||
dp.callback_query.middleware(DIMiddleware(order_service))
|
||||
dp.callback_query.middleware(DBSessionMiddleware(async_session))
|
||||
dp.callback_query.middleware(
|
||||
# DI Middleware Init
|
||||
dp.update.middleware(DIMiddleware(order_service))
|
||||
dp.update.middleware(DBSessionMiddleware(async_session))
|
||||
dp.update.middleware(
|
||||
RepositoryMiddleware(
|
||||
order_repository,
|
||||
category_repository,
|
||||
product_repository,
|
||||
order_items_repository,
|
||||
invoice_repository,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from aiogram import BaseMiddleware
|
||||
|
||||
from repositories import OrderRepository, CategoriesRepository
|
||||
from repositories.invoices import InvoiceRepository
|
||||
from repositories.order_items import OrderItemRepository
|
||||
from repositories.products import ProductRepository
|
||||
|
||||
@@ -12,15 +13,19 @@ class RepositoryMiddleware(BaseMiddleware):
|
||||
categories_repo: CategoriesRepository,
|
||||
products_repo: ProductRepository,
|
||||
order_items_repo: OrderItemRepository,
|
||||
invoice_repo: InvoiceRepository,
|
||||
) -> None:
|
||||
self._order_repo = order_repo
|
||||
self._categories_repo = categories_repo
|
||||
self._products_repo = products_repo
|
||||
self._order_items_repo = order_items_repo
|
||||
self._invoice_repo = invoice_repo
|
||||
|
||||
async def __call__(self, handler, event, data):
|
||||
data["order_repo"] = self._order_repo
|
||||
data["categories_repo"] = self._categories_repo
|
||||
data["products_repo"] = self._products_repo
|
||||
data["order_items_repo"] = self._order_items_repo
|
||||
data["invoice_repo"] = self._invoice_repo
|
||||
|
||||
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(
|
||||
text="🟢 Перейти к оплате",
|
||||
text="🟢 Подтвердить",
|
||||
callback_data=f"payment:{order_id}",
|
||||
)
|
||||
],
|
||||
|
||||
@@ -5,3 +5,8 @@ class CheckoutStorage(StatesGroup):
|
||||
name = State()
|
||||
phone = 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}₽"
|
||||
|
||||
return text
|
||||
|
||||
|
||||
# def get_order_notification(cart: CartDTO):
|
||||
# text
|
||||
# FIXME
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import base64
|
||||
import json
|
||||
from typing import Any
|
||||
import phonenumbers
|
||||
from phonenumbers import NumberParseException
|
||||
|
||||
@@ -8,3 +11,17 @@ def is_valid_phone(phone: str, region="RU") -> bool:
|
||||
return phonenumbers.is_valid_number(parsed)
|
||||
except NumberParseException:
|
||||
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 .categories import CategoriesRepository
|
||||
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 typing import Optional
|
||||
|
||||
from db.models.orders import OrderItem
|
||||
from db.models.orders import Order, OrderItem, OrderStatus
|
||||
|
||||
|
||||
class OrderItemRepository:
|
||||
@@ -29,6 +29,19 @@ class OrderItemRepository:
|
||||
stmt = select(func.count(OrderItem.id)).where(OrderItem.order_id == order_id)
|
||||
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(
|
||||
self, session: AsyncSession, *, order_id: int, product_id: int
|
||||
) -> Optional[OrderItem]:
|
||||
|
||||
@@ -4,4 +4,6 @@ python-dotenv
|
||||
black
|
||||
asyncpg
|
||||
sqlalchemy[asyncio]
|
||||
psycopg2
|
||||
psycopg2
|
||||
phonenumbers
|
||||
ruff
|
||||
@@ -149,13 +149,6 @@ class OrderService:
|
||||
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(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
|
||||
Reference in New Issue
Block a user