diff --git a/api/src/pcapi/core/bookings/api.py b/api/src/pcapi/core/bookings/api.py index 2f78f9f1139..8383a02cd27 100644 --- a/api/src/pcapi/core/bookings/api.py +++ b/api/src/pcapi/core/bookings/api.py @@ -412,9 +412,11 @@ def _book_cinema_external_ticket(booking: Booking, stock: Stock, beneficiary: Us booking=booking, beneficiary=beneficiary, ) - except external_bookings_exceptions.ExternalBookingSoldOutError as exc: + except external_bookings_exceptions.ExternalBookingSoldOutError: logger.exception("Could not book this offer as it's sold out.") - raise exc + raise + except external_bookings_exceptions.ExternalBookingTimeoutException: + raise except Exception as exc: logger.exception("Could not book external ticket: %s", exc) raise external_bookings_exceptions.ExternalBookingException diff --git a/api/src/pcapi/core/external_bookings/api.py b/api/src/pcapi/core/external_bookings/api.py index d6ec9fc9bb7..fd38fecef12 100644 --- a/api/src/pcapi/core/external_bookings/api.py +++ b/api/src/pcapi/core/external_bookings/api.py @@ -12,6 +12,7 @@ from pcapi.core.external_bookings.boost.client import BoostClientAPI from pcapi.core.external_bookings.cds.client import CineDigitalServiceAPI from pcapi.core.external_bookings.cgr.client import CGRClientAPI +from pcapi.core.external_bookings.decorators import catch_cinema_provider_request_timeout from pcapi.core.external_bookings.ems.client import EMSClientAPI import pcapi.core.external_bookings.models as external_bookings_models from pcapi.core.external_bookings.models import Ticket @@ -103,6 +104,7 @@ def get_active_cinema_venue_provider(venue_id: int) -> providers_models.VenuePro return cinema_venue_provider +@catch_cinema_provider_request_timeout def book_event_ticket( booking: bookings_models.Booking, stock: offers_models.Stock, diff --git a/api/src/pcapi/core/external_bookings/boost/client.py b/api/src/pcapi/core/external_bookings/boost/client.py index d8f81b579a8..9fd2dff7d75 100644 --- a/api/src/pcapi/core/external_bookings/boost/client.py +++ b/api/src/pcapi/core/external_bookings/boost/client.py @@ -9,6 +9,7 @@ from pcapi.connectors.serialization import boost_serializers import pcapi.core.bookings.constants as bookings_constants import pcapi.core.bookings.models as bookings_models +from pcapi.core.external_bookings.decorators import catch_cinema_provider_request_timeout import pcapi.core.external_bookings.models as external_bookings_models import pcapi.core.users.models as users_models from pcapi.utils.queue import add_to_queue @@ -68,6 +69,7 @@ def cancel_booking(self, barcodes: list[str]) -> None: sale_cancel = boost_serializers.SaleCancel(sales=sale_cancel_items) boost.put_resource(self.cinema_id, boost.ResourceBoost.CANCEL_ORDER_SALE, sale_cancel) + @catch_cinema_provider_request_timeout def book_ticket( self, show_id: int, booking: bookings_models.Booking, beneficiary: users_models.User ) -> list[external_bookings_models.Ticket]: diff --git a/api/src/pcapi/core/external_bookings/cds/client.py b/api/src/pcapi/core/external_bookings/cds/client.py index c413ec57062..86a715942a2 100644 --- a/api/src/pcapi/core/external_bookings/cds/client.py +++ b/api/src/pcapi/core/external_bookings/cds/client.py @@ -17,8 +17,10 @@ from pcapi.core.bookings.constants import REDIS_EXTERNAL_BOOKINGS_NAME from pcapi.core.bookings.constants import RedisExternalBookingType import pcapi.core.bookings.models as bookings_models +from pcapi.utils.requests import exceptions as requests_exception import pcapi.core.external_bookings.cds.constants as cds_constants import pcapi.core.external_bookings.cds.exceptions as cds_exceptions +from pcapi.core.external_bookings.decorators import catch_cinema_provider_request_timeout import pcapi.core.external_bookings.models as external_bookings_models from pcapi.core.external_bookings.models import Ticket import pcapi.core.users.models as users_models @@ -242,6 +244,7 @@ def cancel_booking(self, barcodes: list[str]) -> None: f"Error while canceling bookings :{sep}{sep.join([f'{barcode} : {error_msg}' for barcode, error_msg in cancel_errors.__root__.items()])}" ) + @catch_cinema_provider_request_timeout def book_ticket( self, show_id: int, booking: bookings_models.Booking, beneficiary: users_models.User ) -> list[Ticket]: diff --git a/api/src/pcapi/core/external_bookings/ems/client.py b/api/src/pcapi/core/external_bookings/ems/client.py index f12099daa9a..363b3850413 100644 --- a/api/src/pcapi/core/external_bookings/ems/client.py +++ b/api/src/pcapi/core/external_bookings/ems/client.py @@ -9,6 +9,7 @@ from pcapi.core.bookings import models as booking_models from pcapi.core.bookings import repository as bookings_repository from pcapi.core.external_bookings import models as external_bookings_models +from pcapi.core.external_bookings.decorators import catch_cinema_provider_request_timeout from pcapi.core.external_bookings.exceptions import ExternalBookingSoldOutError from pcapi.core.users import models as users_models from pcapi.models.feature import FeatureToggle @@ -46,6 +47,7 @@ def get_ticket(self, token: str) -> list[external_bookings_models.Ticket]: for ticket in content.billets ] + @catch_cinema_provider_request_timeout def book_ticket( self, show_id: int, booking: booking_models.Booking, beneficiary: users_models.User ) -> list[external_bookings_models.Ticket]: diff --git a/api/src/pcapi/core/offers/api.py b/api/src/pcapi/core/offers/api.py index 7c585643a75..21d1ef4efff 100644 --- a/api/src/pcapi/core/offers/api.py +++ b/api/src/pcapi/core/offers/api.py @@ -1390,7 +1390,7 @@ def get_shows_remaining_places_from_provider(provider_class: str | None, offer: def _should_try_to_update_offer_stock_quantity(offer: models.Offer) -> bool: # The offer is to update only if it is a cinema offer, and if the venue has a cinema provider - if not offer.subcategory.id == subcategories.SEANCE_CINE.id: + if offer.subcategory.id != subcategories.SEANCE_CINE.id: return False if not offer.lastProviderId: # Manual offer diff --git a/api/src/pcapi/routes/native/v1/bookings.py b/api/src/pcapi/routes/native/v1/bookings.py index 364a7fc7d5f..55fe4c5f53e 100644 --- a/api/src/pcapi/routes/native/v1/bookings.py +++ b/api/src/pcapi/routes/native/v1/bookings.py @@ -80,6 +80,8 @@ def book_offer(user: User, body: BookOfferRequest) -> BookOfferResponse: extra={"offer_id": stock.offer.id, "provider_id": stock.offer.lastProviderId}, ) raise ApiErrors({"code": "CINEMA_PROVIDER_INACTIVE"}) + except external_bookings_exceptions.ExternalBookingTimeoutException: + raise ApiErrors({"code": "PROVIDER_BOOKING_TIMEOUT"}) except external_bookings_exceptions.ExternalBookingException as error: if stock.offer.lastProvider.hasProviderEnableCharlie: logger.info( diff --git a/api/tests/routes/native/v1/bookings_test.py b/api/tests/routes/native/v1/bookings_test.py index dcdbc852bef..34f870a5954 100644 --- a/api/tests/routes/native/v1/bookings_test.py +++ b/api/tests/routes/native/v1/bookings_test.py @@ -16,6 +16,7 @@ from pcapi.core.bookings.models import BookingCancellationReasons from pcapi.core.bookings.models import BookingStatus from pcapi.core.categories import subcategories_v2 as subcategories +from pcapi.core.external_bookings.exceptions import ExternalBookingTimeoutException from pcapi.core.external_bookings.factories import ExternalBookingFactory from pcapi.core.finance import utils as finance_utils from pcapi.core.geography.factories import AddressFactory @@ -124,6 +125,18 @@ def test_inactive_provider(self, mocked_book_offer, client): assert response.status_code == 400 assert response.json["code"] == "CINEMA_PROVIDER_INACTIVE" + @patch("pcapi.core.bookings.api.book_offer") + def test_provider_timeout(self, mocked_book_offer, client): + users_factories.BeneficiaryGrant18Factory(email=self.identifier) + stock = offers_factories.EventStockFactory() + mocked_book_offer.side_effect = ExternalBookingTimeoutException() + + client = client.with_token(self.identifier) + response = client.post("/native/v1/bookings", json={"stockId": stock.id, "quantity": 1}) + + assert response.status_code == 400 + assert response.json["code"] == "PROVIDER_BOOKING_TIMEOUT" + @pytest.mark.parametrize( "subcategoryId,price", [(subcategoryId, 0) for subcategoryId in offer_models.Stock.AUTOMATICALLY_USED_SUBCATEGORIES],