Submit social housing lettings and sales data (CORE)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

442 lines
16 KiB

class User < ApplicationRecord
acts_as_reader
# Include default devise modules. Others available are:
# :omniauthable
devise :database_authenticatable, :recoverable, :rememberable,
:trackable, :lockable, :two_factor_authenticatable, :confirmable, :timeoutable
# Marked as optional because we validate organisation_id below instead so that
# the error message is linked to the right field on the form
belongs_to :organisation, optional: true
has_many :legacy_users
has_many :bulk_uploads
validates :name, presence: true
validates :email, presence: true
validates :email, uniqueness: { allow_blank: true, case_sensitive: true, if: :will_save_change_to_email? }
validates :email, format: { with: Devise.email_regexp, allow_blank: true, if: :will_save_change_to_email? }
validates :password, presence: { if: :password_required? }
validates :password, length: { within: Devise.password_length, allow_blank: true }
validates :password, confirmation: { if: :password_required? }
validates :phone_extension, format: { with: /\A\d+\z/, allow_blank: true, message: I18n.t("validations.not_number", field: "") }
after_validation :send_data_protection_confirmation_reminder, if: :is_dpo_changed?
validates :organisation_id, presence: true
validate :organisation_not_merged
has_paper_trail ignore: %w[last_sign_in_at
current_sign_in_at
current_sign_in_ip
last_sign_in_ip
failed_attempts
unlock_token
locked_at
reset_password_token
reset_password_sent_at
remember_created_at
sign_in_count
updated_at]
has_one_time_password(encrypted: true)
auto_strip_attributes :name, squish: true
ROLES = {
data_provider: 1,
data_coordinator: 2,
support: 99,
}.freeze
LOG_REASSIGNMENT = {
reassign_all: "Yes, change the stock owner and the managing agent",
reassign_stock_owner: "Yes, change the stock owner but keep the managing agent the same",
reassign_managing_agent: "Yes, change the managing agent but keep the stock owner the same",
unassign: "No, unassign the logs",
}.freeze
enum :role, ROLES
scope :search_by_name, ->(name) { where("users.name ILIKE ?", "%#{name}%") }
scope :search_by_email, ->(email) { where("email ILIKE ?", "%#{email}%") }
scope :filter_by_active, -> { where(active: true) }
scope :search_by, ->(param) { search_by_name(param).or(search_by_email(param)) }
scope :sorted_by_organisation_and_role, -> { joins(:organisation).order("organisations.name", role: :desc, name: :asc) }
scope :filter_by_status, lambda { |statuses, _user = nil|
filtered_records = all
scopes = []
statuses.each do |status|
status = status == "active" ? "active_status" : status
status = status == "unconfirmed" ? "not_signed_in" : status
if respond_to?(status, true)
scopes << send(status)
end
end
if scopes.any?
filtered_records = filtered_records.merge(scopes.reduce(&:or))
end
filtered_records
}
scope :not_signed_in, -> { where(last_sign_in_at: nil, active: true) }
scope :deactivated, -> { where(active: false) }
scope :active_status, -> { where(active: true).where.not(last_sign_in_at: nil) }
scope :visible, lambda { |user = nil|
if user && !user.support?
where(discarded_at: nil, organisation: user.organisation.child_organisations + [user.organisation])
else
where(discarded_at: nil)
end
}
attr_accessor :log_reassignment
def lettings_logs
if support?
LettingsLog.all
else
LettingsLog.filter_by_organisation(organisation.absorbed_organisations + [organisation])
end
end
def sales_logs
if support?
SalesLog.all
else
SalesLog.filter_by_organisation(organisation.absorbed_organisations + [organisation])
end
end
def owned_lettings_logs
LettingsLog.filter_by_owning_organisation(organisation.absorbed_organisations + [organisation])
end
def managed_lettings_logs
LettingsLog.filter_by_managing_organisation(organisation.absorbed_organisations + [organisation])
end
def owned_sales_logs
SalesLog.filter_by_owning_organisation(organisation.absorbed_organisations + [organisation])
end
def managed_sales_logs
SalesLog.filter_by_managing_organisation(organisation.absorbed_organisations + [organisation])
end
def schemes
if support?
Scheme.all
else
Scheme.filter_by_owning_organisation(organisation.absorbed_organisations + [organisation] + organisation.parent_organisations)
end
end
def is_key_contact?
is_key_contact
end
def is_key_contact!
update(is_key_contact: true)
end
def is_data_protection_officer?
is_dpo
end
def is_data_protection_officer!
update!(is_dpo: true)
end
def deactivate!(reactivate_with_organisation: false)
update!(
active: false,
confirmed_at: nil,
sign_in_count: 0,
initial_confirmation_sent: false,
reactivate_with_organisation:,
unconfirmed_email: nil,
)
end
def reactivate!
update!(
active: true,
reactivate_with_organisation: false,
)
end
MFA_TEMPLATE_ID = "6bdf5ee1-8e01-4be1-b1f9-747061d8a24c".freeze
RESET_PASSWORD_TEMPLATE_ID = "2c410c19-80a7-481c-a531-2bcb3264f8e6".freeze
CONFIRMABLE_TEMPLATE_ID = "3fc2e3a7-0835-4b84-ab7a-ce51629eb614".freeze
RECONFIRMABLE_TEMPLATE_ID = "bcdec787-f0a7-46e9-8d63-b3e0a06ee455".freeze
USER_REACTIVATED_TEMPLATE_ID = "ac45a899-490e-4f59-ae8d-1256fc0001f9".freeze
FOR_OLD_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID = "3eb80517-1051-4dfc-b4cc-cb18228a3829".freeze
FOR_NEW_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID = "0cdd0be1-7fa5-4808-8225-ae4c5a002352".freeze
ORGANISATION_UPDATE_TEMPLATE_ID = "4b7716c0-cc5c-41dd-92e4-a0dff03bdf5e".freeze
def reset_password_notify_template
RESET_PASSWORD_TEMPLATE_ID
end
def confirmable_template
if last_sign_in_at.present? && (unconfirmed_email.blank? || unconfirmed_email == email)
USER_REACTIVATED_TEMPLATE_ID
elsif initial_confirmation_sent && !confirmed?
RECONFIRMABLE_TEMPLATE_ID
else
CONFIRMABLE_TEMPLATE_ID
end
end
def send_confirmation_instructions
return unless active?
super
update!(initial_confirmation_sent: true)
end
def need_two_factor_authentication?(_request)
return false if Rails.env.development?
return false if Rails.env.review?
support?
end
def send_two_factor_authentication_code(code)
template_id = MFA_TEMPLATE_ID
personalisation = { otp: code }
DeviseNotifyMailer.new.send_email(email, template_id, personalisation)
end
def assignable_roles
if Rails.env.staging? && in_staging_role_update_email_allowlist?
return ROLES
end
return {} unless data_coordinator? || support?
return ROLES if support?
ROLES.except(:support)
end
def in_staging_role_update_email_allowlist?
Rails.application.credentials[:staging_role_update_email_allowlist].include?(email.split("@").last.downcase)
end
def logs_filters(specific_org: false)
if (support? && !specific_org) || organisation.has_managing_agents? || organisation.has_stock_owners?
%w[years status needstypes assigned_to user owning_organisation managing_organisation bulk_upload_id user_text_search owning_organisation_text_search managing_organisation_text_search]
else
%w[years status needstypes assigned_to user bulk_upload_id user_text_search]
end
end
def scheme_filters(specific_org: false)
if (support? && !specific_org) || organisation.has_managing_agents? || organisation.has_stock_owners?
%w[status owning_organisation owning_organisation_text_search]
else
%w[status]
end
end
def bulk_uploads_filters(specific_org: false)
return [] unless support? && !specific_org
%w[user years uploaded_by uploading_organisation user_text_search uploading_organisation_text_search]
end
delegate :name, to: :organisation, prefix: true
def self.download_attributes
%w[id email name organisation_name role old_user_id is_dpo is_key_contact active sign_in_count last_sign_in_at]
end
def self.to_csv
CSV.generate(headers: true) do |csv|
csv << download_attributes
all.find_each do |record|
csv << download_attributes.map { |attr| record.public_send(attr) }
end
end
end
def can_toggle_active?(user)
self != user && (support? || data_coordinator?)
end
def valid_for_authentication?
super && active?
end
def editable_duplicate_lettings_logs_sets
lettings_logs.after_date(FormHandler.instance.lettings_earliest_open_for_editing_collection_start_date).duplicate_sets(id).map { |array_str| array_str ? array_str.map(&:to_i) : [] }
end
def editable_duplicate_sales_logs_sets
sales_logs.after_date(FormHandler.instance.sales_earliest_open_for_editing_collection_start_date).duplicate_sets(id).map { |array_str| array_str ? array_str.map(&:to_i) : [] }
end
def active_unread_notifications
Notification.active.unread_by(self)
end
def newest_active_unread_notification
active_unread_notifications.last
end
def status
return :deleted if discarded_at.present?
return :deactivated unless active
return :unconfirmed unless confirmed?
:active
end
def discard!
self.discarded_at = Time.zone.now
save!(validate: false)
end
def phone_with_extension
return phone if phone_extension.blank?
"#{phone}, Ext. #{phone_extension}"
end
def assigned_to_lettings_logs
lettings_logs.where(assigned_to: self)
end
def assigned_to_sales_logs
sales_logs.where(assigned_to: self)
end
def reassign_logs_and_update_organisation(new_organisation, log_reassignment)
return unless new_organisation
ActiveRecord::Base.transaction do
lettings_logs_to_reassign = assigned_to_lettings_logs.visible
sales_logs_to_reassign = assigned_to_sales_logs.visible
current_organisation = organisation
logs_count = lettings_logs_to_reassign.count + sales_logs_to_reassign.count
return if logs_count.positive? && (log_reassignment.blank? || !LOG_REASSIGNMENT.key?(log_reassignment.to_sym))
update!(organisation: new_organisation)
case log_reassignment
when "reassign_all"
reassign_all_orgs(new_organisation, lettings_logs_to_reassign, sales_logs_to_reassign)
when "reassign_stock_owner"
reassign_stock_owners(new_organisation, lettings_logs_to_reassign, sales_logs_to_reassign)
when "reassign_managing_agent"
reassign_managing_agents(new_organisation, lettings_logs_to_reassign, sales_logs_to_reassign)
when "unassign"
unassign_organisations(lettings_logs_to_reassign, sales_logs_to_reassign, current_organisation)
end
cancel_related_bulk_uploads
send_organisation_change_email(current_organisation, new_organisation, log_reassignment, logs_count)
rescue StandardError => e
Rails.logger.error("User update failed with: #{e.message}")
Sentry.capture_exception(e)
raise ActiveRecord::Rollback
end
end
protected
# Checks whether a password is needed or not. For validations only.
# Passwords are always required if it's a new record, or if the password
# or confirmation are being set somewhere.
def password_required?
!persisted? || !password.nil? || !password_confirmation.nil?
end
private
def organisation_not_merged
if organisation&.merge_date.present? && organisation.merge_date < Time.zone.now
errors.add :organisation_id, I18n.t("validations.organisation.merged")
end
end
def send_data_protection_confirmation_reminder
return unless persisted?
return unless is_dpo?
return if organisation.data_protection_confirmed?
DataProtectionConfirmationMailer.send_confirmation_email(self).deliver_later
end
def reassign_all_orgs(new_organisation, lettings_logs_to_reassign, sales_logs_to_reassign)
lettings_logs_to_reassign.update_all(owning_organisation_id: new_organisation.id, managing_organisation_id: new_organisation.id, values_updated_at: Time.zone.now)
sales_logs_to_reassign.update_all(owning_organisation_id: new_organisation.id, managing_organisation_id: new_organisation.id, values_updated_at: Time.zone.now)
end
def reassign_stock_owners(new_organisation, lettings_logs_to_reassign, sales_logs_to_reassign)
lettings_logs_to_reassign.update_all(owning_organisation_id: new_organisation.id, values_updated_at: Time.zone.now)
sales_logs_to_reassign.update_all(owning_organisation_id: new_organisation.id, values_updated_at: Time.zone.now)
end
def reassign_managing_agents(new_organisation, lettings_logs_to_reassign, sales_logs_to_reassign)
lettings_logs_to_reassign.update_all(managing_organisation_id: new_organisation.id, values_updated_at: Time.zone.now)
sales_logs_to_reassign.update_all(managing_organisation_id: new_organisation.id, values_updated_at: Time.zone.now)
end
def unassign_organisations(lettings_logs_to_reassign, sales_logs_to_reassign, current_organisation)
if User.find_by(name: "Unassigned", organisation: current_organisation)
unassigned_user = User.find_by(name: "Unassigned", organisation: current_organisation)
else
unassigned_user = User.new(
name: "Unassigned",
organisation_id:,
is_dpo: false,
encrypted_password: SecureRandom.hex(10),
email: SecureRandom.uuid,
confirmed_at: Time.zone.now,
active: false,
)
unassigned_user.save!(validate: false)
end
lettings_logs_to_reassign.update_all(assigned_to_id: unassigned_user.id, values_updated_at: Time.zone.now)
sales_logs_to_reassign.update_all(assigned_to_id: unassigned_user.id, values_updated_at: Time.zone.now)
end
def send_organisation_change_email(current_organisation, new_organisation, log_reassignment, logs_count)
reassigned_logs_text = ""
assigned_logs_count = logs_count == 1 ? "is 1 log" : "are #{logs_count} logs"
case log_reassignment
when "reassign_all"
reassigned_logs_text = "There #{assigned_logs_count} assigned to you. The stock owner and managing agent on #{logs_count == 1 ? 'this log' : 'these logs'} has been changed from #{current_organisation.name} to #{new_organisation.name}."
when "reassign_stock_owner"
reassigned_logs_text = "There #{assigned_logs_count} assigned to you. The stock owner on #{logs_count == 1 ? 'this log' : 'these logs'} has been changed from #{current_organisation.name} to #{new_organisation.name}."
when "reassign_managing_agent"
reassigned_logs_text = "There #{assigned_logs_count} assigned to you. The managing agent on #{logs_count == 1 ? 'this log' : 'these logs'} has been changed from #{current_organisation.name} to #{new_organisation.name}."
when "unassign"
reassigned_logs_text = "There #{assigned_logs_count} assigned to you. #{logs_count == 1 ? 'This' : 'These'} have now been unassigned."
end
template_id = ORGANISATION_UPDATE_TEMPLATE_ID
personalisation = {
from_organisation: "#{current_organisation.name} (Organisation ID: #{current_organisation.id})",
to_organisation: "#{new_organisation.name} (Organisation ID: #{new_organisation.id})",
reassigned_logs_text:,
}
DeviseNotifyMailer.new.send_email(email, template_id, personalisation)
end
def cancel_related_bulk_uploads
lettings_bu_ids = LettingsLog.where(assigned_to: self, status: "pending").map(&:bulk_upload_id).compact.uniq
BulkUpload.where(id: lettings_bu_ids).update!(choice: "cancelled-by-moved-user", moved_user_id: id)
sales_bu_ids = SalesLog.where(assigned_to: self, status: "pending").map(&:bulk_upload_id).compact.uniq
BulkUpload.where(id: sales_bu_ids).update!(choice: "cancelled-by-moved-user", moved_user_id: id)
end
end