diff --git a/api/src/pcapi/core/bookings/api.py b/api/src/pcapi/core/bookings/api.py index 3b7d1753aa6..09d91fd95e4 100644 --- a/api/src/pcapi/core/bookings/api.py +++ b/api/src/pcapi/core/bookings/api.py @@ -681,6 +681,7 @@ def cancel_booking_by_beneficiary(user: User, booking: Booking) -> None: def cancel_booking_by_offerer(booking: Booking) -> None: validation.check_booking_can_be_cancelled(booking) _cancel_booking(booking, BookingCancellationReasons.OFFERER, raise_if_error=True) + # if not FeatureToggle.WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING.is_active(): push_notification_job.send_cancel_booking_notification.delay([booking.id]) user_emails_job.send_booking_cancellation_emails_to_user_and_offerer_job.delay(booking.id) diff --git a/api/src/pcapi/core/offers/api.py b/api/src/pcapi/core/offers/api.py index 1b94bc8b85e..3fdd74522dd 100644 --- a/api/src/pcapi/core/offers/api.py +++ b/api/src/pcapi/core/offers/api.py @@ -958,6 +958,7 @@ def _delete_stock(stock: models.Stock, author_id: int | None = None, user_connec transactional_mails.send_booking_cancellation_by_pro_to_beneficiary_email(booking) transactional_mails.send_booking_cancellation_confirmation_by_pro_email(cancelled_bookings) + # if not FeatureToggle.WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING.is_active(): push_notification_job.send_cancel_booking_notification.delay([booking.id for booking in cancelled_bookings]) search.async_index_offer_ids( [stock.offerId], diff --git a/api/src/pcapi/models/feature.py b/api/src/pcapi/models/feature.py index c98694d5791..e4e2c04bbca 100644 --- a/api/src/pcapi/models/feature.py +++ b/api/src/pcapi/models/feature.py @@ -33,6 +33,7 @@ class FeatureToggle(enum.Enum): BENEFICIARY_VALIDATION_AFTER_FRAUD_CHECKS = "Active la validation d'un bénéficiaire via les contrôles de sécurité" DISABLE_ENTERPRISE_API = "Désactiver les appels à l'API entreprise" DISABLE_BOOST_EXTERNAL_BOOKINGS = "Désactiver les réservations externes Boost" + WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING = "Désactiver les notifications d'annulation de réservation" DISABLE_CDS_EXTERNAL_BOOKINGS = "Désactiver les réservations externes CDS" DISABLE_CGR_EXTERNAL_BOOKINGS = "Désactiver les réservations externes CGR" DISABLE_EMS_EXTERNAL_BOOKINGS = "Désactiver les réservations externes EMS" @@ -147,6 +148,7 @@ def nameKey(self) -> str: FEATURES_DISABLED_BY_DEFAULT: tuple[FeatureToggle, ...] = ( FeatureToggle.DISABLE_BOOST_EXTERNAL_BOOKINGS, + FeatureToggle.WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING, FeatureToggle.DISABLE_CDS_EXTERNAL_BOOKINGS, FeatureToggle.DISABLE_CGR_EXTERNAL_BOOKINGS, FeatureToggle.DISABLE_EMS_EXTERNAL_BOOKINGS, diff --git a/api/tests/core/bookings/test_api.py b/api/tests/core/bookings/test_api.py index 58cfeb73678..c91fa8ff575 100644 --- a/api/tests/core/bookings/test_api.py +++ b/api/tests/core/bookings/test_api.py @@ -1438,6 +1438,8 @@ def test_cancel_external_booking_from_charlie_api(self, mocked_cancel_booking): @pytest.mark.usefixtures("db_session") class CancelByOffererTest: + + @override_features(WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING=False) def test_cancel(self): booking = bookings_factories.BookingFactory() @@ -1462,6 +1464,20 @@ def test_cancel(self): "can_be_asynchronously_retried": False, } + @override_features(WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING=True) + @mock.patch("pcapi.workers.push_notification_job.send_cancel_booking_notification.delay") + def test_cancel_with_feature_notification_cancel_booking_activated(self, mocked_cancel_booking_notification_delay): + booking = bookings_factories.BookingFactory() + + api.cancel_booking_by_offerer(booking) + + # cancellation can trigger more than one request to Batch + assert len(push_testing.requests) >= 1 + + assert booking.status is BookingStatus.CANCELLED + assert booking.cancellationReason == BookingCancellationReasons.OFFERER + assert not mocked_cancel_booking_notification_delay.called + def test_raise_if_already_cancelled(self): booking = bookings_factories.CancelledBookingFactory(cancellationReason=BookingCancellationReasons.BENEFICIARY) with pytest.raises(exceptions.BookingIsAlreadyCancelled): diff --git a/api/tests/core/offers/test_api.py b/api/tests/core/offers/test_api.py index 884b64b7315..e54debc4891 100644 --- a/api/tests/core/offers/test_api.py +++ b/api/tests/core/offers/test_api.py @@ -927,6 +927,7 @@ def test_delete_stock_basics(self, mocked_async_index_offer_ids): reason=search.IndexationReason.STOCK_DELETION, ) + @override_features(WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING=False) def test_delete_stock_cancel_bookings_and_send_emails(self): offerer_email = "offerer@example.com" stock = factories.EventStockFactory( @@ -987,6 +988,33 @@ def test_delete_stock_cancel_bookings_and_send_emails(self): "can_be_asynchronously_retried": False, } + @override_features(WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING=True) + @mock.patch("pcapi.workers.push_notification_job.send_cancel_booking_notification.delay") + def test_delete_stock_cancel_bookings_and_send_emails_with_ff_disable_notification_cancel_booking( + self, mocked_cancel_booking_notification + ): + offerer_email = "offerer@example.com" + stock = factories.EventStockFactory( + offer__bookingEmail=offerer_email, + offer__venue__pricing_point="self", + ) + bookings_factories.BookingFactory(stock=stock) + bookings_factories.CancelledBookingFactory(stock=stock) + bookings_factories.UsedBookingFactory(stock=stock) + event4 = finance_factories.UsedBookingFinanceEventFactory(booking__stock=stock) + booking4 = event4.booking + finance_factories.PricingFactory( + event=event4, + booking=booking4, + status=finance_models.PricingStatus.PROCESSED, + ) + + api.delete_stock(stock) + + # cancellation can trigger more than one request to Batch + assert len(push_testing.requests) >= 1 + assert not mocked_cancel_booking_notification.called + def test_can_delete_if_stock_from_provider(self): provider = providers_factories.APIProviderFactory() offer = factories.OfferFactory(lastProvider=provider, idAtProvider="1") diff --git a/api/tests/routes/pro/delete_stock_test.py b/api/tests/routes/pro/delete_stock_test.py index bc42f127b13..7f813cacde8 100644 --- a/api/tests/routes/pro/delete_stock_test.py +++ b/api/tests/routes/pro/delete_stock_test.py @@ -1,8 +1,11 @@ +from unittest import mock + from pcapi.core.bookings import factories as bookings_factory from pcapi.core.bookings.factories import BookingFactory import pcapi.core.offerers.factories as offerers_factories import pcapi.core.offers.factories as offers_factories from pcapi.core.offers.models import OfferValidationStatus +from pcapi.core.testing import override_features from pcapi.core.token import SecureToken from pcapi.core.token.serialization import ConnectAsInternalModel import pcapi.core.users.factories as users_factories @@ -10,6 +13,7 @@ class Returns200Test: + @override_features(WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING=False) def when_current_user_has_rights_on_offer(self, client, db_session): # given offer = offers_factories.OfferFactory() @@ -38,6 +42,30 @@ def when_current_user_has_rights_on_offer(self, client, db_session): "can_be_asynchronously_retried": False, } + @override_features(WIP_DISABLE_NOTIFICATION_CANCEL_BOOKING=True) + @mock.patch("pcapi.workers.push_notification_job.send_cancel_booking_notification.delay") + def when_current_user_has_rights_on_offer_with_disabled_notification_cancel_booking( + self, mocked_cancel_booking_notification, client + ): + # given + offer = offers_factories.OfferFactory() + user_offerer = offerers_factories.UserOffererFactory( + user__email="pro@example.com", + offerer=offer.venue.managingOfferer, + ) + stock = offers_factories.StockFactory(offer=offer) + BookingFactory(stock=stock) + + # when + response = client.with_session_auth("pro@example.com").delete(f"/stocks/{stock.id}") + + # then + assert response.status_code == 200 + assert response.json == {"id": stock.id} + assert stock.isSoftDeleted + assert stock.bookings[0].cancellationUser == user_offerer.user + assert not mocked_cancel_booking_notification.called + def when_current_user_is_connect_as(self, client, db_session): # given offer = offers_factories.OfferFactory()