Files
lux_fiscal/backend/tests/test_orders_service.py
T
lauadminandClaude Opus 5.5 56ac0fc370 Allow editing orders in the order card
Cashiers can edit recipient, TTN, notes, goods and total in the order
modal and save via PATCH /orders/{id}. Edited orders get edited_at and
are no longer overwritten by CRM sync. Editing is blocked once a receipt
exists; changing the TTN resets Nova Poshta tracking fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:23:41 +03:00

151 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Юнит-тесты сервиса заказов. Без БД и без сети — сессия подменяется заглушкой."""
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]