Files
lux_fiscal/backend/app/schemas/orders.py
T
lauadminandClaude Sonnet 5 d13e7ce2b3 Add Nova Poshta tracking: TTN status, COD amount, payment status
Adds NpTrackingClient (Protocol + real/stub impls) and an ARQ worker that
polls Nova Poshta every minute for orders without a receipt, writing
status, net COD amount (Контроль оплати), and payment status onto the
order. Surfaced in the orders table and detail modal. Marks plan stages
3-4 done in README.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 22:33:58 +03:00

108 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Схемы заказов 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 typing import TYPE_CHECKING, Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
if TYPE_CHECKING:
from app.db.models.order import Order
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
@classmethod
def from_order(cls, order: Order) -> 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,
)