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>
This commit is contained in:
@@ -20,6 +20,7 @@ from datetime import UTC, datetime, timedelta
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -39,6 +40,7 @@ from app.services.checkbox.client import (
|
||||
CheckboxError,
|
||||
CheckboxUnavailableError,
|
||||
)
|
||||
from app.services.crm.client import CrmClient, CrmError
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
@@ -65,6 +67,16 @@ _CHECKBOX_TO_STATUS = {
|
||||
# или Checkbox был недоступен) и повторить его из cron'а.
|
||||
_PENDING_RETRY_AFTER = timedelta(minutes=1)
|
||||
|
||||
# Статус заказа в CRM, когда Checkbox принял ЕТТН-чек: заказ можно собирать.
|
||||
CRM_STATUS_AFTER_RECEIPT = "PACKED"
|
||||
# Чеки, при которых заказу нужен этот статус в CRM (Checkbox чек принял).
|
||||
_CRM_STATUS_RECEIPT_STATUSES = (
|
||||
ReceiptStatus.CREATED,
|
||||
ReceiptStatus.DONE,
|
||||
ReceiptStatus.RECEIPT_ERROR,
|
||||
ReceiptStatus.RETURNED,
|
||||
)
|
||||
|
||||
|
||||
class ReceiptValidationError(Exception):
|
||||
"""Заказ нельзя отправить в Checkbox — сообщение показывается кассиру."""
|
||||
@@ -500,6 +512,54 @@ async def sync_ettn_statuses(session: AsyncSession, client: CheckboxClient) -> N
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def sync_crm_statuses(session: AsyncSession, crm: CrmClient) -> None:
|
||||
"""Переводит в CRM заказы с принятым Checkbox чеком в статус PACKED.
|
||||
|
||||
Отдельно от создания чека и идемпотентно по `crm_status_set_at`: сбой CRM
|
||||
не должен ни откатывать уже созданный в Checkbox чек, ни теряться —
|
||||
вызывается сразу после создания и повторяется cron'ом до успеха.
|
||||
"""
|
||||
receipt_ids = list(
|
||||
await session.scalars(
|
||||
select(Receipt.id)
|
||||
.where(Receipt.status.in_(_CRM_STATUS_RECEIPT_STATUSES))
|
||||
.where(Receipt.crm_status_set_at.is_(None))
|
||||
)
|
||||
)
|
||||
for receipt_id in receipt_ids:
|
||||
# Строка блокируется на время вызова CRM: задача создания и cron могут
|
||||
# сработать одновременно, а повторный PACKED откатил бы статус, который
|
||||
# менеджер уже успел сменить дальше. Занято или уже выставлено — пропуск.
|
||||
receipt = await session.scalar(
|
||||
select(Receipt)
|
||||
.where(Receipt.id == receipt_id)
|
||||
.where(Receipt.crm_status_set_at.is_(None))
|
||||
.with_for_update(skip_locked=True)
|
||||
)
|
||||
if receipt is None:
|
||||
continue
|
||||
order_id = receipt.order_id # после rollback атрибуты истекают
|
||||
try:
|
||||
await crm.set_status(order_id=order_id, status=CRM_STATUS_AFTER_RECEIPT)
|
||||
except (CrmError, httpx.HTTPError) as exc:
|
||||
await session.rollback() # снять блокировку строки
|
||||
log.warning("crm_status_set_failed", order_id=order_id, error=repr(exc))
|
||||
continue
|
||||
receipt.crm_status_set_at = datetime.now(UTC)
|
||||
await audit.record(
|
||||
session,
|
||||
action=AuditAction.ORDER_CRM_STATUS_SET,
|
||||
actor_label="worker",
|
||||
entity_type="order",
|
||||
entity_id=receipt.order_id,
|
||||
payload={"status": CRM_STATUS_AFTER_RECEIPT, "receipt_id": str(receipt.id)},
|
||||
)
|
||||
# Коммит на каждый заказ: статус в CRM уже сменён, отметку нельзя терять
|
||||
# из-за сбоя на следующем заказе.
|
||||
await session.commit()
|
||||
log.info("crm_status_set", order_id=receipt.order_id, status=CRM_STATUS_AFTER_RECEIPT)
|
||||
|
||||
|
||||
async def latest_receipts_by_order(
|
||||
session: AsyncSession, order_ids: list[str]
|
||||
) -> dict[str, Receipt]:
|
||||
|
||||
Reference in New Issue
Block a user