Send ETTN payment type and label explicitly, verify receipt total
Payments now carry type=ETTN / label=Експрес-накладна instead of relying on Checkbox defaults; body build fails fast if goods - discounts - prepayment does not equal the COD amount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,10 @@ NP_FINAL_STATUS_CODES = frozenset(
|
||||
{"2", "9", "10", "11", "102", "103", "105", "106", "108"}
|
||||
)
|
||||
|
||||
# Способ оплаты в ЕТТН-чеке (`ETTNPaymentSchema`).
|
||||
ETTN_PAYMENT_TYPE = "ETTN"
|
||||
ETTN_PAYMENT_LABEL = "Експрес-накладна"
|
||||
|
||||
# Статус Checkbox → наш статус. CREATED не меняет ничего.
|
||||
_CHECKBOX_TO_STATUS = {
|
||||
EttnStatus.DONE: ReceiptStatus.DONE,
|
||||
@@ -163,9 +167,27 @@ def build_ettn_body(
|
||||
}
|
||||
)
|
||||
|
||||
# Последняя страховка перед отправкой: сумма чека (товары − скидки − предоплата)
|
||||
# обязана совпасть с оплатой по ЕТТН, иначе Checkbox не сможет фискализировать.
|
||||
receipt_total = goods_total - order_discount - amounts.prepayment_kopecks
|
||||
if receipt_total != amounts.cod_kopecks:
|
||||
raise ReceiptValidationError(
|
||||
f"Сумма чека {receipt_total / 100:.2f} ₴ ≠ наложка {amounts.cod_kopecks / 100:.2f} ₴"
|
||||
)
|
||||
|
||||
receipt_body: dict[str, Any] = {
|
||||
"goods": goods,
|
||||
"payments": [{"value": amounts.cod_kopecks, "ettn": order.waybill_number}],
|
||||
# Оплата «Експрес-накладна»: деньги примет НП при выдаче посылки,
|
||||
# тогда Checkbox и фискализирует чек. Тип и подпись — явно, не полагаясь
|
||||
# на значения по умолчанию Checkbox.
|
||||
"payments": [
|
||||
{
|
||||
"type": ETTN_PAYMENT_TYPE,
|
||||
"label": ETTN_PAYMENT_LABEL,
|
||||
"value": amounts.cod_kopecks,
|
||||
"ettn": order.waybill_number,
|
||||
}
|
||||
],
|
||||
}
|
||||
if discounts:
|
||||
receipt_body["discounts"] = discounts
|
||||
|
||||
@@ -108,7 +108,14 @@ class TestBuildBody:
|
||||
"is_return": False,
|
||||
}
|
||||
]
|
||||
assert rb["payments"] == [{"value": 120000, "ettn": "20450123456789"}]
|
||||
assert rb["payments"] == [
|
||||
{
|
||||
"type": "ETTN",
|
||||
"label": "Експрес-накладна",
|
||||
"value": 120000,
|
||||
"ettn": "20450123456789",
|
||||
}
|
||||
]
|
||||
assert "discounts" not in rb
|
||||
assert rb["delivery"] == {"phone": "+380501112233", "emails": ["a@b.ua"]}
|
||||
|
||||
@@ -131,6 +138,37 @@ class TestBuildBody:
|
||||
]
|
||||
assert rb["payments"][0]["value"] == 85000
|
||||
|
||||
def test_all_goods_with_prices_quantities_and_sums(self) -> None:
|
||||
order = _order(
|
||||
goods=[
|
||||
_good(id="1", sku="A", name="Сукня", price="600.00", amount="1200.00"),
|
||||
_good(id="2", sku="", name="Пояс", price="150.50", quantity="1.000",
|
||||
amount="150.50"),
|
||||
_good(id="3", sku="C", name="Тканина", price="99.99", quantity="1.500",
|
||||
amount="149.99"),
|
||||
],
|
||||
total_amount_kopecks=150049,
|
||||
np_cod_amount_kopecks=130049, # 200 ₴ предоплаты
|
||||
)
|
||||
body = svc.build_ettn_body(order, _register(), svc.resolve_amounts(order, 20000))
|
||||
rb = body["receipt_body"]
|
||||
|
||||
assert [
|
||||
(g["good"]["code"], g["good"]["name"], g["good"]["price"], g["quantity"])
|
||||
for g in rb["goods"]
|
||||
] == [
|
||||
("A", "Сукня", 60000, 2000),
|
||||
("2", "Пояс", 15050, 1000), # пустой SKU → ID товара
|
||||
("C", "Тканина", 9999, 1500),
|
||||
]
|
||||
# 99.99 × 1.5 = 149.985 → CRM округлила до 149.99, скидки по строке нет.
|
||||
assert all("discounts" not in g for g in rb["goods"])
|
||||
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": "Передоплата"}
|
||||
]
|
||||
|
||||
def test_fractional_quantity(self) -> None:
|
||||
order = _order(
|
||||
goods=[_good(price="100.00", quantity="2.250", amount="225.00")],
|
||||
|
||||
Reference in New Issue
Block a user