Skip to content

Commit

Permalink
(PC-31934)[BO] fix: reject product by EAN as incompatible did not sav…
Browse files Browse the repository at this point in the history
…e in database
  • Loading branch information
prouzet committed Sep 20, 2024
1 parent 2c7a847 commit 5da6c23
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 13 deletions.
7 changes: 6 additions & 1 deletion api/src/pcapi/routes/backoffice/multiple_offers/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pcapi.core.offers import api as offers_api
from pcapi.core.offers import models as offers_models
from pcapi.core.permissions import models as perm_models
from pcapi.models import db
from pcapi.models.offer_mixin import OfferValidationStatus
from pcapi.repository import atomic

Expand Down Expand Up @@ -139,17 +140,21 @@ def add_criteria_to_offers() -> utils.BackofficeResponse:
return redirect(url_for(".search_multiple_offers", ean=form.ean.data), code=303)


# FIXME (prouzet, 2024-09-20): @atomic has been temporarily removed because of side effects:
# changes are not persistent in database when NonCancellablePricingError at booking cancellation time.
# => Restore @atomic after deeper investigation, full understanding and fix.
@multiple_offers_blueprint.route("/set-product-gcu-incompatible", methods=["POST"])
@atomic()
@utils.permission_required(perm_models.Permissions.PRO_FRAUD_ACTIONS)
def set_product_gcu_incompatible() -> utils.BackofficeResponse:
form = forms.HiddenEanForm()

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_fraud_action=True):
db.session.commit()
flash("Le produit a été rendu incompatible aux CGU et les offres ont été désactivées", "success")
else:
db.session.rollback()
flash("Une erreur s'est produite lors de l'opération", "warning")

return redirect(url_for(".search_multiple_offers", ean=form.ean.data), code=303)
91 changes: 79 additions & 12 deletions api/tests/routes/backoffice/multiple_offers_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import dataclasses
import datetime

import factory
from flask import url_for
import pytest

from pcapi.core.bookings import factories as booking_factory
from pcapi.core.bookings import factories as bookings_factories
from pcapi.core.bookings import models as bookings_models
from pcapi.core.categories import subcategories_v2 as subcategories
from pcapi.core.criteria import factories as criteria_factories
from pcapi.core.criteria import models as criteria_models
from pcapi.core.finance import factories as finance_factories
from pcapi.core.finance import models as finance_models
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
Expand All @@ -17,6 +21,7 @@
from pcapi.core.permissions import models as perm_models
from pcapi.core.providers import factories as providers_factories
from pcapi.core.testing import assert_num_queries
from pcapi.models import db
from pcapi.models.offer_mixin import OfferValidationStatus
from pcapi.models.offer_mixin import OfferValidationType

Expand All @@ -27,13 +32,13 @@


pytestmark = [
pytest.mark.usefixtures("db_session"),
pytest.mark.backoffice,
]

# TODO Brice Bosson 27/09/2023 : remove products from manual offers when products will be restricted to synchronized offers only


@pytest.mark.usefixtures("db_session")
class MultipleOffersHomeTest(GetEndpointHelper):
endpoint = "backoffice_web.multiple_offers.multiple_offers_home"
needed_permission = perm_models.Permissions.READ_OFFERS
Expand All @@ -44,6 +49,7 @@ def test_get_search_form(self, authenticated_client):
assert response.status_code == 200


@pytest.mark.usefixtures("db_session")
class SearchMultipleOffersTest(GetEndpointHelper):
endpoint = "backoffice_web.multiple_offers.search_multiple_offers"
needed_permission = perm_models.Permissions.READ_OFFERS
Expand Down Expand Up @@ -231,6 +237,7 @@ def test_search_product_from_ean_with_invalid_ean(self, authenticated_client):
assert "La recherche ne correspond pas au format d'un EAN" in html_parser.extract_alert(response.data)


@pytest.mark.usefixtures("db_session")
class AddCriteriaToOffersButtonTest(button_helpers.ButtonHelper):
needed_permission = perm_models.Permissions.MULTIPLE_OFFERS_ACTIONS
button_label = "Tag des offres"
Expand All @@ -243,6 +250,7 @@ def path(self):
return url_for("backoffice_web.multiple_offers.search_multiple_offers", ean="9781234567890")


@pytest.mark.usefixtures("db_session")
class AddCriteriaToOffersTest(PostEndpointHelper):
endpoint = "backoffice_web.multiple_offers.add_criteria_to_offers"
endpoint_kwargs = {"ean": "9781234567890"}
Expand Down Expand Up @@ -281,6 +289,7 @@ def test_edit_product_offers_criteria_from_ean_without_offers(self, authenticate
assert response.status_code == 303


@pytest.mark.usefixtures("db_session")
class SetProductGcuIncompatibleButtonTest(button_helpers.ButtonHelper):
needed_permission = perm_models.Permissions.PRO_FRAUD_ACTIONS
button_label = "Rendre le livre et les offres associées incompatibles avec les CGU"
Expand All @@ -297,6 +306,7 @@ class SetProductGcuIncompatibleTest(PostEndpointHelper):
endpoint_kwargs = {"ean": "9781234567890"}
needed_permission = perm_models.Permissions.PRO_FRAUD_ACTIONS

@pytest.mark.usefixtures("db_session")
@pytest.mark.parametrize(
"validation_status,gcu_compatibility_type",
[
Expand Down Expand Up @@ -343,29 +353,86 @@ def test_edit_product_gcu_compatibility(self, authenticated_client, validation_s
assert offer.lastValidationType == OfferValidationType.CGU_INCOMPATIBLE_PRODUCT
assert datetime.datetime.utcnow() - offer.lastValidationDate < datetime.timedelta(seconds=5)

def test_send_mail_when_edit_product_gcu_compatibility(self, authenticated_client):
@pytest.mark.usefixtures("db_session")
def test_cancel_bookings_and_send_transactional_email(self, authenticated_client):
provider = providers_factories.APIProviderFactory()
product_1 = offers_factories.ThingProductFactory(
description="premier produit inapproprié",
product = offers_factories.ThingProductFactory(
description="Produit inapproprié",
extraData={"ean": "9781234567890"},
gcuCompatibilityType=offers_models.GcuCompatibilityType.COMPATIBLE,
lastProvider=provider,
)
venue = offerers_factories.VenueFactory()
offer1 = offers_factories.OfferFactory(
product=product_1, venue=venue, validation=OfferValidationStatus.APPROVED
)
offers_factories.OfferFactory(product=product_1, venue=venue)

stock = offers_factories.StockFactory(offer=offer1, bookings=[booking_factory.BookingFactory()])
offer1 = offers_factories.OfferFactory(product=product, venue=venue, validation=OfferValidationStatus.APPROVED)
offers_factories.OfferFactory(product=product, venue=venue)
booking = bookings_factories.BookingFactory(stock__offer=offer1)

response = self.post_to_endpoint(authenticated_client, form={"ean": "9781234567890"})

assert response.status_code == 303

assert booking.status == bookings_models.BookingStatus.CANCELLED

assert len(mails_testing.outbox) == 1
assert mails_testing.outbox[0]["To"] == stock.bookings[0].email
assert mails_testing.outbox[0]["To"] == booking.email
assert mails_testing.outbox[0]["template"] == dataclasses.asdict(
TransactionalEmail.BOOKING_CANCELLATION_BY_PRO_TO_BENEFICIARY.value
)
assert mails_testing.outbox[0]["params"]["REJECTED"] == True

@pytest.mark.usefixtures("clean_database")
def test_with_bookings_finance_events_and_pricings(self, authenticated_client):
product = offers_factories.ThingProductFactory(
description="Produit inapproprié",
extraData={"ean": "9781234567890"},
gcuCompatibilityType=offers_models.GcuCompatibilityType.COMPATIBLE,
lastProvider=providers_factories.APIProviderFactory(),
)
offer = offers_factories.OfferFactory(product=product)
stock = offers_factories.StockFactory(offer=offer)

booking = bookings_factories.UsedBookingFactory(stock=stock)
finance_event = finance_factories.UsedBookingFinanceEventFactory(booking=booking)
finance_factories.PricingFactory(
event=finance_event, booking=booking, status=finance_models.PricingStatus.CANCELLED
)

booking = bookings_factories.CancelledBookingFactory(
stock=stock, dateUsed=factory.LazyFunction(datetime.datetime.utcnow)
)
finance_event = finance_factories.UsedBookingFinanceEventFactory(
booking=booking,
venue=offer.venue,
pricingPoint=offer.venue,
status=finance_models.FinanceEventStatus.CANCELLED,
)
finance_factories.PricingFactory(
event=finance_event, booking=booking, status=finance_models.PricingStatus.CANCELLED
)

booking = bookings_factories.ReimbursedBookingFactory(stock=stock)
finance_event = finance_factories.UsedBookingFinanceEventFactory(
booking=booking, venue=offer.venue, pricingPoint=offer.venue
)
finance_factories.PricingFactory(
event=finance_event, booking=booking, status=finance_models.PricingStatus.INVOICED
)

response = self.post_to_endpoint(authenticated_client, form={"ean": "9781234567890"})

assert response.status_code == 303
assert response.location == url_for(
"backoffice_web.multiple_offers.search_multiple_offers", ean="9781234567890", _external=True
)

# ensure that we check that everything is committed, when using @atomic, transaction and with atomic (PC-31934)
db.session.close()
db.session.begin()

product = offers_models.Product.query.one()
offer = offers_models.Offer.query.one()

assert product.gcuCompatibilityType == offers_models.GcuCompatibilityType.FRAUD_INCOMPATIBLE
assert offer.validation == offers_models.OfferValidationStatus.REJECTED
assert offer.lastValidationType == OfferValidationType.CGU_INCOMPATIBLE_PRODUCT
assert datetime.datetime.utcnow() - offer.lastValidationDate < datetime.timedelta(seconds=5)

0 comments on commit 5da6c23

Please sign in to comment.