"""Юнит-тесты сервиса заказов. Без БД и без сети — сессия подменяется заглушкой.""" from __future__ import annotations import asyncio from datetime import UTC, datetime from typing import Any import pytest from app.db.models.order import Order from app.schemas.orders import OrderUpdateIn from app.services.orders import OrderEditError, _parse_crm_datetime, _to_kopecks, update_order class TestToKopecks: def test_converts_decimal_string_to_kopecks(self) -> None: assert _to_kopecks("599.00") == 59900 def test_handles_fractional_kopecks(self) -> None: assert _to_kopecks("12.34") == 1234 def test_handles_integer_without_fraction(self) -> None: assert _to_kopecks("100") == 10000 def test_handles_zero(self) -> None: assert _to_kopecks("0.00") == 0 class TestParseCrmDatetime: def test_parses_crm_format(self) -> None: assert _parse_crm_datetime("2026-09-20 10:00:00") == datetime(2026, 9, 20, 10, 0, 0) class _FakeSession: def __init__(self, order: Order | None) -> None: self.order = order async def scalar(self, query: Any) -> Order | None: return self.order def _order(**overrides: Any) -> Order: fields: dict[str, Any] = { "id": "1", "recipient_name": "Тест", "recipient_phone": "0501112233", "recipient_email": None, "waybill_number": "20450000000001", "notes": None, "total_amount_kopecks": 120000, "goods": [], "is_deleted": False, "receipt_created_at": None, "edited_at": None, "np_status": "Прибув", "np_status_code": "7", "np_cod_amount_kopecks": 120000, "np_payment_status": "NeedPayment", } fields.update(overrides) return Order(**fields) _GOOD = { "id": "1", "sku": "A", "name": "Товар", "price": "600.00", "quantity": "2", "discount_amount": "50.00", } def _payload(**overrides: Any) -> OrderUpdateIn: data: dict[str, Any] = { "recipient_name": "Тест", "recipient_phone": "0501112233", "recipient_email": "", "waybill_number": "20450000000001", "notes": "", "total_amount": "1100.00", "goods": [_GOOD], } data.update(overrides) return OrderUpdateIn.model_validate(data) def _update(order: Order | None, payload: OrderUpdateIn) -> tuple[Order, list[str]]: result = asyncio.run(update_order(_FakeSession(order), "1", payload)) # type: ignore[arg-type] assert result is not None return result class TestUpdateOrder: def test_recomputes_line_amount_and_marks_edited(self) -> None: order, changed = _update(_order(), _payload()) assert order.goods[0]["amount"] == "1150.00" assert order.goods[0]["quantity"] == "2.000" assert order.total_amount_kopecks == 110000 assert order.edited_at is not None assert set(changed) == {"goods", "total_amount"} def test_blank_strings_become_null(self) -> None: order, _ = _update(_order(), _payload(notes=" ")) assert order.notes is None assert order.recipient_email is None def test_new_line_gets_generated_id(self) -> None: good = {"sku": "", "name": "Новый", "price": "10.00", "quantity": "1"} order, _ = _update(_order(), _payload(goods=[good], total_amount="10.00")) assert order.goods[0]["id"].startswith("new-") def test_waybill_change_resets_np_tracking(self) -> None: order, changed = _update(_order(), _payload(waybill_number="20450000000002")) assert "waybill_number" in changed assert order.np_status is None assert order.np_cod_amount_kopecks is None def test_no_changes_does_not_mark_edited(self) -> None: # Снимок в формате CRM ("2", а не "2.000") — формат строк не считается правкой. stored = {**_GOOD, "discount_percent": "4.17", "amount": "1150.00"} order, changed = _update(_order(goods=[stored], total_amount_kopecks=110000), _payload()) assert changed == [] assert order.edited_at is None assert order.goods == [stored] def test_total_above_goods_sum_is_rejected(self) -> None: with pytest.raises(OrderEditError, match="больше суммы товаров"): _update(_order(), _payload(total_amount="1200.00")) def test_discount_above_line_sum_is_rejected(self) -> None: good = {"name": "Товар", "price": "10.00", "quantity": "1", "discount_amount": "11"} with pytest.raises(OrderEditError, match="скидка больше"): _update(_order(), _payload(goods=[good], total_amount="0")) def test_order_with_receipt_is_locked(self) -> None: with pytest.raises(OrderEditError, match="уже создан чек"): _update(_order(receipt_created_at=datetime.now(UTC)), _payload()) def test_missing_or_deleted_order_returns_none(self) -> None: payload = _payload() assert asyncio.run(update_order(_FakeSession(None), "1", payload)) is None # type: ignore[arg-type] deleted = _FakeSession(_order(is_deleted=True)) assert asyncio.run(update_order(deleted, "1", payload)) is None # type: ignore[arg-type]