Files
lux_fiscal/backend/tests/test_nova_poshta_client.py
T
lauadminandClaude Sonnet 5 d13e7ce2b3 Add Nova Poshta tracking: TTN status, COD amount, payment status
Adds NpTrackingClient (Protocol + real/stub impls) and an ARQ worker that
polls Nova Poshta every minute for orders without a receipt, writing
status, net COD amount (Контроль оплати), and payment status onto the
order. Surfaced in the orders table and detail modal. Marks plan stages
3-4 done in README.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 22:33:58 +03:00

113 lines
4.2 KiB
Python

"""Тесты NpTrackingClient. Сеть замокана через respx — реальных запросов не делает."""
from __future__ import annotations
import json
import pytest
import respx
from httpx import Response
from app.core.config import Settings
from app.services.nova_poshta.client import NovaPoshtaError
from app.services.nova_poshta.np_client import _API_URL, NpTrackingClient
STATUS_PAYLOAD = {
"Number": "20451540916703",
"Status": "Відправник самостійно вказав цю накладну, але ще не надав до відправки",
"StatusCode": "1",
"PaymentMethod": "Cash",
"ScheduledDeliveryDate": "22-09-2026 18:00:00",
"ActualDeliveryDate": "",
"AmountToPay": "",
"ExpressWaybillAmountToPay": "827.48",
"AfterpaymentOnGoodsCost": 699,
"PaymentStatus": "",
"ExpressWaybillPaymentStatus": "NeedPayment",
}
def _settings() -> Settings:
return Settings(
secret_key="test-secret-key",
encryption_key="dGVzdC1lbmNyeXB0aW9uLWtleS0zMi1ieXRlcyEh",
nova_poshta_api_key="np-apikey-123",
) # type: ignore[arg-type]
class TestNpTrackingClientGetStatuses:
@respx.mock
async def test_sends_expected_request_body(self) -> None:
route = respx.post(_API_URL).mock(
return_value=Response(200, json={"success": True, "data": [], "errors": []})
)
client = NpTrackingClient(_settings())
await client.get_statuses(waybill_numbers=["20451540916703"])
sent = route.calls.last.request
body = json.loads(sent.content)
assert body["apiKey"] == "np-apikey-123"
assert body["modelName"] == "TrackingDocument"
assert body["calledMethod"] == "getStatusDocuments"
assert body["methodProperties"]["Documents"] == [
{"DocumentNumber": "20451540916703", "Phone": ""}
]
@respx.mock
async def test_parses_successful_response_and_prefers_afterpayment_on_goods_cost(
self,
) -> None:
respx.post(_API_URL).mock(
return_value=Response(
200, json={"success": True, "data": [STATUS_PAYLOAD], "errors": []}
)
)
client = NpTrackingClient(_settings())
statuses = await client.get_statuses(waybill_numbers=["20451540916703"])
assert len(statuses) == 1
status = statuses[0]
assert status.number == "20451540916703"
assert status.status_code == "1"
# "Контроль оплати" — сумма за товар без стоимости доставки и комиссии НП,
# не "сколько заплатить сейчас" (`ExpressWaybillAmountToPay` = 827.48).
assert status.cod_amount == "699"
assert status.payment_status == "NeedPayment"
@respx.mock
async def test_falls_back_to_amount_to_pay_when_afterpayment_missing(self) -> None:
payload = {**STATUS_PAYLOAD, "AfterpaymentOnGoodsCost": 0}
respx.post(_API_URL).mock(
return_value=Response(200, json={"success": True, "data": [payload], "errors": []})
)
client = NpTrackingClient(_settings())
statuses = await client.get_statuses(waybill_numbers=["20451540916703"])
assert statuses[0].cod_amount == "827.48"
@respx.mock
async def test_raises_nova_poshta_error_on_failure(self) -> None:
respx.post(_API_URL).mock(
return_value=Response(
200, json={"success": False, "data": [], "errors": ["Invalid apiKey"]}
)
)
client = NpTrackingClient(_settings())
with pytest.raises(NovaPoshtaError, match="Invalid apiKey"):
await client.get_statuses(waybill_numbers=["20451540916703"])
async def test_returns_empty_list_for_no_documents(self) -> None:
client = NpTrackingClient(_settings())
assert await client.get_statuses(waybill_numbers=[]) == []
async def test_rejects_too_many_documents(self) -> None:
client = NpTrackingClient(_settings())
with pytest.raises(NovaPoshtaError, match="Слишком много"):
await client.get_statuses(waybill_numbers=[str(i) for i in range(101)])