app/models/profile.rb
# frozen_string_literal: true
class Profile < ApplicationRecord
FACIAL_MATCH_IDV_LEVELS = %w[unsupervised_with_selfie in_person].to_set.freeze
belongs_to :user
# rubocop:disable Rails/InverseOf
belongs_to :initiating_service_provider,
class_name: 'ServiceProvider',
foreign_key: 'initiating_service_provider_issuer',
primary_key: 'issuer',
optional: true
# rubocop:enable Rails/InverseOf
has_many :gpo_confirmation_codes, dependent: :destroy
has_one :in_person_enrollment, dependent: :destroy
validates :active, uniqueness: { scope: :user_id, if: :active? }
has_one :establishing_in_person_enrollment,
-> { where(status: :establishing).order(created_at: :desc) },
class_name: 'InPersonEnrollment', foreign_key: :profile_id, inverse_of: :profile,
dependent: :destroy
enum deactivation_reason: {
password_reset: 1,
encryption_error: 2,
gpo_verification_pending_NO_LONGER_USED: 3, # deprecated
verification_cancelled: 4,
in_person_verification_pending_NO_LONGER_USED: 5, # deprecated
}
enum fraud_pending_reason: {
threatmetrix_review: 1,
threatmetrix_reject: 2,
}
enum idv_level: {
legacy_unsupervised: 1,
legacy_in_person: 2,
unsupervised_with_selfie: 3,
in_person: 4,
}
attr_reader :personal_key
# Class methods
def self.active
where(active: true)
end
def self.verified
where.not(verified_at: nil)
end
def self.fraud_rejection
where.not(fraud_rejection_at: nil)
end
def self.fraud_review_pending
where.not(fraud_review_pending_at: nil)
end
def self.gpo_verification_pending
where.not(gpo_verification_pending_at: nil)
end
def self.in_person_verification_pending
where.not(in_person_verification_pending_at: nil)
end
# Instance methods
def fraud_review_pending?
fraud_review_pending_at.present?
end
def fraud_rejection?
fraud_rejection_at.present?
end
def gpo_verification_pending?
gpo_verification_pending_at.present?
end
def pending_reasons
[
*(:gpo_verification_pending if gpo_verification_pending?),
*(:fraud_check_pending if fraud_deactivation_reason?),
*(:in_person_verification_pending if in_person_verification_pending?),
]
end
# rubocop:disable Rails/SkipsModelValidations
def activate(reason_deactivated: nil)
confirm_that_profile_can_be_activated!
now = Time.zone.now
profile_to_deactivate = Profile.find_by(user_id: user_id, active: true)
is_reproof = profile_to_deactivate.present?
is_facial_match_upgrade = is_reproof && facial_match? && !profile_to_deactivate.facial_match?
attrs = {
active: true,
activated_at: now,
}
attrs[:verified_at] = now unless reason_deactivated == :password_reset || verified_at
transaction do
Profile.where(user_id: user_id).update_all(active: false)
update!(attrs)
end
track_facial_match_reproof if is_facial_match_upgrade
send_push_notifications if is_reproof
end
# rubocop:enable Rails/SkipsModelValidations
def tmx_status
return nil unless IdentityConfig.store.in_person_proofing_enforce_tmx
return nil unless FeatureManagement.proofing_device_profiling_decisioning_enabled?
fraud_pending_reason || :threatmetrix_pass
end
def reason_not_to_activate
if pending_reasons.any?
"Attempting to activate profile with pending reasons: #{pending_reasons.join(',')}"
elsif deactivation_reason.present?
"Attempting to activate profile with deactivation reason: #{deactivation_reason}"
end
end
def remove_gpo_deactivation_reason
update!(gpo_verification_pending_at: nil)
update!(deactivation_reason: nil) if gpo_verification_pending_NO_LONGER_USED?
end
def activate_after_passing_review
transaction do
update!(
fraud_review_pending_at: nil,
fraud_rejection_at: nil,
fraud_pending_reason: nil,
)
activate
end
end
def activate_after_fraud_review_unnecessary
transaction do
update!(
fraud_review_pending_at: nil,
fraud_rejection_at: nil,
fraud_pending_reason: nil,
)
activate
end
end
def activate_after_passing_in_person
transaction do
update!(
fraud_review_pending_at: nil,
fraud_rejection_at: nil,
fraud_pending_reason: nil,
deactivation_reason: nil,
in_person_verification_pending_at: nil,
)
activate
end
end
def activate_after_password_reset
if password_reset?
transaction do
update!(
deactivation_reason: nil,
)
activate(reason_deactivated: :password_reset)
end
end
end
def deactivate(reason)
update!(active: false, deactivation_reason: reason)
end
def fraud_deactivation_reason?
fraud_review_pending? || fraud_rejection?
end
def in_person_verification_pending?
in_person_verification_pending_at.present?
end
def deactivate_due_to_gpo_expiration
raise 'Profile is not pending GPO verification' if gpo_verification_pending_at.nil?
update!(
active: false,
gpo_verification_pending_at: nil,
gpo_verification_expired_at: Time.zone.now,
)
end
def deactivate_due_to_in_person_verification_cancelled
update!(
active: false,
in_person_verification_pending_at: nil,
deactivation_reason: deactivation_reason.presence || :verification_cancelled,
)
end
def deactivate_for_in_person_verification
update!(active: false, in_person_verification_pending_at: Time.zone.now)
end
def deactivate_for_gpo_verification
update!(active: false, gpo_verification_pending_at: Time.zone.now)
end
def deactivate_for_fraud_review
update!(
active: false,
fraud_review_pending_at: Time.zone.now,
fraud_rejection_at: nil,
in_person_verification_pending_at: nil,
)
end
def deactivate_due_to_ipp_expiration_during_fraud_review
update!(
active: false,
in_person_verification_pending_at: nil,
fraud_rejection_at: Time.zone.now,
)
end
def reject_for_fraud(notify_user:)
update!(
active: false,
fraud_review_pending_at: nil,
fraud_rejection_at: Time.zone.now,
)
UserAlerts::AlertUserAboutAccountRejected.call(user) if notify_user
end
def decrypt_pii(password)
encryptor = Encryption::Encryptors::PiiEncryptor.new(password)
encrypted_pii_ciphertext_pair = Encryption::RegionalCiphertextPair.new(
single_region_ciphertext: encrypted_pii,
multi_region_ciphertext: encrypted_pii_multi_region,
)
decrypted_json = encryptor.decrypt(encrypted_pii_ciphertext_pair, user_uuid: user.uuid)
Pii::Attributes.new_from_json(decrypted_json)
end
# @return [Pii::Attributes]
def recover_pii(personal_key)
encryptor = Encryption::Encryptors::PiiEncryptor.new(personal_key)
encrypted_pii_recovery_ciphertext_pair = Encryption::RegionalCiphertextPair.new(
single_region_ciphertext: encrypted_pii_recovery,
multi_region_ciphertext: encrypted_pii_recovery_multi_region,
)
decrypted_recovery_json = encryptor.decrypt(
encrypted_pii_recovery_ciphertext_pair, user_uuid: user.uuid
)
return nil if JSON.parse(decrypted_recovery_json).nil?
Pii::Attributes.new_from_json(decrypted_recovery_json)
end
# @param [Pii::Attributes] pii
def encrypt_pii(pii, password)
encrypt_ssn_fingerprint(pii)
encrypt_compound_pii_fingerprint(pii)
encryptor = Encryption::Encryptors::PiiEncryptor.new(password)
self.encrypted_pii, self.encrypted_pii_multi_region = encryptor.encrypt(
pii.to_json, user_uuid: user.uuid
)
encrypt_recovery_pii(pii)
end
# @param [Pii::Attributes] pii
def encrypt_recovery_pii(pii, personal_key: nil)
personal_key ||= personal_key_generator.generate!
encryptor = Encryption::Encryptors::PiiEncryptor.new(
personal_key_generator.normalize(personal_key),
)
self.encrypted_pii_recovery, self.encrypted_pii_recovery_multi_region = encryptor.encrypt(
pii.to_json, user_uuid: user.uuid
)
@personal_key = personal_key
end
# @param [Pii::Attributes] pii
def self.build_compound_pii(pii)
values = [
pii.first_name,
pii.last_name,
pii.zipcode,
pii.dob && DateParser.parse_legacy(pii[:dob]).year,
]
return unless values.all?(&:present?)
values.join(':')
end
def profile_age_in_seconds
(Time.zone.now - created_at).round
end
def facial_match?
FACIAL_MATCH_IDV_LEVELS.include?(idv_level)
end
private
def confirm_that_profile_can_be_activated!
raise reason_not_to_activate if reason_not_to_activate
end
def personal_key_generator
@personal_key_generator ||= PersonalKeyGenerator.new(user)
end
def encrypt_ssn_fingerprint(pii)
ssn = pii.ssn
self.ssn_signature = Pii::Fingerprinter.fingerprint(ssn) if ssn
end
def encrypt_compound_pii_fingerprint(pii)
compound_pii = self.class.build_compound_pii(pii)
if compound_pii
self.name_zip_birth_year_signature = Pii::Fingerprinter.fingerprint(compound_pii)
end
end
def send_push_notifications
event = PushNotification::ReproofCompletedEvent.new(user: user)
PushNotification::HttpPush.deliver(event)
end
def track_facial_match_reproof
SpUpgradedFacialMatchProfile.create(
user: user,
upgraded_at: Time.zone.now,
idv_level: idv_level,
issuer: initiating_service_provider_issuer,
)
end
end