Добавлен заказ накладной
This commit is contained in:
@@ -10,7 +10,10 @@ from app.adapters.delivery_providers.cdek.client import (
|
||||
CDEKProvider,
|
||||
CDEKRequestError,
|
||||
)
|
||||
from app.schemas.payment import InitPaymentRequest
|
||||
from app.adapters.delivery_providers.cdek.order_mapper import (
|
||||
CDEKOrderRegistrationResult,
|
||||
)
|
||||
from tests.payment_fixtures import make_init_payment_request
|
||||
|
||||
|
||||
class StubAuthClient:
|
||||
@@ -51,179 +54,53 @@ class SequenceHTTPClient:
|
||||
return result
|
||||
|
||||
|
||||
def _make_order_request(**overrides: object) -> InitPaymentRequest:
|
||||
payload: dict[str, object] = {
|
||||
"order_uuid": "order-uuid-1",
|
||||
"price": 125000,
|
||||
"type": 2,
|
||||
"tariff_code": 535,
|
||||
"comment": "Test order",
|
||||
"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 test_provider_register_order_posts_cdek_contract_payload_and_maps_response() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={"entity": {"uuid": "cdek-order-uuid"}},
|
||||
request=httpx.Request("POST", "https://api.cdek.test/v2/orders"),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = CDEKProvider(
|
||||
def _make_provider(http_client: SequenceHTTPClient, **kwargs: Any) -> CDEKProvider:
|
||||
return CDEKProvider(
|
||||
CDEKClient(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
auth_client=StubAuthClient(), # type: ignore[arg-type]
|
||||
base_url="https://api.cdek.test/v2",
|
||||
timeout_seconds=7.5,
|
||||
retry_attempts=0,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
result = asyncio.run(provider.register_order(_make_order_request()))
|
||||
|
||||
assert result == "cdek-order-uuid"
|
||||
assert http_client.calls == [
|
||||
{
|
||||
"method": "POST",
|
||||
"url": "https://api.cdek.test/v2/orders",
|
||||
"json": {
|
||||
"number": "order-uuid-1",
|
||||
"type": 2,
|
||||
"tariff_code": 535,
|
||||
"comment": "Test order",
|
||||
"sender": {
|
||||
"name": "Petr Petrov",
|
||||
"email": "sender@example.com",
|
||||
"phones": [{"number": "+79009876543"}],
|
||||
},
|
||||
"recipient": {
|
||||
"name": "Ivan Ivanov",
|
||||
"email": "ivan@example.com",
|
||||
"phones": [{"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": 1000,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"comment": "Package 1",
|
||||
}
|
||||
],
|
||||
},
|
||||
"data": None,
|
||||
"headers": {"Authorization": "Bearer test-token"},
|
||||
"timeout": 7.5,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_provider_register_order_omits_services_when_none_and_converts_package_weights() -> None:
|
||||
def test_provider_register_order_posts_payload_and_maps_response() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={"entity": {"uuid": "cdek-order-uuid"}},
|
||||
json={
|
||||
"entity": {"uuid": "cdek-order-uuid"},
|
||||
"related_entities": [
|
||||
{
|
||||
"type": "waybill",
|
||||
"uuid": "waybill-uuid-1",
|
||||
"url": "https://cdek.test/waybill/1.pdf",
|
||||
}
|
||||
],
|
||||
},
|
||||
request=httpx.Request("POST", "https://api.cdek.test/v2/orders"),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = CDEKProvider(
|
||||
CDEKClient(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
auth_client=StubAuthClient(), # type: ignore[arg-type]
|
||||
base_url="https://api.cdek.test/v2",
|
||||
timeout_seconds=7.5,
|
||||
retry_attempts=0,
|
||||
)
|
||||
)
|
||||
request = _make_order_request(
|
||||
services=None,
|
||||
packages=[
|
||||
{
|
||||
"number": "1",
|
||||
"weight": 1,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"comment": "Package 1",
|
||||
},
|
||||
{
|
||||
"number": "2",
|
||||
"weight": 2,
|
||||
"length": 25,
|
||||
"width": 18,
|
||||
"height": 12,
|
||||
"comment": "Package 2",
|
||||
},
|
||||
],
|
||||
)
|
||||
provider = _make_provider(http_client)
|
||||
|
||||
asyncio.run(provider.register_order(request))
|
||||
result = asyncio.run(provider.register_order(make_init_payment_request()))
|
||||
|
||||
assert result == CDEKOrderRegistrationResult(
|
||||
order_uuid="cdek-order-uuid",
|
||||
waybill_uuid="waybill-uuid-1",
|
||||
waybill_url="https://cdek.test/waybill/1.pdf",
|
||||
)
|
||||
assert http_client.calls[0]["url"] == "https://api.cdek.test/v2/orders"
|
||||
payload = http_client.calls[0]["json"]
|
||||
assert "services" not in payload
|
||||
assert payload["sender"]["phones"] == [{"number": "+79009876543"}]
|
||||
assert payload["recipient"]["phones"] == [{"number": "+79001234567"}]
|
||||
assert payload["packages"] == [
|
||||
{
|
||||
"number": "1",
|
||||
"weight": 1000,
|
||||
"length": 20,
|
||||
"width": 15,
|
||||
"height": 10,
|
||||
"comment": "Package 1",
|
||||
},
|
||||
{
|
||||
"number": "2",
|
||||
"weight": 2000,
|
||||
"length": 25,
|
||||
"width": 18,
|
||||
"height": 12,
|
||||
"comment": "Package 2",
|
||||
},
|
||||
]
|
||||
assert payload["number"] == "order-uuid-1"
|
||||
assert payload["type"] == 2
|
||||
assert payload["tariff_code"] == 535
|
||||
assert payload["print"] == "WAYBILL"
|
||||
assert payload["from_location"]["code"] == 7017
|
||||
assert payload["to_location"]["code"] == 16454
|
||||
assert payload["packages"][0]["weight"] == 1000
|
||||
|
||||
|
||||
def test_cdek_client_register_order_maps_4xx_to_request_error() -> None:
|
||||
@@ -241,7 +118,7 @@ def test_cdek_client_register_order_maps_4xx_to_request_error() -> None:
|
||||
)
|
||||
|
||||
with pytest.raises(CDEKRequestError, match="status 422"):
|
||||
asyncio.run(client.register_order(_make_order_request()))
|
||||
asyncio.run(client.register_order(make_init_payment_request()))
|
||||
|
||||
assert len(http_client.calls) == 1
|
||||
|
||||
@@ -272,41 +149,13 @@ def test_cdek_client_register_order_maps_duplicate_external_id_to_success() -> N
|
||||
retry_attempts=2,
|
||||
)
|
||||
|
||||
result = asyncio.run(client.register_order(_make_order_request()))
|
||||
result = asyncio.run(client.register_order(make_init_payment_request()))
|
||||
|
||||
assert result == "existing-cdek-order-uuid"
|
||||
assert len(http_client.calls) == 1
|
||||
|
||||
|
||||
def test_cdek_client_register_order_4xx_with_unrelated_entity_uuid_raises_request_error() -> None:
|
||||
unrelated_response = httpx.Response(
|
||||
409,
|
||||
json={
|
||||
"entity": {"uuid": "unrelated-uuid"},
|
||||
"requests": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "v2_recipient_phone_invalid",
|
||||
"message": "Recipient phone is invalid",
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
request=httpx.Request("POST", "https://api.cdek.test/v2/orders"),
|
||||
assert result == CDEKOrderRegistrationResult(
|
||||
order_uuid="existing-cdek-order-uuid",
|
||||
waybill_uuid=None,
|
||||
waybill_url=None,
|
||||
)
|
||||
http_client = SequenceHTTPClient([unrelated_response])
|
||||
client = CDEKClient(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
auth_client=StubAuthClient(), # type: ignore[arg-type]
|
||||
base_url="https://api.cdek.test/v2",
|
||||
retry_attempts=2,
|
||||
)
|
||||
|
||||
with pytest.raises(CDEKRequestError):
|
||||
asyncio.run(client.register_order(_make_order_request()))
|
||||
|
||||
assert len(http_client.calls) == 1
|
||||
|
||||
|
||||
@@ -337,7 +186,7 @@ def test_cdek_client_register_order_retries_5xx_and_raises_client_error() -> Non
|
||||
)
|
||||
|
||||
with pytest.raises(CDEKClientError, match="retriable status 503"):
|
||||
asyncio.run(client.register_order(_make_order_request()))
|
||||
asyncio.run(client.register_order(make_init_payment_request()))
|
||||
|
||||
assert len(http_client.calls) == 2
|
||||
assert sleep_calls == [0.25]
|
||||
@@ -358,4 +207,4 @@ def test_cdek_client_register_order_raises_client_error_for_invalid_success_payl
|
||||
)
|
||||
|
||||
with pytest.raises(CDEKClientError, match="response payload is invalid"):
|
||||
asyncio.run(client.register_order(_make_order_request()))
|
||||
asyncio.run(client.register_order(make_init_payment_request()))
|
||||
|
||||
@@ -1,53 +1,209 @@
|
||||
from app.adapters.delivery_providers.cdek.order_mapper import (
|
||||
CDEKOrderRegistrationResult,
|
||||
compose_address_line,
|
||||
map_cdek_existing_order_response,
|
||||
map_cdek_order_request,
|
||||
map_cdek_order_response,
|
||||
)
|
||||
from app.schemas.payment import InitPaymentRequest
|
||||
from app.schemas.payment import Address
|
||||
from tests.payment_fixtures import make_init_payment_request
|
||||
|
||||
|
||||
def _make_order_request(**overrides: object) -> InitPaymentRequest:
|
||||
payload: dict[str, object] = {
|
||||
"order_uuid": "order-uuid-1",
|
||||
"price": 125000,
|
||||
"type": 2,
|
||||
"tariff_code": 535,
|
||||
"comment": "Test order",
|
||||
"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 test_cdek_order_payload_uses_order_uuid_as_external_number() -> None:
|
||||
payload = map_cdek_order_request(_make_order_request())
|
||||
def test_order_payload_uses_order_uuid_and_tariff_code_from_system_data() -> None:
|
||||
payload = map_cdek_order_request(make_init_payment_request())
|
||||
|
||||
assert payload["number"] == "order-uuid-1"
|
||||
assert payload["type"] == 2
|
||||
assert payload["tariff_code"] == 535
|
||||
|
||||
|
||||
def test_order_payload_requests_waybill_print() -> None:
|
||||
payload = map_cdek_order_request(make_init_payment_request())
|
||||
|
||||
assert payload["print"] == "WAYBILL"
|
||||
|
||||
|
||||
def test_map_cdek_order_response_extracts_order_uuid_without_waybill() -> None:
|
||||
result = map_cdek_order_response({"entity": {"uuid": "cdek-order-uuid"}})
|
||||
|
||||
assert result == CDEKOrderRegistrationResult(
|
||||
order_uuid="cdek-order-uuid",
|
||||
waybill_uuid=None,
|
||||
waybill_url=None,
|
||||
)
|
||||
|
||||
|
||||
def test_map_cdek_order_response_extracts_waybill_from_related_entities() -> None:
|
||||
result = map_cdek_order_response(
|
||||
{
|
||||
"entity": {"uuid": "cdek-order-uuid"},
|
||||
"related_entities": [
|
||||
{"type": "delivery", "uuid": "ignored"},
|
||||
{
|
||||
"type": "waybill",
|
||||
"uuid": "waybill-uuid-1",
|
||||
"url": "https://cdek.test/waybill/1.pdf",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
assert result == CDEKOrderRegistrationResult(
|
||||
order_uuid="cdek-order-uuid",
|
||||
waybill_uuid="waybill-uuid-1",
|
||||
waybill_url="https://cdek.test/waybill/1.pdf",
|
||||
)
|
||||
|
||||
|
||||
def test_map_cdek_existing_order_response_returns_waybill_for_duplicate() -> None:
|
||||
result = map_cdek_existing_order_response(
|
||||
{
|
||||
"entity": {"uuid": "existing-uuid"},
|
||||
"requests": [
|
||||
{"errors": [{"code": "v2_entity_already_exists", "message": "dup"}]}
|
||||
],
|
||||
"related_entities": [
|
||||
{
|
||||
"type": "waybill",
|
||||
"uuid": "waybill-existing",
|
||||
"url": "https://cdek.test/waybill/existing.pdf",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
assert result == CDEKOrderRegistrationResult(
|
||||
order_uuid="existing-uuid",
|
||||
waybill_uuid="waybill-existing",
|
||||
waybill_url="https://cdek.test/waybill/existing.pdf",
|
||||
)
|
||||
|
||||
|
||||
def test_order_payload_maps_phones_and_phone_ext_to_additional() -> None:
|
||||
payload = map_cdek_order_request(make_init_payment_request())
|
||||
|
||||
assert payload["sender"]["phones"] == [{"number": "+79009876543"}]
|
||||
assert payload["recipient"]["phones"] == [
|
||||
{"number": "+79001234567", "additional": "101"}
|
||||
]
|
||||
|
||||
|
||||
def test_order_payload_maps_locations_with_address_and_postal_code() -> None:
|
||||
payload = map_cdek_order_request(make_init_payment_request())
|
||||
|
||||
assert payload["from_location"] == {
|
||||
"code": 7017,
|
||||
"address": "Дубай, Sheikh Zayed Road, 10, кв. 201",
|
||||
"postal_code": "12345",
|
||||
}
|
||||
assert payload["to_location"] == {
|
||||
"code": 16454,
|
||||
"address": "Шарджа, Al Wahda, 5",
|
||||
"postal_code": "54321",
|
||||
}
|
||||
|
||||
|
||||
def test_order_payload_for_parcel_includes_dimensions() -> None:
|
||||
payload = map_cdek_order_request(make_init_payment_request())
|
||||
|
||||
package = payload["packages"][0]
|
||||
assert package["weight"] == 1000
|
||||
assert package["length"] == 20
|
||||
assert package["width"] == 15
|
||||
assert package["height"] == 10
|
||||
|
||||
|
||||
def test_order_payload_for_doc_omits_dimensions() -> None:
|
||||
request = make_init_payment_request(
|
||||
content={"description": "Docs"},
|
||||
systemData={
|
||||
"tariff": {
|
||||
"provider": "СДЭК",
|
||||
"serviceName": "Документы",
|
||||
"price": 50000,
|
||||
"deliveryDaysMin": 1,
|
||||
"deliveryDaysMax": 2,
|
||||
"tariffCode": 535,
|
||||
},
|
||||
"parcelType": "doc",
|
||||
"docPackaging": "envelope",
|
||||
"weight": "0.5",
|
||||
"dimensions": None,
|
||||
},
|
||||
)
|
||||
|
||||
payload = map_cdek_order_request(request)
|
||||
package = payload["packages"][0]
|
||||
|
||||
assert package["weight"] == 500
|
||||
assert "length" not in package
|
||||
assert "width" not in package
|
||||
assert "height" not in package
|
||||
|
||||
|
||||
def test_order_payload_omits_shipment_and_delivery_point() -> None:
|
||||
payload = map_cdek_order_request(make_init_payment_request())
|
||||
|
||||
assert "shipment_point" not in payload
|
||||
assert "delivery_point" not in payload
|
||||
|
||||
|
||||
def test_order_payload_includes_company_requisites_for_legal_entity() -> None:
|
||||
request = make_init_payment_request(
|
||||
senderContact={
|
||||
"fullName": "Romashka LLC",
|
||||
"email": "buh@romashka.test",
|
||||
"phone": "+74950000000",
|
||||
"phoneExt": None,
|
||||
"isCompany": True,
|
||||
"companyName": "Romashka LLC",
|
||||
"inn": "7707083893",
|
||||
"kpp": "770701001",
|
||||
}
|
||||
)
|
||||
|
||||
sender = map_cdek_order_request(request)["sender"]
|
||||
|
||||
assert sender["contragent_type"] == "LEGAL_ENTITY"
|
||||
assert sender["company"] == "Romashka LLC"
|
||||
assert sender["inn"] == "7707083893"
|
||||
assert sender["kpp"] == "770701001"
|
||||
|
||||
|
||||
def test_order_payload_propagates_description_to_comments_without_items() -> None:
|
||||
payload = map_cdek_order_request(make_init_payment_request())
|
||||
|
||||
assert payload["comment"] == "Headphones"
|
||||
assert payload["packages"][0]["comment"] == "Headphones"
|
||||
assert "items" not in payload["packages"][0]
|
||||
|
||||
|
||||
def test_order_payload_falls_back_package_comment_to_order_uuid() -> None:
|
||||
payload = map_cdek_order_request(
|
||||
make_init_payment_request(content={"description": None})
|
||||
)
|
||||
|
||||
assert payload["packages"][0]["comment"] == "order-uuid-1"
|
||||
|
||||
|
||||
def test_order_payload_includes_company_for_individual_sender_as_full_name() -> None:
|
||||
payload = map_cdek_order_request(make_init_payment_request())
|
||||
|
||||
assert payload["sender"]["company"] == "Petr Petrov"
|
||||
assert payload["recipient"]["company"] == "Ivan Ivanov"
|
||||
assert "contragent_type" not in payload["sender"]
|
||||
assert "contragent_type" not in payload["recipient"]
|
||||
|
||||
|
||||
def test_compose_address_line_handles_empty_apartment() -> None:
|
||||
address = Address(
|
||||
cityId=1,
|
||||
city="Москва",
|
||||
street="Lenina",
|
||||
house="1",
|
||||
apartment=None,
|
||||
zip="12345",
|
||||
comment=None,
|
||||
)
|
||||
|
||||
assert compose_address_line(address) == "Москва, Lenina, 1"
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.adapters.delivery_providers.cdek.client import (
|
||||
CDEKProvider,
|
||||
CDEKRequestError,
|
||||
)
|
||||
from app.schemas.payment import InitPaymentRequest
|
||||
from tests.payment_fixtures import make_init_payment_request
|
||||
|
||||
|
||||
class StubAuthClient:
|
||||
@@ -52,62 +52,11 @@ class SequenceHTTPClient:
|
||||
return result
|
||||
|
||||
|
||||
def _make_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 test_provider_get_payment_price_posts_tarifflist_payload_and_maps_requested_tariff() -> None:
|
||||
def test_provider_get_payment_price_posts_payload_from_camelcase_request() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"tariff_codes": [
|
||||
{
|
||||
"tariff_code": 234,
|
||||
"tariff_name": "Other tariff",
|
||||
"delivery_sum": "999.00",
|
||||
"currency": "RUB",
|
||||
"period_min": 3,
|
||||
"period_max": 5,
|
||||
},
|
||||
{
|
||||
"tariff_code": 535,
|
||||
"tariff_name": "CDEK tariff",
|
||||
@@ -131,11 +80,9 @@ def test_provider_get_payment_price_posts_tarifflist_payload_and_maps_requested_
|
||||
)
|
||||
)
|
||||
|
||||
result = asyncio.run(provider.get_payment_price(_make_request()))
|
||||
result = asyncio.run(provider.get_payment_price(make_init_payment_request()))
|
||||
|
||||
assert result is not None
|
||||
assert result.provider == "cdek"
|
||||
assert result.service_name == "CDEK tariff"
|
||||
assert result.price == Decimal("1250.00")
|
||||
assert http_client.calls == [
|
||||
{
|
||||
@@ -143,16 +90,8 @@ def test_provider_get_payment_price_posts_tarifflist_payload_and_maps_requested_
|
||||
"url": "https://api.cdek.test/v2/calculator/tarifflist",
|
||||
"json": {
|
||||
"type": 2,
|
||||
"from_location": {
|
||||
"address": "Lenina 1",
|
||||
"city": "Moscow",
|
||||
"country_code": "RU",
|
||||
},
|
||||
"to_location": {
|
||||
"address": "Pushkina 10",
|
||||
"city": "Novosibirsk",
|
||||
"country_code": "RU",
|
||||
},
|
||||
"from_location": {"code": 7017},
|
||||
"to_location": {"code": 16454},
|
||||
"packages": [
|
||||
{
|
||||
"weight": 1000,
|
||||
@@ -161,7 +100,6 @@ def test_provider_get_payment_price_posts_tarifflist_payload_and_maps_requested_
|
||||
"height": 10,
|
||||
}
|
||||
],
|
||||
"services": [{"code": "INSURANCE", "parameter": "1000"}],
|
||||
},
|
||||
"data": None,
|
||||
"headers": {"Authorization": "Bearer test-token"},
|
||||
@@ -170,7 +108,7 @@ def test_provider_get_payment_price_posts_tarifflist_payload_and_maps_requested_
|
||||
]
|
||||
|
||||
|
||||
def test_provider_get_payment_price_omits_services_when_none() -> None:
|
||||
def test_provider_get_payment_price_omits_dimensions_for_doc() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={"tariff_codes": []},
|
||||
@@ -185,11 +123,28 @@ def test_provider_get_payment_price_omits_services_when_none() -> None:
|
||||
retry_attempts=0,
|
||||
)
|
||||
)
|
||||
request = make_init_payment_request(
|
||||
content={"description": "Docs"},
|
||||
systemData={
|
||||
"tariff": {
|
||||
"provider": "СДЭК",
|
||||
"serviceName": "Документы",
|
||||
"price": 50000,
|
||||
"deliveryDaysMin": 1,
|
||||
"deliveryDaysMax": 2,
|
||||
"tariffCode": 535,
|
||||
},
|
||||
"parcelType": "doc",
|
||||
"docPackaging": "envelope",
|
||||
"weight": "0.5",
|
||||
"dimensions": None,
|
||||
},
|
||||
)
|
||||
|
||||
result = asyncio.run(provider.get_payment_price(_make_request(services=None)))
|
||||
asyncio.run(provider.get_payment_price(request))
|
||||
|
||||
assert result is None
|
||||
assert "services" not in http_client.calls[0]["json"]
|
||||
package = http_client.calls[0]["json"]["packages"][0]
|
||||
assert package == {"weight": 500}
|
||||
|
||||
|
||||
def test_client_get_raw_payment_price_maps_4xx_to_request_error() -> None:
|
||||
@@ -207,7 +162,7 @@ def test_client_get_raw_payment_price_maps_4xx_to_request_error() -> None:
|
||||
)
|
||||
|
||||
with pytest.raises(CDEKRequestError, match="status 422"):
|
||||
asyncio.run(client.get_raw_payment_price(_make_request()))
|
||||
asyncio.run(client.get_raw_payment_price(make_init_payment_request()))
|
||||
|
||||
assert len(http_client.calls) == 1
|
||||
|
||||
@@ -239,7 +194,7 @@ def test_client_get_raw_payment_price_retries_5xx_and_raises_client_error() -> N
|
||||
)
|
||||
|
||||
with pytest.raises(CDEKClientError, match="status 503"):
|
||||
asyncio.run(client.get_raw_payment_price(_make_request()))
|
||||
asyncio.run(client.get_raw_payment_price(make_init_payment_request()))
|
||||
|
||||
assert len(http_client.calls) == 2
|
||||
assert sleep_calls == [0.25]
|
||||
@@ -262,4 +217,4 @@ def test_provider_get_payment_price_maps_invalid_success_payload_to_client_error
|
||||
)
|
||||
|
||||
with pytest.raises(CDEKClientError, match="response payload is invalid"):
|
||||
asyncio.run(provider.get_payment_price(_make_request()))
|
||||
asyncio.run(provider.get_payment_price(make_init_payment_request()))
|
||||
|
||||
@@ -9,6 +9,7 @@ from app.services.aggregator import (
|
||||
InitPaymentUnavailableError,
|
||||
InvalidInitPaymentRequestError,
|
||||
)
|
||||
from tests.payment_fixtures import make_init_payment_payload
|
||||
|
||||
|
||||
class StubAggregatorService:
|
||||
@@ -31,45 +32,16 @@ def _install_service_override(app, service: StubAggregatorService) -> None:
|
||||
app.dependency_overrides[get_aggregator_service] = override_service
|
||||
|
||||
|
||||
def _valid_payload() -> dict[str, object]:
|
||||
return {
|
||||
"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",
|
||||
}
|
||||
],
|
||||
}
|
||||
def _post(app, payload: dict[str, object]) -> httpx.Response:
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post("/api/v1/delivery/order", json=payload)
|
||||
|
||||
return asyncio.run(run_request())
|
||||
|
||||
|
||||
def test_post_init_payment_returns_response_and_delegates_to_service() -> None:
|
||||
@@ -77,44 +49,64 @@ def test_post_init_payment_returns_response_and_delegates_to_service() -> None:
|
||||
service = StubAggregatorService(response=expected_response)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
payload = make_init_payment_payload()
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=_valid_payload(),
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
response = _post(app, payload)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == expected_response.model_dump(mode="json")
|
||||
assert service.calls == [InitPaymentRequest(**_valid_payload())]
|
||||
assert service.calls == [InitPaymentRequest.model_validate(payload)]
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_invalid_payload() -> None:
|
||||
def test_post_init_payment_rejects_invalid_tariff_code() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = _valid_payload()
|
||||
invalid_payload["tariff_code"] = "not-a-number"
|
||||
invalid_payload = make_init_payment_payload()
|
||||
invalid_payload["systemData"]["tariff"]["tariffCode"] = "not-a-number"
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=invalid_payload,
|
||||
)
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_snake_case_top_level_field() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = make_init_payment_payload()
|
||||
invalid_payload["order_uuid"] = invalid_payload.pop("orderUuid")
|
||||
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_snake_case_nested_address_field() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = make_init_payment_payload()
|
||||
sender_address = invalid_payload["senderAddress"]
|
||||
sender_address["city_id"] = sender_address.pop("cityId")
|
||||
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_snake_case_nested_tariff_field() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = make_init_payment_payload()
|
||||
tariff = invalid_payload["systemData"]["tariff"]
|
||||
tariff["tariff_code"] = tariff.pop("tariffCode")
|
||||
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
@@ -124,21 +116,10 @@ def test_post_init_payment_rejects_missing_price() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = _valid_payload()
|
||||
del invalid_payload["price"]
|
||||
invalid_payload = make_init_payment_payload()
|
||||
del invalid_payload["systemData"]["tariff"]["price"]
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=invalid_payload,
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
@@ -148,21 +129,10 @@ def test_post_init_payment_rejects_non_positive_price() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = _valid_payload()
|
||||
invalid_payload["price"] = 0
|
||||
invalid_payload = make_init_payment_payload()
|
||||
invalid_payload["systemData"]["tariff"]["price"] = 0
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=invalid_payload,
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
@@ -172,155 +142,102 @@ def test_post_init_payment_rejects_non_integer_price() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = _valid_payload()
|
||||
invalid_payload["price"] = 125000.5
|
||||
invalid_payload = make_init_payment_payload()
|
||||
invalid_payload["systemData"]["tariff"]["price"] = 125000.5
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=invalid_payload,
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_sender_company_field() -> None:
|
||||
def test_post_init_payment_requires_company_fields_when_is_company_true() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = _valid_payload()
|
||||
invalid_payload["sender"] = {
|
||||
**invalid_payload["sender"], # type: ignore[arg-type]
|
||||
"company": "Romashka LLC",
|
||||
}
|
||||
invalid_payload = make_init_payment_payload()
|
||||
invalid_payload["senderContact"]["isCompany"] = True
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=invalid_payload,
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_sender_phones_field() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = _valid_payload()
|
||||
invalid_payload["sender"] = {
|
||||
**invalid_payload["sender"], # type: ignore[arg-type]
|
||||
"phones": [{"number": "+79009876543"}],
|
||||
}
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=invalid_payload,
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_recipient_company_field() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = _valid_payload()
|
||||
invalid_payload["recipient"] = {
|
||||
**invalid_payload["recipient"], # type: ignore[arg-type]
|
||||
"company": "Romashka LLC",
|
||||
}
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=invalid_payload,
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_recipient_phones_field() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = _valid_payload()
|
||||
invalid_payload["recipient"] = {
|
||||
**invalid_payload["recipient"], # type: ignore[arg-type]
|
||||
"phones": [{"number": "+79001234567"}],
|
||||
}
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=invalid_payload,
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_accepts_request_without_services() -> None:
|
||||
def test_post_init_payment_accepts_company_with_full_requisites() -> None:
|
||||
expected_response = InitPaymentResponse(payment_url="https://pay.test/payment/1")
|
||||
service = StubAggregatorService(response=expected_response)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
payload = _valid_payload()
|
||||
del payload["services"]
|
||||
payload = make_init_payment_payload()
|
||||
payload["senderContact"].update(
|
||||
{
|
||||
"isCompany": True,
|
||||
"companyName": "Romashka LLC",
|
||||
"inn": "7707083893",
|
||||
"kpp": "770701001",
|
||||
}
|
||||
)
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post("/api/v1/delivery/order", json=payload)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
response = _post(app, payload)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == expected_response.model_dump(mode="json")
|
||||
assert service.calls == [InitPaymentRequest(**payload)]
|
||||
assert service.calls == [InitPaymentRequest.model_validate(payload)]
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_non_numeric_weight() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = make_init_payment_payload()
|
||||
invalid_payload["systemData"]["weight"] = "string"
|
||||
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_non_numeric_dimension() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = make_init_payment_payload()
|
||||
invalid_payload["systemData"]["dimensions"]["length"] = "string"
|
||||
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_rejects_parcel_without_dimensions() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
invalid_payload = make_init_payment_payload()
|
||||
invalid_payload["systemData"]["dimensions"] = None
|
||||
|
||||
response = _post(app, invalid_payload)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert service.calls == []
|
||||
|
||||
|
||||
def test_post_init_payment_accepts_doc_without_dimensions() -> None:
|
||||
expected_response = InitPaymentResponse(payment_url="https://pay.test/payment/1")
|
||||
service = StubAggregatorService(response=expected_response)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
payload = make_init_payment_payload()
|
||||
payload["systemData"]["parcelType"] = "doc"
|
||||
payload["systemData"]["docPackaging"] = "envelope"
|
||||
payload["systemData"]["dimensions"] = None
|
||||
|
||||
response = _post(app, payload)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert service.calls == [InitPaymentRequest.model_validate(payload)]
|
||||
|
||||
|
||||
def test_post_init_payment_maps_invalid_request_to_400() -> None:
|
||||
@@ -331,18 +248,7 @@ def test_post_init_payment_maps_invalid_request_to_400() -> None:
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=_valid_payload(),
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
response = _post(app, make_init_payment_payload())
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
@@ -353,37 +259,6 @@ def test_post_init_payment_maps_invalid_request_to_400() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_post_init_payment_maps_price_mismatch_to_400() -> None:
|
||||
service = StubAggregatorService(
|
||||
response=None,
|
||||
error=InvalidInitPaymentRequestError("price mismatch"),
|
||||
)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=_valid_payload(),
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"detail": {
|
||||
"code": "invalid_init_payment_request",
|
||||
"message": "Payment request contains invalid or unsupported TBank data.",
|
||||
}
|
||||
}
|
||||
assert service.calls == [InitPaymentRequest(**_valid_payload())]
|
||||
|
||||
|
||||
def test_post_init_payment_maps_service_exception_to_503() -> None:
|
||||
service = StubAggregatorService(
|
||||
response=None,
|
||||
@@ -392,18 +267,7 @@ def test_post_init_payment_maps_service_exception_to_503() -> None:
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=_valid_payload(),
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
response = _post(app, make_init_payment_payload())
|
||||
|
||||
assert response.status_code == 503
|
||||
assert response.json() == {
|
||||
@@ -412,53 +276,3 @@ def test_post_init_payment_maps_service_exception_to_503() -> None:
|
||||
"message": "Payment initialization is temporarily unavailable.",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_post_init_payment_maps_temporary_cdek_validation_error_to_503() -> None:
|
||||
service = StubAggregatorService(
|
||||
response=None,
|
||||
error=InitPaymentUnavailableError("CDEK validation unavailable"),
|
||||
)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/order",
|
||||
json=_valid_payload(),
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
|
||||
assert response.status_code == 503
|
||||
assert response.json() == {
|
||||
"detail": {
|
||||
"code": "init_payment_unavailable",
|
||||
"message": "Payment initialization is temporarily unavailable.",
|
||||
}
|
||||
}
|
||||
assert service.calls == [InitPaymentRequest(**_valid_payload())]
|
||||
|
||||
|
||||
def test_delivery_order_endpoint_is_removed() -> None:
|
||||
service = StubAggregatorService(response=None)
|
||||
app = create_app()
|
||||
_install_service_override(app, service)
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
return await client.post("/api/v1/delivery/order", json=_valid_payload())
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
|
||||
assert response.status_code == 404
|
||||
assert service.calls == []
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Shared fixtures for the camelCase init-payment contract."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.schemas.payment import InitPaymentRequest
|
||||
|
||||
|
||||
def make_init_payment_payload(**overrides: Any) -> dict[str, Any]:
|
||||
"""Return a valid camelCase JSON payload for /api/v1/delivery/order."""
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"orderUuid": "order-uuid-1",
|
||||
"senderAddress": {
|
||||
"cityId": 1,
|
||||
"city": "Дубай",
|
||||
"street": "Sheikh Zayed Road",
|
||||
"house": "10",
|
||||
"apartment": "201",
|
||||
"zip": "12345",
|
||||
"comment": "Domofon 12",
|
||||
},
|
||||
"senderContact": {
|
||||
"fullName": "Petr Petrov",
|
||||
"email": "sender@example.com",
|
||||
"phone": "+79009876543",
|
||||
"phoneExt": None,
|
||||
"isCompany": False,
|
||||
"companyName": None,
|
||||
"inn": None,
|
||||
"kpp": None,
|
||||
},
|
||||
"receiverAddress": {
|
||||
"cityId": 2,
|
||||
"city": "Шарджа",
|
||||
"street": "Al Wahda",
|
||||
"house": "5",
|
||||
"apartment": None,
|
||||
"zip": "54321",
|
||||
"comment": None,
|
||||
},
|
||||
"receiverContact": {
|
||||
"fullName": "Ivan Ivanov",
|
||||
"email": "ivan@example.com",
|
||||
"phone": "+79001234567",
|
||||
"phoneExt": "101",
|
||||
"isCompany": False,
|
||||
"companyName": None,
|
||||
"inn": None,
|
||||
"kpp": None,
|
||||
},
|
||||
"content": {
|
||||
"description": "Headphones",
|
||||
},
|
||||
"pickupDate": "2026-05-15T10:00:00.000Z",
|
||||
"deliveryDate": "2026-05-18T18:00:00.000Z",
|
||||
"accountEmail": "client@example.com",
|
||||
"systemData": {
|
||||
"tariff": {
|
||||
"provider": "СДЭК",
|
||||
"serviceName": "Экспресс лайт",
|
||||
"price": 125000,
|
||||
"deliveryDaysMin": 1,
|
||||
"deliveryDaysMax": 2,
|
||||
"tariffCode": 535,
|
||||
},
|
||||
"parcelType": "parcel",
|
||||
"docPackaging": None,
|
||||
"weight": "1.0",
|
||||
"dimensions": {
|
||||
"length": "20",
|
||||
"width": "15",
|
||||
"height": "10",
|
||||
},
|
||||
},
|
||||
}
|
||||
payload.update(overrides)
|
||||
return payload
|
||||
|
||||
|
||||
def make_init_payment_request(**overrides: Any) -> InitPaymentRequest:
|
||||
return InitPaymentRequest.model_validate(make_init_payment_payload(**overrides))
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from app.repositories.order import OrderData, OrderRepository
|
||||
from app.repositories.order.models import Base, Order
|
||||
from tests.payment_fixtures import make_init_payment_payload
|
||||
|
||||
|
||||
def _make_order_data(**overrides: object) -> OrderData:
|
||||
@@ -17,40 +18,9 @@ def _make_order_data(**overrides: object) -> OrderData:
|
||||
"order_uuid": "order-uuid-1",
|
||||
"payment_url": "https://pay.test/payment/1",
|
||||
"price": 125000,
|
||||
"delivery_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",
|
||||
}
|
||||
],
|
||||
"account_email": "client@example.com",
|
||||
"payload": make_init_payment_payload(),
|
||||
}
|
||||
payload.update(overrides)
|
||||
return OrderData(**payload)
|
||||
@@ -92,46 +62,20 @@ 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.delivery_type == 2
|
||||
assert persisted_order.tariff_code == 535
|
||||
assert persisted_order.sender == order_data.sender
|
||||
assert persisted_order.recipient == order_data.recipient
|
||||
assert persisted_order.from_location == order_data.from_location
|
||||
assert persisted_order.to_location == order_data.to_location
|
||||
assert persisted_order.packages == order_data.packages
|
||||
assert persisted_order.services == order_data.services
|
||||
assert persisted_order.comment == "Test payment"
|
||||
assert persisted_order.account_email == "client@example.com"
|
||||
assert persisted_order.payload == order_data.payload
|
||||
assert persisted_order.payment_status is None
|
||||
assert persisted_order.tbank_payment_id is None
|
||||
assert persisted_order.cdek_order_uuid is None
|
||||
assert persisted_order.cdek_waybill_uuid is None
|
||||
assert persisted_order.cdek_waybill_url is None
|
||||
assert persisted_order.created_at is not None
|
||||
assert persisted_order.updated_at is not None
|
||||
|
||||
asyncio.run(_with_repository(run))
|
||||
|
||||
|
||||
def test_create_order_persists_nullable_services_and_comment() -> None:
|
||||
async def run(
|
||||
repository: OrderRepository,
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
) -> None:
|
||||
order_data = _make_order_data(services=None, comment=None)
|
||||
|
||||
async with repository.session() as session:
|
||||
await repository.create_order(session, order_data)
|
||||
|
||||
async with session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(Order).where(Order.order_uuid == "order-uuid-1")
|
||||
)
|
||||
persisted_order = result.scalar_one()
|
||||
|
||||
assert persisted_order.services is None
|
||||
assert persisted_order.comment is None
|
||||
|
||||
asyncio.run(_with_repository(run))
|
||||
|
||||
|
||||
def test_create_order_rejects_duplicate_order_uuid() -> None:
|
||||
async def run(
|
||||
repository: OrderRepository,
|
||||
@@ -229,7 +173,7 @@ def test_mark_payment_status_returns_none_for_missing_order() -> None:
|
||||
asyncio.run(_with_repository(run))
|
||||
|
||||
|
||||
def test_mark_cdek_order_registered_persists_cdek_order_uuid() -> None:
|
||||
def test_mark_cdek_order_registered_persists_cdek_order_and_waybill() -> None:
|
||||
async def run(
|
||||
repository: OrderRepository,
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
@@ -242,6 +186,8 @@ def test_mark_cdek_order_registered_persists_cdek_order_uuid() -> None:
|
||||
session,
|
||||
"order-uuid-1",
|
||||
"cdek-order-uuid-1",
|
||||
"waybill-uuid-1",
|
||||
"https://cdek.test/waybill/1.pdf",
|
||||
)
|
||||
|
||||
async with session_factory() as session:
|
||||
@@ -252,6 +198,36 @@ def test_mark_cdek_order_registered_persists_cdek_order_uuid() -> None:
|
||||
|
||||
assert order is not None
|
||||
assert persisted_order.cdek_order_uuid == "cdek-order-uuid-1"
|
||||
assert persisted_order.cdek_waybill_uuid == "waybill-uuid-1"
|
||||
assert persisted_order.cdek_waybill_url == "https://cdek.test/waybill/1.pdf"
|
||||
|
||||
asyncio.run(_with_repository(run))
|
||||
|
||||
|
||||
def test_mark_cdek_order_registered_allows_missing_waybill_fields() -> None:
|
||||
async def run(
|
||||
repository: OrderRepository,
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
) -> None:
|
||||
async with repository.session() as session:
|
||||
await repository.create_order(session, _make_order_data())
|
||||
|
||||
async with repository.session() as session:
|
||||
await repository.mark_cdek_order_registered(
|
||||
session,
|
||||
"order-uuid-1",
|
||||
"cdek-order-uuid-1",
|
||||
)
|
||||
|
||||
async with session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(Order).where(Order.order_uuid == "order-uuid-1")
|
||||
)
|
||||
persisted_order = result.scalar_one()
|
||||
|
||||
assert persisted_order.cdek_order_uuid == "cdek-order-uuid-1"
|
||||
assert persisted_order.cdek_waybill_uuid is None
|
||||
assert persisted_order.cdek_waybill_url is None
|
||||
|
||||
asyncio.run(_with_repository(run))
|
||||
|
||||
|
||||
@@ -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