Add CRM order queue: live sync, view modal, receipt tabs, delete
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>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""Тесты ExoCrmClient. Сеть замокана через respx — реальных запросов не делает."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
from httpx import Response
|
||||
|
||||
from app.core.config import Settings
|
||||
from app.services.crm.client import CrmError
|
||||
from app.services.crm.exo_client import ExoCrmClient
|
||||
|
||||
BASE_URL = "https://crm.example.test/api/1.1/"
|
||||
|
||||
|
||||
def _settings() -> Settings:
|
||||
return Settings(
|
||||
secret_key="test-secret-key",
|
||||
encryption_key="dGVzdC1lbmNyeXB0aW9uLWtleS0zMi1ieXRlcyEh",
|
||||
crm_base_url=BASE_URL,
|
||||
crm_api_key="apikey123",
|
||||
crm_secret_key="secret123",
|
||||
crm_shop_key="shopkey123",
|
||||
crm_sid=1,
|
||||
) # type: ignore[arg-type]
|
||||
|
||||
|
||||
ORDER_PAYLOAD = {
|
||||
"ID": "1",
|
||||
"CreateDateTime": "2026-09-20 10:00:00",
|
||||
"RecipientDName": "Іван Іванов",
|
||||
"RecipientPhone": "+380501112233",
|
||||
"RecipientEmail": None,
|
||||
"Waybill_Number": "",
|
||||
"Notes": None,
|
||||
"Total": {
|
||||
"Cost": "0.00",
|
||||
"Quantity": 1,
|
||||
"Weight": 0,
|
||||
"DiscountAmount": "0.00",
|
||||
"DiscountPercent": "0.00",
|
||||
"Amount": "100.00",
|
||||
},
|
||||
"Goods": [
|
||||
{
|
||||
"ID": "10",
|
||||
"SKU": "SKU-1",
|
||||
"Name": "Товар",
|
||||
"Price": "100.00",
|
||||
"Quantity": "1.000",
|
||||
"DiscountAmount": "0.00",
|
||||
"DiscountPercent": "0.00",
|
||||
"Amount": "100.00",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class TestExoCrmClientGetOrders:
|
||||
@respx.mock
|
||||
async def test_sends_expected_request_body(self) -> None:
|
||||
route = respx.post(BASE_URL).mock(
|
||||
return_value=Response(200, json={"status": "OK", "result": []})
|
||||
)
|
||||
client = ExoCrmClient(_settings())
|
||||
|
||||
await client.get_orders(status="APPROVED")
|
||||
|
||||
sent = route.calls.last.request
|
||||
body = sent.content
|
||||
import json
|
||||
|
||||
parsed = json.loads(body)
|
||||
assert parsed["apikey"] == "apikey123"
|
||||
assert parsed["object"] == "Orders"
|
||||
assert parsed["method"] == "GetOrders"
|
||||
assert parsed["params"]["sid"] == 1
|
||||
assert parsed["params"]["key"] == "shopkey123"
|
||||
assert parsed["params"]["Status"] == "APPROVED"
|
||||
assert "md5sum" in parsed
|
||||
|
||||
@respx.mock
|
||||
async def test_parses_successful_response_into_order_out(self) -> None:
|
||||
respx.post(BASE_URL).mock(
|
||||
return_value=Response(200, json={"status": "OK", "result": [ORDER_PAYLOAD]})
|
||||
)
|
||||
client = ExoCrmClient(_settings())
|
||||
|
||||
orders = await client.get_orders(status="APPROVED")
|
||||
|
||||
assert len(orders) == 1
|
||||
order = orders[0]
|
||||
assert order.id == "1"
|
||||
assert order.recipient_name == "Іван Іванов"
|
||||
assert order.total.quantity == "1"
|
||||
assert order.goods[0].sku == "SKU-1"
|
||||
|
||||
@respx.mock
|
||||
async def test_raises_crm_error_on_error_status(self) -> None:
|
||||
respx.post(BASE_URL).mock(
|
||||
return_value=Response(
|
||||
200, json={"status": "ERROR", "errors": {"1005": "Checksum Error"}}
|
||||
)
|
||||
)
|
||||
client = ExoCrmClient(_settings())
|
||||
|
||||
with pytest.raises(CrmError, match="Checksum Error"):
|
||||
await client.get_orders(status="APPROVED")
|
||||
Reference in New Issue
Block a user