Skip to content

Commit

Permalink
(PC-29947)[API] feat: send mail instead of notif to warn beneficiary …
Browse files Browse the repository at this point in the history
…of rejected offer
  • Loading branch information
vroullier-pass committed May 24, 2024
1 parent ed8b86f commit 6d49a58
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
def send_booking_cancellation_emails_to_user_and_offerer(
booking: Booking,
reason: BookingCancellationReasons | None,
rejected_by_gcu_incompatibility: bool = False,
rejected_by_fraud_action: bool = False,
) -> None:
if reason is None:
logger.error(
Expand All @@ -35,8 +35,8 @@ def send_booking_cancellation_emails_to_user_and_offerer(
send_booking_cancellation_by_pro_to_beneficiary_email(booking)
send_booking_cancellation_confirmation_by_pro_to_pro_email(booking)
elif reason == BookingCancellationReasons.FRAUD:
if rejected_by_gcu_incompatibility:
send_booking_cancellation_by_pro_to_beneficiary_email(booking, rejected_by_gcu_incompatibility)
if rejected_by_fraud_action:
send_booking_cancellation_by_pro_to_beneficiary_email(booking, rejected_by_fraud_action)
send_booking_cancellation_by_beneficiary_to_pro_email(booking)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

def get_booking_cancellation_by_pro_to_beneficiary_email_data(
booking: Booking,
rejected_by_gcu_incompatibility: bool,
rejected_by_fraud_action: bool,
) -> models.TransactionalEmailData:
stock = booking.stock
offer = stock.offer
Expand Down Expand Up @@ -40,13 +40,13 @@ def get_booking_cancellation_by_pro_to_beneficiary_email_data(
"USER_FIRST_NAME": booking.firstName,
"USER_LAST_NAME": booking.lastName,
"VENUE_NAME": venue_name,
"REJECTED": rejected_by_gcu_incompatibility,
"REJECTED": rejected_by_fraud_action,
},
)


def send_booking_cancellation_by_pro_to_beneficiary_email(
booking: Booking, rejected_by_gcu_incompatibility: bool = False
booking: Booking, rejected_by_fraud_action: bool = False
) -> None:
data = get_booking_cancellation_by_pro_to_beneficiary_email_data(booking, rejected_by_gcu_incompatibility)
data = get_booking_cancellation_by_pro_to_beneficiary_email_data(booking, rejected_by_fraud_action)
mails.send(recipients=[booking.email], data=data)
6 changes: 3 additions & 3 deletions api/src/pcapi/core/offers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ def add_criteria_to_offers(
def reject_inappropriate_products(
eans: list[str],
author: users_models.User | None,
rejected_by_gcu_incompatibility: bool = False,
rejected_by_fraud_action: bool = False,
send_booking_cancellation_emails: bool = True,
) -> bool:
products = models.Product.query.filter(
Expand Down Expand Up @@ -926,7 +926,7 @@ def reject_inappropriate_products(
product.isGcuCompatible = False
product.gcuCompatibilityType = (
models.GcuCompatibilityType.FRAUD_INCOMPATIBLE
if rejected_by_gcu_incompatibility
if rejected_by_fraud_action
else models.GcuCompatibilityType.PROVIDER_INCOMPATIBLE
)

Expand Down Expand Up @@ -954,7 +954,7 @@ def reject_inappropriate_products(
transactional_mails.send_booking_cancellation_emails_to_user_and_offerer(
booking,
reason=BookingCancellationReasons.FRAUD,
rejected_by_gcu_incompatibility=rejected_by_gcu_incompatibility,
rejected_by_fraud_action=rejected_by_fraud_action,
)

logger.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def set_product_gcu_incompatible() -> utils.BackofficeResponse:

if not form.validate():
flash(utils.build_form_error_msg(form), "warning")
elif offers_api.reject_inappropriate_products([form.ean.data], current_user, rejected_by_gcu_incompatibility=True):
elif offers_api.reject_inappropriate_products([form.ean.data], current_user, rejected_by_fraud_action=True):
flash("Le produit a été rendu incompatible aux CGU et les offres ont été désactivées", "success")
else:
flash("Une erreur s'est produite lors de l'opération", "warning")
Expand Down
11 changes: 3 additions & 8 deletions api/src/pcapi/routes/backoffice/offers/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
from pcapi.routes.backoffice.forms import empty as empty_forms
from pcapi.utils import regions as regions_utils
from pcapi.utils import string as string_utils
from pcapi.workers import push_notification_job

from . import forms

Expand Down Expand Up @@ -737,13 +736,9 @@ def _batch_reject_offers(offer_ids: list[int]) -> None:
repository.save(offer)

cancelled_bookings = bookings_api.cancel_bookings_from_rejected_offer(offer)

if cancelled_bookings:
# FIXME: La notification indique que l'offreur a annulé alors que c'est la fraude
# TODO(PC-23550): SPIKE avec marketing https://passculture.atlassian.net/browse/PC-23550
# Il faudrait utiliser send_booking_cancellation_emails_to_user_and_offerer et retirer cette notification soit la déplacer dedans, mais un mail est mieux (TBD)
push_notification_job.send_cancel_booking_notification.delay(
[booking.id for booking in cancelled_bookings]
for booking in cancelled_bookings:
transactional_mails.send_booking_cancellation_by_pro_to_beneficiary_email(
booking, rejected_by_fraud_action=True
)

repository.save(offer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ def test_should_return_event_data_when_booking_is_on_an_event(self):
)

# When
email_data = get_booking_cancellation_by_pro_to_beneficiary_email_data(
booking, rejected_by_gcu_incompatibility=False
)
email_data = get_booking_cancellation_by_pro_to_beneficiary_email_data(booking, rejected_by_fraud_action=False)

# Then
assert email_data.params == {
Expand Down Expand Up @@ -99,9 +97,7 @@ def test_should_return_thing_data_when_booking_is_on_a_thing(self):
)

# When
email_data = get_booking_cancellation_by_pro_to_beneficiary_email_data(
booking, rejected_by_gcu_incompatibility=False
)
email_data = get_booking_cancellation_by_pro_to_beneficiary_email_data(booking, rejected_by_fraud_action=False)

# Then
assert email_data.template == models.Template(
Expand Down Expand Up @@ -134,9 +130,7 @@ def test_should_return_thing_data_when_booking_is_on_an_online_offer(self):
)

# When
email_data = get_booking_cancellation_by_pro_to_beneficiary_email_data(
booking, rejected_by_gcu_incompatibility=False
)
email_data = get_booking_cancellation_by_pro_to_beneficiary_email_data(booking, rejected_by_fraud_action=False)

# Then
assert email_data.params == {
Expand Down Expand Up @@ -165,7 +159,7 @@ def test_should_not_display_the_price_when_booking_is_on_a_free_offer(self):

# When
sendiblue_data = get_booking_cancellation_by_pro_to_beneficiary_email_data(
booking, rejected_by_gcu_incompatibility=False
booking, rejected_by_fraud_action=False
)

# Then
Expand All @@ -182,7 +176,7 @@ def test_should_display_the_price_multiplied_by_quantity_when_it_is_a_duo_offer(

# When
sendiblue_data = get_booking_cancellation_by_pro_to_beneficiary_email_data(
booking, rejected_by_gcu_incompatibility=False
booking, rejected_by_fraud_action=False
)

# Then
Expand Down
48 changes: 44 additions & 4 deletions api/tests/routes/backoffice/offers_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
import datetime
import decimal
from io import BytesIO
Expand All @@ -21,6 +22,7 @@
from pcapi.core.finance import models as finance_models
from pcapi.core.geography import factories as geography_factories
from pcapi.core.mails import testing as mails_testing
from pcapi.core.mails.transactional.sendinblue_template_ids import TransactionalEmail
from pcapi.core.offerers import factories as offerers_factories
from pcapi.core.offerers import models as offerers_models
from pcapi.core.offers import factories as offers_factories
Expand Down Expand Up @@ -1401,6 +1403,11 @@ class RejectOfferTest(PostEndpointHelper):

def test_reject_offer(self, legit_user, authenticated_client):
offer_to_reject = offers_factories.OfferFactory(validation=offers_models.OfferValidationStatus.APPROVED)
confirmed_booking = bookings_factories.BookingFactory(
user=users_factories.BeneficiaryGrant18Factory(),
stock__offer=offer_to_reject,
status=BookingStatus.CONFIRMED,
)

response = self.post_to_endpoint(authenticated_client, offer_id=offer_to_reject.id)
assert response.status_code == 303
Expand All @@ -1414,6 +1421,17 @@ def test_reject_offer(self, legit_user, authenticated_client):
assert offer_to_reject.lastValidationDate.date() == datetime.date.today()
assert offer_to_reject.lastValidationPrice is None

assert len(mails_testing.outbox) == 2
assert mails_testing.outbox[0]["To"] == confirmed_booking.user.email
assert mails_testing.outbox[0]["template"] == dataclasses.asdict(
TransactionalEmail.BOOKING_CANCELLATION_BY_PRO_TO_BENEFICIARY.value
)
assert mails_testing.outbox[0]["params"]["REJECTED"] is True
assert mails_testing.outbox[1]["To"] == offer_to_reject.venue.bookingEmail
assert mails_testing.outbox[1]["template"] == dataclasses.asdict(
TransactionalEmail.OFFER_VALIDATED_TO_REJECTED_TO_PRO.value
)


class GetRejectOfferFormTest(GetEndpointHelper):
endpoint = "backoffice_web.offer.get_reject_offer_form"
Expand Down Expand Up @@ -1462,26 +1480,48 @@ class BatchOfferRejectTest(PostEndpointHelper):

def test_batch_reject_offers(self, legit_user, authenticated_client):
beneficiary = users_factories.BeneficiaryGrant18Factory()
offers = offers_factories.OfferFactory.create_batch(3, validation=offers_models.OfferValidationStatus.DRAFT)
draft_offer = offers_factories.OfferFactory(validation=offers_models.OfferValidationStatus.DRAFT)
pending_offer = offers_factories.OfferFactory(validation=offers_models.OfferValidationStatus.PENDING)
confirmed_offer = offers_factories.OfferFactory(validation=offers_models.OfferValidationStatus.APPROVED)
confirmed_booking = bookings_factories.BookingFactory(
user=beneficiary, stock__offer=offers[0], status=BookingStatus.CONFIRMED
user=beneficiary, stock__offer=confirmed_offer, status=BookingStatus.CONFIRMED
)
parameter_ids = ",".join(str(offer.id) for offer in offers)
parameter_ids = ",".join(str(offer.id) for offer in [draft_offer, pending_offer, confirmed_offer])

assert confirmed_booking.status == BookingStatus.CONFIRMED

response = self.post_to_endpoint(authenticated_client, form={"object_ids": parameter_ids})

assert confirmed_booking.status == BookingStatus.CANCELLED
assert response.status_code == 303
for offer in offers:
for offer in [draft_offer, pending_offer, confirmed_offer]:
db.session.refresh(offer)
assert offer.lastValidationDate.strftime("%d/%m/%Y") == datetime.date.today().strftime("%d/%m/%Y")
assert offer.isActive is False
assert offer.lastValidationType is OfferValidationType.MANUAL
assert offer.validation is offers_models.OfferValidationStatus.REJECTED
assert offer.lastValidationAuthor == legit_user

assert len(mails_testing.outbox) == 4
emails_dict = {email_data["To"]: email_data for email_data in mails_testing.outbox}
assert confirmed_booking.user.email in emails_dict
assert emails_dict[confirmed_booking.user.email]["template"] == dataclasses.asdict(
TransactionalEmail.BOOKING_CANCELLATION_BY_PRO_TO_BENEFICIARY.value
)
assert emails_dict[confirmed_booking.user.email]["params"]["REJECTED"] is True
assert draft_offer.venue.bookingEmail in emails_dict
assert emails_dict[draft_offer.venue.bookingEmail]["template"] == dataclasses.asdict(
TransactionalEmail.OFFER_REJECTION_TO_PRO.value
)
assert pending_offer.venue.bookingEmail in emails_dict
assert emails_dict[pending_offer.venue.bookingEmail]["template"] == dataclasses.asdict(
TransactionalEmail.OFFER_PENDING_TO_REJECTED_TO_PRO.value
)
assert confirmed_offer.venue.bookingEmail in emails_dict
assert emails_dict[confirmed_offer.venue.bookingEmail]["template"] == dataclasses.asdict(
TransactionalEmail.OFFER_VALIDATED_TO_REJECTED_TO_PRO.value
)


class GetOfferDetailsTest(GetEndpointHelper):
endpoint = "backoffice_web.offer.get_offer_details"
Expand Down

0 comments on commit 6d49a58

Please sign in to comment.