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:
@@ -323,3 +323,70 @@ class TestCancel:
|
||||
receipt.id,
|
||||
user=None, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
class CrmSession:
|
||||
"""Фейковая сессия для sync_crm_statuses: scalars → id, scalar → строка с «блокировкой»."""
|
||||
|
||||
def __init__(self, *receipts: Receipt) -> None:
|
||||
self.receipts = {r.id: r for r in receipts}
|
||||
self.added: list[Any] = []
|
||||
self.commits = 0
|
||||
self.rollbacks = 0
|
||||
|
||||
async def scalars(self, _: Any) -> list[uuid.UUID]:
|
||||
return [r.id for r in self.receipts.values() if r.crm_status_set_at is None]
|
||||
|
||||
async def scalar(self, query: Any) -> Receipt | None:
|
||||
receipt_id = query.whereclause.clauses[0].right.value
|
||||
receipt = self.receipts[receipt_id]
|
||||
return receipt if receipt.crm_status_set_at is None else None
|
||||
|
||||
def add(self, obj: Any) -> None:
|
||||
self.added.append(obj)
|
||||
|
||||
async def commit(self) -> None:
|
||||
self.commits += 1
|
||||
|
||||
async def rollback(self) -> None:
|
||||
self.rollbacks += 1
|
||||
|
||||
|
||||
class TestSyncCrmStatuses:
|
||||
async def test_sets_packed_once(self) -> None:
|
||||
from app.services.crm.stub_client import StubCrmClient
|
||||
|
||||
order, register = _order(), _register()
|
||||
receipt = _pending(order, register)
|
||||
receipt.status = ReceiptStatus.CREATED
|
||||
session = CrmSession(receipt)
|
||||
crm = StubCrmClient()
|
||||
|
||||
await svc.sync_crm_statuses(session, crm) # type: ignore[arg-type]
|
||||
await svc.sync_crm_statuses(session, crm) # type: ignore[arg-type]
|
||||
|
||||
assert crm.statuses == {"100": "PACKED"}
|
||||
assert receipt.crm_status_set_at is not None
|
||||
assert session.commits == 1 # второй проход ничего не делает
|
||||
|
||||
async def test_crm_failure_is_retried_later(self) -> None:
|
||||
from app.services.crm.client import CrmError
|
||||
from app.services.crm.stub_client import StubCrmClient
|
||||
|
||||
order, register = _order(), _register()
|
||||
receipt = _pending(order, register)
|
||||
receipt.status = ReceiptStatus.CREATED
|
||||
session = CrmSession(receipt)
|
||||
crm = StubCrmClient()
|
||||
|
||||
async def broken(**_: Any) -> None:
|
||||
raise CrmError("CRM вернула ошибку")
|
||||
|
||||
real_set_status = crm.set_status
|
||||
crm.set_status = broken # type: ignore[method-assign]
|
||||
await svc.sync_crm_statuses(session, crm) # type: ignore[arg-type]
|
||||
assert receipt.crm_status_set_at is None and session.rollbacks == 1
|
||||
|
||||
crm.set_status = real_set_status # type: ignore[method-assign]
|
||||
await svc.sync_crm_statuses(session, crm) # type: ignore[arg-type]
|
||||
assert crm.statuses == {"100": "PACKED"}
|
||||
|
||||
Reference in New Issue
Block a user