def recredit_underage_users() -> None: sixteen_years_ago = datetime.now() - relativedelta(years=16) eighteen_years_ago = datetime.now() - relativedelta(years=18) users: list[users_models.User] = (users_models.User.query.filter( users_models.User.has_underage_beneficiary_role).filter( users_models.User.dateOfBirth > eighteen_years_ago).filter( users_models.User.dateOfBirth <= sixteen_years_ago).options( joinedload(users_models.User.deposits).joinedload( payments_models.Deposit.recredits)).all()) users_to_recredit = [ user for user in users if has_celebrated_their_birthday_since_activation(user) and not has_been_recredited(user) ] recredits = [ payments_models.Recredit( depositId=user.deposit.id, deposit=user.deposit, amount=deposit_conf.RECREDIT_TYPE_AMOUNT_MAPPING[ deposit_conf.RECREDIT_TYPE_AGE_MAPPING[user.age]], recreditType=deposit_conf.RECREDIT_TYPE_AGE_MAPPING[user.age], ) for user in users_to_recredit ] with transaction(): for recredit in recredits: recredit.deposit.amount += recredit.amount repository.save(*recredits) logger.info("Recredited %s underage users deposits", len(users_to_recredit))
def mark_as_used_with_uncancelling(booking: Booking) -> None: """Mark a booking as used from cancelled status. This function should be called only if the booking has been cancelled by mistake or fraudulently after the offer was retrieved (for example, when a beneficiary retrieved a book from a library and then cancelled their booking before the library marked it as used). """ # I'm not 100% sure the transaction is required here # It is not clear to me wether or not Flask-SQLAlchemy will make # a rollback if we raise a validation exception. # Since I lock the stock, I really want to make sure the lock is # removed ASAP. with transaction(): if booking.isCancelled or booking.status == BookingStatus.CANCELLED: booking.uncancel_booking_set_used() stock = offers_repository.get_and_lock_stock(stock_id=booking.stockId) stock.dnBookedQuantity += booking.quantity db.session.add(stock) db.session.add(booking) db.session.commit() logger.info("Booking was uncancelled and marked as used", extra={"bookingId": booking.id}) if booking.individualBookingId is not None: update_external_user(booking.individualBooking.user)
def report_offer(user: User, offer: Offer, reason: str, custom_reason: Optional[str]) -> None: try: # transaction() handles the commit/rollback operations # # UNIQUE_VIOLATION, CHECK_VIOLATION and STRING_DATA_RIGHT_TRUNCATION # errors are specific ones: # either the user tried to report the same error twice, which is not # allowed, or the client sent a invalid report (eg. OTHER without # custom reason / custom reason too long). # # Other errors are unexpected and are therefore re-raised as is. with transaction(): report = OfferReport(user=user, offer=offer, reason=reason, customReasonContent=custom_reason) db.session.add(report) except exc.IntegrityError as error: if error.orig.pgcode == UNIQUE_VIOLATION: raise OfferAlreadyReportedError() from error if error.orig.pgcode == CHECK_VIOLATION: raise ReportMalformed() from error raise offer_report_emails.send_report_notification(user, offer, reason, custom_reason)
def _cancel_booking(booking: Booking, reason: BookingCancellationReasons) -> None: """Cancel booking and update a user's credit information on Batch""" with transaction(): stock = offers_repository.get_and_lock_stock(stock_id=booking.stockId) db.session.refresh(booking) try: booking.cancel_booking() except (BookingIsAlreadyUsed, BookingIsAlreadyCancelled) as e: logger.info( str(e), extra={ "booking": booking.id, "reason": str(reason), }, ) return booking.cancellationReason = reason stock.dnBookedQuantity -= booking.quantity repository.save(booking, stock) logger.info( "Booking has been cancelled", extra={ "booking": booking.id, "reason": str(reason), }, ) if booking.individualBooking is not None: update_external_user(booking.individualBooking.user) search.async_index_offer_ids([booking.stock.offerId])
def delete_api_key(api_key_prefix: str): with transaction(): try: api.delete_api_key_by_user(current_user, api_key_prefix) except orm_exc.NoResultFound: raise ApiErrors({"prefix": "not found"}, 404) except ApiKeyDeletionDenied: raise ApiErrors({"api_key": "deletion forbidden"}, 403)
def check_and_activate_beneficiary( userId: int, deposit_source: str = None, has_activated_account: Optional[bool] = True ) -> users_models.User: with pcapi_repository.transaction(): user = users_repository.get_and_lock_user(userId) # TODO: Handle switch from underage_beneficiary to beneficiary if user.is_beneficiary or not user.hasCompletedIdCheck: db.session.rollback() return user user = activate_beneficiary(user, deposit_source, has_activated_account) return user
def update_cultural_survey(user: User, body: serializers.CulturalSurveyRequest) -> None: with transaction(): if not body.needs_to_fill_cultural_survey: user.needsToFillCulturalSurvey = False if body.cultural_survey_id: logger.info("User %s updated cultural survey", user.id, extra={"actor": user.id}) user.culturalSurveyId = body.cultural_survey_id user.culturalSurveyFilledDate = datetime.now() return
def update_cultural_survey(user: User, body: serializers.CulturalSurveyRequest) -> None: with transaction(): if not body.needs_to_fill_cultural_survey: user.needsToFillCulturalSurvey = False if body.cultural_survey_id: if user.culturalSurveyId: raise ApiErrors({ "culturalSurveyId": "L'utilisateur a déjà rempli le formulaire" }) user.culturalSurveyId = body.cultural_survey_id user.culturalSurveyFilledDate = datetime.now() return
def validate_phone_number(body: serialization_beneficiaries.ValidatePhoneNumberRequest) -> None: user = current_user._get_current_object() with transaction(): try: users_api.validate_phone_number_and_activate_user(user, body.code) except users_exceptions.ExpiredCode: raise ApiErrors({"message": "Le code saisi a expiré", "code": "EXPIRED_VALIDATION_CODE"}, status_code=400) except users_exceptions.NotValidCode: raise ApiErrors({"message": "Le code est invalide", "code": "INVALID_VALIDATION_CODE"}, status_code=400) except users_exceptions.InvalidPhoneNumber: raise ApiErrors( {"message": "Le numéro de téléphone est invalide", "code": "INVALID_PHONE_NUMBER"}, status_code=400 ) except users_exceptions.PhoneVerificationException: raise ApiErrors({"message": "L'envoi du code a échoué", "code": "CODE_SENDING_FAILURE"}, status_code=400)
def refuse_educational_booking( educational_booking_id: int) -> EducationalBooking: educational_booking = educational_repository.find_educational_booking_by_id( educational_booking_id) if educational_booking is None: raise exceptions.EducationalBookingNotFound() if educational_booking.status == EducationalBookingStatus.REFUSED: return educational_booking with transaction(): stock = offers_repository.get_and_lock_stock( stock_id=educational_booking.booking.stockId) booking = educational_booking.booking db.session.refresh(educational_booking.booking) try: educational_booking.mark_as_refused() except ( exceptions.EducationalBookingNotRefusable, exceptions.EducationalBookingAlreadyCancelled, ) as exception: logger.error( "User from adage trying to refuse educational booking that cannot be refused", extra={ "educational_booking_id": educational_booking_id, "exception_type": exception.__class__.__name__, }, ) raise exception stock.dnBookedQuantity -= booking.quantity repository.save(booking, educational_booking) logger.info( "Booking has been cancelled", extra={ "booking": booking.id, "reason": str(booking.cancellationReason), }, ) search.async_index_offer_ids([stock.offerId]) return educational_booking
def validate_phone_number( user: User, body: serializers.ValidatePhoneNumberRequest) -> None: with transaction(): try: api.validate_phone_number_and_activate_user(user, body.code) except exceptions.PhoneValidationAttemptsLimitReached: raise ApiErrors( { "message": "Le nombre de tentatives maximal est dépassé", "code": "TOO_MANY_VALIDATION_ATTEMPTS" }, status_code=400, ) except exceptions.ExpiredCode: raise ApiErrors( { "message": "Le code saisi a expiré", "code": "EXPIRED_VALIDATION_CODE" }, status_code=400) except exceptions.NotValidCode: raise ApiErrors( { "message": "Le code est invalide", "code": "INVALID_VALIDATION_CODE" }, status_code=400) except exceptions.InvalidPhoneNumber: raise ApiErrors( { "message": "Le numéro de téléphone est invalide", "code": "INVALID_PHONE_NUMBER" }, status_code=400) except exceptions.PhoneVerificationException: raise ApiErrors( { "message": "L'envoi du code a échoué", "code": "CODE_SENDING_FAILURE" }, status_code=400)
def confirm_educational_booking( educational_booking_id: int) -> EducationalBooking: educational_booking = educational_repository.find_educational_booking_by_id( educational_booking_id) if educational_booking is None: raise exceptions.EducationalBookingNotFound() booking: bookings_models.Booking = educational_booking.booking if booking.status == bookings_models.BookingStatus.CONFIRMED: return educational_booking validation.check_educational_booking_status(educational_booking) validation.check_confirmation_limit_date_has_not_passed( educational_booking) educational_institution_id = educational_booking.educationalInstitutionId educational_year_id = educational_booking.educationalYearId with transaction(): deposit = educational_repository.get_and_lock_educational_deposit( educational_institution_id, educational_year_id) validation.check_institution_fund( educational_institution_id, educational_year_id, booking.total_amount, deposit, ) booking.mark_as_confirmed() repository.save(booking) logger.info( "Head of institution confirmed an educational offer", extra={ "bookingId": booking.id, }, ) if booking.stock.offer.bookingEmail: mails.send(recipients=[booking.stock.offer.bookingEmail], data=_build_booking_confirmation_mail_data(booking)) return educational_booking
def update_beneficiary_mandatory_information( user: User, address: str, city: str, postal_code: str, activity: str, phone_number: Optional[str] = None ) -> None: user_initial_roles = user.roles update_payload = { "address": address, "city": city, "postalCode": postal_code, "departementCode": PostalCode(postal_code).get_departement_code(), "activity": activity, "hasCompletedIdCheck": True, } if not FeatureToggle.ENABLE_PHONE_VALIDATION.is_active() and not user.phoneNumber and phone_number: update_payload["phoneNumber"] = phone_number with transaction(): User.query.filter(User.id == user.id).update(update_payload) db.session.refresh(user) if ( not steps_to_become_beneficiary(user) and fraud_api.has_user_passed_fraud_checks(user) and not fraud_api.is_user_fraudster(user) ): subscription_api.check_and_activate_beneficiary(user.id) else: update_external_user(user) new_user_roles = user.roles underage_user_has_been_activated = ( UserRole.UNDERAGE_BENEFICIARY in new_user_roles and UserRole.UNDERAGE_BENEFICIARY not in user_initial_roles ) logger.info( "User id check profile updated", extra={"user": user.id, "has_been_activated": user.has_beneficiary_role or underage_user_has_been_activated}, )
def _cancel_bookings_from_stock(stock: Stock, reason: BookingCancellationReasons) -> list[Booking]: """ Cancel multiple bookings and update the users' credit information on Batch. Note that this will not reindex the stock.offer in Algolia """ deleted_bookings: list[Booking] = [] with transaction(): stock = offers_repository.get_and_lock_stock(stock_id=stock.id) for booking in stock.bookings: try: booking.cancel_booking() except (BookingIsAlreadyUsed, BookingIsAlreadyCancelled) as e: logger.info(str(e), extra={"booking": booking.id, "reason": str(reason)}) else: booking.cancellationReason = reason stock.dnBookedQuantity -= booking.quantity deleted_bookings.append(booking) repository.save(*deleted_bookings) for booking in deleted_bookings: if booking.individualBooking is not None: update_external_user(booking.individualBooking.user) return deleted_bookings
def book_educational_offer(redactor_informations: RedactorInformation, stock_id: int) -> EducationalBooking: redactor = educational_repository.find_redactor_by_email( redactor_informations.email) if not redactor: redactor = _create_redactor(redactor_informations) educational_institution = educational_repository.find_educational_institution_by_uai_code( redactor_informations.uai) validation.check_institution_exists(educational_institution) # The call to transaction here ensures we free the FOR UPDATE lock # on the stock if validation issues an exception with transaction(): stock = offers_repository.get_and_lock_stock(stock_id=stock_id) validation.check_stock_is_bookable(stock) educational_year = educational_repository.find_educational_year_by_date( stock.beginningDatetime) validation.check_educational_year_exists(educational_year) educational_booking = EducationalBooking( educationalInstitution=educational_institution, educationalYear=educational_year, educationalRedactor=redactor, confirmationLimitDate=stock.bookingLimitDatetime, ) booking = bookings_models.Booking( educationalBooking=educational_booking, stockId=stock.id, amount=stock.price, token=bookings_repository.generate_booking_token(), venueId=stock.offer.venueId, offererId=stock.offer.venue.managingOffererId, status=bookings_models.BookingStatus.PENDING, ) booking.dateCreated = datetime.utcnow() booking.cancellationLimitDate = compute_cancellation_limit_date( stock.beginningDatetime, booking.dateCreated) stock.dnBookedQuantity += EAC_DEFAULT_BOOKED_QUANTITY repository.save(booking) logger.info( "Redactor booked an educational offer", extra={ "redactor": redactor_informations.email, "offerId": stock.offerId, "stockId": stock.id, "bookingId": booking.id, }, ) if stock.offer.bookingEmail: mails.send(recipients=[stock.offer.bookingEmail], data=_build_prebooking_mail_data(booking)) search.async_index_offer_ids([stock.offerId]) return booking
def book_offer( beneficiary: User, stock_id: int, quantity: int, ) -> Booking: """ Return a booking or raise an exception if it's not possible. Update a user's credit information on Batch. """ # The call to transaction here ensures we free the FOR UPDATE lock # on the stock if validation issues an exception with transaction(): stock = offers_repository.get_and_lock_stock(stock_id=stock_id) validation.check_offer_is_not_educational(stock) validation.check_can_book_free_offer(beneficiary, stock) validation.check_offer_already_booked(beneficiary, stock.offer) validation.check_quantity(stock.offer, quantity) validation.check_stock_is_bookable(stock, quantity) total_amount = quantity * stock.price validation.check_expenses_limits(beneficiary, total_amount, stock.offer) from pcapi.core.offers.api import is_activation_code_applicable # To avoid import loops if is_activation_code_applicable(stock): validation.check_activation_code_available(stock) # FIXME (dbaty, 2020-10-20): if we directly set relations (for # example with `booking.user = beneficiary`) instead of foreign keys, # the session tries to add the object when `get_user_expenses()` # is called because autoflush is enabled. As such, the PostgreSQL # exceptions (tooManyBookings and insufficientFunds) may raise at # this point and will bubble up. If we want them to be caught, we # have to set foreign keys, so that the session is NOT autoflushed # in `get_user_expenses` and is only committed in `repository.save()` # where exceptions are caught. Since we are using flask-sqlalchemy, # I don't think that we should use autoflush, nor should we use # the `pcapi.repository.repository` module. booking = Booking( userId=beneficiary.id, stockId=stock.id, amount=stock.price, quantity=quantity, token=generate_booking_token(), venueId=stock.offer.venueId, offererId=stock.offer.venue.managingOffererId, ) booking.dateCreated = datetime.datetime.utcnow() booking.cancellationLimitDate = compute_cancellation_limit_date(stock.beginningDatetime, booking.dateCreated) if is_activation_code_applicable(stock): booking.activationCode = offers_repository.get_available_activation_code(stock) if FeatureToggle.AUTO_ACTIVATE_DIGITAL_BOOKINGS.is_active(): booking.mark_as_used() individual_booking = IndividualBooking( booking=booking, depositId=beneficiary.deposit.id if beneficiary.has_active_deposit else None, userId=beneficiary.id, ) stock.dnBookedQuantity += booking.quantity repository.save(individual_booking, stock) logger.info( "Beneficiary booked an offer", extra={ "actor": beneficiary.id, "offer": stock.offerId, "stock": stock.id, "booking": booking.id, "used": booking.isUsed, }, ) try: user_emails.send_individual_booking_confirmation_email_to_offerer(individual_booking) except MailServiceException as error: logger.exception("Could not send booking=%s confirmation email to offerer: %s", booking.id, error) try: user_emails.send_individual_booking_confirmation_email_to_beneficiary(individual_booking) except MailServiceException as error: logger.exception("Could not send booking=%s confirmation email to beneficiary: %s", booking.id, error) search.async_index_offer_ids([stock.offerId]) update_external_user(individual_booking.user) return individual_booking.booking