diff --git a/backend/app/services/receipts.py b/backend/app/services/receipts.py index c073d1f..a97dc67 100644 --- a/backend/app/services/receipts.py +++ b/backend/app/services/receipts.py @@ -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 diff --git a/backend/tests/test_receipts_service.py b/backend/tests/test_receipts_service.py index 2a577e8..61648c1 100644 --- a/backend/tests/test_receipts_service.py +++ b/backend/tests/test_receipts_service.py @@ -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")],