Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PC-34083)[API] fix: pro: draft offer: always check if EAN is used #15965

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions api/src/pcapi/core/offers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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_other_offer_with_ean_does_not_exist(ean, offer.venue, offer.id)

Comment on lines +281 to +284
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pour l'update je pense que tu peux enlever cette partie. Effectivement dans certain cas le check ne sera pas parfait (si on n'a pas changé les "extraData" et que pourtant entre temps une offre a été publiée avec l'ean qui était dans l'extraData existant) mais ce n'est pas si grave.

Le plus important c'est de bloquer la publication de l'offre.

for key, value in updates.items():
setattr(offer, key, value)
db.session.add(offer)
Expand Down Expand Up @@ -1047,6 +1052,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_other_offer_with_ean_does_not_exist(ean, offer.venue, offer.id)

update_offer_fraud_information(offer, user)

if publication_date is not None:
Expand Down
4 changes: 4 additions & 0 deletions api/src/pcapi/core/offers/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ def _create(
return super()._create(model_class, *args, **kwargs)


class DraftOfferFactory(OfferFactory):
isActive = False

Comment on lines +222 to +224
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je crois qu'il faut que tu rajoutes cela pour que ta factory soit complète.

Suggested change
class DraftOfferFactory(OfferFactory):
isActive = False
class DraftOfferFactory(OfferFactory):
isActive = False
validation = OfferValidationStatus.DRAFT


class ArtistProductLinkFactory(BaseFactory):
class Meta:
model = artist_models.ArtistProductLink
Expand Down
15 changes: 9 additions & 6 deletions api/src/pcapi/core/offers/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -1515,15 +1515,18 @@ def get_next_offer_id_from_database() -> int:
return db.session.execute(sequence)


def has_active_offer_with_ean(ean: str | None, venue: offerers_models.Venue) -> bool:
def has_active_offer_with_ean(ean: str | None, venue: offerers_models.Venue, offer_id: int | None) -> bool:
if not ean:
# We should never be there (an ean or an ean must be given), in case we are alert sentry.
logger.error("Could not search for an offer without ean")
return db.session.query(
models.Offer.query.filter(
models.Offer.venue == venue, models.Offer.isActive.is_(True), models.Offer.extraData["ean"].astext == ean
).exists()
).scalar()
base_query = models.Offer.query.filter(
models.Offer.venue == venue, models.Offer.isActive.is_(True), models.Offer.extraData["ean"].astext == ean
)

if offer_id is not None:
base_query = base_query.filter(models.Offer.id != offer_id)

return db.session.query(base_query.exists()).scalar()


def get_movie_product_by_allocine_id(allocine_id: str) -> models.Product | None:
Expand Down
14 changes: 9 additions & 5 deletions api/src/pcapi/core/offers/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,9 +620,11 @@ def check_offer_extra_data(

try:
ean = extra_data.get(ExtraDataFieldEnum.EAN.value)
if ean and (not offer or (offer.extraData and ean != offer.extraData.get(ExtraDataFieldEnum.EAN.value))):
if ean:
_check_ean_field(extra_data, ExtraDataFieldEnum.EAN.value)
check_ean_does_not_exist(ean, venue)

offer_id = offer.id if offer else None
check_other_offer_with_ean_does_not_exist(ean, venue, offer_id)
except (exceptions.EanFormatException, exceptions.OfferAlreadyExists) as e:
errors.add_client_error(e)

Expand Down Expand Up @@ -657,8 +659,10 @@ def check_product_for_venue_and_subcategory(
)


def check_ean_does_not_exist(ean: str | None, venue: offerers_models.Venue) -> None:
if repository.has_active_offer_with_ean(ean, venue):
def check_other_offer_with_ean_does_not_exist(
ean: str | None, venue: offerers_models.Venue, offer_id: int | None = None
) -> None:
if repository.has_active_offer_with_ean(ean, venue, offer_id):
if ean:
raise exceptions.OfferAlreadyExists("ean")

Expand Down Expand Up @@ -690,7 +694,7 @@ def check_product_cgu_and_offerer(
# We can only check the existence of an offer with this EAN if the offerer has one venue.
if len(not_virtual_venues) == 1:
try:
check_ean_does_not_exist(ean, not_virtual_venues[0])
check_other_offer_with_ean_does_not_exist(ean, not_virtual_venues[0])
except exceptions.OfferAlreadyExists:
raise api_errors.ApiErrors(
errors={"ean": ["Une offre avec cet EAN existe déjà. Vous pouvez la retrouver dans l'onglet Offres."]},
Expand Down
2 changes: 2 additions & 0 deletions api/src/pcapi/routes/pro/offers.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,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)

Expand Down
33 changes: 32 additions & 1 deletion api/tests/routes/pro/patch_draft_offer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -52,7 +53,7 @@ def test_patch_draft_offer(self, client):
def test_patch_draft_offer_without_product_with_new_ean_should_succeed(self, client):
user_offerer = offerers_factories.UserOffererFactory(user__email="[email protected]")
venue = offerers_factories.VenueFactory(managingOfferer=user_offerer.offerer)
offer = offers_factories.OfferFactory(
offer = offers_factories.DraftOfferFactory(
name="Name",
subcategoryId=subcategories.LIVRE_PAPIER.id,
venue=venue,
Expand Down Expand Up @@ -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 = "[email protected]"

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:
Expand Down
38 changes: 38 additions & 0 deletions api/tests/routes/pro/patch_publish_offer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = "[email protected]"

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."]
}
Loading