Files
lauadminandClaude Opus 5.5 cbf9832e5b Set CRM order status to PACKED after Checkbox accepts the receipt
- ExoCrmClient.set_status: live SetStatus needs {Orders: [id], Status} and
  replies per order, unlike the documented {ID, Status}
- receipts.crm_status_set_at (migration 0006); set right after creation,
  retried by cron, row-locked to avoid a repeat PACKED overwriting a newer status
- CRM errors under capitalized 'Errors' and non-JSON replies are reported

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 14:04:26 +03:00

87 lines
4.4 KiB
Python
Raw Permalink 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.
"""ЕТТН-чеки Checkbox по ТТН Nova Poshta с контролем оплаты.
Чек создаётся в Checkbox как шаблон, привязанный к ТТН; фискализирует его сам
Checkbox, когда клиент оплачивает посылку в отделении. Здесь — наша сторона:
кто и когда запросил, что отправили и в каком статусе чек сейчас.
Машина состояний:
pending ──► created ──► done | returned | receipt_error | cancelled
└──────► failed
created ──(отмена кассиром)──► cancelled
Из `failed`/`cancelled` заказ возвращается в очередь и чек можно создать заново.
"""
from __future__ import annotations
import enum
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import BigInteger, DateTime, Enum, ForeignKey, Index, String, Text, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
class ReceiptStatus(enum.StrEnum):
PENDING = "pending" # запись создана, задача в очереди worker'а
CREATED = "created" # Checkbox принял ЕТТН-чек, ждём оплату посылки
DONE = "done" # Checkbox фискализировал чек
RETURNED = "returned" # посылка вернулась отправителю
RECEIPT_ERROR = "receipt_error" # Checkbox не смог фискализировать
CANCELLED = "cancelled" # отменён (кассиром или в Checkbox)
FAILED = "failed" # Checkbox отклонил создание
# Статусы, при которых у заказа нет «живого» чека и его можно создать заново.
CLOSED_STATUSES = (ReceiptStatus.CANCELLED, ReceiptStatus.FAILED)
class Receipt(UUIDPrimaryKeyMixin, TimestampMixin, Base):
__tablename__ = "receipts"
order_id: Mapped[str] = mapped_column(
ForeignKey("orders.id", ondelete="RESTRICT"), index=True, nullable=False
)
cash_register_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("cash_registers.id", ondelete="RESTRICT"), index=True, nullable=False
)
created_by_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("users.id", ondelete="SET NULL")
)
waybill_number: Mapped[str] = mapped_column(String(64), nullable=False)
total_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False)
prepayment_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False)
cod_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False)
status: Mapped[ReceiptStatus] = mapped_column(
Enum(ReceiptStatus, name="receipt_status", values_callable=lambda e: [i.value for i in e]),
nullable=False,
default=ReceiptStatus.PENDING,
index=True,
)
checkbox_ettn_id: Mapped[str | None] = mapped_column(String(64), index=True)
checkbox_status: Mapped[str | None] = mapped_column(String(32))
checkbox_receipt_id: Mapped[str | None] = mapped_column(String(64))
error: Mapped[str | None] = mapped_column(Text)
# Снимок отправленного в Checkbox тела — для разбора спорных случаев.
request_body: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
last_checked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
# Когда заказу в CRM выставлен статус после создания чека (PACKED). NULL при
# живом чеке — ещё не выставлен (CRM была недоступна), cron повторит.
crm_status_set_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
__table_args__ = (
# Не больше одного «живого» чека на заказ — защита от двойного нажатия
# и гонки между кассирами на уровне БД.
Index(
"uq_receipts_active_order",
"order_id",
unique=True,
postgresql_where=text("status NOT IN ('cancelled', 'failed')"),
),
)