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>
128 lines
4.3 KiB
Python
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
|