Exemple #1
0
def update_pending_offer_validation(
        offer: Offer, validation_status: OfferValidationStatus) -> bool:
    offer = offer_queries.get_offer_by_id(offer.id)
    if offer.validation != OfferValidationStatus.PENDING:
        logger.info(
            "Offer validation status cannot be updated, initial validation status is not PENDING. %s",
            extra={"offer": offer.id},
        )
        return False
    offer.validation = validation_status
    if validation_status == OfferValidationStatus.APPROVED:
        offer.isActive = True

    try:
        db.session.commit()
    except Exception as exception:  # pylint: disable=broad-except
        logger.exception(
            "Could not update offer validation status: %s",
            extra={
                "offer": offer.id,
                "validation_status": validation_status,
                "exc": str(exception)
            },
        )
        return False
    search.async_index_offer_ids([offer.id])
    logger.info("Offer validation status updated", extra={"offer": offer.id})
    return True
Exemple #2
0
    def after_model_change(self,
                           form: wtforms.Form,
                           offer: Offer,
                           is_created: bool = False) -> None:
        if hasattr(form, "validation"):
            previous_validation = form._fields["validation"].object_data
            new_validation = offer.validation
            if previous_validation != new_validation:
                offer.lastValidationDate = datetime.utcnow()
                if new_validation == OfferValidationStatus.APPROVED:
                    offer.isActive = True
                if new_validation == OfferValidationStatus.REJECTED:
                    offer.isActive = False
                    cancelled_bookings = cancel_bookings_from_rejected_offer(
                        offer)
                    if cancelled_bookings:
                        send_cancel_booking_notification.delay(
                            [booking.id for booking in cancelled_bookings])

                repository.save(offer)

                recipients = ([
                    offer.venue.bookingEmail
                ] if offer.venue.bookingEmail else [
                    recipient.user.email
                    for recipient in offer.venue.managingOfferer.UserOfferers
                ])
                send_offer_validation_status_update_email(
                    offer, new_validation, recipients)
                send_offer_validation_notification_to_administration(
                    new_validation, offer)

                flash("Le statut de l'offre a bien été modifié", "success")

        search.async_index_offer_ids([offer.id])
Exemple #3
0
def update_offer(  # pylint: disable=redefined-builtin
    offer: Offer,
    bookingEmail: str = UNCHANGED,
    description: str = UNCHANGED,
    isNational: bool = UNCHANGED,
    name: str = UNCHANGED,
    extraData: dict = UNCHANGED,
    type: str = UNCHANGED,
    url: str = UNCHANGED,
    withdrawalDetails: str = UNCHANGED,
    isActive: bool = UNCHANGED,
    isDuo: bool = UNCHANGED,
    durationMinutes: int = UNCHANGED,
    mediaUrls: List[str] = UNCHANGED,
    ageMin: int = UNCHANGED,
    ageMax: int = UNCHANGED,
    conditions: str = UNCHANGED,
    venueId: str = UNCHANGED,
    productId: str = UNCHANGED,
) -> Offer:
    # fmt: off
    modifications = {
        field: new_value
        for field, new_value in locals().items()
        if field != 'offer'
        and new_value is not UNCHANGED  # has the user provided a value for this field
        and getattr(offer, field) != new_value  # is the value different from what we have on database?
    }
    # fmt: on
    if not modifications:
        return offer

    validation.check_offer_is_editable(offer)

    if offer.isFromAllocine:
        validation.check_update_only_allowed_offer_fields_for_allocine_offer(set(modifications))

    offer.populate_from_dict(modifications)
    if offer.product.owningOfferer and offer.product.owningOfferer == offer.venue.managingOfferer:
        offer.product.populate_from_dict(modifications)
        product_has_been_updated = True
    else:
        product_has_been_updated = False

    if offer.isFromAllocine:
        offer.fieldsUpdated = list(set(offer.fieldsUpdated) | set(modifications))

    repository.save(offer)
    if product_has_been_updated:
        repository.save(offer.product)

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=offer.id)

    return offer
Exemple #4
0
    def test_on_datetime_list_returns_string_with_date_in_ISO_8601_list(self):
        # Given
        offer = Offer()
        offer.stocks = [create_stock(beginning_datetime=now, offer=offer)]

        # When
        serialized_list = serialize(offer.dateRange)

        # Then
        for dt in serialized_list:  # pylint: disable=not-an-iterable
            self._assert_is_in_ISO_8601_format(dt)
Exemple #5
0
def _complete_common_offer_fields(offer: Offer, offer_data: PostOfferBodyModel,
                                  venue: Venue) -> None:
    offer.venue = venue
    offer.bookingEmail = offer_data.booking_email
    offer.externalTicketOfficeUrl = offer_data.external_ticket_office_url
    offer.audioDisabilityCompliant = offer_data.audio_disability_compliant
    offer.mentalDisabilityCompliant = offer_data.mental_disability_compliant
    offer.motorDisabilityCompliant = offer_data.motor_disability_compliant
    offer.visualDisabilityCompliant = offer_data.visual_disability_compliant
    offer.validation = OfferValidationStatus.DRAFT
    offer.isEducational = offer_data.is_educational
Exemple #6
0
def test_create_payment_for_booking_when_iban_is_on_venue_should_take_payment_info_from_venue(
        app):
    # given
    user = create_user()
    stock = create_stock(price=10, quantity=5)
    offerer = create_offerer(name="Test Offerer")
    venue = create_venue(
        offerer,
        name="Test Venue",
    )
    booking = create_booking(user=user, quantity=1, stock=stock)

    create_bank_information(bic="Lajr93",
                            iban="B135TGGEG532TG",
                            offerer=offerer)
    create_bank_information(bic="LokiJU76",
                            iban="KD98765RFGHZ788",
                            venue=venue)

    booking.stock.offer = Offer()
    booking.stock.offer.venue = venue
    booking.stock.offer.venue.managingOfferer = offerer
    booking_reimbursement = BookingReimbursement(
        booking, ReimbursementRules.PHYSICAL_OFFERS, Decimal(10))

    # when
    payment = create_payment_for_booking(booking_reimbursement)

    # then
    assert payment.iban == "KD98765RFGHZ788"
    assert payment.bic == "LOKIJU76"
Exemple #7
0
def test_create_payment_for_booking_takes_recipient_name_and_siren_from_offerer(
        app):
    # given
    user = create_user()
    stock = create_stock(price=10, quantity=5)
    booking = create_booking(user=user, quantity=1, stock=stock)
    booking.stock.offer = Offer()
    offerer = create_offerer(name="Test Offerer", siren="123456789")
    venue = create_venue(offerer, name="Test Venue")

    create_bank_information(bic="QSDFGH8Z555",
                            iban="CF13QSDFGH456789",
                            offerer=offerer)
    create_bank_information(bic=None, iban=None, venue=venue)

    booking.stock.offer.venue = venue
    booking.stock.offer.venue.managingOfferer = offerer
    booking_reimbursement = BookingReimbursement(
        booking, ReimbursementRules.PHYSICAL_OFFERS, Decimal(10))

    # when
    payment = create_payment_for_booking(booking_reimbursement)

    # then
    assert payment.recipientName == "Test Offerer"
    assert payment.recipientSiren == "123456789"
Exemple #8
0
def test_create_payment_for_booking_with_common_information(app):
    # given
    user = create_user()
    stock = create_stock(price=10, quantity=5)
    booking = create_booking(user=user, quantity=1, stock=stock)
    booking.stock.offer = Offer()
    booking.stock.offer.venue = VenueSQLEntity()
    offerer = create_offerer()
    create_bank_information(bic="QSDFGH8Z555",
                            iban="CF13QSDFGH456789",
                            offerer=offerer)
    booking.stock.offer.venue.managingOfferer = offerer
    booking_reimbursement = BookingReimbursement(
        booking, ReimbursementRules.PHYSICAL_OFFERS, Decimal(10))

    # when
    payment = create_payment_for_booking(booking_reimbursement)

    # then
    assert payment.booking == booking
    assert payment.amount == Decimal(10)
    assert payment.reimbursementRule == ReimbursementRules.PHYSICAL_OFFERS.value.description
    assert payment.reimbursementRate == ReimbursementRules.PHYSICAL_OFFERS.value.rate
    assert payment.comment is None
    assert payment.author == "batch"
    assert payment.transactionLabel == "pass Culture Pro - remboursement 2nde quinzaine 10-2018"
Exemple #9
0
def test_create_payment_for_booking_when_no_iban_on_venue_should_take_payment_info_from_offerer(
        app):
    # given
    user = create_user()
    stock = create_stock(price=10, quantity=5)
    offerer = create_offerer(name="Test Offerer")
    venue = create_venue(offerer, name="Test Venue")

    create_bank_information(bic="QsdFGH8Z555",
                            iban="cf13QSDFGH456789",
                            offerer=offerer)
    create_bank_information(bic=None, iban=None, venue=venue)

    booking = create_booking(user=user, quantity=1, stock=stock)
    booking.stock.offer = Offer()
    booking.stock.offer.venue = venue
    booking.stock.offer.venue.managingOfferer = offerer
    booking_reimbursement = BookingReimbursement(
        booking, ReimbursementRules.PHYSICAL_OFFERS, Decimal(10))

    # when
    payment = create_payment_for_booking(booking_reimbursement)

    # then
    assert payment.iban == "CF13QSDFGH456789"
    assert payment.bic == "QSDFGH8Z555"
Exemple #10
0
def create_booking_for_thing(
    amount: int = 50,
    date_created: datetime = datetime.utcnow(),
    is_cancelled: bool = False,
    quantity: int = 1,
    product_type: ThingType = ThingType.JEUX,
    url: str = None,
    user: User = None,
) -> Booking:
    product = Product(from_dict={"url": url, "type": str(product_type)})
    offer = Offer(from_dict={"url": url, "type": str(product_type)})
    stock = Stock()
    booking = Booking(from_dict={"amount": amount})
    offer.product = product
    stock.offer = offer
    booking.stock = stock
    booking.quantity = quantity
    booking.user = user
    booking.isCancelled = is_cancelled
    booking.dateCreated = date_created

    return booking
Exemple #11
0
def create_booking_for_event(  # pylint: disable=redefined-builtin
    amount: int = 50,
    date_created: datetime = datetime.utcnow(),
    is_cancelled: bool = False,
    quantity: int = 1,
    type: EventType = EventType.CINEMA,
    user: User = None,
) -> Booking:
    product = Product(from_dict={"type": str(type)})
    offer = Offer()
    stock = Stock()
    booking = Booking(from_dict={"amount": amount})
    offer.product = product
    stock.offer = offer
    booking.stock = stock
    booking.quantity = quantity
    booking.user = user
    booking.isCancelled = is_cancelled
    booking.token = random_token()
    booking.dateCreated = date_created

    return booking
Exemple #12
0
    def fill_offer_attributes(self, offer: Offer) -> None:
        offer.bookingEmail = self.venue.bookingEmail
        offer.description = self.product.description
        offer.extraData = self.product.extraData
        offer.name = self.product.name
        offer.productId = self.product.id
        offer.venueId = self.venue.id
        offer.type = self.product.type

        is_new_offer_to_create = not offer.id
        if is_new_offer_to_create:
            next_id = self.get_next_offer_id_from_sequence()
            offer.id = next_id

        self.offer_id = offer.id
Exemple #13
0
def _initialize_offer_with_new_data(offer_data: PostOfferBodyModel,
                                    subcategory: subcategories.Subcategory,
                                    venue: Venue) -> Offer:
    data = offer_data.dict(by_alias=True)
    product = Product()
    if data.get("url"):
        data["isNational"] = True
    product.populate_from_dict(data)
    offer = Offer()
    offer.populate_from_dict(data)
    offer.product = product
    offer.subcategoryId = subcategory.id if subcategory else None
    offer.product.owningOfferer = venue.managingOfferer
    return offer
Exemple #14
0
def test_create_payment_for_booking_with_not_processable_status_when_no_bank_information_linked_to_venue_or_offerer(
):
    # given
    user = create_user()
    stock = create_stock(price=10, quantity=5)
    booking = create_booking(user=user, quantity=1, stock=stock)
    booking.stock.offer = Offer()
    booking.stock.offer.venue = VenueSQLEntity()
    booking.stock.offer.venue.managingOfferer = create_offerer(
        name="Test Offerer")
    booking_reimbursement = BookingReimbursement(
        booking, ReimbursementRules.PHYSICAL_OFFERS, Decimal(10))

    # when
    payment = create_payment_for_booking(booking_reimbursement)

    # then
    assert len(payment.statuses) == 1
    assert payment.statuses[0].status == TransactionStatus.NOT_PROCESSABLE
    assert payment.statuses[0].detail == "IBAN et BIC manquants sur l'offreur"
Exemple #15
0
def test_create_payment_for_booking_with_pending_status(app):
    # given
    user = create_user()
    stock = create_stock(price=10, quantity=5)
    booking = create_booking(user=user, quantity=1, stock=stock)
    booking.stock.offer = Offer()
    booking.stock.offer.venue = VenueSQLEntity()
    offerer = create_offerer()
    booking.stock.offer.venue.managingOfferer = offerer
    create_bank_information(bic="QSDFGH8Z555",
                            iban="CF13QSDFGH456789",
                            offerer=offerer)
    booking_reimbursement = BookingReimbursement(
        booking, ReimbursementRules.PHYSICAL_OFFERS, Decimal(10))

    # when
    payment = create_payment_for_booking(booking_reimbursement)

    # then
    assert len(payment.statuses) == 1
    assert payment.statuses[0].status == TransactionStatus.PENDING
    assert payment.statuses[0].detail is None
    assert payment.statuses[0].date == datetime(2018, 10, 15, 9, 21, 34)
Exemple #16
0
def create_offer(offer_data: PostOfferBodyModel, user: User) -> models.Offer:
    venue = load_or_raise_error(VenueSQLEntity, offer_data.venue_id)

    ensure_current_user_has_rights(rights=RightsType.editor, offerer_id=venue.managingOffererId, user=user)

    if offer_data.product_id:
        product = load_or_raise_error(Product, offer_data.product_id)
        offer = models.Offer(
            product=product,
            type=product.type,
            name=product.name,
            description=product.description,
            url=product.url,
            mediaUrls=product.mediaUrls,
            conditions=product.conditions,
            ageMin=product.ageMin,
            ageMax=product.ageMax,
            durationMinutes=product.durationMinutes,
            isNational=product.isNational,
            extraData=product.extraData,
        )
    else:
        if offer_data.type == str(EventType.ACTIVATION):
            validation.check_user_can_create_activation_event(user)
        data = offer_data.dict(by_alias=True)
        product = Product()
        if data.get("url"):
            data["isNational"] = True
        product.populate_from_dict(data)
        offer = Offer()
        offer.populate_from_dict(data)
        offer.product = product
        offer.product.owningOfferer = venue.managingOfferer

    offer.venue = venue
    offer.bookingEmail = offer_data.booking_email
    repository.save(offer)
    admin_emails.send_offer_creation_notification_to_administration(offer, user, mailing.send_raw_email)

    return offer
Exemple #17
0
def _initialize_book_offer_from_template(
        offer_data: PostOfferBodyModel) -> Offer:
    product = _load_product_by_isbn_and_check_is_gcu_compatible_or_raise_error(
        offer_data.extra_data["isbn"])
    extra_data = product.extraData
    extra_data.update(offer_data.extra_data)
    offer = Offer(
        product=product,
        subcategoryId=product.subcategoryId,
        name=offer_data.name,
        description=offer_data.description
        if offer_data.description else product.description,
        url=offer_data.url if offer_data.url else product.url,
        mediaUrls=offer_data.url if offer_data.url else product.url,
        conditions=offer_data.conditions
        if offer_data.conditions else product.conditions,
        ageMin=offer_data.age_min if offer_data.age_min else product.ageMin,
        ageMax=offer_data.age_max if offer_data.age_max else product.ageMax,
        isNational=offer_data.is_national
        if offer_data.is_national else product.isNational,
        extraData=extra_data,
    )
    return offer
Exemple #18
0
def create_offer_with_thing_product(
    venue: VenueSQLEntity,
    author_name: str = "Test Author",
    booking_email: Optional[str] = "*****@*****.**",
    date_created: datetime = datetime.utcnow(),
    description: Optional[str] = None,
    id_at_providers: str = None,
    idx: int = None,
    is_active: bool = True,
    is_digital: bool = False,
    is_national: bool = False,
    is_offline_only: bool = False,
    media_urls: Iterable[str] = ("test/urls", ),
    product: Product = None,
    thing_name: str = "Test Book",
    thing_type: ThingType = ThingType.AUDIOVISUEL,
    thumb_count: int = 0,
    url: Optional[str] = None,
    last_provider_id: int = None,
    last_provider: Provider = None,
    extra_data: Dict = None,
    withdrawal_details: Optional[str] = None,
) -> Offer:
    offer = Offer()
    if product:
        offer.product = product
        offer.productId = product.id
        offer.name = product.name
        offer.type = product.type
        offer.mediaUrls = product.mediaUrls
        offer.extraData = product.extraData
        offer.url = product.url
        offer.isNational = product.isNational
        offer.description = product.description
    else:
        if is_digital:
            url = "fake/url"
        if is_offline_only:
            thing_type = ThingType.CINEMA_ABO

        offer.product = create_product_with_thing_type(
            thing_name=thing_name,
            thing_type=thing_type,
            media_urls=media_urls,
            author_name=author_name,
            url=url,
            thumb_count=thumb_count,
            is_national=is_national,
            description=description,
        )
        offer.name = thing_name
        offer.type = str(thing_type)
        offer.mediaUrls = media_urls
        offer.extraData = {"author": author_name}
        offer.url = url
        offer.isNational = is_national
        offer.description = description
    offer.venue = venue
    offer.dateCreated = date_created
    offer.bookingEmail = booking_email
    offer.isActive = is_active
    offer.lastProviderId = last_provider_id
    offer.lastProvider = last_provider
    offer.id = idx
    offer.withdrawalDetails = withdrawal_details

    if extra_data:
        offer.extraData = extra_data

    if id_at_providers:
        offer.idAtProviders = id_at_providers
    elif venue is not None:
        offer.idAtProviders = "%s@%s" % (offer.product.idAtProviders,
                                         venue.siret or venue.id)

    return offer
Exemple #19
0
def create_offer_with_event_product(
    venue: VenueSQLEntity = None,
    booking_email: str = "*****@*****.**",
    date_created: datetime = datetime.utcnow(),
    description: Optional[str] = None,
    duration_minutes: Optional[int] = 60,
    event_name: str = "Test event",
    event_type: EventType = EventType.SPECTACLE_VIVANT,
    id_at_providers: str = None,
    idx: int = None,
    is_active: bool = True,
    is_duo: bool = False,
    is_national: bool = False,
    last_provider_id: int = None,
    product: Product = None,
    last_provider: Provider = None,
    thumb_count: int = 0,
    withdrawal_details: Optional[str] = None,
) -> Offer:
    offer = Offer()
    if product is None:
        product = create_product_with_event_type(
            event_name=event_name,
            event_type=event_type,
            duration_minutes=duration_minutes,
            thumb_count=thumb_count,
            is_national=is_national,
        )
    offer.product = product
    offer.venue = venue
    offer.name = product.name
    offer.type = product.type
    offer.description = description
    offer.isNational = product.isNational
    offer.durationMinutes = product.durationMinutes
    offer.dateCreated = date_created
    offer.bookingEmail = booking_email
    offer.isActive = is_active
    offer.id = idx
    offer.lastProviderId = last_provider_id
    offer.lastProvider = last_provider
    offer.idAtProviders = id_at_providers
    offer.isDuo = is_duo
    offer.withdrawalDetails = withdrawal_details

    return offer
Exemple #20
0
def test_equality():
    assert Offer(id=1) == Offer(id=1)
    assert Offer(id=1) != Offer(id=2)
    assert Offer(id=1) != User(id=1)
    assert Offer(id=1) != 1
Exemple #21
0
def create_offer(offer_data: PostOfferBodyModel, user: User) -> Offer:
    venue = load_or_raise_error(Venue, offer_data.venue_id)

    check_user_has_access_to_offerer(user, offerer_id=venue.managingOffererId)

    if offer_data.product_id:
        product = load_or_raise_error(Product, offer_data.product_id)
        offer = Offer(
            product=product,
            type=product.type,
            name=product.name,
            description=product.description,
            url=product.url,
            mediaUrls=product.mediaUrls,
            conditions=product.conditions,
            ageMin=product.ageMin,
            ageMax=product.ageMax,
            durationMinutes=product.durationMinutes,
            isNational=product.isNational,
            extraData=product.extraData,
        )
    else:
        if offer_data.type == str(EventType.ACTIVATION):
            validation.check_user_can_create_activation_event(user)
        data = offer_data.dict(by_alias=True)
        product = Product()
        if data.get("url"):
            data["isNational"] = True
        product.populate_from_dict(data)
        offer = Offer()
        offer.populate_from_dict(data)
        offer.product = product
        offer.product.owningOfferer = venue.managingOfferer

    offer.venue = venue
    offer.bookingEmail = offer_data.booking_email
    offer.externalTicketOfficeUrl = offer_data.external_ticket_office_url
    offer.audioDisabilityCompliant = offer_data.audio_disability_compliant
    offer.mentalDisabilityCompliant = offer_data.mental_disability_compliant
    offer.motorDisabilityCompliant = offer_data.motor_disability_compliant
    offer.visualDisabilityCompliant = offer_data.visual_disability_compliant
    repository.save(offer)
    admin_emails.send_offer_creation_notification_to_administration(
        offer, user)

    return offer
Exemple #22
0
    def fill_offer_attributes(self, allocine_offer: Offer):
        allocine_offer.venueId = self.venue.id
        allocine_offer.bookingEmail = self.venue.bookingEmail
        if "description" in self.movie_information:
            allocine_offer.description = self.movie_information["description"]
        if "duration" in self.movie_information:
            allocine_offer.durationMinutes = self.movie_information["duration"]
        if not allocine_offer.extraData:
            allocine_offer.extraData = {}
        if "visa" in self.movie_information:
            allocine_offer.extraData["visa"] = self.movie_information["visa"]
        if "stageDirector" in self.movie_information:
            allocine_offer.extraData["stageDirector"] = self.movie_information[
                "stageDirector"]

        movie_version = (ORIGINAL_VERSION_SUFFIX if _is_original_version_offer(
            allocine_offer.idAtProviders) else FRENCH_VERSION_SUFFIX)

        allocine_offer.name = f"{self.movie_information['title']} - {movie_version}"
        allocine_offer.type = str(EventType.CINEMA)
        allocine_offer.productId = self.last_product_id

        is_new_offer_to_insert = allocine_offer.id is None

        if is_new_offer_to_insert:
            allocine_offer.id = get_next_offer_id_from_database()
            allocine_offer.isDuo = self.isDuo

        if movie_version == ORIGINAL_VERSION_SUFFIX:
            self.last_vo_offer_id = allocine_offer.id
        else:
            self.last_vf_offer_id = allocine_offer.id
Exemple #23
0
    def fill_offer_attributes(self, allocine_offer: Offer) -> None:
        allocine_offer.venueId = self.venue.id
        allocine_offer.bookingEmail = self.venue.bookingEmail
        allocine_offer.withdrawalDetails = self.venue.withdrawalDetails

        self.update_from_movie_information(allocine_offer,
                                           self.movie_information)

        allocine_offer.extraData["theater"] = {
            "allocine_movie_id": self.movie_information["internal_id"],
            "allocine_room_id": self.room_internal_id,
        }

        if "visa" in self.movie_information:
            allocine_offer.extraData["visa"] = self.movie_information["visa"]
        if "stageDirector" in self.movie_information:
            allocine_offer.extraData["stageDirector"] = self.movie_information[
                "stageDirector"]

        movie_version = (ORIGINAL_VERSION_SUFFIX if _is_original_version_offer(
            allocine_offer.idAtProviders) else FRENCH_VERSION_SUFFIX)

        allocine_offer.name = f"{self.movie_information['title']} - {movie_version}"
        allocine_offer.subcategoryId = subcategories.SEANCE_CINE.id
        allocine_offer.productId = self.last_product_id

        is_new_offer_to_insert = allocine_offer.id is None

        if is_new_offer_to_insert:
            allocine_offer.id = get_next_offer_id_from_database()
            allocine_offer.isDuo = self.isDuo

        if movie_version == ORIGINAL_VERSION_SUFFIX:
            self.last_vo_offer_id = allocine_offer.id
        else:
            self.last_vf_offer_id = allocine_offer.id
Exemple #24
0
def update_offer(
    offer: Offer,
    bookingEmail: str = UNCHANGED,
    description: str = UNCHANGED,
    isNational: bool = UNCHANGED,
    name: str = UNCHANGED,
    extraData: dict = UNCHANGED,
    externalTicketOfficeUrl: str = UNCHANGED,
    url: str = UNCHANGED,
    withdrawalDetails: str = UNCHANGED,
    isActive: bool = UNCHANGED,
    isDuo: bool = UNCHANGED,
    durationMinutes: int = UNCHANGED,
    mediaUrls: list[str] = UNCHANGED,
    ageMin: int = UNCHANGED,
    ageMax: int = UNCHANGED,
    conditions: str = UNCHANGED,
    venueId: str = UNCHANGED,
    productId: str = UNCHANGED,
    audioDisabilityCompliant: bool = UNCHANGED,
    mentalDisabilityCompliant: bool = UNCHANGED,
    motorDisabilityCompliant: bool = UNCHANGED,
    visualDisabilityCompliant: bool = UNCHANGED,
) -> Offer:
    validation.check_validation_status(offer)
    # fmt: off
    modifications = {
        field: new_value
        for field, new_value in locals().items()
        if field != 'offer' and new_value is
        not UNCHANGED  # has the user provided a value for this field
        and getattr(offer, field) !=
        new_value  # is the value different from what we have on database?
    }
    # fmt: on
    if not modifications:
        return offer

    if offer.isFromProvider:
        validation.check_update_only_allowed_fields_for_offer_from_provider(
            set(modifications), offer.lastProvider)

    offer.populate_from_dict(modifications)
    if offer.product.owningOfferer and offer.product.owningOfferer == offer.venue.managingOfferer:
        offer.product.populate_from_dict(modifications)
        product_has_been_updated = True
    else:
        product_has_been_updated = False

    if offer.isFromAllocine:
        offer.fieldsUpdated = list(
            set(offer.fieldsUpdated) | set(modifications))

    repository.save(offer)
    logger.info("Offer has been updated", extra={"offer": offer.id})
    if product_has_been_updated:
        repository.save(offer.product)
        logger.info("Product has been updated",
                    extra={"product": offer.product.id})

    search.async_index_offer_ids([offer.id])

    return offer