From 50b1a8f80cef8b13135be117e2d6229eba2acc62 Mon Sep 17 00:00:00 2001 From: hexdev Date: Sat, 14 Feb 2026 23:09:21 +0700 Subject: [PATCH] invoices, order handling, db updates --- .../536f700c6d2b_invoice_id_text_fs.py | 44 ++++++++++++ .../ac19c319304a_invoices_creation.py | 47 ++++++++++++ .../versions/f7372a4db67d_invoice_id_text.py | 44 ++++++++++++ config.py | 6 +- db/models/__init__.py | 11 ++- db/models/invoices.py | 24 +++++++ dto/checkout.py | 5 -- startup.sh => entrypoints/startup.sh | 13 ++++ handlers/admins/__init__.py | 4 +- handlers/admins/inline_mode.py | 71 +++++++++++++++++++ handlers/client/__init__.py | 2 + handlers/client/checkout.py | 58 ++++++++++++++- handlers/client/invoices.py | 29 ++++++++ handlers/client/menu.py | 16 +++-- main.py | 21 ++---- middlewares/repository.py | 5 ++ misc/filters.py | 9 +++ misc/kb/admins.py | 34 +++++++++ misc/kb/client.py | 2 +- misc/states.py | 5 ++ misc/texts.py | 5 -- misc/utils.py | 17 +++++ postgresql.fish | 1 - repositories/__init__.py | 8 ++- repositories/invoices.py | 45 ++++++++++++ repositories/order_items.py | 15 +++- requirements.txt | 4 +- services/orders.py | 7 -- 28 files changed, 508 insertions(+), 44 deletions(-) create mode 100644 alembic/versions/536f700c6d2b_invoice_id_text_fs.py create mode 100644 alembic/versions/ac19c319304a_invoices_creation.py create mode 100644 alembic/versions/f7372a4db67d_invoice_id_text.py create mode 100644 db/models/invoices.py rename startup.sh => entrypoints/startup.sh (60%) create mode 100644 handlers/admins/inline_mode.py create mode 100644 handlers/client/invoices.py create mode 100644 misc/filters.py delete mode 100755 postgresql.fish create mode 100644 repositories/invoices.py diff --git a/alembic/versions/536f700c6d2b_invoice_id_text_fs.py b/alembic/versions/536f700c6d2b_invoice_id_text_fs.py new file mode 100644 index 0000000..d5b8b70 --- /dev/null +++ b/alembic/versions/536f700c6d2b_invoice_id_text_fs.py @@ -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 ### diff --git a/alembic/versions/ac19c319304a_invoices_creation.py b/alembic/versions/ac19c319304a_invoices_creation.py new file mode 100644 index 0000000..e1f5409 --- /dev/null +++ b/alembic/versions/ac19c319304a_invoices_creation.py @@ -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 ### diff --git a/alembic/versions/f7372a4db67d_invoice_id_text.py b/alembic/versions/f7372a4db67d_invoice_id_text.py new file mode 100644 index 0000000..a86cc3a --- /dev/null +++ b/alembic/versions/f7372a4db67d_invoice_id_text.py @@ -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 ### diff --git a/config.py b/config.py index 08aab4e..d45c8e8 100644 --- a/config.py +++ b/config.py @@ -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 diff --git a/db/models/__init__.py b/db/models/__init__.py index 6865b9d..5b5f54e 100644 --- a/db/models/__init__.py +++ b/db/models/__init__.py @@ -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", +] diff --git a/db/models/invoices.py b/db/models/invoices.py new file mode 100644 index 0000000..53f0ed9 --- /dev/null +++ b/db/models/invoices.py @@ -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()) diff --git a/dto/checkout.py b/dto/checkout.py index 66df67e..58de54a 100644 --- a/dto/checkout.py +++ b/dto/checkout.py @@ -11,8 +11,3 @@ class CheckoutContext: name: Optional[str] = None phone: Optional[str] = None address: Optional[str] = None - - -@dataclass -class PaymentContext: - notification_msg: Message diff --git a/startup.sh b/entrypoints/startup.sh similarity index 60% rename from startup.sh rename to entrypoints/startup.sh index 890afa5..e3344a1 100644 --- a/startup.sh +++ b/entrypoints/startup.sh @@ -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 \ No newline at end of file diff --git a/handlers/admins/__init__.py b/handlers/admins/__init__.py index e411497..df4f58b 100644 --- a/handlers/admins/__init__.py +++ b/handlers/admins/__init__.py @@ -1 +1,3 @@ -routers = [] +from .inline_mode import router as inline_router + +routers = [inline_router] diff --git a/handlers/admins/inline_mode.py b/handlers/admins/inline_mode.py new file mode 100644 index 0000000..ca88765 --- /dev/null +++ b/handlers/admins/inline_mode.py @@ -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"💸 Счёт на {amount}₽.", + reply_markup=payment_link(invoice_link), + ) diff --git a/handlers/client/__init__.py b/handlers/client/__init__.py index 7b55806..7cc4294 100644 --- a/handlers/client/__init__.py +++ b/handlers/client/__init__.py @@ -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, diff --git a/handlers/client/checkout.py b/handlers/client/checkout.py index 8234b10..d121eb9 100644 --- a/handlers/client/checkout.py +++ b/handlers/client/checkout.py @@ -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"📍 Ваш адрес: {ctx.address}\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( + "🟢 Ваш заказ успешно отправлен!\n" + "📦 Мы свяжемся с вами для уточнения деталей в ближайшее время.\n\n" + "‼️ Чтобы убедиться, что вы общаетесь с доверенным контактом ШВЕЙТЕХЦЕНТР, перешлите любое сообщение человека в этот чат.", + 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"👤 Имя: {ctx.name}\n" + f"📱 Номер телефона: {ctx.phone}\n" + f"📍 Адрес: {ctx.address}\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 diff --git a/handlers/client/invoices.py b/handlers/client/invoices.py new file mode 100644 index 0000000..b421334 --- /dev/null +++ b/handlers/client/invoices.py @@ -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"🟢 Получен счёт на {invoice.amount}₽" + ) # TODO: Process payment diff --git a/handlers/client/menu.py b/handlers/client/menu.py index 6bcee3e..2b55301 100644 --- a/handlers/client/menu.py +++ b/handlers/client/menu.py @@ -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 diff --git a/main.py b/main.py index aa4465e..eebf905 100644 --- a/main.py +++ b/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, ) ) diff --git a/middlewares/repository.py b/middlewares/repository.py index ecf0d3c..4558609 100644 --- a/middlewares/repository.py +++ b/middlewares/repository.py @@ -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) diff --git a/misc/filters.py b/misc/filters.py new file mode 100644 index 0000000..aec09db --- /dev/null +++ b/misc/filters.py @@ -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 diff --git a/misc/kb/admins.py b/misc/kb/admins.py index e69de29..4c44b57 100644 --- a/misc/kb/admins.py +++ b/misc/kb/admins.py @@ -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() diff --git a/misc/kb/client.py b/misc/kb/client.py index bf78318..d36bfa4 100644 --- a/misc/kb/client.py +++ b/misc/kb/client.py @@ -214,7 +214,7 @@ def order_specs_confirmation(order_id: int) -> InlineKeyboardMarkup: [ [ InlineKeyboardButton( - text="🟢 Перейти к оплате", + text="🟢 Подтвердить", callback_data=f"payment:{order_id}", ) ], diff --git a/misc/states.py b/misc/states.py index 55bb514..6d86e95 100644 --- a/misc/states.py +++ b/misc/states.py @@ -5,3 +5,8 @@ class CheckoutStorage(StatesGroup): name = State() phone = State() address = State() + confirmation = State() + + +class PaymentStorage(StatesGroup): + refresh = State() diff --git a/misc/texts.py b/misc/texts.py index 0c93fe6..d4f9417 100644 --- a/misc/texts.py +++ b/misc/texts.py @@ -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 diff --git a/misc/utils.py b/misc/utils.py index df374c5..0b3b551 100644 --- a/misc/utils.py +++ b/misc/utils.py @@ -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")) diff --git a/postgresql.fish b/postgresql.fish deleted file mode 100755 index 1f96f03..0000000 --- a/postgresql.fish +++ /dev/null @@ -1 +0,0 @@ -docker run --name test-postgres -e POSTGRES_PASSWORD=HEXDEVFUCKINGSUCKS! -p 5432:5432 -d postgres \ No newline at end of file diff --git a/repositories/__init__.py b/repositories/__init__.py index 7d3fed3..6fcc878 100644 --- a/repositories/__init__.py +++ b/repositories/__init__.py @@ -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", +] diff --git a/repositories/invoices.py b/repositories/invoices.py new file mode 100644 index 0000000..a939a36 --- /dev/null +++ b/repositories/invoices.py @@ -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) diff --git a/repositories/order_items.py b/repositories/order_items.py index a66d7d4..3f987af 100644 --- a/repositories/order_items.py +++ b/repositories/order_items.py @@ -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]: diff --git a/requirements.txt b/requirements.txt index 0ea2e53..2c1c3f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ python-dotenv black asyncpg sqlalchemy[asyncio] -psycopg2 \ No newline at end of file +psycopg2 +phonenumbers +ruff \ No newline at end of file diff --git a/services/orders.py b/services/orders.py index 4c04d7a..c08a6d6 100644 --- a/services/orders.py +++ b/services/orders.py @@ -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,