Exemple #1
0
def create_stock_with_thing_offer(
    offerer: Offerer,
    venue: Venue,
    offer: Offer = None,
    price: Optional[Decimal] = 10,
    quantity: int = 50,
    name: str = "Test Book",
    booking_email: str = "*****@*****.**",
    soft_deleted: bool = False,
    url: str = None,
    booking_limit_datetime: datetime = None,
    thing_type: ThingType = ThingType.AUDIOVISUEL,
) -> Stock:
    stock = Stock()
    stock.offerer = offerer
    stock.price = price

    if offer:
        stock.offer = offer
    else:
        stock.offer = create_offer_with_thing_product(venue,
                                                      thing_name=name,
                                                      thing_type=thing_type)

    stock.offer.bookingEmail = booking_email
    stock.bookingLimitDatetime = booking_limit_datetime
    stock.offer.url = url
    stock.offer.venue = venue
    stock.quantity = quantity
    stock.isSoftDeleted = soft_deleted

    return stock
Exemple #2
0
 def fill_stock_attributes(self, stock: Stock) -> None:
     bookings_quantity = count_not_cancelled_bookings_quantity_by_stock_id(stock.id)
     stock.quantity = self.provider_stocks["available"] + bookings_quantity
     stock.bookingLimitDatetime = None
     stock.offerId = self.offer_id
     stock.price = (
         self.provider_stocks["price"]
         if self.price_divider_to_euro is None
         else _fill_stock_price(int(self.provider_stocks["price"]), self.price_divider_to_euro)
     )
     stock.dateModified = datetime.now()
Exemple #3
0
def test_get_last_update_for_provider_should_return_none_when_last_provider_id_matches_given_id_and_date_modified_at_last_provider_is_none(
):
    # Given
    provider_id = 1
    pc_object = Stock()
    pc_object.lastProviderId = provider_id
    pc_object.dateModifiedAtLastProvider = None

    # When
    date_modified_at_last_provider = get_last_update_for_provider(
        provider_id=provider_id, pc_obj=pc_object)

    # Then
    assert date_modified_at_last_provider is None
Exemple #4
0
def test_get_last_update_for_provider_should_return_date_modified_at_last_provider_when_provided(
):
    # Given
    provider_id = 1
    modification_date = datetime(2019, 1, 1)
    pc_object = Stock()
    pc_object.lastProviderId = provider_id
    pc_object.dateModifiedAtLastProvider = modification_date

    # When
    date_modified_at_last_provider = get_last_update_for_provider(
        provider_id=provider_id, pc_obj=pc_object)

    # Then
    assert date_modified_at_last_provider == modification_date
Exemple #5
0
def delete_stock(stock: Stock) -> None:
    validation.check_stock_is_deletable(stock)

    stock.isSoftDeleted = True

    cancelled_bookings = []
    for booking in stock.bookings:
        if not booking.isCancelled and not booking.isUsed:
            booking.isCancelled = True
            cancelled_bookings.append(booking)

    repository.save(stock, *cancelled_bookings)

    if cancelled_bookings:
        try:
            user_emails.send_batch_cancellation_emails_to_users(cancelled_bookings, mailing.send_raw_email)
        except mailing.MailServiceException as exc:
            app.logger.exception("Could not notify beneficiaries about deletion of stock=%s: %s", stock.id, exc)
        try:
            user_emails.send_offerer_bookings_recap_email_after_offerer_cancellation(
                cancelled_bookings, mailing.send_raw_email
            )
        except mailing.MailServiceException as exc:
            app.logger.exception("Could not notify offerer about deletion of stock=%s: %s", stock.id, exc)

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=stock.offerId)
Exemple #6
0
def create_booking(
    user: User,
    amount: Optional[Union[Decimal, float]] = None,
    date_created: datetime = datetime.utcnow(),
    date_used: datetime = None,
    idx: int = None,
    is_cancelled: bool = False,
    is_used: bool = False,
    quantity: int = 1,
    stock: Stock = None,
    token: str = None,
    venue: VenueSQLEntity = None,
) -> Booking:
    booking = Booking()
    offerer = create_offerer(siren="987654321",
                             address="Test address",
                             city="Test city",
                             postal_code="93000",
                             name="Test name")
    if venue is None:
        venue = create_venue(
            offerer=offerer,
            name="Test offerer",
            booking_email="*****@*****.**",
            address="123 rue test",
            postal_code="93000",
            city="Test city",
            departement_code="93",
        )
    if stock is None:
        price = amount if amount is not None else 10
        product_with_thing_type = create_offer_with_thing_product(venue)
        stock = create_stock_with_thing_offer(offerer=offerer,
                                              venue=venue,
                                              offer=product_with_thing_type,
                                              price=price)

    if not stock.offer:
        stock.offer = create_offer_with_thing_product(venue)

    booking.user = user
    booking.amount = amount if amount is not None else stock.price
    booking.dateCreated = date_created
    booking.dateUsed = date_used
    booking.id = idx
    booking.isCancelled = is_cancelled
    booking.isUsed = is_used
    booking.quantity = quantity
    booking.stock = stock
    booking.token = token if token is not None else random_token()
    booking.userId = user.id
    booking.confirmationDate = bookings_api.compute_confirmation_date(
        stock.beginningDatetime, date_created)

    return booking
    def test_returns_humanized_ids_for_foreign_keys(self, app):
        # given
        user = create_user(idx=12, postal_code=None)
        booking = create_booking(user=user, stock=Stock(), idx=13)
        booking.userId = user.id

        # when
        dict_result = as_dict(booking, includes=[])

        # then
        assert dict_result["userId"] == "BQ"
Exemple #8
0
def delete_stock(stock_id: str) -> StockResponseIdModel:
    # fmt: off
    stock = (Stock.queryNotSoftDeleted().filter_by(
        id=dehumanize(stock_id)).join(Offer, VenueSQLEntity).first_or_404())
    # fmt: on

    offerer_id = stock.offer.venue.managingOffererId
    ensure_current_user_has_rights(RightsType.editor, offerer_id)

    offers_api.delete_stock(stock)

    return StockResponseIdModel.from_orm(stock)
Exemple #9
0
def create_stock_from_offer(
        offer: Offer,
        price: float = 9.90,
        quantity: Optional[int] = 10,
        soft_deleted: bool = False,
        booking_limit_datetime: datetime = None,
        beginning_datetime: datetime = None,
        idx: int = None,
        date_modified: datetime = datetime.utcnow(),
) -> Stock:
    stock = Stock()
    stock.id = idx
    stock.offer = offer
    stock.price = price
    stock.quantity = quantity
    stock.isSoftDeleted = soft_deleted
    stock.bookingLimitDatetime = booking_limit_datetime
    stock.beginningDatetime = beginning_datetime
    stock.dateModified = date_modified

    return stock
Exemple #10
0
def upsert_stocks(
    offer_id: int, stock_data_list: List[Union[StockCreationBodyModel,
                                               StockEditionBodyModel]]
) -> List[Stock]:
    stocks = []
    edited_stocks = []
    edited_stocks_previous_beginnings = {}

    offer = offer_queries.get_offer_by_id(offer_id)

    for stock_data in stock_data_list:
        if isinstance(stock_data, StockEditionBodyModel):
            stock = Stock.queryNotSoftDeleted().filter_by(
                id=stock_data.id).first_or_404()
            if stock.offerId != offer_id:
                errors = ApiErrors()
                errors.add_error(
                    "global",
                    "Vous n'avez pas les droits d'accès suffisant pour accéder à cette information."
                )
                errors.status_code = 403
                raise errors
            edited_stocks_previous_beginnings[
                stock.id] = stock.beginningDatetime
            edited_stock = _edit_stock(
                stock,
                price=stock_data.price,
                quantity=stock_data.quantity,
                beginning=stock_data.beginning_datetime,
                booking_limit_datetime=stock_data.booking_limit_datetime,
            )
            edited_stocks.append(edited_stock)
            stocks.append(edited_stock)
        else:
            created_stock = _create_stock(
                offer=offer,
                price=stock_data.price,
                quantity=stock_data.quantity,
                beginning=stock_data.beginning_datetime,
                booking_limit_datetime=stock_data.booking_limit_datetime,
            )
            stocks.append(created_stock)

    repository.save(*stocks)

    for stock in edited_stocks:
        previous_beginning = edited_stocks_previous_beginnings[stock.id]
        if stock.beginningDatetime != previous_beginning:
            _notify_beneficiaries_upon_stock_edit(stock)
    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=offer.id)

    return stocks
Exemple #11
0
def _edit_stock(
    stock: Stock,
    price: float,
    quantity: int,
    beginning: datetime.datetime,
    booking_limit_datetime: datetime.datetime,
) -> Stock:
    validation.check_stock_is_updatable(stock)
    validation.check_required_dates_for_stock(stock.offer, beginning,
                                              booking_limit_datetime)
    validation.check_stock_price(price)
    validation.check_stock_quantity(quantity, stock.bookingsQuantity)

    # FIXME (dbaty, 2020-11-25): We need this ugly workaround because
    # the frontend sends us datetimes like "2020-12-03T14:00:00Z"
    # (note the "Z" suffix). Pydantic deserializes it as a datetime
    # *with* a timezone. However, datetimes are stored in the database
    # as UTC datetimes *without* any timezone. Thus, we wrongly detect
    # a change for the "beginningDatetime" field for Allocine stocks:
    # because we do not allow it to be changed, we raise an error when
    # we should not.
    def as_utc_without_timezone(d: datetime.datetime) -> datetime.datetime:
        return d.astimezone(pytz.utc).replace(tzinfo=None)

    if beginning:
        beginning = as_utc_without_timezone(beginning)
    if booking_limit_datetime:
        booking_limit_datetime = as_utc_without_timezone(
            booking_limit_datetime)

    updates = {
        "price": price,
        "quantity": quantity,
        "beginningDatetime": beginning,
        "bookingLimitDatetime": booking_limit_datetime,
    }

    if stock.offer.isFromAllocine:
        # fmt: off
        updated_fields = {
            attr
            for attr, new_value in updates.items()
            if new_value != getattr(stock, attr)
        }
        # fmt: on
        validation.check_update_only_allowed_stock_fields_for_allocine_offer(
            updated_fields)
        stock.fieldsUpdated = list(updated_fields)

    for model_attr, value in updates.items():
        setattr(stock, model_attr, value)

    return stock
Exemple #12
0
    def fill_stock_attributes(self, allocine_stock: Stock):
        showtime_uuid = _get_showtimes_uuid_by_idAtProvider(
            allocine_stock.idAtProviders)
        showtime = _find_showtime_by_showtime_uuid(
            self.filtered_movie_showtimes, showtime_uuid)

        parsed_showtimes = retrieve_showtime_information(showtime)
        diffusion_version = parsed_showtimes["diffusionVersion"]

        allocine_stock.offerId = (self.last_vo_offer_id
                                  if diffusion_version == ORIGINAL_VERSION else
                                  self.last_vf_offer_id)

        local_tz = get_department_timezone(self.venue.departementCode)
        date_in_utc = _format_date_from_local_timezone_to_utc(
            parsed_showtimes["startsAt"], local_tz)
        allocine_stock.beginningDatetime = date_in_utc

        is_new_stock_to_insert = allocine_stock.id is None
        if is_new_stock_to_insert:
            allocine_stock.fieldsUpdated = []

        if "bookingLimitDatetime" not in allocine_stock.fieldsUpdated:
            allocine_stock.bookingLimitDatetime = date_in_utc

        if "quantity" not in allocine_stock.fieldsUpdated:
            allocine_stock.quantity = self.quantity

        if "price" not in allocine_stock.fieldsUpdated:
            allocine_stock.price = self.apply_allocine_price_rule(
                allocine_stock)
Exemple #13
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 #14
0
def test_queryNotSoftDeleted_should_not_return_soft_deleted(app):
    # Given
    offerer = create_offerer()
    venue = create_venue(offerer)
    stock = create_stock_with_event_offer(offerer, venue)
    stock.isSoftDeleted = True
    repository.save(stock)

    # When
    result = Stock.queryNotSoftDeleted().all()

    # Then
    assert not result
Exemple #15
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 #16
0
def delete_stock(stock_id: str) -> StockIdResponseModel:
    # fmt: off
    stock = (
        Stock.queryNotSoftDeleted()
            .filter_by(id=dehumanize(stock_id))
            .join(Offer, Venue)
            .first_or_404()
    )
    # fmt: on

    offerer_id = stock.offer.venue.managingOffererId
    check_user_has_access_to_offerer(current_user, offerer_id)

    offers_api.delete_stock(stock)

    return StockIdResponseModel.from_orm(stock)
Exemple #17
0
def edit_stock(stock_id: str,
               body: StockEditionBodyModel) -> StockResponseIdModel:
    stock = Stock.queryNotSoftDeleted().filter_by(
        id=dehumanize(stock_id)).join(Offer, VenueSQLEntity).first_or_404()

    offerer_id = stock.offer.venue.managingOffererId
    ensure_current_user_has_rights(RightsType.editor, offerer_id)

    stock = offers_api.edit_stock(
        stock,
        price=body.price,
        quantity=body.quantity,
        beginning=body.beginning_datetime,
        booking_limit_datetime=body.booking_limit_datetime,
    )

    return StockResponseIdModel.from_orm(stock)
Exemple #18
0
 def fill_stock_attributes(self, stock: Stock) -> None:
     bookings_quantity = count_not_cancelled_bookings_quantity_by_stock_id(
         stock.id)
     stock.quantity = self.provider_stocks["available"] + bookings_quantity
     stock.bookingLimitDatetime = None
     stock.offerId = self.offer_id
     if self.provider_stocks["price"] and self.price_divider_to_euro:
         stock.price = int(
             self.provider_stocks["price"]) / self.price_divider_to_euro
     else:
         # Beware: price may be None. repository.save() will catch and skip the stock
         stock.price = self.provider_stocks["price"]
     stock.dateModified = datetime.now()
Exemple #19
0
def _create_stock(
    offer: Offer,
    price: float,
    quantity: int = None,
    beginning: datetime.datetime = None,
    booking_limit_datetime: datetime.datetime = None,
) -> Stock:
    validation.check_required_dates_for_stock(offer, beginning,
                                              booking_limit_datetime)
    validation.check_offer_is_editable(offer)
    validation.check_stocks_are_editable_for_offer(offer)
    validation.check_stock_price(price)
    validation.check_stock_quantity(quantity)

    return Stock(
        offer=offer,
        price=price,
        quantity=quantity,
        beginningDatetime=beginning,
        bookingLimitDatetime=booking_limit_datetime,
    )
Exemple #20
0
def create_stock(
    beginning_datetime: Optional[datetime] = None,
    booking_limit_datetime: Optional[datetime] = None,
    date_created: datetime = datetime.utcnow(),
    date_modified: datetime = datetime.utcnow(),
    date_modified_at_last_provider: Optional[datetime] = None,
    idx: Optional[int] = None,
    id_at_providers: Optional[str] = None,
    is_soft_deleted: bool = False,
    last_provider_id: Optional[int] = None,
    offer: Optional[Offer] = None,
    price: float = 10,
    quantity: Optional[int] = None,
) -> Stock:
    stock = Stock()
    stock.quantity = quantity
    stock.beginningDatetime = beginning_datetime
    stock.bookingLimitDatetime = booking_limit_datetime
    stock.dateCreated = date_created
    stock.dateModified = date_modified
    stock.dateModifiedAtLastProvider = date_modified_at_last_provider
    if idx:
        stock.id = idx
    stock.idAtProviders = id_at_providers
    stock.isSoftDeleted = is_soft_deleted
    stock.lastProviderId = last_provider_id
    stock.offer = offer
    stock.price = price

    return stock
Exemple #21
0
def edit_stock(
    stock: Stock,
    price: int = None,
    quantity: int = None,
    beginning: datetime.datetime = None,
    booking_limit_datetime: datetime.datetime = None,
) -> Stock:
    validation.check_stock_is_updatable(stock)
    validation.check_required_dates_for_stock(stock.offer, beginning, booking_limit_datetime)

    # FIXME (dbaty, 2020-11-25): We need this ugly workaround because
    # the frontend sends us datetimes like "2020-12-03T14:00:00Z"
    # (note the "Z" suffix). Pydantic deserializes it as a datetime
    # *with* a timezone. However, datetimes are stored in the database
    # as UTC datetimes *without* any timezone. Thus, we wrongly detect
    # a change for the "beginningDatetime" field for Allocine stocks:
    # because we do not allow it to be changed, we raise an error when
    # we should not.
    def as_utc_without_timezone(d: datetime.datetime) -> datetime.datetime:
        return d.astimezone(pytz.utc).replace(tzinfo=None)

    if beginning:
        beginning = as_utc_without_timezone(beginning)
    if booking_limit_datetime:
        booking_limit_datetime = as_utc_without_timezone(booking_limit_datetime)

    updates = {
        "price": price,
        "quantity": quantity,
        "beginningDatetime": beginning,
        "bookingLimitDatetime": booking_limit_datetime,
    }

    if stock.offer.isFromAllocine:
        # fmt: off
        updated_fields = {
            attr
            for attr, new_value in updates.items()
            if new_value != getattr(stock, attr)
        }
        # fmt: on
        validation.check_update_only_allowed_stock_fields_for_allocine_offer(updated_fields)
        stock.fieldsUpdated = list(updated_fields)

    previous_beginning = stock.beginningDatetime

    for model_attr, value in updates.items():
        setattr(stock, model_attr, value)
    repository.save(stock)

    if beginning != previous_beginning:
        bookings = bookings_repository.find_not_cancelled_bookings_by_stock(stock)
        if bookings:
            bookings = update_confirmation_dates(bookings, beginning)
            date_in_two_days = datetime.datetime.utcnow() + datetime.timedelta(days=2)
            check_event_is_in_more_than_48_hours = beginning > date_in_two_days
            if check_event_is_in_more_than_48_hours:
                bookings = _invalidate_bookings(bookings)
            try:
                user_emails.send_batch_stock_postponement_emails_to_users(bookings, send_email=mailing.send_raw_email)
            except mailing.MailServiceException as exc:
                # fmt: off
                app.logger.exception(
                    "Could not notify beneficiaries about update of stock=%s: %s",
                    stock.id,
                    exc,
                )
                # fmt: on

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

    return stock
Exemple #22
0
def create_stock_from_event_occurrence(
    event_occurrence: Dict,
    price: int = 10,
    quantity: int = 10,
    soft_deleted: bool = False,
    recap_sent: bool = False,
    booking_limit_date: datetime = None,
) -> Stock:
    stock = Stock()
    stock.beginningDatetime = event_occurrence["beginningDatetime"]
    stock.offerId = event_occurrence["offerId"]
    stock.offer = event_occurrence["offer"]
    stock.price = price
    stock.quantity = quantity
    stock.isSoftDeleted = soft_deleted

    if recap_sent:
        stock.bookingRecapSent = datetime.utcnow()

    if booking_limit_date is None:
        stock.bookingLimitDatetime = event_occurrence["beginningDatetime"]
    else:
        stock.bookingLimitDatetime = booking_limit_date

    return stock
Exemple #23
0
def create_stock_with_event_offer(
        offerer: Offerer,
        venue: Venue,
        price: int = 10,
        booking_email: str = "*****@*****.**",
        quantity: int = 10,
        is_soft_deleted: bool = False,
        event_type: EventType = EventType.JEUX,
        name: str = "Mains, sorts et papiers",
        offer_id: int = None,
        beginning_datetime: datetime = datetime.utcnow() + timedelta(hours=72),
        thumb_count: int = 0,
        booking_limit_datetime: datetime = datetime.utcnow() +
    timedelta(hours=71),
        date_created: datetime = datetime.utcnow(),
        date_modified_at_last_provider: datetime = datetime.utcnow(),
        date_modifed: datetime = datetime.utcnow(),
) -> Stock:
    stock = Stock()
    stock.offerer = offerer
    stock.price = price
    stock.quantity = quantity
    stock.beginningDatetime = beginning_datetime
    stock.bookingLimitDatetime = booking_limit_datetime
    stock.dateCreated = date_created
    stock.dateModifiedAtLastProvider = date_modified_at_last_provider
    stock.dateModified = date_modifed
    stock.offer = create_offer_with_event_product(
        venue,
        event_name=name,
        event_type=event_type,
        booking_email=booking_email,
        is_national=False,
        thumb_count=thumb_count,
    )
    stock.offer.id = offer_id
    stock.isSoftDeleted = is_soft_deleted

    return stock