Wires the CRM (exoCRM GetOrders) into the dashboard as a locally persisted order queue instead of the previous static mockup: - CrmClient Protocol + ExoCrmClient/StubCrmClient for the CRM's signed JSON-RPC API - Order model + migration, synced from CRM on each queue view; soft-deleted orders stay hidden across re-syncs - GET/DELETE /api/v1/orders with "no receipt"/"receipt issued" tabs (the latter is empty until Checkbox fiscalization lands) - Dashboard: real order list, item-detail modal, tab switcher, one-click delete Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
39 lines
1.7 KiB
Python
39 lines
1.7 KiB
Python
"""Контрольная сумма запросов к CRM API (см. документацию "My CRM API 1.1")."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from typing import Any
|
|
|
|
|
|
def _collect_values_sorted(obj: Any) -> list[Any]:
|
|
"""Рекурсивно сортирует ключи на каждом уровне и собирает значения depth-first."""
|
|
values: list[Any] = []
|
|
if isinstance(obj, dict):
|
|
for key in sorted(obj.keys()):
|
|
values.extend(_collect_values_sorted(obj[key]))
|
|
elif isinstance(obj, list):
|
|
for item in obj:
|
|
values.extend(_collect_values_sorted(item))
|
|
else:
|
|
values.append(obj)
|
|
return values
|
|
|
|
|
|
def _to_str(value: Any) -> str:
|
|
# Булевы значения CRM ожидает в контрольной сумме как "1"/"" (PHP-style
|
|
# truthy-приведение) — это нигде не задокументировано и подобрано опытным
|
|
# путём: "True"/"False" и "true"/"false" оба дают "Checksum Error".
|
|
if isinstance(value, bool):
|
|
return "1" if value else ""
|
|
return str(value)
|
|
|
|
|
|
def compute_md5sum(payload: dict[str, Any], secret_key: str) -> str:
|
|
"""Считает md5sum по алгоритму из документации CRM: отсортировать все ключи
|
|
(включая вложенные), конкатенировать все значения, добавить приватный ключ, взять MD5.
|
|
"""
|
|
values = _collect_values_sorted(payload)
|
|
concatenated = "".join(_to_str(v) for v in values)
|
|
return hashlib.md5((concatenated + secret_key).encode("utf-8")).hexdigest()
|