Fix ETTN creation on live Checkbox

- prepayment as DISCOUNT: live API rejects PRE_PAYMENT with 400 third_party.generic
- normalize lowercase ETTN statuses returned by the live API
- show Checkbox error code instead of bare 'Internal Server Error'
- page size 50 for ETTN list, 429 rate limit is retryable, pause polling on it
- phone in 380XXXXXXXXX format like portal receipts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 13:49:50 +03:00
co-authored by Claude Opus 5.5
parent 912a37c00c
commit 3a6ad85d11
7 changed files with 73 additions and 26 deletions
+32
View File
@@ -148,3 +148,35 @@ async def test_bad_pin_raises() -> None:
)
with pytest.raises(CheckboxError, match="Невірний пін-код"):
await _client().sign_in(CREDS)
@respx.mock
async def test_lowercase_status_from_live_api_is_normalized() -> None:
_signin()
respx.get(f"{BASE}/api/v1/ettn/e-1").mock(
return_value=Response(200, json={**ETTN, "status": "done"})
)
ettn = await _client().get_ettn(CREDS, "e-1")
assert ettn.status == "DONE"
@respx.mock
async def test_third_party_error_code_is_shown() -> None:
_signin()
respx.post(f"{BASE}/api/v1/ettn").mock(
return_value=Response(
400, json={"code": "third_party.generic", "message": "Internal Server Error"}
)
)
with pytest.raises(CheckboxError, match=r"third_party\.generic"):
await _client().create_ettn(CREDS, {})
@respx.mock
async def test_rate_limit_is_retryable() -> None:
_signin()
respx.post(f"{BASE}/api/v1/ettn").mock(
return_value=Response(429, json={"message": "Занадто часто виконуються запити"})
)
with pytest.raises(CheckboxUnavailableError):
await _client().create_ettn(CREDS, {})
+4 -6
View File
@@ -117,7 +117,7 @@ class TestBuildBody:
}
]
assert "discounts" not in rb
assert rb["delivery"] == {"phone": "+380501112233", "emails": ["a@b.ua"]}
assert rb["delivery"] == {"phone": "380501112233", "emails": ["a@b.ua"]}
def test_prepayment_line_and_order_discounts_and_tax(self) -> None:
order = _order(
@@ -132,10 +132,8 @@ class TestBuildBody:
assert rb["goods"][0]["discounts"] == [
{"type": "DISCOUNT", "mode": "VALUE", "value": 10000}
]
assert [(d["type"], d["value"]) for d in rb["discounts"]] == [
("DISCOUNT", 5000),
("PRE_PAYMENT", 20000),
]
# Скидка заказа 50 ₴ + предоплата 200 ₴ — одной «Знижкой», без PRE_PAYMENT.
assert [(d["type"], d["value"]) for d in rb["discounts"]] == [("DISCOUNT", 25000)]
assert rb["payments"][0]["value"] == 85000
def test_all_goods_with_prices_quantities_and_sums(self) -> None:
@@ -166,7 +164,7 @@ class TestBuildBody:
goods_total = sum(svc._line_sum(g["good"]["price"], g["quantity"]) for g in rb["goods"])
assert goods_total - 20000 == rb["payments"][0]["value"] == 130049
assert rb["discounts"] == [
{"type": "PRE_PAYMENT", "mode": "VALUE", "value": 20000, "name": "Передоплата"}
{"type": "DISCOUNT", "mode": "VALUE", "value": 20000, "name": "Знижка"}
]
def test_fractional_quantity(self) -> None: