def initialize_account( user: User, password: str, apps_flyer_user_id: str = None, apps_flyer_platform: str = None, send_activation_mail: bool = True, remote_updates: bool = True, ) -> User: user.setPassword(password) if apps_flyer_user_id and apps_flyer_platform: if user.externalIds is None: user.externalIds = {} user.externalIds["apps_flyer"] = {"user": apps_flyer_user_id, "platform": apps_flyer_platform.upper()} repository.save(user) logger.info("Created user account", extra={"user": user.id}) delete_all_users_tokens(user) if remote_updates: update_external_user(user) if not user.isEmailValidated and send_activation_mail: request_email_confirmation(user) return user
def activate_beneficiary( user: users_models.User, deposit_source: str = None, has_activated_account: Optional[bool] = True ) -> users_models.User: if not deposit_source: beneficiary_import = subscription_repository.get_beneficiary_import_for_beneficiary(user) if not beneficiary_import: raise exceptions.BeneficiaryImportMissingException() eligibility = beneficiary_import.eligibilityType deposit_source = beneficiary_import.get_detailed_source() else: eligibility = users_models.EligibilityType.AGE18 if eligibility == users_models.EligibilityType.UNDERAGE: user.add_underage_beneficiary_role() elif eligibility == users_models.EligibilityType.AGE18: user.add_beneficiary_role() else: raise users_exception.InvalidEligibilityTypeException() if "apps_flyer" in user.externalIds: apps_flyer_job.log_user_becomes_beneficiary_event_job.delay(user.id) deposit = payments_api.create_deposit(user, deposit_source=deposit_source, eligibility=eligibility) db.session.add_all((user, deposit)) db.session.commit() logger.info("Activated beneficiary and created deposit", extra={"user": user.id, "source": deposit_source}) db.session.refresh(user) update_external_user(user) _send_beneficiary_activation_email(user, has_activated_account) return user
def update_user_profile( user: User, body: serializers.UserProfileUpdateRequest ) -> serializers.UserProfileResponse: api.update_notification_subscription(user, body.subscriptions) update_external_user(user) return serializers.UserProfileResponse.from_orm(user)
def signup_webapp(): objects_to_save = [] check_valid_signup_webapp(request) new_user = User(from_dict=request.json) new_user.email = sanitize_email(new_user.email) new_user.notificationSubscriptions = asdict( NotificationSubscriptions(marketing_email=bool(request.json.get("contact_ok"))) ) if settings.IS_INTEGRATION: objects_to_save.append(payments_api.create_deposit(new_user, "test")) else: authorized_emails, departement_codes = get_authorized_emails_and_dept_codes(ttl_hash=get_ttl_hash()) departement_code = _get_departement_code_when_authorized_or_error(authorized_emails, departement_codes) new_user.departementCode = departement_code new_user.remove_admin_role() new_user.remove_beneficiary_role() new_user.isEmailValidated = True new_user.needsToFillCulturalSurvey = False new_user.hasSeenTutorials = True objects_to_save.append(new_user) repository.save(*objects_to_save) update_external_user(new_user) return jsonify(as_dict(new_user, includes=BENEFICIARY_INCLUDES)), 201
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 _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 mark_as_unused(booking: Booking) -> None: validation.check_can_be_mark_as_unused(booking) booking.mark_as_unused_set_confirmed() repository.save(booking) logger.info("Booking was marked as unused", extra={"booking": booking.id}) if booking.individualBookingId is not None: update_external_user(booking.individualBooking.user)
def after_model_change(self, form: Form, model: User, is_created: bool) -> None: update_external_user(model) if is_created and not send_activation_email(model): token = create_reset_password_token(model) flash( f"L'envoi d'email a échoué. Le mot de passe peut être réinitialisé depuis le lien suivant : {build_pc_webapp_reset_password_link(token.value)}", "error", ) super().after_model_change(form, model, is_created)
def test_update_external_pro_user(): user = ProFactory() n_query_get_user = 1 n_query_is_pro = 1 with assert_num_queries(n_query_get_user + n_query_is_pro): update_external_user(user) assert len(batch_testing.requests) == 0 assert len(sendinblue_testing.sendinblue_requests) == 1
def test_update_external_user(): user = BeneficiaryGrant18Factory() IndividualBookingFactory(individualBooking__user=user) n_query_get_user = 1 n_query_get_bookings = 1 n_query_get_deposit = 1 n_query_is_pro = 1 n_query_get_last_favorite = 1 with assert_num_queries(n_query_get_user + n_query_get_bookings + n_query_get_deposit + n_query_is_pro + n_query_get_last_favorite): update_external_user(user) assert len(batch_testing.requests) == 1 assert len(sendinblue_testing.sendinblue_requests) == 1
def save(cls, beneficiary_pre_subscription: BeneficiaryPreSubscription, user: Optional[User] = None) -> User: user_sql_entity = beneficiary_pre_subscription_sql_converter.to_model( beneficiary_pre_subscription, user=user) users_api.attach_beneficiary_import_details( user_sql_entity, beneficiary_pre_subscription) repository.save(user_sql_entity) if not users_api.steps_to_become_beneficiary(user_sql_entity): user_sql_entity = subscription_api.check_and_activate_beneficiary( user_sql_entity.id, beneficiary_pre_subscription.deposit_source, has_activated_account=user is not None) else: update_external_user(user_sql_entity) return user_sql_entity
def validate_email(body: ValidateEmailRequest) -> ValidateEmailResponse: user = users_repo.get_user_with_valid_token( body.email_validation_token, [TokenType.EMAIL_VALIDATION], use_token=False ) if not user: raise ApiErrors({"token": ["Le token de validation d'email est invalide."]}) user.isEmailValidated = True repository.save(user) update_external_user(user) response = ValidateEmailResponse( access_token=users_api.create_user_access_token(user), refresh_token=create_refresh_token(identity=user.email), ) return response
def update_last_connection_date(user): previous_connection_date = user.lastConnectionDate last_connection_date = datetime.utcnow() should_save_last_connection_date = ( not previous_connection_date or last_connection_date - previous_connection_date > timedelta(minutes=15) ) should_update_sendinblue_last_connection_date = should_save_last_connection_date and ( not previous_connection_date or last_connection_date.date() - previous_connection_date.date() >= timedelta(days=1) ) if should_save_last_connection_date: user.lastConnectionDate = last_connection_date repository.save(user) if should_update_sendinblue_last_connection_date: update_external_user(user, skip_batch=True)
def add_to_favorite(): mediation = None offer_id = request.json.get("offerId") mediation_id = request.json.get("mediationId") check_offer_id_is_present_in_request(offer_id) offer = load_or_404(Offer, offer_id) if mediation_id is not None: mediation = load_or_404(Mediation, mediation_id) favorite_sql_entity = Favorite() favorite_sql_entity.mediation = mediation favorite_sql_entity.offer = offer favorite_sql_entity.user = current_user repository.save(favorite_sql_entity) favorite = favorite_domain_converter.to_domain(favorite_sql_entity) update_external_user(current_user) return jsonify(serialize_favorite(favorite)), 201
def process_beneficiary_application( information: fraud_models.DMSContent, procedure_id: int, preexisting_account: Optional[users_models.User] = None, ) -> None: """ Create/update a user account and complete the import process. Note that a 'user' is not always a beneficiary. """ user = create_beneficiary_from_application(information, user=preexisting_account) user.hasCompletedIdCheck = True try: repository.save(user) except ApiErrors as api_errors: logger.warning( "[BATCH][REMOTE IMPORT BENEFICIARIES] Could not save application %s, because of error: %s - Procedure %s", information.application_id, api_errors, procedure_id, ) return logger.info( "[BATCH][REMOTE IMPORT BENEFICIARIES] Successfully created user for application %s - Procedure %s", information.application_id, procedure_id, ) beneficiary_import = save_beneficiary_import_with_status( ImportStatus.CREATED, information.application_id, source=BeneficiaryImportSources.demarches_simplifiees, source_id=procedure_id, user=user, ) if not users_api.steps_to_become_beneficiary(user): deposit_source = beneficiary_import.get_detailed_source() subscription_api.activate_beneficiary(user, deposit_source) else: users_external.update_external_user(user)
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 post_new_password(): validate_new_password_request(request) token = request.get_json()["token"] new_password = request.get_json()["newPassword"] check_password_strength("newPassword", new_password) user = users_repo.get_user_with_valid_token(token, [TokenType.RESET_PASSWORD]) if not user: errors = ApiErrors() errors.add_error( "token", "Votre lien de changement de mot de passe est invalide.") raise errors user.setPassword(new_password) if not user.isEmailValidated: user.isEmailValidated = True update_external_user(user) repository.save(user) return "", 204
def has_completed_id_check(user: User) -> None: user.hasCompletedIdCheck = True repository.save(user) update_external_user(user)
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