Bind Nova Poshta API key to cash register (#3)

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>
This commit is contained in:
2026-09-25 13:02:40 +03:00
co-authored by Claude Opus 5.5
parent 2b92d82693
commit 8b3c2b6d63
22 changed files with 531 additions and 73 deletions
@@ -15,17 +15,32 @@ _FIXTURE_STATUSES: dict[str, dict] = {
"AmountToPay": "1200.00",
"AfterpaymentOnGoodsCost": 1200,
"PaymentStatus": "Paid",
"PhoneSender": "380501112233",
}
}
class StubNovaPoshtaClient:
def __init__(self, statuses: dict[str, dict] | None = None) -> None:
self._statuses = statuses if statuses is not None else _FIXTURE_STATUSES
"""`statuses` — ответы NP по ТТН; `by_key` — отдельные ответы для конкретных
ключей API (кабинет отправителя видит свои ТТН полностью, чужие — урезанно)."""
async def get_statuses(self, *, waybill_numbers: list[str]) -> list[TrackingStatusOut]:
def __init__(
self,
statuses: dict[str, dict] | None = None,
*,
by_key: dict[str, dict[str, dict]] | None = None,
) -> None:
self._statuses = statuses if statuses is not None else _FIXTURE_STATUSES
self._by_key = by_key or {}
self.calls: list[tuple[str, list[str]]] = []
async def get_statuses(
self, *, api_key: str, waybill_numbers: list[str]
) -> list[TrackingStatusOut]:
self.calls.append((api_key, list(waybill_numbers)))
statuses = self._by_key.get(api_key, self._statuses)
return [
TrackingStatusOut.model_validate(self._statuses[number])
TrackingStatusOut.model_validate(statuses[number])
for number in waybill_numbers
if number in self._statuses
if number in statuses
]