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))
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
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])
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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)
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
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
Exemplo n.º 13
0
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},
    )
Exemplo n.º 14
0
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
Exemplo n.º 15
0
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
Exemplo n.º 16
0
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