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
+17 -8
View File
@@ -261,13 +261,11 @@ def credentials(register: CashRegister) -> CheckboxCredentials:
)
async def get_default_register(session: AsyncSession) -> CashRegister | None:
return await session.scalar(
select(CashRegister)
.where(CashRegister.is_active.is_(True))
.order_by(CashRegister.is_default.desc(), CashRegister.created_at)
.limit(1)
async def _active_registers(session: AsyncSession) -> dict[uuid.UUID, CashRegister]:
registers = await session.scalars(
select(CashRegister).where(CashRegister.is_active.is_(True))
)
return {register.id: register for register in registers}
# --- Создание ----------------------------------------------------------------
@@ -300,10 +298,13 @@ async def request_receipts(
Без commit и без обращения к Checkbox: вызывающий код коммитит и ставит
задачи worker'у (`create_ettn_for_receipt`) по `result.created`.
Ошибки по отдельным заказам не мешают остальным — массовое действие.
Чек создаётся от кассы, чей ключ НП видит ТТН заказа как свою
(`orders.cash_register_id`, проставляет `orders.sync_np_statuses`).
"""
result = RequestResult(created=[], errors={})
register = await get_default_register(session)
if register is None:
registers = await _active_registers(session)
if not registers:
for order_id, _ in items:
result.errors[order_id] = "Не настроена касса Checkbox"
return result
@@ -323,6 +324,13 @@ async def request_receipts(
if order_id in busy:
result.errors[order_id] = "По заказу уже есть чек"
continue
register = registers.get(order.cash_register_id) if order.cash_register_id else None
if register is None:
result.errors[order_id] = (
"Касса не определена: ТТН не найдена ни одним ключом Новой Почты "
"активных касс"
)
continue
try:
amounts = resolve_amounts(order, prepayment)
body = build_ettn_body(order, register, amounts)
@@ -359,6 +367,7 @@ async def request_receipts(
payload={
"order_id": order.id,
"waybill_number": order.waybill_number,
"cash_register_id": str(register.id),
"cod_kopecks": amounts.cod_kopecks,
"prepayment_kopecks": amounts.prepayment_kopecks,
},