- All frontend pages, labels, notices and errors; html lang=uk, uk-UA money format - Brand "Assistant System" in the top bar and page title - Backend error details returned to the UI (auth, orders, receipts, cash registers, Checkbox/CRM/NP errors) and CLI output - Tests updated for the new messages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
4.2 KiB
Python
128 lines
4.2 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
|