"""Схемы заказов CRM. Поля валидируются напрямую из "сырых" PascalCase-ключей ответа CRM через `Field(alias=...)`, а наружу (в JSON фронту) отдаются как чистые snake_case имена. Денежные значения остаются строками "как есть" от CRM — конвенция integer-kopecks из CLAUDE.md относится к будущим персистентным моделям `Order`/`Receipt`, а не к этому read-only проксирующему эндпоинту. """ from __future__ import annotations from datetime import datetime from decimal import Decimal from typing import TYPE_CHECKING, Any from pydantic import BaseModel, ConfigDict, Field, field_validator if TYPE_CHECKING: from app.db.models.order import Order from app.db.models.receipt import Receipt class OrderGoodOut(BaseModel): model_config = ConfigDict(populate_by_name=True) id: str = Field(alias="ID") sku: str = Field(alias="SKU") name: str = Field(alias="Name") price: str = Field(alias="Price") quantity: str = Field(alias="Quantity") discount_amount: str | None = Field(default=None, alias="DiscountAmount") discount_percent: str | None = Field(default=None, alias="DiscountPercent") amount: str = Field(alias="Amount") class OrderTotalOut(BaseModel): model_config = ConfigDict(populate_by_name=True) cost: str = Field(alias="Cost") quantity: str = Field(alias="Quantity") weight: str = Field(alias="Weight") discount_amount: str = Field(alias="DiscountAmount") discount_percent: str = Field(alias="DiscountPercent") amount: str = Field(alias="Amount") # CRM возвращает Quantity/Weight то числом, то строкой в зависимости от # заказа — приводим к строке единообразно для фронта. @field_validator("quantity", "weight", mode="before") @classmethod def _stringify(cls, value: Any) -> Any: return str(value) if value is not None else value class OrderOut(BaseModel): model_config = ConfigDict(populate_by_name=True) id: str = Field(alias="ID") create_date_time: str = Field(alias="CreateDateTime") recipient_name: str | None = Field(default=None, alias="RecipientDName") recipient_phone: str | None = Field(default=None, alias="RecipientPhone") recipient_email: str | None = Field(default=None, alias="RecipientEmail") waybill_number: str | None = Field(default=None, alias="Waybill_Number") notes: str | None = Field(default=None, alias="Notes") total: OrderTotalOut = Field(alias="Total") goods: list[OrderGoodOut] = Field(default_factory=list, alias="Goods") class OrderRowOut(BaseModel): """Ответ `GET /orders` — строится из локальной таблицы `orders`, не из CRM напрямую.""" model_config = ConfigDict(from_attributes=True) id: str create_date_time: datetime recipient_name: str | None recipient_phone: str | None recipient_email: str | None waybill_number: str | None notes: str | None total_amount: str goods: list[OrderGoodOut] has_receipt: bool np_status: str | None np_status_code: str | None np_cod_amount: str | None np_payment_status: str | None # Последний ЕТТН-чек по заказу (в т.ч. отменённый/неудачный — для показа причины). receipt_id: str | None = None receipt_status: str | None = None receipt_error: str | None = None receipt_prepayment: str | None = None edited: bool = False @classmethod def from_order(cls, order: Order, receipt: Receipt | None = None) -> OrderRowOut: return cls( id=order.id, create_date_time=order.create_date_time, recipient_name=order.recipient_name, recipient_phone=order.recipient_phone, recipient_email=order.recipient_email, waybill_number=order.waybill_number, notes=order.notes, total_amount=f"{order.total_amount_kopecks / 100:.2f}", goods=[OrderGoodOut.model_validate(good) for good in order.goods], has_receipt=order.receipt_created_at is not None, np_status=order.np_status, np_status_code=order.np_status_code, np_cod_amount=( f"{order.np_cod_amount_kopecks / 100:.2f}" if order.np_cod_amount_kopecks is not None else None ), np_payment_status=order.np_payment_status, receipt_id=str(receipt.id) if receipt else None, receipt_status=receipt.status.value if receipt else None, receipt_error=receipt.error if receipt else None, receipt_prepayment=( f"{receipt.prepayment_kopecks / 100:.2f}" if receipt else None ), edited=order.edited_at is not None, ) class OrderGoodIn(BaseModel): """Позиция заказа из формы редактирования. `amount` считает сервер.""" id: str | None = Field(default=None, max_length=64) sku: str = Field(default="", max_length=128) name: str = Field(min_length=1, max_length=512) price: Decimal = Field(ge=0, max_digits=12, decimal_places=2) quantity: Decimal = Field(gt=0, max_digits=12, decimal_places=3) discount_amount: Decimal = Field(default=Decimal(0), ge=0, max_digits=12, decimal_places=2) @field_validator("sku", "name", mode="after") @classmethod def _strip(cls, value: str) -> str: return value.strip() class OrderUpdateIn(BaseModel): """`PATCH /orders/{id}` — полная замена редактируемых полей заказа.""" recipient_name: str | None = Field(default=None, max_length=255) recipient_phone: str | None = Field(default=None, max_length=32) recipient_email: str | None = Field(default=None, max_length=320) waybill_number: str | None = Field(default=None, max_length=64) notes: str | None = None total_amount: Decimal = Field(ge=0, max_digits=12, decimal_places=2) goods: list[OrderGoodIn] = Field(min_length=1) # Пустая строка из формы = «не указано». @field_validator( "recipient_name", "recipient_phone", "recipient_email", "waybill_number", "notes" ) @classmethod def _blank_to_none(cls, value: str | None) -> str | None: if value is None: return None return value.strip() or None