invoice payment! prerelease
This commit is contained in:
31
alembic/versions/41a6b57158ea_added_new_enums_for_invoice.py
Normal file
31
alembic/versions/41a6b57158ea_added_new_enums_for_invoice.py
Normal file
@@ -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 ###
|
||||||
@@ -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 ###
|
||||||
12
config.py
12
config.py
@@ -1,10 +1,19 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CardInfo:
|
||||||
|
number: str
|
||||||
|
holder: str
|
||||||
|
|
||||||
|
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
### Environment Variables ###
|
### Environment Variables ###
|
||||||
|
|
||||||
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
|
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
|
||||||
@@ -14,8 +23,7 @@ POSTGRES_URL = os.getenv(
|
|||||||
"postgresql+asyncpg://postgres:HEXDEVFUCKINGSUCKS!@localhost:5432/postgres",
|
"postgresql+asyncpg://postgres:HEXDEVFUCKINGSUCKS!@localhost:5432/postgres",
|
||||||
)
|
)
|
||||||
|
|
||||||
POST_TOKEN = os.getenv("POST_TOKEN", "")
|
card_info = CardInfo(os.getenv("CARD_NUMBER", ""), os.getenv("CARD_HOLDER", ""))
|
||||||
POST_AUTH_KEY = os.getenv("POST_AUTH_KEY", "")
|
|
||||||
|
|
||||||
### Constants ###
|
### Constants ###
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from db.base import Base
|
|||||||
|
|
||||||
class InvoiceStatus(E_cls):
|
class InvoiceStatus(E_cls):
|
||||||
PENDING = "PENDING"
|
PENDING = "PENDING"
|
||||||
|
RECEIVED = "RECEIVED"
|
||||||
PAID = "PAID"
|
PAID = "PAID"
|
||||||
EXPIRED = "EXPIRED"
|
EXPIRED = "EXPIRED"
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class OrderItem(Base):
|
|||||||
__tablename__ = "order_items"
|
__tablename__ = "order_items"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
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"))
|
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
|
||||||
quantity: Mapped[int] = mapped_column(default=1)
|
quantity: Mapped[int] = mapped_column(default=1)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ if [[ "$VIRTUAL_ENV" != "" ]]
|
|||||||
then
|
then
|
||||||
echo "process is in venv, skipping."
|
echo "process is in venv, skipping."
|
||||||
else
|
else
|
||||||
echo "entering venv."
|
echo "venv not found, entering..."
|
||||||
source $(pwd)/venv/bin/activate
|
source $(pwd)/venv/bin/activate
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ async def payment_create(
|
|||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
order_service: OrderService,
|
order_service: OrderService,
|
||||||
|
order_repo: OrderRepository,
|
||||||
):
|
):
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
ctx: Optional[CheckoutContext] = data.get("ctx")
|
ctx: Optional[CheckoutContext] = data.get("ctx")
|
||||||
@@ -150,16 +151,15 @@ async def payment_create(
|
|||||||
|
|
||||||
await state.clear()
|
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:
|
try:
|
||||||
|
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)
|
||||||
await cb.bot.send_message(
|
await cb.bot.send_message(
|
||||||
NOTIFICATION_CHANNEL,
|
NOTIFICATION_CHANNEL,
|
||||||
"🟢 Новый заказ.\n\n"
|
"🟢 Новый заказ.\n\n"
|
||||||
@@ -173,6 +173,11 @@ async def payment_create(
|
|||||||
cb.from_user.id, bool(cb.from_user.username), cb.from_user.username
|
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:
|
except Exception as e:
|
||||||
|
await cb.message.edit_text(
|
||||||
|
"🍃 Что-то пошло не так, повторите попытку позже...",
|
||||||
|
reply_markup=return_menu,
|
||||||
|
)
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiogram import Router
|
from aiogram import Router, F
|
||||||
from aiogram.filters import CommandObject, CommandStart
|
from aiogram.filters import CommandObject, CommandStart
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
|
CallbackQuery,
|
||||||
Message,
|
Message,
|
||||||
)
|
)
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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 repositories.invoices import InvoiceRepository
|
||||||
|
from config import card_info
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -24,6 +30,86 @@ async def activate_invoice(
|
|||||||
|
|
||||||
invoice_id = int(payload.split(":")[1])
|
invoice_id = int(payload.split(":")[1])
|
||||||
invoice = await invoice_repo.get_invoice_by_id(session, invoice_id)
|
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(
|
await msg.answer(
|
||||||
f"<b>🟢 Получен счёт на {invoice.amount}₽</b>"
|
f"<b>💎 Счёт на {invoice.amount}₽</b>\n\n"
|
||||||
) # TODO: Process payment
|
"<i>🔸 Проведите оплату по реквизитам ниже:</i>\n"
|
||||||
|
f"🪪 Номер карты: <code>{card_info.number}</code>\n"
|
||||||
|
f"👤 Держатель карты: <b>{card_info.holder}</b>",
|
||||||
|
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(
|
||||||
|
"<b>💸 Вы отметили платёж как оплаченый.</b> Если вы сделали это ошибочно, проведите платёж в ближайшее время.",
|
||||||
|
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="<b><i>🟡 Счёт помечен как оплаченый, жду подтверждения...</i></b>",
|
||||||
|
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"<b>✅ Платёж на {invoice.amount}₽ успешно оплачен.</b>",
|
||||||
|
)
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ async def main_menu_cb(
|
|||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await cb.message.reply_to_message.delete()
|
||||||
|
except Exception:
|
||||||
|
...
|
||||||
|
|
||||||
cart_items = await order_items_repo.get_items_count_by_customer(
|
cart_items = await order_items_repo.get_items_count_by_customer(
|
||||||
session, cb.from_user.id
|
session, cb.from_user.id
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ async def add_to_cart(
|
|||||||
else:
|
else:
|
||||||
show_cart = False
|
show_cart = False
|
||||||
quantity = 0
|
quantity = 0
|
||||||
await order_service.clear_cart_item(
|
await order_service.clear_from_cart(
|
||||||
session, customer=cb.from_user.id, product_id=product_id
|
session, customer=cb.from_user.id, product_id=product_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -32,3 +32,15 @@ def payment_link(url: str) -> InlineKeyboardMarkup:
|
|||||||
return InlineKeyboardBuilder(
|
return InlineKeyboardBuilder(
|
||||||
[[InlineKeyboardButton(text="💸 Оплатить", url=url)]]
|
[[InlineKeyboardButton(text="💸 Оплатить", url=url)]]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_payment(invoice_id: int):
|
||||||
|
return InlineKeyboardBuilder(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🟢 Подтвердить", callback_data=f"verify_payment:{invoice_id}"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
).as_markup()
|
||||||
|
|||||||
@@ -221,3 +221,16 @@ def order_specs_confirmation(order_id: int) -> InlineKeyboardMarkup:
|
|||||||
[InlineKeyboardButton(text="❌", callback_data="menu:main")],
|
[InlineKeyboardButton(text="❌", callback_data="menu:main")],
|
||||||
]
|
]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_payment(invoice_id: int):
|
||||||
|
return InlineKeyboardBuilder(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="✅ Подтвердить оплату",
|
||||||
|
callback_data=f"refresh_payment:{invoice_id}",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
).as_markup()
|
||||||
|
|||||||
@@ -43,3 +43,13 @@ class InvoiceRepository:
|
|||||||
.values(inline_message_id=inline_message_id)
|
.values(inline_message_id=inline_message_id)
|
||||||
)
|
)
|
||||||
await session.execute(stmt)
|
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)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import delete, select
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
@@ -46,3 +46,15 @@ class OrderRepository:
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
return order
|
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()
|
||||||
|
|||||||
@@ -126,29 +126,6 @@ class OrderService:
|
|||||||
|
|
||||||
return order_item.quantity
|
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(
|
async def build_pagination_cart_dto(
|
||||||
self,
|
self,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
|
|||||||
Reference in New Issue
Block a user