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,