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>
48 lines
1.7 KiB
Python
48 lines
1.7 KiB
Python
"""Реальный клиент Nova Poshta (`TrackingDocument.getStatusDocuments`)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
|
|
from app.schemas.tracking import TrackingStatusOut
|
|
from app.services.nova_poshta.client import NovaPoshtaError
|
|
|
|
_API_URL = "https://api.novaposhta.ua/v2.0/json/"
|
|
|
|
# NP отклоняет запросы с более чем 100 накладными за раз.
|
|
_MAX_DOCUMENTS_PER_REQUEST = 100
|
|
|
|
|
|
class NpTrackingClient:
|
|
async def get_statuses(
|
|
self, *, api_key: str, waybill_numbers: list[str]
|
|
) -> list[TrackingStatusOut]:
|
|
if not waybill_numbers:
|
|
return []
|
|
if len(waybill_numbers) > _MAX_DOCUMENTS_PER_REQUEST:
|
|
raise NovaPoshtaError(
|
|
f"Слишком много ТТН за один запрос: {len(waybill_numbers)} "
|
|
f"(максимум {_MAX_DOCUMENTS_PER_REQUEST})"
|
|
)
|
|
|
|
body = {
|
|
"apiKey": api_key,
|
|
"modelName": "TrackingDocument",
|
|
"calledMethod": "getStatusDocuments",
|
|
"methodProperties": {
|
|
"Documents": [{"DocumentNumber": number, "Phone": ""} for number in waybill_numbers]
|
|
},
|
|
}
|
|
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
response = await client.post(_API_URL, json=body)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
if not data.get("success"):
|
|
errors = data.get("errors") or []
|
|
message = "; ".join(str(error) for error in errors)
|
|
raise NovaPoshtaError(f"NP вернул ошибку: {message or 'неизвестная ошибка'}")
|
|
|
|
return [TrackingStatusOut.model_validate(item) for item in data.get("data", [])]
|