Files
lux_fiscal/backend/app/schemas/receipts.py
lauadminandClaude Opus 5.5 8b3c2b6d63 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>
2026-09-25 13:02:40 +03:00

128 lines
4.3 KiB
Python

"""Схемы API ЕТТН-чеков и касс Checkbox."""
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from app.core import crypto
from app.db.models.cash_register import CashRegister
from app.db.models.receipt import Receipt, ReceiptStatus
def _money(kopecks: int) -> str:
return f"{kopecks / 100:.2f}"
class ReceiptOut(BaseModel):
id: uuid.UUID
order_id: str
waybill_number: str
status: ReceiptStatus
total_amount: str
prepayment_amount: str
cod_amount: str
checkbox_ettn_id: str | None
checkbox_status: str | None
checkbox_receipt_id: str | None
error: str | None
created_at: datetime
last_checked_at: datetime | None
@classmethod
def from_receipt(cls, receipt: Receipt) -> ReceiptOut:
return cls(
id=receipt.id,
order_id=receipt.order_id,
waybill_number=receipt.waybill_number,
status=receipt.status,
total_amount=_money(receipt.total_kopecks),
prepayment_amount=_money(receipt.prepayment_kopecks),
cod_amount=_money(receipt.cod_kopecks),
checkbox_ettn_id=receipt.checkbox_ettn_id,
checkbox_status=receipt.checkbox_status,
checkbox_receipt_id=receipt.checkbox_receipt_id,
error=receipt.error,
created_at=receipt.created_at,
last_checked_at=receipt.last_checked_at,
)
class ReceiptRequestItem(BaseModel):
order_id: str = Field(min_length=1, max_length=32)
# None — предоплата = сумма заказа − наложка НП.
prepayment_kopecks: int | None = Field(default=None, ge=0)
class ReceiptCreateRequest(BaseModel):
items: list[ReceiptRequestItem] = Field(min_length=1, max_length=200)
class ReceiptCreateResponse(BaseModel):
created: list[ReceiptOut]
# order_id → причина, по которой чек не создан.
errors: dict[str, str]
# --- Кассы -------------------------------------------------------------------
def _masked(encrypted: str) -> str:
try:
return crypto.mask(crypto.decrypt(encrypted))
except crypto.DecryptionError:
return "не расшифровывается — введите заново"
class CashRegisterOut(BaseModel):
id: uuid.UUID
name: str
fiscal_number: str | None
license_key_masked: str
# None — ключ НП не задан: касса не опрашивает посылки и не получает заказы.
np_api_key_masked: str | None
tax_codes: list[Any]
is_active: bool
is_default: bool
@classmethod
def from_register(cls, register: CashRegister) -> CashRegisterOut:
return cls(
id=register.id,
name=register.name,
fiscal_number=register.fiscal_number,
license_key_masked=_masked(register.license_key_enc),
np_api_key_masked=(
_masked(register.np_api_key_enc) if register.np_api_key_enc else None
),
tax_codes=register.tax_codes,
is_active=register.is_active,
is_default=register.is_default,
)
class CashRegisterCreate(BaseModel):
name: str = Field(min_length=1, max_length=255)
fiscal_number: str | None = Field(default=None, max_length=64)
license_key: str = Field(min_length=1, max_length=255)
pin_code: str = Field(min_length=1, max_length=32)
np_api_key: str | None = Field(default=None, min_length=1, max_length=255)
tax_codes: list[int | str] = Field(default_factory=list, max_length=2)
is_default: bool = True
class CashRegisterUpdate(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str | None = Field(default=None, min_length=1, max_length=255)
fiscal_number: str | None = Field(default=None, max_length=64)
license_key: str | None = Field(default=None, min_length=1, max_length=255)
pin_code: str | None = Field(default=None, min_length=1, max_length=32)
np_api_key: str | None = Field(default=None, min_length=1, max_length=255)
tax_codes: list[int | str] | None = Field(default=None, max_length=2)
is_active: bool | None = None
is_default: bool | None = None