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
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()
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
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
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)
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"
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)
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
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
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
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)
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
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
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
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)
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)
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()
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, )
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
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
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
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