diff --git a/alembic/versions/41a6b57158ea_added_new_enums_for_invoice.py b/alembic/versions/41a6b57158ea_added_new_enums_for_invoice.py
new file mode 100644
index 0000000..777dfe0
--- /dev/null
+++ b/alembic/versions/41a6b57158ea_added_new_enums_for_invoice.py
@@ -0,0 +1,31 @@
+"""added new enums for invoice
+
+Revision ID: 41a6b57158ea
+Revises: 92ae4c5060c4
+Create Date: 2026-02-15 13:44:11.676492
+
+"""
+
+from typing import Sequence, Union
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "41a6b57158ea"
+down_revision: Union[str, Sequence[str], None] = "92ae4c5060c4"
+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.execute("ALTER TYPE invoicestatus ADD VALUE 'RECEIVED'")
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
diff --git a/alembic/versions/92ae4c5060c4_order_items_ondelete_cascade.py b/alembic/versions/92ae4c5060c4_order_items_ondelete_cascade.py
new file mode 100644
index 0000000..79ff4d4
--- /dev/null
+++ b/alembic/versions/92ae4c5060c4_order_items_ondelete_cascade.py
@@ -0,0 +1,39 @@
+"""order_items -> ondelete=cascade
+
+Revision ID: 92ae4c5060c4
+Revises: 536f700c6d2b
+Create Date: 2026-02-15 13:03:35.591725
+
+"""
+
+from typing import Sequence, Union
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "92ae4c5060c4"
+down_revision: Union[str, Sequence[str], None] = "536f700c6d2b"
+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.drop_constraint(
+ op.f("order_items_order_id_fkey"), "order_items", type_="foreignkey"
+ )
+ op.create_foreign_key(
+ None, "order_items", "orders", ["order_id"], ["id"], ondelete="CASCADE"
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_constraint(None, "order_items", type_="foreignkey")
+ op.create_foreign_key(
+ op.f("order_items_order_id_fkey"), "order_items", "orders", ["order_id"], ["id"]
+ )
+ # ### end Alembic commands ###
diff --git a/config.py b/config.py
index d45c8e8..16a3b1f 100644
--- a/config.py
+++ b/config.py
@@ -1,10 +1,19 @@
+from dataclasses import dataclass
import os
import logging
from dotenv import load_dotenv
+
+@dataclass
+class CardInfo:
+ number: str
+ holder: str
+
+
load_dotenv(override=True)
logger = logging.getLogger(__name__)
+
### Environment Variables ###
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
@@ -14,8 +23,7 @@ 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", "")
+card_info = CardInfo(os.getenv("CARD_NUMBER", ""), os.getenv("CARD_HOLDER", ""))
### Constants ###
diff --git a/db/models/invoices.py b/db/models/invoices.py
index 53f0ed9..c27ca7a 100644
--- a/db/models/invoices.py
+++ b/db/models/invoices.py
@@ -9,6 +9,7 @@ from db.base import Base
class InvoiceStatus(E_cls):
PENDING = "PENDING"
+ RECEIVED = "RECEIVED"
PAID = "PAID"
EXPIRED = "EXPIRED"
diff --git a/db/models/orders.py b/db/models/orders.py
index 5914ef9..7000f82 100644
--- a/db/models/orders.py
+++ b/db/models/orders.py
@@ -29,7 +29,7 @@ class OrderItem(Base):
__tablename__ = "order_items"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
- order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"))
+ order_id: Mapped[int] = mapped_column(ForeignKey("orders.id", ondelete="CASCADE"))
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
quantity: Mapped[int] = mapped_column(default=1)
diff --git a/entrypoints/startup.sh b/entrypoints/startup.sh
index e3344a1..0c27bf3 100644
--- a/entrypoints/startup.sh
+++ b/entrypoints/startup.sh
@@ -7,7 +7,7 @@ if [[ "$VIRTUAL_ENV" != "" ]]
then
echo "process is in venv, skipping."
else
- echo "entering venv."
+ echo "venv not found, entering..."
source $(pwd)/venv/bin/activate
fi
diff --git a/handlers/client/checkout.py b/handlers/client/checkout.py
index d121eb9..0907cf5 100644
--- a/handlers/client/checkout.py
+++ b/handlers/client/checkout.py
@@ -137,6 +137,7 @@ async def payment_create(
state: FSMContext,
session: AsyncSession,
order_service: OrderService,
+ order_repo: OrderRepository,
):
data = await state.get_data()
ctx: Optional[CheckoutContext] = data.get("ctx")
@@ -150,16 +151,15 @@ async def payment_create(
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.message.edit_text(
+ "🟢 Ваш заказ успешно отправлен!\n"
+ "📦 Мы свяжемся с вами для уточнения деталей в ближайшее время.\n\n"
+ "‼️ Чтобы убедиться, что вы общаетесь с доверенным контактом ШВЕЙТЕХЦЕНТР, перешлите любое сообщение человека в этот чат.",
+ reply_markup=return_menu,
+ )
+
+ cart = await order_service.build_full_cart_dto(session, cb.from_user.id)
await cb.bot.send_message(
NOTIFICATION_CHANNEL,
"🟢 Новый заказ.\n\n"
@@ -173,6 +173,11 @@ async def payment_create(
cb.from_user.id, bool(cb.from_user.username), cb.from_user.username
),
)
+ await order_repo.remove_order_by_customer(session, cb.from_user.id)
except Exception as e:
+ await cb.message.edit_text(
+ "🍃 Что-то пошло не так, повторите попытку позже...",
+ reply_markup=return_menu,
+ )
logger.exception(e)
return
diff --git a/handlers/client/invoices.py b/handlers/client/invoices.py
index b421334..d29c134 100644
--- a/handlers/client/invoices.py
+++ b/handlers/client/invoices.py
@@ -1,13 +1,19 @@
import logging
-from aiogram import Router
+from aiogram import Router, F
from aiogram.filters import CommandObject, CommandStart
+from aiogram.fsm.context import FSMContext
from aiogram.types import (
+ CallbackQuery,
Message,
)
from sqlalchemy.ext.asyncio import AsyncSession
+from db.models.invoices import InvoiceStatus
+from misc.kb.admins import verify_payment
+from misc.kb.client import confirm_payment, return_menu
from repositories.invoices import InvoiceRepository
+from config import card_info
router = Router()
logger = logging.getLogger(__name__)
@@ -24,6 +30,86 @@ async def activate_invoice(
invoice_id = int(payload.split(":")[1])
invoice = await invoice_repo.get_invoice_by_id(session, invoice_id)
+
+ if not invoice:
+ await msg.answer(
+ "⏳ Счёт истёк. Повторите попытку позже.", reply_markup=return_menu
+ )
+ return
+
+ if invoice.status != InvoiceStatus.PENDING:
+ await msg.answer("⏳ Этот счёт недоступен.", reply_markup=return_menu)
+ return
+
+ if not (card_info.number and card_info.holder):
+ await msg.answer(
+ "🍃 Оплата недоступна, повторите попытку позже.", reply_markup=return_menu
+ )
+ return
+
await msg.answer(
- f"🟢 Получен счёт на {invoice.amount}₽"
- ) # TODO: Process payment
+ f"💎 Счёт на {invoice.amount}₽\n\n"
+ "🔸 Проведите оплату по реквизитам ниже:\n"
+ f"🪪 Номер карты: {card_info.number}\n"
+ f"👤 Держатель карты: {card_info.holder}",
+ reply_markup=confirm_payment(invoice_id),
+ )
+
+
+@router.callback_query(F.data.startswith("refresh_payment:"))
+async def refresh_payment(
+ cb: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
+ invoice_repo: InvoiceRepository,
+):
+ await state.clear()
+
+ invoice_id = int(cb.data.split(":")[1])
+ invoice = await invoice_repo.get_invoice_by_id(session, invoice_id)
+
+ if not invoice:
+ await cb.message.edit_text("🍃 Оплата недоступна.")
+ return
+
+ await cb.message.edit_reply_markup()
+ await cb.message.reply(
+ "💸 Вы отметили платёж как оплаченый. Если вы сделали это ошибочно, проведите платёж в ближайшее время.",
+ reply_markup=return_menu,
+ )
+
+ await invoice_repo.update_invoice_status(
+ session, invoice_id=invoice.id, status=InvoiceStatus.PAID
+ )
+
+ await cb.bot.edit_message_text(
+ inline_message_id=invoice.inline_message_id,
+ text="🟡 Счёт помечен как оплаченый, жду подтверждения...",
+ reply_markup=verify_payment(invoice.id),
+ )
+
+
+@router.callback_query(F.data.startswith("verify_payment:"))
+async def verify_payment_cb(
+ cb: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
+ invoice_repo: InvoiceRepository,
+):
+ await state.clear()
+
+ invoice_id = int(cb.data.split(":")[1])
+ invoice = await invoice_repo.get_invoice_by_id(session, invoice_id)
+
+ if cb.from_user.id != invoice.creator_id:
+ await cb.answer("❌ Подтвердить может только создатель счёта")
+ return
+
+ await invoice_repo.update_invoice_status(
+ session, invoice_id=invoice_id, status=InvoiceStatus.RECEIVED
+ )
+
+ await cb.bot.edit_message_text(
+ inline_message_id=invoice.inline_message_id,
+ text=f"✅ Платёж на {invoice.amount}₽ успешно оплачен.",
+ )
diff --git a/handlers/client/menu.py b/handlers/client/menu.py
index 2b55301..b0425fc 100644
--- a/handlers/client/menu.py
+++ b/handlers/client/menu.py
@@ -37,6 +37,11 @@ async def main_menu_cb(
):
await state.clear()
+ try:
+ await cb.message.reply_to_message.delete()
+ except Exception:
+ ...
+
cart_items = await order_items_repo.get_items_count_by_customer(
session, cb.from_user.id
)
diff --git a/handlers/client/products.py b/handlers/client/products.py
index 3af58cb..e06514d 100644
--- a/handlers/client/products.py
+++ b/handlers/client/products.py
@@ -146,7 +146,7 @@ async def add_to_cart(
else:
show_cart = False
quantity = 0
- await order_service.clear_cart_item(
+ await order_service.clear_from_cart(
session, customer=cb.from_user.id, product_id=product_id
)
diff --git a/misc/kb/admins.py b/misc/kb/admins.py
index 4c44b57..d70db04 100644
--- a/misc/kb/admins.py
+++ b/misc/kb/admins.py
@@ -32,3 +32,15 @@ def payment_link(url: str) -> InlineKeyboardMarkup:
return InlineKeyboardBuilder(
[[InlineKeyboardButton(text="💸 Оплатить", url=url)]]
).as_markup()
+
+
+def verify_payment(invoice_id: int):
+ return InlineKeyboardBuilder(
+ [
+ [
+ InlineKeyboardButton(
+ text="🟢 Подтвердить", callback_data=f"verify_payment:{invoice_id}"
+ )
+ ]
+ ]
+ ).as_markup()
diff --git a/misc/kb/client.py b/misc/kb/client.py
index d36bfa4..6f1e502 100644
--- a/misc/kb/client.py
+++ b/misc/kb/client.py
@@ -221,3 +221,16 @@ def order_specs_confirmation(order_id: int) -> InlineKeyboardMarkup:
[InlineKeyboardButton(text="❌", callback_data="menu:main")],
]
).as_markup()
+
+
+def confirm_payment(invoice_id: int):
+ return InlineKeyboardBuilder(
+ [
+ [
+ InlineKeyboardButton(
+ text="✅ Подтвердить оплату",
+ callback_data=f"refresh_payment:{invoice_id}",
+ )
+ ]
+ ]
+ ).as_markup()
diff --git a/repositories/invoices.py b/repositories/invoices.py
index a939a36..bb3afac 100644
--- a/repositories/invoices.py
+++ b/repositories/invoices.py
@@ -43,3 +43,13 @@ class InvoiceRepository:
.values(inline_message_id=inline_message_id)
)
await session.execute(stmt)
+
+ async def update_invoice_status(
+ self, session: AsyncSession, *, invoice_id: int, status: InvoiceStatus
+ ):
+ stmt = (
+ update(Invoice.__table__)
+ .where(Invoice.id == invoice_id)
+ .values(status=status)
+ )
+ await session.execute(stmt)
diff --git a/repositories/orders.py b/repositories/orders.py
index 7e174d3..286bacc 100644
--- a/repositories/orders.py
+++ b/repositories/orders.py
@@ -1,5 +1,5 @@
from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select
+from sqlalchemy import delete, select
from typing import Optional
from sqlalchemy.orm import selectinload
@@ -46,3 +46,15 @@ class OrderRepository:
await session.commit()
return order
+
+ async def remove_order_by_id(self, session: AsyncSession, order_id: int):
+ stmt = delete(Order.__table__).where(Order.id == order_id)
+
+ await session.execute(stmt)
+ await session.commit()
+
+ async def remove_order_by_customer(self, session: AsyncSession, customer: int):
+ stmt = delete(Order.__table__).where(Order.customer == customer)
+
+ await session.execute(stmt)
+ await session.commit()
diff --git a/services/orders.py b/services/orders.py
index c08a6d6..90a4b08 100644
--- a/services/orders.py
+++ b/services/orders.py
@@ -126,29 +126,6 @@ class OrderService:
return order_item.quantity
- async def clear_cart_item(
- self, session: AsyncSession, *, customer: int, product_id: int
- ) -> None:
- order = await self.order_repo.get_draft_order_by_user(session, customer)
-
- if not order:
- raise OrderServiceException(
- "attempt of clearing a non-existing cart, potential callback_query exploit, ignoring..."
- )
-
- order_item = await self.order_items_repo.get_item_by_order_and_product(
- session, order_id=order.id, product_id=product_id
- )
-
- if not order_item:
- raise OrderServiceException(
- "attempt of clearing an order_item that isn't in a cart, potential callback_query exploit, ignoring..."
- )
-
- await self.order_items_repo.update_order_item_quantity(
- session, order_item_id=order_item.id, quantity=0
- )
-
async def build_pagination_cart_dto(
self,
session: AsyncSession,