Добавлен CI, добавлена отправка письма о получении оплаты, добавлен order_id в success_url
Deploy / deploy (push) Successful in 1m0s

This commit is contained in:
Раис Юсупалиев
2026-05-27 23:29:51 +03:00
parent 50124fb2c9
commit 23dd12e51c
27 changed files with 592 additions and 166 deletions
+97 -17
View File
@@ -3,10 +3,12 @@
import asyncio
import hashlib
import json
from collections.abc import Iterable, Mapping, Sequence
from collections.abc import Callable, Iterable, Mapping, Sequence
from contextlib import AbstractAsyncContextManager
from datetime import datetime, timezone
from decimal import Decimal
from typing import Protocol
from uuid import uuid4
import structlog
@@ -113,7 +115,7 @@ class PaymentPriceValidationAdapterProtocol(Protocol):
class OrderRegistrationAdapterProtocol(Protocol):
async def register_order(
self, request: InitPaymentRequest
self, request: InitPaymentRequest, order_uuid: str
) -> CDEKOrderRegistrationResult: ...
@@ -144,6 +146,16 @@ class OrderRepositoryProtocol(Protocol):
) -> object | None: ...
class EmailSenderProtocol(Protocol):
async def send_email(
self,
*,
to: str,
subject: str,
body: str,
) -> None: ...
class FilterAndSortPricesFn(Protocol):
def __call__(
self,
@@ -165,12 +177,14 @@ class AggregatorService:
) = None,
order_repository: OrderRepositoryProtocol | None = None,
order_registration_adapter: OrderRegistrationAdapterProtocol | None = None,
email_sender: EmailSenderProtocol | None = None,
address_suggestion_providers: Sequence[AddressSuggestionProvider] = (),
address_suggestion_country_to_provider: Mapping[str, str] | None = None,
*,
weight_round_scale: int = DEFAULT_WEIGHT_ROUND_SCALE,
provider_price_multiplier: Decimal = DEFAULT_PROVIDER_PRICE_MULTIPLIER,
filter_and_sort_prices_fn: FilterAndSortPricesFn = filter_and_sort_prices,
order_uuid_factory: Callable[[], str] = lambda: str(uuid4()),
) -> None:
self._providers = tuple(providers)
self._cache = cache
@@ -178,9 +192,11 @@ class AggregatorService:
self._payment_price_validation_adapter = payment_price_validation_adapter
self._order_repository = order_repository
self._order_registration_adapter = order_registration_adapter
self._email_sender = email_sender
self._weight_round_scale = weight_round_scale
self._provider_price_multiplier = provider_price_multiplier
self._filter_and_sort_prices = filter_and_sort_prices_fn
self._order_uuid_factory = order_uuid_factory
self._address_suggestion_providers: dict[str, AddressSuggestionProvider] = {
provider.name: provider for provider in address_suggestion_providers
}
@@ -259,17 +275,19 @@ class AggregatorService:
if self._payment_adapter is None:
raise InitPaymentUnavailableError("Payment adapter is not configured.")
await self._validate_init_payment_price(request)
order_uuid = self._order_uuid_factory()
await self._validate_init_payment_price(request, order_uuid)
try:
payment_url = await self._payment_adapter.create_payment_link(
order_uuid=request.order_uuid,
order_uuid=order_uuid,
amount_kopecks=request.system_data.tariff.price,
)
except TBankPaymentRequestError as exc:
logger.exception(
"payment_init_rejected",
order_uuid=request.order_uuid,
order_uuid=order_uuid,
provider_status_code=exc.status_code,
provider_error_code=exc.error_code,
provider_error_message=exc.provider_message,
@@ -287,12 +305,15 @@ class AggregatorService:
"Payment initialization is temporarily unavailable."
) from exc
await self._persist_order(request=request, payment_url=payment_url)
await self._persist_order(
request=request, order_uuid=order_uuid, payment_url=payment_url
)
return InitPaymentResponse(payment_url=payment_url)
async def _validate_init_payment_price(
self,
request: InitPaymentRequest,
order_uuid: str,
) -> None:
if self._payment_price_validation_adapter is None:
raise InitPaymentUnavailableError(
@@ -308,7 +329,7 @@ class AggregatorService:
except ProviderRequestError as exc:
logger.warning(
"init_payment_price_validation_request_rejected",
order_uuid=request.order_uuid,
order_uuid=order_uuid,
tariff_code=tariff_code,
requested_price_kopecks=requested_price,
error=str(exc),
@@ -319,7 +340,7 @@ class AggregatorService:
except ProviderClientError as exc:
logger.warning(
"init_payment_price_validation_unavailable",
order_uuid=request.order_uuid,
order_uuid=order_uuid,
tariff_code=tariff_code,
requested_price_kopecks=requested_price,
error=str(exc),
@@ -330,7 +351,7 @@ class AggregatorService:
except Exception as exc:
logger.exception(
"init_payment_price_validation_unexpected_error",
order_uuid=request.order_uuid,
order_uuid=order_uuid,
tariff_code=tariff_code,
requested_price_kopecks=requested_price,
)
@@ -341,7 +362,7 @@ class AggregatorService:
if provider_price is None:
logger.warning(
"init_payment_price_validation_tariff_not_found",
order_uuid=request.order_uuid,
order_uuid=order_uuid,
tariff_code=tariff_code,
requested_price_kopecks=requested_price,
)
@@ -360,7 +381,7 @@ class AggregatorService:
):
logger.warning(
"init_payment_price_mismatch",
order_uuid=request.order_uuid,
order_uuid=order_uuid,
tariff_code=tariff_code,
requested_price_kopecks=requested_price,
expected_price_kopecks=expected_amount_kopecks,
@@ -405,13 +426,64 @@ class AggregatorService:
if existing_cdek_order_uuid:
return "OK"
registration_result = await self._register_cdek_order(order)
registration_result = await self._register_cdek_order(
order, order_uuid=notification.OrderId
)
await self._save_cdek_order_uuid(
order_uuid=notification.OrderId,
cdek_order_uuid=registration_result.order_uuid,
)
await self._send_payment_confirmation_email(
order, order_uuid=notification.OrderId
)
return "OK"
async def _send_payment_confirmation_email(
self,
order: object,
*,
order_uuid: str,
) -> None:
if self._email_sender is None or self._order_repository is None:
return
if getattr(order, "payment_email_sent_at", None) is not None:
return
account_email = getattr(order, "account_email", None)
if account_email is None:
return
try:
await self._email_sender.send_email(
to=account_email,
subject=f"Оплата по заказу {order_uuid} принята",
body=(
f"Здравствуйте!\n\n"
f"Оплата по заказу {order_uuid} успешно принята.\n"
f"Накладная будет отправлена на этот адрес в ближайшее время.\n\n"
f"Спасибо за заказ!"
),
)
async with self._order_repository.session() as session:
await self._order_repository.record_payment_email_sent(
session,
order_uuid=order_uuid,
sent_at=datetime.now(timezone.utc),
)
logger.info(
"payment_confirmation_email_sent",
order_uuid=order_uuid,
account_email=account_email,
)
except Exception:
logger.warning(
"payment_confirmation_email_failed",
order_uuid=order_uuid,
account_email=account_email,
exc_info=True,
)
async def _load_order_and_mark_payment_status(
self,
notification: TBankPaymentNotification,
@@ -464,7 +536,7 @@ class AggregatorService:
) from exc
async def _register_cdek_order(
self, order: object
self, order: object, *, order_uuid: str
) -> CDEKOrderRegistrationResult:
if self._order_registration_adapter is None:
raise TBankPaymentNotificationProcessingError(
@@ -473,7 +545,9 @@ class AggregatorService:
try:
request = self._to_init_payment_request_from_order(order)
return await self._order_registration_adapter.register_order(request)
return await self._order_registration_adapter.register_order(
request, order_uuid
)
except Exception as exc:
logger.exception(
"cdek_order_registration_failed",
@@ -526,6 +600,7 @@ class AggregatorService:
self,
*,
request: InitPaymentRequest,
order_uuid: str,
payment_url: str,
) -> None:
if self._order_repository is None:
@@ -535,22 +610,27 @@ class AggregatorService:
async with self._order_repository.session() as session:
await self._order_repository.create_order(
session,
self._to_order_data(request=request, payment_url=payment_url),
self._to_order_data(
request=request,
order_uuid=order_uuid,
payment_url=payment_url,
),
)
except Exception:
logger.exception(
"order_persistence_failed",
order_uuid=request.order_uuid,
order_uuid=order_uuid,
)
@staticmethod
def _to_order_data(
*,
request: InitPaymentRequest,
order_uuid: str,
payment_url: str,
) -> OrderData:
return OrderData(
order_uuid=request.order_uuid,
order_uuid=order_uuid,
payment_url=payment_url,
price=request.system_data.tariff.price,
tariff_code=request.system_data.tariff.tariff_code,