260 lines
9.6 KiB
Python
260 lines
9.6 KiB
Python
"""Delivery API controller skeleton."""
|
|
|
|
import structlog
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from fastapi.responses import PlainTextResponse
|
|
|
|
from app.adapters.postgres.engine import create_postgres_engine, create_postgres_session_factory
|
|
from app.adapters.address_suggestions.dadata import DadataAddressSuggestionProvider
|
|
from app.adapters.address_suggestions.tomtom import TomTomAddressSuggestionProvider
|
|
from app.adapters.address_suggestions.yandex_geosuggest import (
|
|
YandexGeosuggestAddressSuggestionProvider,
|
|
)
|
|
from app.adapters.delivery_providers.cdek import CDEKProvider
|
|
from app.adapters.email import SMTPEmailSender
|
|
from app.adapters.tbank import TBankAdapter
|
|
from app.config import Settings
|
|
from app.controllers.http_client import build_controller_http_client
|
|
from app.repositories.cache.redis_cache import PriceCache
|
|
from app.repositories.order import OrderRepository
|
|
from app.schemas.payment import (
|
|
InitPaymentRequest,
|
|
InitPaymentResponse,
|
|
TBankPaymentNotification,
|
|
)
|
|
from app.schemas.request import DeliveryCalculationRequest, SuggestAddressRequest
|
|
from app.schemas.response import AddressSuggestion, DeliveryPrice
|
|
from app.services.aggregator import (
|
|
AddressSuggestionsUnavailableError,
|
|
AggregatorService,
|
|
AggregatorServiceError,
|
|
InvalidAddressSuggestRequestError,
|
|
InvalidDeliveryRequestError,
|
|
InvalidInitPaymentRequestError,
|
|
InvalidTBankPaymentNotificationError,
|
|
InitPaymentUnavailableError,
|
|
TBankPaymentNotificationProcessingError,
|
|
UnsupportedAddressSuggestionCountryError,
|
|
)
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
router = APIRouter(prefix="/delivery", tags=["delivery"])
|
|
|
|
|
|
_AGGREGATOR_SERVICE_STATE_KEY = "aggregator_service"
|
|
|
|
|
|
def _build_aggregator_service(settings: Settings) -> AggregatorService:
|
|
http_client = build_controller_http_client(
|
|
timeout_seconds=settings.adapter.cdek_timeout_seconds
|
|
)
|
|
cdek_provider = CDEKProvider.from_adapter_config(
|
|
http_client=http_client,
|
|
adapter_config=settings.adapter,
|
|
)
|
|
payment_adapter = TBankAdapter.from_config(
|
|
http_client=http_client,
|
|
config=settings.tbank_payment,
|
|
)
|
|
dadata_provider = DadataAddressSuggestionProvider.from_config(
|
|
http_client=http_client,
|
|
config=settings.address_suggestions.dadata,
|
|
)
|
|
yandex_geosuggest_provider = YandexGeosuggestAddressSuggestionProvider.from_config(
|
|
http_client=http_client,
|
|
config=settings.address_suggestions.yandex_geosuggest,
|
|
)
|
|
tomtom_provider = TomTomAddressSuggestionProvider.from_config(
|
|
http_client=http_client,
|
|
config=settings.address_suggestions.tomtom,
|
|
)
|
|
providers = (cdek_provider,)
|
|
cache = PriceCache.from_repository_config(settings.repository)
|
|
postgres_engine = create_postgres_engine(settings.postgres)
|
|
postgres_session_factory = create_postgres_session_factory(postgres_engine)
|
|
order_repository = OrderRepository(session_factory=postgres_session_factory)
|
|
email_sender = SMTPEmailSender(
|
|
smtp_host=settings.email.smtp_host,
|
|
smtp_port=settings.email.smtp_port,
|
|
username=settings.email.username,
|
|
password=settings.email.password,
|
|
from_address=settings.email.from_address,
|
|
use_tls=settings.email.use_tls,
|
|
timeout_seconds=settings.email.timeout_seconds,
|
|
)
|
|
service = AggregatorService(
|
|
providers=providers,
|
|
cache=cache,
|
|
payment_adapter=payment_adapter,
|
|
payment_price_validation_adapter=cdek_provider,
|
|
order_repository=order_repository,
|
|
order_registration_adapter=cdek_provider,
|
|
email_sender=email_sender,
|
|
address_suggestion_providers=(
|
|
dadata_provider,
|
|
yandex_geosuggest_provider,
|
|
tomtom_provider,
|
|
),
|
|
address_suggestion_country_to_provider=(
|
|
settings.address_suggestions.country_to_provider
|
|
),
|
|
weight_round_scale=settings.business_logic.weight_round_scale,
|
|
provider_price_multiplier=settings.business_logic.provider_price_multiplier,
|
|
)
|
|
return service
|
|
|
|
|
|
async def get_aggregator_service(request: Request) -> AggregatorService:
|
|
cached_service = getattr(request.app.state, _AGGREGATOR_SERVICE_STATE_KEY, None)
|
|
if cached_service is not None:
|
|
return cached_service
|
|
|
|
settings = request.app.state.settings
|
|
service = _build_aggregator_service(settings)
|
|
setattr(request.app.state, _AGGREGATOR_SERVICE_STATE_KEY, service)
|
|
return service
|
|
|
|
|
|
@router.post(
|
|
"/price",
|
|
response_model=list[DeliveryPrice],
|
|
)
|
|
async def get_delivery_price(
|
|
delivery_request: DeliveryCalculationRequest,
|
|
service: AggregatorService = Depends(get_aggregator_service),
|
|
) -> list[DeliveryPrice]:
|
|
try:
|
|
return await service.get_all_prices(delivery_request)
|
|
except InvalidDeliveryRequestError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail={
|
|
"code": "invalid_delivery_request",
|
|
"message": "Delivery request contains unknown or unsupported location.",
|
|
},
|
|
) from exc
|
|
except AggregatorServiceError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail={
|
|
"code": "aggregator_service_error",
|
|
"message": "Delivery price aggregation is temporarily unavailable.",
|
|
},
|
|
) from exc
|
|
|
|
|
|
@router.post(
|
|
"/suggest-address",
|
|
response_model=list[AddressSuggestion],
|
|
)
|
|
async def suggest_addresses(
|
|
address_request: SuggestAddressRequest,
|
|
service: AggregatorService = Depends(get_aggregator_service),
|
|
) -> list[AddressSuggestion]:
|
|
logger.info(
|
|
"suggest_address_requested",
|
|
city=address_request.city,
|
|
query=address_request.query,
|
|
limit=address_request.limit,
|
|
)
|
|
try:
|
|
return await service.suggest_addresses(address_request)
|
|
except UnsupportedAddressSuggestionCountryError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail={
|
|
"code": "unsupported_address_suggestion_country",
|
|
"message": "Address suggestions are not configured for the requested country.",
|
|
},
|
|
) from exc
|
|
except InvalidAddressSuggestRequestError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail={
|
|
"code": "invalid_address_suggest_request",
|
|
"message": "Address suggestion request contains invalid or unsupported provider data.",
|
|
},
|
|
) from exc
|
|
except AddressSuggestionsUnavailableError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail={
|
|
"code": "address_suggestions_unavailable",
|
|
"message": "Address suggestions are temporarily unavailable.",
|
|
},
|
|
) from exc
|
|
|
|
|
|
@router.post(
|
|
"/order",
|
|
response_model=InitPaymentResponse,
|
|
)
|
|
async def init_payment(
|
|
payment_request: InitPaymentRequest,
|
|
service: AggregatorService = Depends(get_aggregator_service),
|
|
) -> InitPaymentResponse:
|
|
try:
|
|
return await service.init_payment(payment_request)
|
|
except InvalidInitPaymentRequestError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail={
|
|
"code": "invalid_init_payment_request",
|
|
"message": "Payment request contains invalid or unsupported TBank data.",
|
|
},
|
|
) from exc
|
|
except InitPaymentUnavailableError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail={
|
|
"code": "init_payment_unavailable",
|
|
"message": "Payment initialization is temporarily unavailable.",
|
|
},
|
|
) from exc
|
|
except AggregatorServiceError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail={
|
|
"code": "init_payment_unavailable",
|
|
"message": "Payment initialization is temporarily unavailable.",
|
|
},
|
|
) from exc
|
|
|
|
|
|
@router.post(
|
|
"/tbank/notifications",
|
|
response_class=PlainTextResponse,
|
|
)
|
|
async def handle_tbank_payment_notification(
|
|
notification: TBankPaymentNotification,
|
|
service: AggregatorService = Depends(get_aggregator_service),
|
|
) -> PlainTextResponse:
|
|
try:
|
|
response_body = await service.handle_tbank_payment_notification(notification)
|
|
except InvalidTBankPaymentNotificationError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail={
|
|
"code": "invalid_tbank_payment_notification",
|
|
"message": "TBank payment notification token is invalid.",
|
|
},
|
|
) from exc
|
|
except TBankPaymentNotificationProcessingError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail={
|
|
"code": "tbank_payment_notification_processing_unavailable",
|
|
"message": "TBank payment notification processing is temporarily unavailable.",
|
|
},
|
|
) from exc
|
|
except AggregatorServiceError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail={
|
|
"code": "tbank_payment_notification_processing_unavailable",
|
|
"message": "TBank payment notification processing is temporarily unavailable.",
|
|
},
|
|
) from exc
|
|
return PlainTextResponse(response_body)
|