Добавлен заказ накладной
This commit is contained in:
@@ -16,6 +16,7 @@ from app.services.aggregator import (
|
||||
InitPaymentUnavailableError,
|
||||
InvalidInitPaymentRequestError,
|
||||
)
|
||||
from tests.payment_fixtures import make_init_payment_payload, make_init_payment_request
|
||||
|
||||
|
||||
class StubPaymentAdapter:
|
||||
@@ -94,49 +95,6 @@ class StubOrderRepository:
|
||||
return object()
|
||||
|
||||
|
||||
def _make_init_payment_request(**overrides: object) -> InitPaymentRequest:
|
||||
payload: dict[str, object] = {
|
||||
"order_uuid": "order-uuid-1",
|
||||
"price": 125000,
|
||||
"type": 2,
|
||||
"tariff_code": 535,
|
||||
"comment": "Test payment",
|
||||
"sender": {
|
||||
"name": "Petr Petrov",
|
||||
"email": "sender@example.com",
|
||||
"phone": {"number": "+79009876543"},
|
||||
},
|
||||
"recipient": {
|
||||
"name": "Ivan Ivanov",
|
||||
"email": "ivan@example.com",
|
||||
"phone": {"number": "+79001234567"},
|
||||
},
|
||||
"from_location": {
|
||||
"address": "Lenina 1",
|
||||
"city": "Moscow",
|
||||
"country_code": "RU",
|
||||
},
|
||||
"to_location": {
|
||||
"address": "Pushkina 10",
|
||||
"city": "Novosibirsk",
|
||||
"country_code": "RU",
|
||||
},
|
||||
"services": [{"code": "INSURANCE", "parameter": "1000"}],
|
||||
"packages": [
|
||||
{
|
||||
"number": "1",
|
||||
"weight": 1,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"comment": "Package 1",
|
||||
}
|
||||
],
|
||||
}
|
||||
payload.update(overrides)
|
||||
return InitPaymentRequest(**payload)
|
||||
|
||||
|
||||
def _make_cdek_price(**overrides: object) -> DeliveryPrice:
|
||||
payload = {
|
||||
"provider": "cdek",
|
||||
@@ -151,7 +109,7 @@ def _make_cdek_price(**overrides: object) -> DeliveryPrice:
|
||||
|
||||
|
||||
def test_init_payment_validates_cdek_price_before_tbank_and_returns_payment_url() -> None:
|
||||
request = _make_init_payment_request()
|
||||
request = make_init_payment_request()
|
||||
events: list[str] = []
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(
|
||||
response=_make_cdek_price(),
|
||||
@@ -172,8 +130,9 @@ def test_init_payment_validates_cdek_price_before_tbank_and_returns_payment_url(
|
||||
assert events == ["cdek", "tbank"]
|
||||
|
||||
|
||||
def test_init_payment_persists_order_after_successful_payment_link() -> None:
|
||||
request = _make_init_payment_request()
|
||||
def test_init_payment_persists_order_payload_after_successful_payment_link() -> None:
|
||||
payload = make_init_payment_payload()
|
||||
request = InitPaymentRequest.model_validate(payload)
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
order_repository = StubOrderRepository()
|
||||
@@ -184,59 +143,21 @@ def test_init_payment_persists_order_after_successful_payment_link() -> None:
|
||||
order_repository=order_repository,
|
||||
)
|
||||
|
||||
result = asyncio.run(service.init_payment(request))
|
||||
asyncio.run(service.init_payment(request))
|
||||
|
||||
assert result == InitPaymentResponse(payment_url="https://pay.test/payment/1")
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == [("order-uuid-1", 125000)]
|
||||
assert order_repository.calls == [
|
||||
(
|
||||
order_repository.session_value,
|
||||
OrderData(
|
||||
order_uuid="order-uuid-1",
|
||||
payment_url="https://pay.test/payment/1",
|
||||
price=125000,
|
||||
delivery_type=2,
|
||||
tariff_code=535,
|
||||
sender={
|
||||
"name": "Petr Petrov",
|
||||
"email": "sender@example.com",
|
||||
"phone": {"number": "+79009876543"},
|
||||
},
|
||||
recipient={
|
||||
"name": "Ivan Ivanov",
|
||||
"email": "ivan@example.com",
|
||||
"phone": {"number": "+79001234567"},
|
||||
},
|
||||
from_location={
|
||||
"address": "Lenina 1",
|
||||
"city": "Moscow",
|
||||
"country_code": "RU",
|
||||
},
|
||||
to_location={
|
||||
"address": "Pushkina 10",
|
||||
"city": "Novosibirsk",
|
||||
"country_code": "RU",
|
||||
},
|
||||
packages=[
|
||||
{
|
||||
"number": "1",
|
||||
"weight": 1,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"comment": "Package 1",
|
||||
}
|
||||
],
|
||||
services=[{"code": "INSURANCE", "parameter": "1000"}],
|
||||
comment="Test payment",
|
||||
),
|
||||
)
|
||||
]
|
||||
assert len(order_repository.calls) == 1
|
||||
session, order_data = order_repository.calls[0]
|
||||
assert session is order_repository.session_value
|
||||
assert order_data.order_uuid == "order-uuid-1"
|
||||
assert order_data.payment_url == "https://pay.test/payment/1"
|
||||
assert order_data.price == 125000
|
||||
assert order_data.tariff_code == 535
|
||||
assert order_data.account_email == "client@example.com"
|
||||
assert order_data.payload == request.model_dump(mode="json", by_alias=True)
|
||||
|
||||
|
||||
def test_init_payment_returns_payment_url_when_order_persistence_fails() -> None:
|
||||
request = _make_init_payment_request()
|
||||
request = make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
order_repository = StubOrderRepository(error=RuntimeError("database down"))
|
||||
@@ -250,13 +171,12 @@ def test_init_payment_returns_payment_url_when_order_persistence_fails() -> None
|
||||
result = asyncio.run(service.init_payment(request))
|
||||
|
||||
assert result == InitPaymentResponse(payment_url="https://pay.test/payment/1")
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == [("order-uuid-1", 125000)]
|
||||
assert len(order_repository.calls) == 1
|
||||
|
||||
|
||||
def test_init_payment_maps_provider_request_errors_to_invalid_payment_error() -> None:
|
||||
request = _make_init_payment_request()
|
||||
request = make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(error=TBankPaymentRequestError("bad payload"))
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
service = AggregatorService(
|
||||
@@ -268,30 +188,9 @@ def test_init_payment_maps_provider_request_errors_to_invalid_payment_error() ->
|
||||
with pytest.raises(InvalidInitPaymentRequestError):
|
||||
asyncio.run(service.init_payment(request))
|
||||
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == [("order-uuid-1", 125000)]
|
||||
|
||||
|
||||
def test_init_payment_delegates_request_with_services_none_without_new_logic() -> None:
|
||||
request = _make_init_payment_request(services=None)
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/2")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
)
|
||||
|
||||
result = asyncio.run(service.init_payment(request))
|
||||
|
||||
assert result == InitPaymentResponse(payment_url="https://pay.test/payment/2")
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == [("order-uuid-1", 125000)]
|
||||
assert request.services is None
|
||||
|
||||
|
||||
def test_init_payment_maps_client_failures_to_unavailable_error() -> None:
|
||||
request = _make_init_payment_request()
|
||||
request = make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(error=TBankPaymentAdapterError("transport down"))
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
service = AggregatorService(
|
||||
@@ -303,19 +202,18 @@ def test_init_payment_maps_client_failures_to_unavailable_error() -> None:
|
||||
with pytest.raises(InitPaymentUnavailableError):
|
||||
asyncio.run(service.init_payment(request))
|
||||
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == [("order-uuid-1", 125000)]
|
||||
|
||||
|
||||
def test_init_payment_without_configured_adapter_raises_unavailable_error() -> None:
|
||||
service = AggregatorService(providers=[])
|
||||
|
||||
with pytest.raises(InitPaymentUnavailableError):
|
||||
asyncio.run(service.init_payment(_make_init_payment_request()))
|
||||
asyncio.run(service.init_payment(make_init_payment_request()))
|
||||
|
||||
|
||||
def test_init_payment_rejects_cdek_price_mismatch_without_tbank_or_repository_calls() -> None:
|
||||
request = _make_init_payment_request(price=124999)
|
||||
payload = make_init_payment_payload()
|
||||
payload["systemData"]["tariff"]["price"] = 124999
|
||||
request = InitPaymentRequest.model_validate(payload)
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
order_repository = StubOrderRepository()
|
||||
@@ -329,13 +227,12 @@ def test_init_payment_rejects_cdek_price_mismatch_without_tbank_or_repository_ca
|
||||
with pytest.raises(InvalidInitPaymentRequestError):
|
||||
asyncio.run(service.init_payment(request))
|
||||
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == []
|
||||
assert order_repository.calls == []
|
||||
|
||||
|
||||
def test_init_payment_rejects_when_cdek_does_not_return_requested_tariff() -> None:
|
||||
request = _make_init_payment_request()
|
||||
request = make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=None)
|
||||
service = AggregatorService(
|
||||
@@ -347,12 +244,11 @@ def test_init_payment_rejects_when_cdek_does_not_return_requested_tariff() -> No
|
||||
with pytest.raises(InvalidInitPaymentRequestError):
|
||||
asyncio.run(service.init_payment(request))
|
||||
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == []
|
||||
|
||||
|
||||
def test_init_payment_maps_cdek_request_error_to_invalid_payment_error() -> None:
|
||||
request = _make_init_payment_request()
|
||||
request = make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(
|
||||
error=ProviderRequestError("bad CDEK payload")
|
||||
@@ -366,12 +262,9 @@ def test_init_payment_maps_cdek_request_error_to_invalid_payment_error() -> None
|
||||
with pytest.raises(InvalidInitPaymentRequestError):
|
||||
asyncio.run(service.init_payment(request))
|
||||
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == []
|
||||
|
||||
|
||||
def test_init_payment_maps_cdek_client_error_to_unavailable_error() -> None:
|
||||
request = _make_init_payment_request()
|
||||
request = make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(
|
||||
error=ProviderClientError("CDEK unavailable")
|
||||
@@ -384,6 +277,3 @@ def test_init_payment_maps_cdek_client_error_to_unavailable_error() -> None:
|
||||
|
||||
with pytest.raises(InitPaymentUnavailableError):
|
||||
asyncio.run(service.init_payment(request))
|
||||
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == []
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from app.adapters.delivery_providers.cdek.order_mapper import (
|
||||
CDEKOrderRegistrationResult,
|
||||
)
|
||||
from app.adapters.tbank.base import TBankPaymentNotificationTokenError
|
||||
from app.schemas.payment import InitPaymentRequest, TBankPaymentNotification
|
||||
from app.services.aggregator import (
|
||||
@@ -11,6 +14,11 @@ from app.services.aggregator import (
|
||||
InvalidTBankPaymentNotificationError,
|
||||
TBankPaymentNotificationProcessingError,
|
||||
)
|
||||
from tests.payment_fixtures import make_init_payment_payload
|
||||
|
||||
|
||||
def _default_payload() -> dict[str, Any]:
|
||||
return make_init_payment_payload()
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -18,57 +26,14 @@ class StoredOrder:
|
||||
order_uuid: str = "order-uuid-1"
|
||||
payment_url: str = "https://pay.test/payment/1"
|
||||
price: int = 125000
|
||||
delivery_type: int = 2
|
||||
tariff_code: int = 535
|
||||
comment: str | None = "Test payment"
|
||||
sender: dict[str, Any] | None = None
|
||||
recipient: dict[str, Any] | None = None
|
||||
from_location: dict[str, Any] | None = None
|
||||
to_location: dict[str, Any] | None = None
|
||||
packages: list[dict[str, Any]] | None = None
|
||||
services: list[dict[str, Any]] | None = None
|
||||
account_email: str = "client@example.com"
|
||||
payload: dict[str, Any] = field(default_factory=_default_payload)
|
||||
payment_status: str | None = None
|
||||
tbank_payment_id: int | None = None
|
||||
cdek_order_uuid: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.sender is None:
|
||||
self.sender = {
|
||||
"name": "Petr Petrov",
|
||||
"email": "sender@example.com",
|
||||
"phone": {"number": "+79009876543"},
|
||||
}
|
||||
if self.recipient is None:
|
||||
self.recipient = {
|
||||
"name": "Ivan Ivanov",
|
||||
"email": "ivan@example.com",
|
||||
"phone": {"number": "+79001234567"},
|
||||
}
|
||||
if self.from_location is None:
|
||||
self.from_location = {
|
||||
"address": "Lenina 1",
|
||||
"city": "Moscow",
|
||||
"country_code": "RU",
|
||||
}
|
||||
if self.to_location is None:
|
||||
self.to_location = {
|
||||
"address": "Pushkina 10",
|
||||
"city": "Novosibirsk",
|
||||
"country_code": "RU",
|
||||
}
|
||||
if self.packages is None:
|
||||
self.packages = [
|
||||
{
|
||||
"number": "1",
|
||||
"weight": 1,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"comment": "Package 1",
|
||||
}
|
||||
]
|
||||
if self.services is None:
|
||||
self.services = [{"code": "INSURANCE", "parameter": "1000"}]
|
||||
cdek_waybill_uuid: str | None = None
|
||||
cdek_waybill_url: str | None = None
|
||||
|
||||
|
||||
class StubPaymentAdapter:
|
||||
@@ -148,9 +113,20 @@ class StubOrderRepository:
|
||||
session: object,
|
||||
order_uuid: str,
|
||||
cdek_order_uuid: str,
|
||||
cdek_waybill_uuid: str | None = None,
|
||||
cdek_waybill_url: str | None = None,
|
||||
) -> StoredOrder | None:
|
||||
self.calls.append(
|
||||
("mark_cdek_order_registered", (session, order_uuid, cdek_order_uuid))
|
||||
(
|
||||
"mark_cdek_order_registered",
|
||||
(
|
||||
session,
|
||||
order_uuid,
|
||||
cdek_order_uuid,
|
||||
cdek_waybill_uuid,
|
||||
cdek_waybill_url,
|
||||
),
|
||||
)
|
||||
)
|
||||
if self._mark_cdek_errors:
|
||||
error = self._mark_cdek_errors.pop(0)
|
||||
@@ -161,6 +137,8 @@ class StubOrderRepository:
|
||||
if order is None:
|
||||
return None
|
||||
order.cdek_order_uuid = cdek_order_uuid
|
||||
order.cdek_waybill_uuid = cdek_waybill_uuid
|
||||
order.cdek_waybill_url = cdek_waybill_url
|
||||
return order
|
||||
|
||||
|
||||
@@ -168,14 +146,22 @@ class StubCDEKOrderAdapter:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
responses: list[str] | None = None,
|
||||
responses: list[CDEKOrderRegistrationResult] | None = None,
|
||||
error: Exception | None = None,
|
||||
) -> None:
|
||||
self._responses = responses or ["cdek-order-uuid-1"]
|
||||
self._responses = responses or [
|
||||
CDEKOrderRegistrationResult(
|
||||
order_uuid="cdek-order-uuid-1",
|
||||
waybill_uuid=None,
|
||||
waybill_url=None,
|
||||
)
|
||||
]
|
||||
self._error = error
|
||||
self.calls: list[InitPaymentRequest] = []
|
||||
|
||||
async def register_order(self, request: InitPaymentRequest) -> str:
|
||||
async def register_order(
|
||||
self, request: InitPaymentRequest
|
||||
) -> CDEKOrderRegistrationResult:
|
||||
self.calls.append(request)
|
||||
if self._error is not None:
|
||||
raise self._error
|
||||
@@ -201,7 +187,15 @@ def test_confirmed_notification_registers_cdek_order_and_saves_uuid() -> None:
|
||||
order = StoredOrder()
|
||||
payment_adapter = StubPaymentAdapter()
|
||||
order_repository = StubOrderRepository(orders=[order])
|
||||
cdek_adapter = StubCDEKOrderAdapter(responses=["cdek-order-uuid-1"])
|
||||
cdek_adapter = StubCDEKOrderAdapter(
|
||||
responses=[
|
||||
CDEKOrderRegistrationResult(
|
||||
order_uuid="cdek-order-uuid-1",
|
||||
waybill_uuid="waybill-uuid-1",
|
||||
waybill_url="https://cdek.test/waybill/1.pdf",
|
||||
)
|
||||
]
|
||||
)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=payment_adapter,
|
||||
@@ -217,6 +211,8 @@ def test_confirmed_notification_registers_cdek_order_and_saves_uuid() -> None:
|
||||
assert order.payment_status == "CONFIRMED"
|
||||
assert order.tbank_payment_id == 8347568144
|
||||
assert order.cdek_order_uuid == "cdek-order-uuid-1"
|
||||
assert order.cdek_waybill_uuid == "waybill-uuid-1"
|
||||
assert order.cdek_waybill_url == "https://cdek.test/waybill/1.pdf"
|
||||
assert len(cdek_adapter.calls) == 1
|
||||
assert cdek_adapter.calls[0].order_uuid == "order-uuid-1"
|
||||
|
||||
@@ -317,7 +313,18 @@ def test_repeated_confirmed_after_cdek_uuid_save_failure_uses_same_external_id()
|
||||
mark_cdek_errors=[RuntimeError("db down"), None],
|
||||
)
|
||||
cdek_adapter = StubCDEKOrderAdapter(
|
||||
responses=["same-cdek-order-uuid", "same-cdek-order-uuid"]
|
||||
responses=[
|
||||
CDEKOrderRegistrationResult(
|
||||
order_uuid="same-cdek-order-uuid",
|
||||
waybill_uuid=None,
|
||||
waybill_url=None,
|
||||
),
|
||||
CDEKOrderRegistrationResult(
|
||||
order_uuid="same-cdek-order-uuid",
|
||||
waybill_uuid=None,
|
||||
waybill_url=None,
|
||||
),
|
||||
]
|
||||
)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
|
||||
Reference in New Issue
Block a user