Добавлен провайдер доставки CSE: SOAP-адаптер (Calc + SaveDocuments), маршрутизация init-payment по провайдеру, обобщение tariff_code до строки, география CSE в cities_map
Deploy / deploy (push) Failing after 52s
Deploy / deploy (push) Failing after 52s
This commit is contained in:
@@ -34,7 +34,7 @@ def test_map_cdek_response_maps_all_tariffs_to_unified_model() -> None:
|
||||
assert [price.currency for price in result] == ["RUB", "USD"]
|
||||
assert [price.delivery_days_min for price in result] == [2, 5]
|
||||
assert [price.delivery_days_max for price in result] == [4, 7]
|
||||
assert [price.tariff_code for price in result] == [7, 136]
|
||||
assert [price.tariff_code for price in result] == ["7", "136"]
|
||||
|
||||
|
||||
def test_map_cdek_response_returns_none_tariff_code_when_missing() -> None:
|
||||
|
||||
@@ -129,7 +129,7 @@ def test_order_payload_for_doc_omits_dimensions() -> None:
|
||||
"price": 50000,
|
||||
"deliveryDaysMin": 1,
|
||||
"deliveryDaysMax": 2,
|
||||
"tariffCode": 535,
|
||||
"tariffCode": "535",
|
||||
},
|
||||
"parcelType": "doc",
|
||||
"docPackaging": "envelope",
|
||||
|
||||
@@ -132,7 +132,7 @@ def test_provider_get_payment_price_omits_dimensions_for_doc() -> None:
|
||||
"price": 50000,
|
||||
"deliveryDaysMin": 1,
|
||||
"deliveryDaysMax": 2,
|
||||
"tariffCode": 535,
|
||||
"tariffCode": "535",
|
||||
},
|
||||
"parcelType": "doc",
|
||||
"docPackaging": "envelope",
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import asyncio
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.adapters.delivery_providers.cse import (
|
||||
CSEClient,
|
||||
CSEProvider,
|
||||
CSERequestError,
|
||||
)
|
||||
from app.adapters.delivery_providers.cse import order_mapper
|
||||
from app.adapters.delivery_providers.cse.order_mapper import (
|
||||
CSEOrderRegistrationParams,
|
||||
)
|
||||
from app.adapters.delivery_providers.cse.soap import (
|
||||
build_envelope,
|
||||
parse_response,
|
||||
)
|
||||
from app.schemas.request import DeliveryCalculationRequest, DeliveryEntity
|
||||
from tests.payment_fixtures import make_init_payment_request
|
||||
|
||||
_CALC_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
|
||||
<soap:Body>
|
||||
<m:CalcResponse xmlns:m="http://www.cargo3.ru">
|
||||
<m:return>
|
||||
<m:Key>Calc</m:Key>
|
||||
<m:List>
|
||||
<m:Key>Destination</m:Key>
|
||||
<m:List>
|
||||
<m:Key>Tariff</m:Key>
|
||||
<m:Value>tariff-guid-1</m:Value>
|
||||
<m:Fields><m:Key>Total</m:Key><m:Value>1114.92</m:Value></m:Fields>
|
||||
<m:Fields><m:Key>CurrencyName</m:Key><m:Value>RUR</m:Value></m:Fields>
|
||||
<m:Fields><m:Key>Service</m:Key><m:Value>Россия доставка</m:Value></m:Fields>
|
||||
<m:Fields><m:Key>MinPeriod</m:Key><m:Value>3</m:Value></m:Fields>
|
||||
<m:Fields><m:Key>MaxPeriod</m:Key><m:Value>5</m:Value></m:Fields>
|
||||
</m:List>
|
||||
<m:List>
|
||||
<m:Key>Tariff</m:Key>
|
||||
<m:Value>tariff-guid-2</m:Value>
|
||||
<m:Fields><m:Key>Total</m:Key><m:Value>2000.00</m:Value></m:Fields>
|
||||
<m:Fields><m:Key>CurrencyName</m:Key><m:Value>RUR</m:Value></m:Fields>
|
||||
<m:Fields><m:Key>Service</m:Key><m:Value>Экспресс</m:Value></m:Fields>
|
||||
<m:Fields><m:Key>MinPeriod</m:Key><m:Value>1</m:Value></m:Fields>
|
||||
<m:Fields><m:Key>MaxPeriod</m:Key><m:Value>2</m:Value></m:Fields>
|
||||
</m:List>
|
||||
</m:List>
|
||||
</m:return>
|
||||
</m:CalcResponse>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"""
|
||||
|
||||
_SAVE_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
|
||||
<soap:Body>
|
||||
<m:SaveDocumentsResponse xmlns:m="http://www.cargo3.ru">
|
||||
<m:return>
|
||||
<m:Key>SaveDocuments</m:Key>
|
||||
<m:List>
|
||||
<m:Key>Order</m:Key>
|
||||
<m:Properties><m:Key>Number</m:Key><m:Value>CSE-000123</m:Value></m:Properties>
|
||||
</m:List>
|
||||
</m:return>
|
||||
</m:SaveDocumentsResponse>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"""
|
||||
|
||||
_ERROR_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
|
||||
<soap:Body>
|
||||
<m:CalcResponse xmlns:m="http://www.cargo3.ru">
|
||||
<m:return>
|
||||
<m:Key>Calc</m:Key>
|
||||
<m:Properties>
|
||||
<m:Key>Error</m:Key>
|
||||
<m:List><m:Key>Description</m:Key><m:Value>1001</m:Value></m:List>
|
||||
</m:Properties>
|
||||
</m:return>
|
||||
</m:CalcResponse>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"""
|
||||
|
||||
|
||||
class SequenceHTTPClient:
|
||||
def __init__(self, results: list[Any]) -> None:
|
||||
self._results = results
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
|
||||
async def post(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
content: bytes | None = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> httpx.Response:
|
||||
self.calls.append({"url": url, "content": content, "headers": headers})
|
||||
result = self._results[len(self.calls) - 1]
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
return result
|
||||
|
||||
|
||||
def _params() -> CSEOrderRegistrationParams:
|
||||
return CSEOrderRegistrationParams(
|
||||
payer="payer-1",
|
||||
payment_method="pm-1",
|
||||
shipping_method="sm-1",
|
||||
urgency="urgency-1",
|
||||
)
|
||||
|
||||
|
||||
def _build_client(results: list[Any]) -> tuple[CSEClient, SequenceHTTPClient]:
|
||||
http_client = SequenceHTTPClient(results)
|
||||
client = CSEClient(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
base_url="http://lk-test.cse.ru/1c/ws/web1c.1cws",
|
||||
login="test",
|
||||
password="2016",
|
||||
registration_params=_params(),
|
||||
retry_attempts=0,
|
||||
)
|
||||
return client, http_client
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_references(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
order_mapper, "resolve_cse_geography", lambda city_id: f"geo-{city_id}"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
order_mapper, "resolve_type_of_cargo", lambda parcel_type: "cargo-guid"
|
||||
)
|
||||
|
||||
|
||||
def _calc_request() -> DeliveryCalculationRequest:
|
||||
return DeliveryCalculationRequest(
|
||||
entity=DeliveryEntity.INDIVIDUAL,
|
||||
from_city=1,
|
||||
to_city=2,
|
||||
weight_kg=1.0,
|
||||
length_cm=10.0,
|
||||
width_cm=10.0,
|
||||
height_cm=10.0,
|
||||
)
|
||||
|
||||
|
||||
def test_build_envelope_contains_credentials_and_payload() -> None:
|
||||
body = order_mapper.build_calc_body_for_calculation(_calc_request())
|
||||
envelope = build_envelope("Calc", login="test", password="2016", body=body)
|
||||
text = envelope.decode("utf-8")
|
||||
|
||||
assert "Calc" in text
|
||||
assert "<m:login>test</m:login>" in text or ">test<" in text
|
||||
assert "geo-1" in text
|
||||
assert "geo-2" in text
|
||||
|
||||
|
||||
def test_parse_calc_response_round_trip() -> None:
|
||||
root = parse_response(_CALC_RESPONSE, "Calc")
|
||||
assert root.key == "Calc"
|
||||
assert root.items[0].items[0].field_value("Total") == "1114.92"
|
||||
|
||||
|
||||
def test_provider_get_prices_maps_all_tariffs() -> None:
|
||||
response = httpx.Response(200, text=_CALC_RESPONSE)
|
||||
client, _ = _build_client([response])
|
||||
provider = CSEProvider(client)
|
||||
|
||||
prices = asyncio.run(provider.get_prices(_calc_request()))
|
||||
|
||||
assert [price.provider for price in prices] == ["cse", "cse"]
|
||||
assert [price.tariff_code for price in prices] == ["tariff-guid-1", "tariff-guid-2"]
|
||||
assert prices[0].price == Decimal("1114.92")
|
||||
assert prices[0].currency == "RUB"
|
||||
assert prices[0].delivery_days_min == 3
|
||||
assert prices[0].delivery_days_max == 5
|
||||
assert prices[0].service_name == "Россия доставка"
|
||||
|
||||
|
||||
def test_provider_get_payment_price_selects_matching_tariff() -> None:
|
||||
response = httpx.Response(200, text=_CALC_RESPONSE)
|
||||
client, _ = _build_client([response])
|
||||
provider = CSEProvider(client)
|
||||
request = make_init_payment_request(
|
||||
systemData={
|
||||
"tariff": {
|
||||
"provider": "cse",
|
||||
"serviceName": "Экспресс",
|
||||
"price": 200000,
|
||||
"deliveryDaysMin": 1,
|
||||
"deliveryDaysMax": 2,
|
||||
"tariffCode": "tariff-guid-2",
|
||||
},
|
||||
"parcelType": "parcel",
|
||||
"weight": "1.0",
|
||||
"dimensions": {"length": "10", "width": "10", "height": "10"},
|
||||
}
|
||||
)
|
||||
|
||||
price = asyncio.run(provider.get_payment_price(request))
|
||||
|
||||
assert price is not None
|
||||
assert price.tariff_code == "tariff-guid-2"
|
||||
assert price.price == Decimal("2000.00")
|
||||
|
||||
|
||||
def test_provider_register_order_returns_document_number() -> None:
|
||||
response = httpx.Response(200, text=_SAVE_RESPONSE)
|
||||
client, http_client = _build_client([response])
|
||||
provider = CSEProvider(client)
|
||||
|
||||
result = asyncio.run(
|
||||
provider.register_order(make_init_payment_request(), "order-uuid-1")
|
||||
)
|
||||
|
||||
assert result.order_number == "CSE-000123"
|
||||
assert http_client.calls[0]["url"] == "http://lk-test.cse.ru/1c/ws/web1c.1cws"
|
||||
|
||||
|
||||
def test_application_error_in_response_raises_request_error() -> None:
|
||||
response = httpx.Response(200, text=_ERROR_RESPONSE)
|
||||
client, _ = _build_client([response])
|
||||
provider = CSEProvider(client)
|
||||
|
||||
with pytest.raises(CSERequestError):
|
||||
asyncio.run(provider.get_prices(_calc_request()))
|
||||
@@ -63,7 +63,7 @@ def test_post_init_payment_rejects_invalid_tariff_code() -> None:
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = make_init_payment_payload()
|
||||
invalid_payload["systemData"]["tariff"]["tariffCode"] = "not-a-number"
|
||||
invalid_payload["systemData"]["tariff"]["tariffCode"] = ""
|
||||
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
|
||||
@@ -57,12 +57,12 @@ def make_init_payment_payload(**overrides: Any) -> dict[str, Any]:
|
||||
"agreeTerms": True,
|
||||
"systemData": {
|
||||
"tariff": {
|
||||
"provider": "СДЭК",
|
||||
"provider": "cdek",
|
||||
"serviceName": "Экспресс лайт",
|
||||
"price": 125000,
|
||||
"deliveryDaysMin": 1,
|
||||
"deliveryDaysMax": 2,
|
||||
"tariffCode": 535,
|
||||
"tariffCode": "535",
|
||||
},
|
||||
"parcelType": "parcel",
|
||||
"docPackaging": None,
|
||||
|
||||
@@ -19,7 +19,8 @@ def _make_order_data(**overrides: object) -> OrderData:
|
||||
"order_uuid": "order-uuid-1",
|
||||
"payment_url": "https://pay.test/payment/1",
|
||||
"price": 125000,
|
||||
"tariff_code": 535,
|
||||
"tariff_code": "535",
|
||||
"provider": "cdek",
|
||||
"account_email": "client@example.com",
|
||||
"payload": make_init_payment_payload(),
|
||||
}
|
||||
@@ -63,7 +64,7 @@ def test_create_order_persists_all_required_fields() -> None:
|
||||
assert persisted_order.order_uuid == "order-uuid-1"
|
||||
assert persisted_order.payment_url == "https://pay.test/payment/1"
|
||||
assert persisted_order.price == 125000
|
||||
assert persisted_order.tariff_code == 535
|
||||
assert persisted_order.tariff_code == "535"
|
||||
assert persisted_order.account_email == "client@example.com"
|
||||
assert persisted_order.payload == order_data.payload
|
||||
assert persisted_order.payment_status is None
|
||||
|
||||
@@ -119,7 +119,7 @@ def test_init_payment_validates_cdek_price_before_tbank_and_returns_payment_url(
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
payment_price_validation_adapters={"cdek": validation_adapter},
|
||||
order_uuid_factory=lambda: "order-uuid-1",
|
||||
)
|
||||
|
||||
@@ -140,7 +140,7 @@ def test_init_payment_persists_order_payload_after_successful_payment_link() ->
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
payment_price_validation_adapters={"cdek": validation_adapter},
|
||||
order_repository=order_repository,
|
||||
order_uuid_factory=lambda: "order-uuid-1",
|
||||
)
|
||||
@@ -153,7 +153,7 @@ def test_init_payment_persists_order_payload_after_successful_payment_link() ->
|
||||
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.tariff_code == "535"
|
||||
assert order_data.account_email == "client@example.com"
|
||||
assert order_data.payload == request.model_dump(mode="json", by_alias=True)
|
||||
|
||||
@@ -166,7 +166,7 @@ def test_init_payment_returns_payment_url_when_order_persistence_fails() -> None
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
payment_price_validation_adapters={"cdek": validation_adapter},
|
||||
order_repository=order_repository,
|
||||
order_uuid_factory=lambda: "order-uuid-1",
|
||||
)
|
||||
@@ -185,7 +185,7 @@ def test_init_payment_maps_provider_request_errors_to_invalid_payment_error() ->
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
payment_price_validation_adapters={"cdek": validation_adapter},
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidInitPaymentRequestError):
|
||||
@@ -199,7 +199,7 @@ def test_init_payment_maps_client_failures_to_unavailable_error() -> None:
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
payment_price_validation_adapters={"cdek": validation_adapter},
|
||||
)
|
||||
|
||||
with pytest.raises(InitPaymentUnavailableError):
|
||||
@@ -223,7 +223,7 @@ def test_init_payment_rejects_cdek_price_mismatch_without_tbank_or_repository_ca
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
payment_price_validation_adapters={"cdek": validation_adapter},
|
||||
order_repository=order_repository,
|
||||
)
|
||||
|
||||
@@ -241,7 +241,7 @@ def test_init_payment_rejects_when_cdek_does_not_return_requested_tariff() -> No
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
payment_price_validation_adapters={"cdek": validation_adapter},
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidInitPaymentRequestError):
|
||||
@@ -259,7 +259,7 @@ def test_init_payment_maps_cdek_request_error_to_invalid_payment_error() -> None
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
payment_price_validation_adapters={"cdek": validation_adapter},
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidInitPaymentRequestError):
|
||||
@@ -275,7 +275,7 @@ def test_init_payment_maps_cdek_client_error_to_unavailable_error() -> None:
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=adapter,
|
||||
payment_price_validation_adapter=validation_adapter,
|
||||
payment_price_validation_adapters={"cdek": validation_adapter},
|
||||
)
|
||||
|
||||
with pytest.raises(InitPaymentUnavailableError):
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from app.schemas.payment import InitPaymentRequest, TBankPaymentNotification
|
||||
from app.schemas.response import DeliveryPrice
|
||||
from app.services.aggregator import AggregatorService
|
||||
from tests.payment_fixtures import make_init_payment_payload, make_init_payment_request
|
||||
|
||||
|
||||
class StubPaymentAdapter:
|
||||
async def create_payment_link(self, order_uuid: str, amount_kopecks: int) -> str:
|
||||
return "https://pay.test/payment/1"
|
||||
|
||||
def verify_payment_notification(self, notification: TBankPaymentNotification) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class StubValidationAdapter:
|
||||
def __init__(self, response: DeliveryPrice | None) -> None:
|
||||
self._response = response
|
||||
self.calls: list[InitPaymentRequest] = []
|
||||
|
||||
async def get_payment_price(
|
||||
self, request: InitPaymentRequest
|
||||
) -> DeliveryPrice | None:
|
||||
self.calls.append(request)
|
||||
return self._response
|
||||
|
||||
|
||||
@dataclass
|
||||
class CSEResult:
|
||||
order_number: str = "CSE-000123"
|
||||
|
||||
|
||||
class StubCSERegistrationAdapter:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[InitPaymentRequest, str]] = []
|
||||
|
||||
async def register_order(
|
||||
self, request: InitPaymentRequest, order_uuid: str
|
||||
) -> CSEResult:
|
||||
self.calls.append((request, order_uuid))
|
||||
return CSEResult()
|
||||
|
||||
|
||||
class StubCDEKRegistrationAdapter:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[InitPaymentRequest, str]] = []
|
||||
|
||||
async def register_order(
|
||||
self, request: InitPaymentRequest, order_uuid: str
|
||||
) -> object:
|
||||
self.calls.append((request, order_uuid))
|
||||
raise AssertionError("CDEK adapter must not be called for a CSE order.")
|
||||
|
||||
|
||||
def _cse_price() -> DeliveryPrice:
|
||||
return DeliveryPrice.model_construct(
|
||||
provider="cse",
|
||||
service_name="CSE tariff",
|
||||
price=Decimal("1250.00"),
|
||||
currency="RUB",
|
||||
delivery_days_min=1,
|
||||
delivery_days_max=2,
|
||||
tariff_code="cse-guid",
|
||||
)
|
||||
|
||||
|
||||
def _cse_request() -> InitPaymentRequest:
|
||||
return make_init_payment_request(
|
||||
systemData={
|
||||
"tariff": {
|
||||
"provider": "cse",
|
||||
"serviceName": "CSE tariff",
|
||||
"price": 125000,
|
||||
"deliveryDaysMin": 1,
|
||||
"deliveryDaysMax": 2,
|
||||
"tariffCode": "cse-guid",
|
||||
},
|
||||
"parcelType": "parcel",
|
||||
"weight": "1.0",
|
||||
"dimensions": {"length": "10", "width": "10", "height": "10"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_init_payment_routes_validation_to_tariff_provider() -> None:
|
||||
cse_validation = StubValidationAdapter(_cse_price())
|
||||
cdek_validation = StubValidationAdapter(None)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
payment_price_validation_adapters={
|
||||
"cdek": cdek_validation,
|
||||
"cse": cse_validation,
|
||||
},
|
||||
provider_price_multiplier=Decimal("1"),
|
||||
order_uuid_factory=lambda: "order-uuid-1",
|
||||
)
|
||||
|
||||
asyncio.run(service.init_payment(_cse_request()))
|
||||
|
||||
assert len(cse_validation.calls) == 1
|
||||
assert cdek_validation.calls == []
|
||||
|
||||
|
||||
@dataclass
|
||||
class StoredOrder:
|
||||
order_uuid: str = "order-uuid-1"
|
||||
provider: str = "cse"
|
||||
payload: dict[str, Any] = field(
|
||||
default_factory=lambda: _cse_request().model_dump(mode="json", by_alias=True)
|
||||
)
|
||||
payment_status: str | None = None
|
||||
tbank_payment_id: int | None = None
|
||||
cdek_order_uuid: str | None = None
|
||||
cse_order_number: str | None = None
|
||||
payment_email_sent_at: object | None = None
|
||||
account_email: str | None = None
|
||||
|
||||
|
||||
class StubSession:
|
||||
async def __aenter__(self) -> object:
|
||||
return object()
|
||||
|
||||
async def __aexit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class StubOrderRepository:
|
||||
def __init__(self, order: StoredOrder) -> None:
|
||||
self._order = order
|
||||
|
||||
def session(self) -> StubSession:
|
||||
return StubSession()
|
||||
|
||||
async def get_order_by_order_uuid(self, session: object, order_uuid: str) -> StoredOrder:
|
||||
return self._order
|
||||
|
||||
async def mark_payment_status(
|
||||
self, session: object, order_uuid: str, status: str, payment_id: int
|
||||
) -> StoredOrder:
|
||||
self._order.payment_status = status
|
||||
self._order.tbank_payment_id = payment_id
|
||||
return self._order
|
||||
|
||||
async def mark_cse_order_registered(
|
||||
self, session: object, order_uuid: str, cse_order_number: str
|
||||
) -> StoredOrder:
|
||||
self._order.cse_order_number = cse_order_number
|
||||
return self._order
|
||||
|
||||
async def mark_cdek_order_registered(
|
||||
self, session: object, order_uuid: str, cdek_order_uuid: str
|
||||
) -> StoredOrder:
|
||||
raise AssertionError("CDEK persistence must not be used for a CSE order.")
|
||||
|
||||
|
||||
def _notification() -> TBankPaymentNotification:
|
||||
return TBankPaymentNotification(
|
||||
TerminalKey="TestTerminal",
|
||||
OrderId="order-uuid-1",
|
||||
Success=True,
|
||||
Status="CONFIRMED",
|
||||
PaymentId=8347568144,
|
||||
ErrorCode="0",
|
||||
Amount=125000,
|
||||
Token="signed-token",
|
||||
)
|
||||
|
||||
|
||||
def test_notification_routes_registration_to_order_provider() -> None:
|
||||
order = StoredOrder()
|
||||
cse_registration = StubCSERegistrationAdapter()
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=StubOrderRepository(order),
|
||||
order_registration_adapters={
|
||||
"cdek": StubCDEKRegistrationAdapter(),
|
||||
"cse": cse_registration,
|
||||
},
|
||||
)
|
||||
|
||||
result = asyncio.run(service.handle_tbank_payment_notification(_notification()))
|
||||
|
||||
assert result == "OK"
|
||||
assert len(cse_registration.calls) == 1
|
||||
assert order.cse_order_number == "CSE-000123"
|
||||
@@ -26,12 +26,14 @@ class StoredOrder:
|
||||
order_uuid: str = "order-uuid-1"
|
||||
payment_url: str = "https://pay.test/payment/1"
|
||||
price: int = 125000
|
||||
tariff_code: int = 535
|
||||
tariff_code: str = "535"
|
||||
provider: str = "cdek"
|
||||
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
|
||||
cse_order_number: str | None = None
|
||||
cdek_waybill_uuid: str | None = None
|
||||
cdek_waybill_url: str | None = None
|
||||
payment_email_sent_at: object | None = None
|
||||
@@ -216,7 +218,7 @@ def test_confirmed_notification_registers_cdek_order_and_saves_uuid() -> None:
|
||||
providers=[],
|
||||
payment_adapter=payment_adapter,
|
||||
order_repository=order_repository,
|
||||
order_registration_adapter=cdek_adapter,
|
||||
order_registration_adapters={"cdek": cdek_adapter},
|
||||
)
|
||||
notification = _make_notification()
|
||||
|
||||
@@ -240,7 +242,7 @@ def test_duplicate_confirmed_notification_does_not_call_cdek() -> None:
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=StubOrderRepository(orders=[order]),
|
||||
order_registration_adapter=cdek_adapter,
|
||||
order_registration_adapters={"cdek": cdek_adapter},
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
@@ -259,7 +261,7 @@ def test_non_confirmed_notification_acknowledges_without_cdek() -> None:
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=StubOrderRepository(orders=[order]),
|
||||
order_registration_adapter=cdek_adapter,
|
||||
order_registration_adapters={"cdek": cdek_adapter},
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
@@ -282,7 +284,7 @@ def test_invalid_token_does_not_access_repository_or_cdek() -> None:
|
||||
verify_error=TBankPaymentNotificationTokenError("bad token")
|
||||
),
|
||||
order_repository=order_repository,
|
||||
order_registration_adapter=cdek_adapter,
|
||||
order_registration_adapters={"cdek": cdek_adapter},
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidTBankPaymentNotificationError):
|
||||
@@ -298,7 +300,7 @@ def test_missing_order_returns_processing_error_without_cdek() -> None:
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=StubOrderRepository(),
|
||||
order_registration_adapter=cdek_adapter,
|
||||
order_registration_adapters={"cdek": cdek_adapter},
|
||||
)
|
||||
|
||||
with pytest.raises(TBankPaymentNotificationProcessingError):
|
||||
@@ -313,7 +315,7 @@ def test_cdek_registration_failure_returns_processing_error() -> None:
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=StubOrderRepository(orders=[StoredOrder()]),
|
||||
order_registration_adapter=cdek_adapter,
|
||||
order_registration_adapters={"cdek": cdek_adapter},
|
||||
)
|
||||
|
||||
with pytest.raises(TBankPaymentNotificationProcessingError):
|
||||
@@ -346,7 +348,7 @@ def test_repeated_confirmed_after_cdek_uuid_save_failure_uses_same_external_id()
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=order_repository,
|
||||
order_registration_adapter=cdek_adapter,
|
||||
order_registration_adapters={"cdek": cdek_adapter},
|
||||
)
|
||||
notification = _make_notification()
|
||||
|
||||
@@ -372,7 +374,7 @@ def test_confirmed_notification_sends_payment_confirmation_email() -> None:
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=StubOrderRepository(orders=[order]),
|
||||
order_registration_adapter=StubCDEKOrderAdapter(),
|
||||
order_registration_adapters={"cdek": StubCDEKOrderAdapter()},
|
||||
email_sender=email_sender,
|
||||
)
|
||||
|
||||
@@ -394,7 +396,7 @@ def test_non_confirmed_notification_does_not_send_email() -> None:
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=StubOrderRepository(orders=[order]),
|
||||
order_registration_adapter=StubCDEKOrderAdapter(),
|
||||
order_registration_adapters={"cdek": StubCDEKOrderAdapter()},
|
||||
email_sender=email_sender,
|
||||
)
|
||||
|
||||
@@ -421,7 +423,7 @@ def test_duplicate_notification_does_not_resend_payment_email() -> None:
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=StubOrderRepository(orders=[order]),
|
||||
order_registration_adapter=StubCDEKOrderAdapter(),
|
||||
order_registration_adapters={"cdek": StubCDEKOrderAdapter()},
|
||||
email_sender=email_sender,
|
||||
)
|
||||
|
||||
@@ -440,7 +442,7 @@ def test_email_failure_does_not_break_notification_handling() -> None:
|
||||
providers=[],
|
||||
payment_adapter=StubPaymentAdapter(),
|
||||
order_repository=StubOrderRepository(orders=[order]),
|
||||
order_registration_adapter=StubCDEKOrderAdapter(),
|
||||
order_registration_adapters={"cdek": StubCDEKOrderAdapter()},
|
||||
email_sender=email_sender,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user