"""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.registry import ( build_delivery_provider_registry, resolve_delivery_provider_timeout_seconds, ) 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.runtime.metrics import get_cache_metrics 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=resolve_delivery_provider_timeout_seconds( settings.delivery_providers ) ) delivery_provider_registry = build_delivery_provider_registry( http_client=http_client, config=settings.delivery_providers, ) 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, ) cache = PriceCache.from_repository_config( settings.repository, metrics=get_cache_metrics(), ) 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=delivery_provider_registry.providers, cache=cache, payment_adapter=payment_adapter, payment_price_validation_adapters=( delivery_provider_registry.payment_price_validation_adapters ), order_repository=order_repository, order_registration_adapters=( delivery_provider_registry.order_registration_adapters ), 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: logger.info( "tbank_notification_received", order_id=notification.OrderId, status=notification.Status, success=notification.Success, payment_id=notification.PaymentId, error_code=notification.ErrorCode, amount=notification.Amount, ) try: response_body = await service.handle_tbank_payment_notification(notification) except InvalidTBankPaymentNotificationError as exc: logger.warning( "tbank_notification_rejected", order_id=notification.OrderId, reason="invalid_token", ) 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: logger.error( "tbank_notification_processing_failed", order_id=notification.OrderId, exc_info=True, ) 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: logger.error( "tbank_notification_processing_failed", order_id=notification.OrderId, exc_info=True, ) 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)