import asyncio from collections.abc import Awaitable, Callable from typing import Any import pytest from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import create_async_engine from app.repositories.order import OrderData, OrderRepository from app.repositories.order.models import Base, Order from tests.payment_fixtures import make_init_payment_payload def _make_order_data(**overrides: object) -> OrderData: payload: dict[str, Any] = { "order_uuid": "order-uuid-1", "payment_url": "https://pay.test/payment/1", "price": 125000, "tariff_code": 535, "account_email": "client@example.com", "payload": make_init_payment_payload(), } payload.update(overrides) return OrderData(**payload) async def _with_repository( test_fn: Callable[ [OrderRepository, async_sessionmaker[AsyncSession]], Awaitable[None], ], ) -> None: engine = create_async_engine("sqlite+aiosqlite:///:memory:") try: async with engine.begin() as connection: await connection.run_sync(Base.metadata.create_all) session_factory = async_sessionmaker(engine, expire_on_commit=False) await test_fn(OrderRepository(session_factory=session_factory), session_factory) finally: await engine.dispose() def test_create_order_persists_all_required_fields() -> None: async def run( repository: OrderRepository, session_factory: async_sessionmaker[AsyncSession], ) -> None: order_data = _make_order_data() async with repository.session() as session: order = await repository.create_order(session, order_data) async with session_factory() as session: result = await session.execute( select(Order).where(Order.order_uuid == "order-uuid-1") ) persisted_order = result.scalar_one() assert order.id == persisted_order.id assert persisted_order.order_uuid == "order-uuid-1" assert persisted_order.payment_url == "https://pay.test/payment/1" assert persisted_order.price == 125000 assert persisted_order.tariff_code == 535 assert persisted_order.account_email == "client@example.com" assert persisted_order.payload == order_data.payload assert persisted_order.payment_status is None assert persisted_order.tbank_payment_id is None assert persisted_order.cdek_order_uuid is None assert persisted_order.cdek_waybill_uuid is None assert persisted_order.cdek_waybill_url is None assert persisted_order.created_at is not None assert persisted_order.updated_at is not None asyncio.run(_with_repository(run)) def test_create_order_rejects_duplicate_order_uuid() -> None: async def run( repository: OrderRepository, _session_factory: async_sessionmaker[AsyncSession], ) -> None: order_data = _make_order_data() async with repository.session() as session: await repository.create_order(session, order_data) with pytest.raises(IntegrityError): async with repository.session() as session: await repository.create_order(session, order_data) asyncio.run(_with_repository(run)) def test_get_order_by_order_uuid_returns_persisted_order() -> None: async def run( repository: OrderRepository, _session_factory: async_sessionmaker[AsyncSession], ) -> None: order_data = _make_order_data() async with repository.session() as session: await repository.create_order(session, order_data) async with repository.session() as session: order = await repository.get_order_by_order_uuid(session, "order-uuid-1") assert order is not None assert order.order_uuid == "order-uuid-1" assert order.payment_url == "https://pay.test/payment/1" asyncio.run(_with_repository(run)) def test_get_order_by_order_uuid_returns_none_for_missing_order() -> None: async def run( repository: OrderRepository, _session_factory: async_sessionmaker[AsyncSession], ) -> None: async with repository.session() as session: order = await repository.get_order_by_order_uuid(session, "missing-order") assert order is None asyncio.run(_with_repository(run)) def test_mark_payment_status_persists_status_and_payment_id() -> None: async def run( repository: OrderRepository, session_factory: async_sessionmaker[AsyncSession], ) -> None: async with repository.session() as session: await repository.create_order(session, _make_order_data()) async with repository.session() as session: order = await repository.mark_payment_status( session, "order-uuid-1", "CONFIRMED", 8347568144, ) async with session_factory() as session: result = await session.execute( select(Order).where(Order.order_uuid == "order-uuid-1") ) persisted_order = result.scalar_one() assert order is not None assert persisted_order.payment_status == "CONFIRMED" assert persisted_order.tbank_payment_id == 8347568144 asyncio.run(_with_repository(run)) def test_mark_payment_status_returns_none_for_missing_order() -> None: async def run( repository: OrderRepository, _session_factory: async_sessionmaker[AsyncSession], ) -> None: async with repository.session() as session: order = await repository.mark_payment_status( session, "missing-order", "CONFIRMED", 8347568144, ) assert order is None asyncio.run(_with_repository(run)) def test_mark_cdek_order_registered_persists_cdek_order_and_waybill() -> None: async def run( repository: OrderRepository, session_factory: async_sessionmaker[AsyncSession], ) -> None: async with repository.session() as session: await repository.create_order(session, _make_order_data()) async with repository.session() as session: order = await repository.mark_cdek_order_registered( session, "order-uuid-1", "cdek-order-uuid-1", "waybill-uuid-1", "https://cdek.test/waybill/1.pdf", ) async with session_factory() as session: result = await session.execute( select(Order).where(Order.order_uuid == "order-uuid-1") ) persisted_order = result.scalar_one() assert order is not None assert persisted_order.cdek_order_uuid == "cdek-order-uuid-1" assert persisted_order.cdek_waybill_uuid == "waybill-uuid-1" assert persisted_order.cdek_waybill_url == "https://cdek.test/waybill/1.pdf" asyncio.run(_with_repository(run)) def test_mark_cdek_order_registered_allows_missing_waybill_fields() -> None: async def run( repository: OrderRepository, session_factory: async_sessionmaker[AsyncSession], ) -> None: async with repository.session() as session: await repository.create_order(session, _make_order_data()) async with repository.session() as session: await repository.mark_cdek_order_registered( session, "order-uuid-1", "cdek-order-uuid-1", ) async with session_factory() as session: result = await session.execute( select(Order).where(Order.order_uuid == "order-uuid-1") ) persisted_order = result.scalar_one() assert persisted_order.cdek_order_uuid == "cdek-order-uuid-1" assert persisted_order.cdek_waybill_uuid is None assert persisted_order.cdek_waybill_url is None asyncio.run(_with_repository(run)) def test_mark_cdek_order_registered_is_idempotent_for_same_uuid() -> None: async def run( repository: OrderRepository, session_factory: async_sessionmaker[AsyncSession], ) -> None: async with repository.session() as session: await repository.create_order(session, _make_order_data()) async with repository.session() as session: await repository.mark_cdek_order_registered( session, "order-uuid-1", "cdek-order-uuid-1", ) async with repository.session() as session: await repository.mark_cdek_order_registered( session, "order-uuid-1", "cdek-order-uuid-1", ) async with session_factory() as session: result = await session.execute(select(Order)) orders = result.scalars().all() assert len(orders) == 1 assert orders[0].cdek_order_uuid == "cdek-order-uuid-1" asyncio.run(_with_repository(run)) def test_mark_cdek_order_registered_returns_none_for_missing_order() -> None: async def run( repository: OrderRepository, _session_factory: async_sessionmaker[AsyncSession], ) -> None: async with repository.session() as session: order = await repository.mark_cdek_order_registered( session, "missing-order", "cdek-order-uuid-1", ) assert order is None asyncio.run(_with_repository(run))