"""Тесты 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)] )