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>
113 lines
4.2 KiB
Python
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)])
|