From 3b9f3e89252dc0be9613194fa3076cd0fd738540 Mon Sep 17 00:00:00 2001 From: jbaudet-pass <187622442+jbaudet-pass@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:51:11 +0100 Subject: [PATCH] (PC-34083)[API] fix: pro: draft offer: always check if EAN is used Before publishing or updating a draft offer, the EAN should always be checked: if it is already used by another published (managed by the same venue), the edit or updated operation should fail. --- api/src/pcapi/core/offers/api.py | 9 +++++ api/src/pcapi/routes/pro/offers.py | 2 + .../routes/pro/patch_draft_offer_test.py | 31 +++++++++++++++ .../routes/pro/patch_publish_offer_test.py | 38 +++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/api/src/pcapi/core/offers/api.py b/api/src/pcapi/core/offers/api.py index 8b07fb07bc1..80ba77ea3e4 100644 --- a/api/src/pcapi/core/offers/api.py +++ b/api/src/pcapi/core/offers/api.py @@ -27,6 +27,7 @@ from pcapi.core.bookings.models import BookingCancellationReasons import pcapi.core.bookings.repository as bookings_repository from pcapi.core.categories import subcategories_v2 as subcategories +from pcapi.core.categories.subcategories_v2 import ExtraDataFieldEnum import pcapi.core.criteria.models as criteria_models from pcapi.core.educational import exceptions as educational_exceptions from pcapi.core.educational import models as educational_models @@ -277,6 +278,10 @@ def update_draft_offer(offer: models.Offer, body: offers_schemas.PatchDraftOffer offer.subcategoryId, formatted_extra_data, offer.venue, is_from_private_api=True, offer=offer ) + if offer.extraData: + if ean := offer.extraData.get(ExtraDataFieldEnum.EAN.value): + validation.check_ean_does_not_exist(ean, offer.venue) + for key, value in updates.items(): setattr(offer, key, value) db.session.add(offer) @@ -1043,6 +1048,10 @@ def publish_offer( publication_date = _format_publication_date(publication_date, offer.venue.timezone) validation.check_publication_date(offer, publication_date) + if offer.extraData: + if ean := offer.extraData.get(ExtraDataFieldEnum.EAN.value): + validation.check_ean_does_not_exist(ean, offer.venue) + update_offer_fraud_information(offer, user) if publication_date is not None: diff --git a/api/src/pcapi/routes/pro/offers.py b/api/src/pcapi/routes/pro/offers.py index 76c01ee43d4..e7d560b39d2 100644 --- a/api/src/pcapi/routes/pro/offers.py +++ b/api/src/pcapi/routes/pro/offers.py @@ -373,6 +373,8 @@ def patch_publish_offer( offers_api.publish_offer(offer, current_user, publication_date=body.publicationDate) except exceptions.FutureOfferException as exc: raise api_errors.ApiErrors(exc.errors, status_code=400) + except (exceptions.OfferCreationBaseException, exceptions.OfferEditionBaseException) as exc: + raise api_errors.ApiErrors(exc.errors, status_code=400) return offers_serialize.GetIndividualOfferResponseModel.from_orm(offer) diff --git a/api/tests/routes/pro/patch_draft_offer_test.py b/api/tests/routes/pro/patch_draft_offer_test.py index 27d75484823..c0d46fcabe7 100644 --- a/api/tests/routes/pro/patch_draft_offer_test.py +++ b/api/tests/routes/pro/patch_draft_offer_test.py @@ -14,6 +14,7 @@ import pcapi.core.providers.factories as providers_factories from pcapi.core.providers.repository import get_provider_by_local_class import pcapi.core.users.factories as users_factories +from pcapi.models.offer_mixin import OfferValidationStatus from pcapi.utils.date import format_into_utc_date @@ -674,6 +675,36 @@ def when_trying_to_patch_product(self, client): assert response.status_code == 400 assert response.json["product_id"] == ["Vous ne pouvez pas changer cette information"] + def test_cannot_edit_details_if_ean_is_already_used(self, client): + ean = "0000000000001" + email = "user@example.com" + + user_offerer = offerers_factories.UserOffererFactory(user__email=email) + venue = offerers_factories.VenueFactory( + managingOfferer=user_offerer.offerer, venueTypeCode=VenueTypeCode.RECORD_STORE + ) + + product = offers_factories.ProductFactory(subcategoryId=subcategories.LIVRE_PAPIER.id, extraData={"ean": ean}) + offers_factories.OfferFactory( + subcategoryId=subcategories.LIVRE_PAPIER.id, + venue=venue, + product=product, + validation=OfferValidationStatus.APPROVED, + extraData={"ean": ean}, + ) + + offer = offers_factories.OfferFactory( + venue=venue, isActive=False, validation=OfferValidationStatus.DRAFT, product=product, extraData={"ean": ean} + ) + + data = {"name": "some other name"} + response = client.with_session_auth(email).patch(f"offers/draft/{offer.id}", json=data) + + assert response.status_code == 400 + assert response.json == { + "ean": ["Une offre avec cet EAN existe déjà. Vous pouvez la retrouver dans l’onglet Offres."] + } + @pytest.mark.usefixtures("db_session") class Returns403Test: diff --git a/api/tests/routes/pro/patch_publish_offer_test.py b/api/tests/routes/pro/patch_publish_offer_test.py index a24b809f2ea..5501b4891b2 100644 --- a/api/tests/routes/pro/patch_publish_offer_test.py +++ b/api/tests/routes/pro/patch_publish_offer_test.py @@ -5,6 +5,7 @@ from pcapi.core.categories import subcategories_v2 as subcategories import pcapi.core.offerers.factories as offerers_factories +from pcapi.core.offerers.schemas import VenueTypeCode import pcapi.core.offers.factories as offers_factories import pcapi.core.offers.models as offers_models from pcapi.core.testing import assert_num_queries @@ -242,3 +243,40 @@ def test_patch_publish_future_offer( assert response.json["publication_date"] == ["Impossible de sélectionner une date de publication dans le passé"] offer = offers_models.Offer.query.get(stock.offerId) assert offer.validation == OfferValidationStatus.DRAFT + + def test_cannot_publish_offer_if_ean_is_already_used( + self, + client, + ): + ean = "0000000000001" + email = "user@example.com" + + user_offerer = offerers_factories.UserOffererFactory(user__email=email) + venue = offerers_factories.VenueFactory( + managingOfferer=user_offerer.offerer, venueTypeCode=VenueTypeCode.RECORD_STORE + ) + + product = offers_factories.ProductFactory(subcategoryId=subcategories.LIVRE_PAPIER.id, extraData={"ean": ean}) + offers_factories.OfferFactory( + subcategoryId=subcategories.LIVRE_PAPIER.id, + venue=venue, + product=product, + validation=OfferValidationStatus.APPROVED, + extraData={"ean": ean}, + ) + + offer = offers_factories.StockFactory( + offer__venue=venue, + offer__isActive=False, + offer__validation=OfferValidationStatus.DRAFT, + offer__product=product, + offer__extraData={"ean": ean}, + ).offer + + client = client.with_session_auth(email) + response = client.patch("/offers/publish", json={"id": offer.id}) + + assert response.status_code == 400 + assert response.json == { + "ean": ["Une offre avec cet EAN existe déjà. Vous pouvez la retrouver dans l’onglet Offres."] + }