Files
lux_fiscal/backend/app/schemas/tracking.py
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

52 lines
2.8 KiB
Python

"""Схема статуса ТТН Nova Poshta (`TrackingDocument.getStatusDocuments`).
Поля валидируются из "сырых" PascalCase-ключей ответа NP через `Field(alias=...)`,
как и `OrderOut` для CRM. Суммы остаются строками "как есть" от NP — в
integer-копейки переводится только на границе с персистентной моделью.
"""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, model_validator
class TrackingStatusOut(BaseModel):
model_config = ConfigDict(populate_by_name=True)
number: str = Field(alias="Number")
status: str = Field(alias="Status")
status_code: str = Field(alias="StatusCode")
payment_method: str | None = Field(default=None, alias="PaymentMethod")
scheduled_delivery_date: str | None = Field(default=None, alias="ScheduledDeliveryDate")
actual_delivery_date: str | None = Field(default=None, alias="ActualDeliveryDate")
# Чистая сумма к перечислению продавцу за товар — поле `AfterpaymentOnGoodsCost`
# ("Контроль оплати" в кабинете NP). В отличие от `AmountToPay`/
# `ExpressWaybillAmountToPay` (сколько получатель должен заплатить НП прямо
# сейчас — включает стоимость доставки и комиссию НП и обнуляется после
# оплаты), это поле — зафиксированная при создании ТТН сумма за товар без
# доставки и комиссии, и не меняется по ходу доставки. `AmountToPay`/
# `ExpressWaybillAmountToPay` — запасной вариант на случай, если NP для
# какого-то типа накладной не возвращает `AfterpaymentOnGoodsCost`.
cod_amount: str | None = Field(default=None)
# Статус оплаты аналогично раздвоен на `PaymentStatus`/`ExpressWaybillPaymentStatus`.
payment_status: str | None = Field(default=None)
@model_validator(mode="before")
@classmethod
def _fold_express_fields(cls, data: Any) -> Any:
if not isinstance(data, dict):
return data
data = dict(data)
afterpayment = data.get("AfterpaymentOnGoodsCost")
fallback = data.get("AmountToPay") or data.get("ExpressWaybillAmountToPay") or None
data.setdefault("cod_amount", str(afterpayment) if afterpayment else fallback)
data.setdefault(
"payment_status",
data.get("PaymentStatus") or data.get("ExpressWaybillPaymentStatus") or None,
)
return data