Files
lux_fiscal/backend/app/services/crm/exo_client.py
T
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

79 lines
3.5 KiB
Python

"""Реальный клиент exoCRM (`GetOrders`, `SetStatus`)."""
from __future__ import annotations
from typing import Any
import httpx
from app.core.config import Settings
from app.schemas.orders import OrderOut
from app.services.crm.checksum import compute_md5sum
from app.services.crm.client import CrmError
def _errors_text(data: dict[str, Any]) -> str:
# Ключ бывает и `errors` (dict код → текст), и `Errors` (список строк).
errors = data.get("errors") or data.get("Errors") or {}
if isinstance(errors, dict):
return "; ".join(f"{code}: {text}" for code, text in errors.items())
return "; ".join(str(error) for error in errors)
class ExoCrmClient:
def __init__(self, settings: Settings) -> None:
self._base_url = settings.crm_base_url
self._api_key = settings.crm_api_key
self._secret_key = settings.crm_secret_key
self._shop_key = settings.crm_shop_key
self._sid = settings.crm_sid
async def _post(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
body = {"apikey": self._api_key, "object": "Orders", "method": method, "params": params}
body["md5sum"] = compute_md5sum(body, self._secret_key)
async with httpx.AsyncClient(timeout=30) as client:
response = await client.post(self._base_url, json=body)
response.raise_for_status()
try:
data = response.json()
except ValueError as exc:
# PHP-notice'ы CRM перед JSON — признак неверных параметров.
raise CrmError(f"CRM вернула не JSON: {response.text[:300]}") from exc
if not isinstance(data, dict):
raise CrmError(f"Неожиданный ответ CRM: {str(data)[:300]}")
return data
async def get_orders(self, *, status: str) -> list[OrderOut]:
data = await self._post(
"GetOrders",
{
"sid": self._sid,
"key": self._shop_key,
"Status": status,
"ReturnGoods": True,
"ReturnTotals": True,
},
)
if data.get("status") != "OK":
raise CrmError(f"CRM вернула ошибку: {_errors_text(data) or 'неизвестная ошибка'}")
return [OrderOut.model_validate(order) for order in data.get("result") or []]
async def set_status(self, *, order_id: str, status: str) -> None:
"""`SetStatus`. Формат проверен на боевой CRM и расходится с документацией.
Документация показывает `params: {"ID": ..., "Status": ...}`, но CRM на это
отвечает «Undefined order list.». Рабочий вариант — список ID в `Orders`,
а ответ — результат по каждому заказу без общего `status: OK`:
{"123901": {"Status": "Success", "ChangeStatus": "Success"}}
"""
data = await self._post("SetStatus", {"Orders": [order_id], "Status": status})
result = data.get(order_id)
if isinstance(result, dict) and result.get("Status") == "Success":
return
details = _errors_text(data) or (
str(result) if result is not None else "нет ответа по заказу"
)
raise CrmError(f"CRM не сменила статус заказа {order_id} на {status}: {details}")