Добавлена валидация цены
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import asyncio
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from app.adapters.delivery_providers.base import ProviderClientError, ProviderRequestError
|
||||
from app.adapters.tbank.base import (
|
||||
TBankPaymentAdapterError,
|
||||
TBankPaymentRequestError,
|
||||
)
|
||||
from app.repositories.order import OrderData
|
||||
from app.schemas.payment import InitPaymentRequest, InitPaymentResponse
|
||||
from app.schemas.response import DeliveryPrice
|
||||
from app.services.aggregator import (
|
||||
AggregatorService,
|
||||
InitPaymentUnavailableError,
|
||||
@@ -21,13 +24,17 @@ class StubPaymentAdapter:
|
||||
*,
|
||||
response: str | None = None,
|
||||
error: Exception | None = None,
|
||||
events: list[str] | None = None,
|
||||
) -> None:
|
||||
self._response = response
|
||||
self._error = error
|
||||
self._events = events
|
||||
self.calls: list[tuple[str, int]] = []
|
||||
|
||||
async def create_payment_link(self, order_uuid: str, amount_kopecks: int) -> str:
|
||||
self.calls.append((order_uuid, amount_kopecks))
|
||||
if self._events is not None:
|
||||
self._events.append("tbank")
|
||||
if self._error is not None:
|
||||
raise self._error
|
||||
if self._response is None:
|
||||
@@ -35,6 +42,31 @@ class StubPaymentAdapter:
|
||||
return self._response
|
||||
|
||||
|
||||
class StubPaymentPriceValidationAdapter:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
response: DeliveryPrice | None = None,
|
||||
error: Exception | None = None,
|
||||
events: list[str] | None = None,
|
||||
) -> None:
|
||||
self._response = response
|
||||
self._error = error
|
||||
self._events = events
|
||||
self.calls: list[InitPaymentRequest] = []
|
||||
|
||||
async def get_payment_price(
|
||||
self,
|
||||
request: InitPaymentRequest,
|
||||
) -> DeliveryPrice | None:
|
||||
self.calls.append(request)
|
||||
if self._events is not None:
|
||||
self._events.append("cdek")
|
||||
if self._error is not None:
|
||||
raise self._error
|
||||
return self._response
|
||||
|
||||
|
||||
class StubOrderSessionContext:
|
||||
def __init__(self, session: object) -> None:
|
||||
self._session = session
|
||||
@@ -105,30 +137,57 @@ def _make_init_payment_request(**overrides: object) -> InitPaymentRequest:
|
||||
return InitPaymentRequest(**payload)
|
||||
|
||||
|
||||
def test_init_payment_delegates_to_adapter_and_returns_payment_url() -> None:
|
||||
def _make_cdek_price(**overrides: object) -> DeliveryPrice:
|
||||
payload = {
|
||||
"provider": "cdek",
|
||||
"service_name": "CDEK tariff",
|
||||
"price": Decimal("1250.00"),
|
||||
"currency": "RUB",
|
||||
"delivery_days_min": 1,
|
||||
"delivery_days_max": 2,
|
||||
}
|
||||
payload.update(overrides)
|
||||
return DeliveryPrice.model_construct(**payload)
|
||||
|
||||
|
||||
def test_init_payment_validates_cdek_price_before_tbank_and_returns_payment_url() -> None:
|
||||
request = _make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
service = AggregatorService(providers=[], payment_adapter=adapter)
|
||||
events: list[str] = []
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(
|
||||
response=_make_cdek_price(),
|
||||
events=events,
|
||||
)
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1", events=events)
|
||||
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/1")
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == [("order-uuid-1", 125000)]
|
||||
assert events == ["cdek", "tbank"]
|
||||
|
||||
|
||||
def test_init_payment_persists_order_after_successful_payment_link() -> None:
|
||||
request = _make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
order_repository = StubOrderRepository()
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
order_repository=order_repository,
|
||||
)
|
||||
|
||||
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 order_repository.calls == [
|
||||
(
|
||||
@@ -179,16 +238,19 @@ def test_init_payment_persists_order_after_successful_payment_link() -> None:
|
||||
def test_init_payment_returns_payment_url_when_order_persistence_fails() -> None:
|
||||
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"))
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
order_repository=order_repository,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -196,22 +258,34 @@ def test_init_payment_returns_payment_url_when_order_persistence_fails() -> None
|
||||
def test_init_payment_maps_provider_request_errors_to_invalid_payment_error() -> None:
|
||||
request = _make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(error=TBankPaymentRequestError("bad payload"))
|
||||
service = AggregatorService(providers=[], payment_adapter=adapter)
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
)
|
||||
|
||||
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")
|
||||
service = AggregatorService(providers=[], payment_adapter=adapter)
|
||||
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
|
||||
|
||||
@@ -219,11 +293,17 @@ def test_init_payment_delegates_request_with_services_none_without_new_logic() -
|
||||
def test_init_payment_maps_client_failures_to_unavailable_error() -> None:
|
||||
request = _make_init_payment_request()
|
||||
adapter = StubPaymentAdapter(error=TBankPaymentAdapterError("transport down"))
|
||||
service = AggregatorService(providers=[], payment_adapter=adapter)
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
)
|
||||
|
||||
with pytest.raises(InitPaymentUnavailableError):
|
||||
asyncio.run(service.init_payment(request))
|
||||
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == [("order-uuid-1", 125000)]
|
||||
|
||||
|
||||
@@ -232,3 +312,78 @@ def test_init_payment_without_configured_adapter_raises_unavailable_error() -> N
|
||||
|
||||
with pytest.raises(InitPaymentUnavailableError):
|
||||
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)
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=_make_cdek_price())
|
||||
order_repository = StubOrderRepository()
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
order_repository=order_repository,
|
||||
)
|
||||
|
||||
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()
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(response=None)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
)
|
||||
|
||||
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()
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(
|
||||
error=ProviderRequestError("bad CDEK payload")
|
||||
)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
)
|
||||
|
||||
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()
|
||||
adapter = StubPaymentAdapter(response="https://pay.test/payment/1")
|
||||
validation_adapter = StubPaymentPriceValidationAdapter(
|
||||
error=ProviderClientError("CDEK unavailable")
|
||||
)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
)
|
||||
|
||||
with pytest.raises(InitPaymentUnavailableError):
|
||||
asyncio.run(service.init_payment(request))
|
||||
|
||||
assert validation_adapter.calls == [request]
|
||||
assert adapter.calls == []
|
||||
|
||||
Reference in New Issue
Block a user