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
+47
View File
@@ -16,6 +16,7 @@ from app.core import crypto
from app.db.models.cash_register import CashRegister
from app.db.models.order import Order
from app.db.models.receipt import Receipt, ReceiptStatus
from app.db.models.user import User
from app.schemas.checkbox import EttnStatus
from app.services import receipts as svc
from app.services.checkbox.client import CheckboxRateLimitedError, CheckboxUnavailableError
@@ -182,6 +183,52 @@ class TestBuildBody:
assert "delivery" not in body["receipt_body"]
class TestRequestReceipts:
"""Чек создаётся от кассы, к которой опрос НП привязал заказ."""
class Session:
def __init__(self, registers: list[CashRegister], orders: list[Order]) -> None:
self.registers, self.orders, self.added = registers, orders, []
async def scalars(self, statement: Any) -> list[Any]:
entity = statement.column_descriptions[0]["entity"]
if entity is CashRegister:
return self.registers
if entity is Order:
return self.orders
return [] # активные чеки заказов
def add(self, obj: Any) -> None:
self.added.append(obj)
@staticmethod
def _user() -> User:
return User(id=uuid.uuid4(), email="c@x.ua")
async def test_uses_order_register(self) -> None:
default, other = _register(), _register()
order = _order(cash_register_id=other.id)
session = self.Session([default, other], [order])
result = await svc.request_receipts(
session, [(order.id, None)], user=self._user() # type: ignore[arg-type]
)
assert result.errors == {}
assert result.created[0].cash_register_id == other.id
async def test_unbound_order_is_rejected(self) -> None:
order = _order(cash_register_id=None)
session = self.Session([_register()], [order])
result = await svc.request_receipts(
session, [(order.id, None)], user=self._user() # type: ignore[arg-type]
)
assert result.created == []
assert "Касса не определена" in result.errors[order.id]
class FakeSession:
def __init__(self, *objects: Any) -> None:
self.objects = {(type(o), o.id): o for o in objects}