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-31217)[API]feat: add new validation for post and patch offer regarding EAN code #13872

Merged
merged 1 commit into from
Sep 4, 2024
Merged
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
8 changes: 7 additions & 1 deletion api/src/pcapi/core/offers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,10 @@ def _get_coherent_venue_with_subcategory(


def create_draft_offer(
body: offers_schemas.PostDraftOfferBodyModel, venue: offerers_models.Venue, is_from_private_api: bool = True
body: offers_schemas.PostDraftOfferBodyModel,
venue: offerers_models.Venue,
product: offers_models.Product | None = None,
is_from_private_api: bool = True,
) -> models.Offer:
validation.check_offer_subcategory_is_valid(body.subcategory_id)
if feature.FeatureToggle.WIP_SUGGESTED_SUBCATEGORIES.is_active():
Expand All @@ -219,6 +222,8 @@ def create_draft_offer(
body.extra_data = _format_extra_data(body.subcategory_id, body.extra_data) or {}
validation.check_offer_extra_data(body.subcategory_id, body.extra_data, venue, is_from_private_api)

validation.check_product_for_venue_and_subcategory(product, body.subcategory_id, venue.venueTypeCode)

fields = {key: value for key, value in body.dict(by_alias=True).items() if key != "venueId"}
fields.update(_get_accessibility_compliance_fields(venue))
offer = models.Offer(
Expand All @@ -227,6 +232,7 @@ def create_draft_offer(
offererAddress=venue.offererAddress,
isActive=False,
validation=models.OfferValidationStatus.DRAFT,
product=product,
)
db.session.add(offer)

Expand Down
4 changes: 4 additions & 0 deletions api/src/pcapi/core/offers/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ class EanFormatException(OfferCreationBaseException):
pass


class ProductNotFoundForOfferCreation(OfferCreationBaseException):
pass


class FutureOfferException(OfferCreationBaseException):
pass

Expand Down
2 changes: 1 addition & 1 deletion api/src/pcapi/core/offers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ def __table_args__(self):
lastValidationPrice: decimal.Decimal = sa.Column(sa.Numeric(10, 2), nullable=True)
name: str = sa.Column(sa.String(140), nullable=False)
priceCategories: sa_orm.Mapped[list["PriceCategory"]] = sa.orm.relationship("PriceCategory", back_populates="offer")
product: Product = sa.orm.relationship(Product, backref="offers")
product: sa_orm.Mapped["Product | None"] = sa.orm.relationship(Product, backref="offers")
productId: int = sa.Column(sa.BigInteger, sa.ForeignKey("product.id"), index=True, nullable=True)
rankingWeight = sa.Column(sa.Integer, nullable=True)
subcategoryId: str = sa.Column(sa.Text, nullable=False, index=True)
Expand Down
7 changes: 7 additions & 0 deletions api/src/pcapi/core/offers/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pcapi.routes.serialization import BaseModel
from pcapi.serialization.utils import to_camel
from pcapi.validation.routes.offers import check_offer_name_length_is_valid
from pcapi.validation.routes.offers import check_offer_product_update


class PostDraftOfferBodyModel(BaseModel):
Expand All @@ -19,6 +20,7 @@ class PostDraftOfferBodyModel(BaseModel):
description: str | None = None
extra_data: typing.Any = None
duration_minutes: int | None = None
product_id: int | None

@validator("name", pre=True)
def validate_name(cls, name: str, values: dict) -> str:
Expand All @@ -42,6 +44,11 @@ def validate_name(cls, name: str, values: dict) -> str:
check_offer_name_length_is_valid(name)
return name

@validator("extra_data", pre=True)
def validate_extra_data(cls, extra_data: dict[str, typing.Any]) -> dict[str, typing.Any]:
check_offer_product_update(extra_data)
return extra_data

class Config:
alias_generator = to_camel
extra = "forbid"
Expand Down
21 changes: 21 additions & 0 deletions api/src/pcapi/core/offers/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from pcapi.domain import show_types
from pcapi.models import api_errors
from pcapi.models.offer_mixin import OfferValidationStatus
from pcapi.routes.native.v1.serialization.offerers import VenueTypeCode
from pcapi.routes.public.books_stocks import serialization
from pcapi.utils import date

Expand Down Expand Up @@ -571,6 +572,26 @@ def check_offer_extra_data(
raise errors


def check_product_for_venue_and_subcategory(
product: models.Product | None,
subcategory_id: str | None,
venue_type_code: VenueTypeCode,
) -> None:
if venue_type_code != VenueTypeCode.RECORD_STORE:
return

if subcategory_id not in [
subcategories.SUPPORT_PHYSIQUE_MUSIQUE_CD.id,
subcategories.SUPPORT_PHYSIQUE_MUSIQUE_VINYLE.id,
]:
return
if product is not None:
return
raise exceptions.ProductNotFoundForOfferCreation(
ExtraDataFieldEnum.EAN.value, "EAN non reconnu. Assurez-vous qu'il n'y ait pas d'erreur de saisie."
)


def check_ean_does_not_exist(ean: str | None, venue: offerers_models.Venue) -> None:
if repository.has_active_offer_with_ean(ean, venue):
if ean:
Expand Down
4 changes: 3 additions & 1 deletion api/src/pcapi/core/search/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,9 @@ def get_last_x_days_booking_count_by_offer(offers: abc.Iterable[offers_models.Of
default_dict = get_offers_booking_count_by_id([offer.id for offer in offers_without_product])

for offer in offers_with_product:
default_dict[offer.id] = offer.product.last_30_days_booking if offer.product.last_30_days_booking else 0
default_dict[offer.id] = (
offer.product.last_30_days_booking if offer.product and offer.product.last_30_days_booking else 0
)

return default_dict

Expand Down
10 changes: 9 additions & 1 deletion api/src/pcapi/routes/pro/offers.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,19 @@ def post_draft_offer(
.options(sqla.orm.joinedload(offerers_models.Venue.offererAddress))
.first_or_404()
)

ean_code = body.extra_data.get("ean", None) if body.extra_data is not None else None
product = (
models.Product.query.filter(models.Product.extraData["ean"].astext == ean_code)
.filter(models.Product.id == body.product_id)
.one_or_none()
)

rest.check_user_has_access_to_offerer(current_user, venue.managingOffererId)

try:
with repository.transaction():
offer = offers_api.create_draft_offer(body, venue)
offer = offers_api.create_draft_offer(body, venue, product)
except exceptions.OfferCreationBaseException as error:
raise api_errors.ApiErrors(error.errors, status_code=400)

Expand Down
1 change: 1 addition & 0 deletions api/src/pcapi/routes/serialization/offers_serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ class GetIndividualOfferResponseModel(BaseModel, AccessibilityComplianceMixin):
name: str
priceCategories: list[PriceCategoryResponseModel] | None
subcategoryId: SubcategoryIdEnum
productId: int | None
thumbUrl: str | None
externalTicketOfficeUrl: str | None
url: str | None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def create_data_event_occurrences(event_offers_by_name: dict[str, offers_models.
price_categories: dict[decimal.Decimal, offers_models.PriceCategory] = {}
for index, beginning_datetime in enumerate(EVENT_OCCURRENCE_BEGINNING_DATETIMES, start=1):
name = "{} / {} / {} ".format(
event_offer_with_occurrences.product.name,
event_offer_with_occurrences.product.name if event_offer_with_occurrences.product else "",
event_offer_with_occurrences.venue.name,
beginning_datetime.strftime(date_utils.DATE_ISO_FORMAT),
)
Expand All @@ -52,7 +52,10 @@ def create_data_event_occurrences(event_offers_by_name: dict[str, offers_models.
if price_counter > 2:
price = price + price_counter

if event_offer_with_occurrences.product.subcategoryId in subcategories.ACTIVATION_SUBCATEGORIES:
if (
event_offer_with_occurrences.product
and event_offer_with_occurrences.product.subcategoryId in subcategories.ACTIVATION_SUBCATEGORIES
):
price = decimal.Decimal(0)

if price in price_categories:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ def create_data_event_stocks(event_occurrences_by_name: dict[str, EventOccurrenc
price = price + price_counter
short_names_to_increase_price.append(short_name)

if event_occurrence_with_stocks.offer.product.subcategoryId in subcategories.ACTIVATION_SUBCATEGORIES:
if (
event_occurrence_with_stocks.offer.product
and event_occurrence_with_stocks.offer.product.subcategoryId in subcategories.ACTIVATION_SUBCATEGORIES
):
price = Decimal(0)

name = event_occurrence_with_stocks_name + " / " + str(available) + " / " + str(price) + " / " + "DATA"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def create_e2e_event_occurrences(event_offers_by_name: dict[str, offers_models.O
price_categories: dict[decimal.Decimal, offers_models.PriceCategory] = {}
for index, beginning_datetime in enumerate(EVENT_OCCURRENCE_BEGINNING_DATETIMES, start=1):
name = "{} / {} / {} ".format(
event_offer_with_occurrences.product.name,
event_offer_with_occurrences.product.name if event_offer_with_occurrences.product else "",
event_offer_with_occurrences.venue.name,
beginning_datetime.strftime(date_utils.DATE_ISO_FORMAT),
)
Expand All @@ -53,7 +53,10 @@ def create_e2e_event_occurrences(event_offers_by_name: dict[str, offers_models.O
if price_counter > 2:
price = price + price_counter

if event_offer_with_occurrences.product.subcategoryId in subcategories.ACTIVATION_SUBCATEGORIES:
if (
event_offer_with_occurrences.product
and event_offer_with_occurrences.product.subcategoryId in subcategories.ACTIVATION_SUBCATEGORIES
):
price = decimal.Decimal(0)

if price in price_categories:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def create_industrial_event_occurrences(
price_categories: dict[decimal.Decimal, offers_models.PriceCategory] = {}
for index, beginning_datetime in enumerate(EVENT_OCCURRENCE_BEGINNING_DATETIMES, start=1):
name = "{} / {} / {} ".format(
event_offer_with_occurrences.product.name,
event_offer_with_occurrences.product.name if event_offer_with_occurrences.product else "",
event_offer_with_occurrences.venue.name,
beginning_datetime.strftime(date_utils.DATE_ISO_FORMAT),
)
Expand All @@ -55,7 +55,10 @@ def create_industrial_event_occurrences(
if price_counter > 2:
price = price + price_counter

if event_offer_with_occurrences.product.subcategoryId in subcategories.ACTIVATION_SUBCATEGORIES:
if (
event_offer_with_occurrences.product
and event_offer_with_occurrences.product.subcategoryId in subcategories.ACTIVATION_SUBCATEGORIES
):
price = decimal.Decimal(0)

if price in price_categories:
Expand Down
9 changes: 9 additions & 0 deletions api/src/pcapi/validation/routes/offers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

from pcapi.models.api_errors import ApiErrors


Expand All @@ -15,3 +17,10 @@ def check_collective_offer_name_length_is_valid(offer_name: str) -> None:
api_error = ApiErrors()
api_error.add_error("name", "Le titre de l’offre doit faire au maximum 110 caractères.")
raise api_error


def check_offer_product_update(extra_data: dict[str, Any]) -> None:
if extra_data.get("ean", False):
api_error = ApiErrors()
api_error.add_error("ean", "Vous ne pouvez pas changer cette information")
raise api_error
2 changes: 1 addition & 1 deletion api/tests/core/offers/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ def test_post_draft_offer_body_model(self):
class PatchDraftOfferBodyModelTest:
def test_patch_draft_offer_body_model(self):
_ = PatchDraftOfferBodyModel(
name="Name", description="description", extraData={"ean": "12345678910111"}, durationMinutes=12
name="Name", description="description", extraData={"artist": "An-2"}, durationMinutes=12
)
1 change: 1 addition & 0 deletions api/tests/routes/pro/get_offer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def test_returns_an_event_stock(self, client):
"bookingsCount": 0,
"bookingEmail": "[email protected]",
"dateCreated": "2020-10-15T00:00:00Z",
"productId": None,
"publicationDate": None,
"description": "Tatort, but slower",
"durationMinutes": 60,
Expand Down
39 changes: 39 additions & 0 deletions api/tests/routes/pro/patch_draft_offer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pcapi.core.offers.factories as offers_factories
from pcapi.core.offers.models import Offer
import pcapi.core.users.factories as users_factories
from pcapi.routes.native.v1.serialization.offerers import VenueTypeCode
from pcapi.utils.date import format_into_utc_date


Expand Down Expand Up @@ -36,6 +37,7 @@ def test_patch_draft_offer(self, client):
assert response.status_code == 200
assert response.json["id"] == offer.id
assert response.json["venue"]["id"] == offer.venue.id
assert response.json["productId"] == None

updated_offer = Offer.query.get(offer.id)
assert updated_offer.name == "New name"
Expand Down Expand Up @@ -86,6 +88,43 @@ def when_trying_to_patch_forbidden_attributes(self, client):
for key in forbidden_keys:
assert key in response.json

def when_trying_to_patch_ean(self, client):
user_offerer = offerers_factories.UserOffererFactory(user__email="[email protected]")
venue = offerers_factories.VenueFactory(
managingOfferer=user_offerer.offerer, venueTypeCode=VenueTypeCode.RECORD_STORE
)
offer = offers_factories.OfferFactory(
name="Name",
subcategoryId=subcategories.LIVRE_PAPIER.id,
venue=venue,
description="description",
)

data = {"extraData": {"ean": "1234567891234"}}
response = client.with_session_auth("[email protected]").patch(f"offers/draft/{offer.id}", json=data)

assert response.status_code == 400
assert response.json["ean"] == ["Vous ne pouvez pas changer cette information"]

def when_trying_to_patch_product(self, client):
user_offerer = offerers_factories.UserOffererFactory(user__email="[email protected]")
venue = offerers_factories.VenueFactory(
managingOfferer=user_offerer.offerer, venueTypeCode=VenueTypeCode.RECORD_STORE
)
offer = offers_factories.OfferFactory(
name="Name",
subcategoryId=subcategories.LIVRE_PAPIER.id,
venue=venue,
description="description",
)
product = offers_factories.ProductFactory(subcategoryId=subcategories.LIVRE_PAPIER.id)

data = {"product_id": product.id}
response = client.with_session_auth("[email protected]").patch(f"offers/draft/{offer.id}", json=data)

assert response.status_code == 400
assert response.json["product_id"] == ["Vous ne pouvez pas changer cette information"]


@pytest.mark.usefixtures("db_session")
class Returns403Test:
Expand Down
Loading
Loading