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 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, "delivery_type": 2, "tariff_code": 535, "comment": "Test payment", "sender": { "name": "Petr Petrov", "email": "sender@example.com", "phone": {"number": "+79009876543"}, }, "recipient": { "name": "Ivan Ivanov", "email": "ivan@example.com", "phone": {"number": "+79001234567"}, }, "from_location": { "address": "Lenina 1", "city": "Moscow", "country_code": "RU", }, "to_location": { "address": "Pushkina 10", "city": "Novosibirsk", "country_code": "RU", }, "services": [{"code": "INSURANCE", "parameter": "1000"}], "packages": [ { "number": "1", "weight": 1, "length": 20, "width": 15, "height": 10, "comment": "Package 1", } ], } 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.delivery_type == 2 assert persisted_order.tariff_code == 535 assert persisted_order.sender == order_data.sender assert persisted_order.recipient == order_data.recipient assert persisted_order.from_location == order_data.from_location assert persisted_order.to_location == order_data.to_location assert persisted_order.packages == order_data.packages assert persisted_order.services == order_data.services assert persisted_order.comment == "Test payment" 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.created_at is not None assert persisted_order.updated_at is not None asyncio.run(_with_repository(run)) def test_create_order_persists_nullable_services_and_comment() -> None: async def run( repository: OrderRepository, session_factory: async_sessionmaker[AsyncSession], ) -> None: order_data = _make_order_data(services=None, comment=None) async with repository.session() as session: 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 persisted_order.services is None assert persisted_order.comment is 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_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: order = 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 order is not None assert persisted_order.cdek_order_uuid == "cdek-order-uuid-1" 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))