Each cash register stores its own encrypted NP API key. Status polling uses register keys and binds an order to the register whose key sees the TTN as its own (PhoneSender present); ETTN receipts are created from that register. Migration 0008 moves the old NOVA_POSHTA_API_KEY into the default register. Closes #3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
"""Тесты NpTrackingClient. Сеть замокана через respx — реальных запросов не делает."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
import respx
|
|
from httpx import Response
|
|
|
|
from app.schemas.tracking import TrackingStatusOut
|
|
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",
|
|
}
|
|
|
|
|
|
API_KEY = "np-apikey-123"
|
|
|
|
|
|
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()
|
|
|
|
await client.get_statuses(api_key=API_KEY, waybill_numbers=["20451540916703"])
|
|
|
|
sent = route.calls.last.request
|
|
body = json.loads(sent.content)
|
|
assert body["apiKey"] == API_KEY
|
|
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()
|
|
|
|
statuses = await client.get_statuses(api_key=API_KEY, 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"
|
|
assert not status.is_own # в STATUS_PAYLOAD нет PhoneSender
|
|
|
|
def test_is_own_by_sender_phone(self) -> None:
|
|
own = TrackingStatusOut.model_validate({**STATUS_PAYLOAD, "PhoneSender": "380961112233"})
|
|
foreign = TrackingStatusOut.model_validate({**STATUS_PAYLOAD, "PhoneSender": ""})
|
|
assert own.is_own
|
|
assert not foreign.is_own
|
|
|
|
@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()
|
|
|
|
statuses = await client.get_statuses(api_key=API_KEY, 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()
|
|
|
|
with pytest.raises(NovaPoshtaError, match="Invalid apiKey"):
|
|
await client.get_statuses(api_key=API_KEY, waybill_numbers=["20451540916703"])
|
|
|
|
async def test_returns_empty_list_for_no_documents(self) -> None:
|
|
client = NpTrackingClient()
|
|
|
|
assert await client.get_statuses(api_key=API_KEY, waybill_numbers=[]) == []
|
|
|
|
async def test_rejects_too_many_documents(self) -> None:
|
|
client = NpTrackingClient()
|
|
|
|
with pytest.raises(NovaPoshtaError, match="Слишком много"):
|
|
await client.get_statuses(
|
|
api_key=API_KEY, waybill_numbers=[str(i) for i in range(101)]
|
|
)
|