Class: Account

Inherits:
ApplicationRecord show all
Includes:
Memery, Models::Auditable, PgSearch::Model
Defined in:
app/models/account.rb

Overview

== Schema Information

Table name: accounts
Database name: primary

id :integer not null, primary key
authentication_mode :integer default(0), not null
authentication_token :string(255)
confirmation_sent_at :datetime
confirmation_token :string(255)
confirmed_at :datetime
current_sign_in_at :datetime
current_sign_in_ip :string(255)
disabled :boolean default(FALSE), not null
email :citext
encrypted_password :string(255) default("")
failed_attempts :integer default(0)
ignore_ip_visit_check :boolean default(FALSE), not null
inherited_role_ids :integer default([]), is an Array
inherited_role_names :string default([]), is an Array
invitation_accepted_at :datetime
invitation_created_at :datetime
invitation_limit :integer
invitation_sent_at :datetime
invitation_token :string(255)
invited_by_type :string(255)
is_guest :boolean
last_login :datetime
last_sign_in_at :datetime
last_sign_in_ip :string(255)
locked_at :datetime
login :citext not null
name :string(100) default("")
password_salt :string(255) default("")
remember_created_at :datetime
require_myp_migration :boolean default(FALSE)
reset_password_sent_at :datetime
reset_password_token :string(255)
return_path_for_invite :string(255)
sign_in_count :integer default(0)
unlock_token :string(255)
created_at :datetime
updated_at :datetime
invited_by_id :integer
my_projects_user_id :integer
party_id :integer

Indexes

index_accounts_on_authentication_token (authentication_token) UNIQUE
index_accounts_on_confirmation_token (confirmation_token) UNIQUE
index_accounts_on_email (email)
index_accounts_on_inherited_role_names (inherited_role_names) USING gin
index_accounts_on_invitation_token (invitation_token)
index_accounts_on_invited_by_id (invited_by_id)
index_accounts_on_login (login) UNIQUE
index_accounts_on_my_projects_user_id (my_projects_user_id)
index_accounts_on_party_id (party_id)
index_accounts_on_reset_password_token (reset_password_token) UNIQUE
index_accounts_on_unlock_token (unlock_token) UNIQUE

Foreign Keys

accounts_party_id_fkey (party_id => parties.id)

Defined Under Namespace

Classes: Inviter

Constant Summary collapse

AUTH_INTERNAL =
0
AUTH_GOOGLE =
1

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Has and belongs to many collapse

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_id, *arguments) ⇒ Object (protected)



472
473
474
475
476
477
478
479
480
481
482
# File 'app/models/account.rb', line 472

def method_missing(method_id, *arguments, &)
  if /^is_\w+?/.match?(method_id.to_s)
    self.class.send :define_method, method_id do
      role_to_check = method_id.to_s.match(/^is_(\w+)?/)[1]
      send(:has_role?, role_to_check)
    end
    send(method_id)
  else
    super
  end
end

Instance Attribute Details

#authentication_modeObject (readonly)



106
# File 'app/models/account.rb', line 106

validates :authentication_mode, presence: true, inclusion: { in: [AUTH_INTERNAL, AUTH_GOOGLE] }

#emailObject (readonly)



108
# File 'app/models/account.rb', line 108

validates :email, presence: true, email_format: true

#email_reset_instructionsObject

Returns the value of attribute email_reset_instructions.



121
122
123
# File 'app/models/account.rb', line 121

def email_reset_instructions
  @email_reset_instructions
end

#invitation_codeObject

Returns the value of attribute invitation_code.



121
122
123
# File 'app/models/account.rb', line 121

def invitation_code
  @invitation_code
end

#loginObject (readonly)



107
# File 'app/models/account.rb', line 107

validates :login, presence: true, uniqueness: true

#marketing_sign_upObject

Returns the value of attribute marketing_sign_up.



121
122
123
# File 'app/models/account.rb', line 121

def 
  @marketing_sign_up
end

#passwordObject (readonly)

from devise validatable, we only keep password validations, not email since they require uniqueness if present, which we do not

Validations:

  • Presence ({ if: :password_required? })
  • Confirmation ({ if: -> { password.present? } })
  • Length ({ within: Devise.password_length, allow_blank: true })


112
# File 'app/models/account.rb', line 112

validates :password, presence: { if: :password_required? }

#password_confirmationObject (readonly)



118
# File 'app/models/account.rb', line 118

validates :password_confirmation, presence: true, if: -> { password.present? }

#require_passwordObject

Returns the value of attribute require_password.



121
122
123
# File 'app/models/account.rb', line 121

def require_password
  @require_password
end

#skip_notificationObject

Returns the value of attribute skip_notification.



121
122
123
# File 'app/models/account.rb', line 121

def skip_notification
  @skip_notification
end

Class Method Details

.account_api_signed_in(api_authentication_token) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'app/models/account.rb', line 144

def self.(api_authentication_token)
  # find account with any api_authentications using the api_authentication_token and return the account if present and not expired, otherwise, if expired, remove the api_authentication from the account
  res = nil
  api_auth = ApiAuthentication.find_by(api_authentication_token: api_authentication_token)
  if api_auth
    if api_auth.expired?
      api_auth.destroy
      res = nil
    else
      res = api_auth.
    end
  end
  res
end

.active_accountsActiveRecord::Relation<Account>

A relation of Accounts that are active accounts. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Account>)

See Also:



100
# File 'app/models/account.rb', line 100

scope :active_accounts, -> { where.not(disabled: true) }

.email_loginsActiveRecord::Relation<Account>

A relation of Accounts that are email logins. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Account>)

See Also:



101
# File 'app/models/account.rb', line 101

scope :email_logins, -> { where("accounts.login LIKE '%@%'") }

.get_unique_login_for_email(email) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'app/models/account.rb', line 159

def self.(email)
   = email
  if Account.where(login: email).exists?
    name_part, = email.split('@')
     = name_part
    counter = 0
     = 
    while Account.where(login: ).exists?
      counter += 1
       = "#{}-#{counter}"
    end
  end
  
end

.new_with_session(params, session) ⇒ Object



138
139
140
141
142
# File 'app/models/account.rb', line 138

def self.new_with_session(params, session)
  super.tap do ||
    . ||= session.dig('devise.facebook_data', 'extra', 'raw_info', 'email')
  end
end

Instance Method Details

#abilityObject



195
196
197
# File 'app/models/account.rb', line 195

def ability
  @ability ||= Ability.new(party)
end

#account_created_notify_reps_and_master_accountObject



410
411
412
413
414
# File 'app/models/account.rb', line 410

def 
  return if party&.is_employee? || skip_notification

  notify(activity: 'created_account') if party.present?
end

#active_for_authentication?Boolean

Returns:

  • (Boolean)


178
179
180
181
# File 'app/models/account.rb', line 178

def active_for_authentication?
  Rails.logger.debug { "active_for_authentication? disabled?: #{disabled?}" }
  super && !disabled?
end

#api_authenticationsActiveRecord::Relation<ApiAuthentication>

Returns:

See Also:



96
# File 'app/models/account.rb', line 96

has_many :api_authentications, dependent: :destroy

#api_sign_in!(is_guest = false) ⇒ Object

Here we manage api_authentications which is used only for API authentication, we don't want it to mix or reset the usual web based authentication token which is used for employee 'login as this customer" login or "continue as guest" accounts for email link login and is reset on successful registration or social login authentication. We want API authentication - the only kind of api login mechanism via email/password, social login or continue as guest via a non web based app - to be independent



317
318
319
320
321
322
323
# File 'app/models/account.rb', line 317

def api_sign_in!(is_guest = false)
  # create an api_authentication and return the api_authentication_token
  api_auth = api_authentications.build
  api_auth.is_guest = is_guest
  save!
  api_auth.api_authentication_token
end

#api_sign_out!(api_authentication_token) ⇒ Object



325
326
327
328
329
# File 'app/models/account.rb', line 325

def api_sign_out!(api_authentication_token)
  # remove api_authentication matching the api_authentication_token
  api_auth = api_authentications.find_by(api_authentication_token: api_authentication_token)
  api_auth&.destroy
end

#append_token(path_or_url, persist = nil) ⇒ Object



378
379
380
381
382
383
384
385
386
387
388
# File 'app/models/account.rb', line 378

def append_token(path_or_url, persist = nil)
  restore_authentication_token!
  uri = Addressable::URI.parse(path_or_url)
  new_query_ar = uri.query ? Addressable::URI.form_unencode(uri.query) : []
  new_query_ar << ['auth_token', authentication_token]
  new_query_ar << ['account_login', ]
  new_query_ar << ['persist', persist] if persist
  new_query_ar << ['quar_legacy_dt_params', 1]
  uri.query = Addressable::URI.form_encode(new_query_ar)
  uri.to_s
end

#apply_omniauth(omniauth) ⇒ Object

If you use a social media account login, then this extracts the name and stores it in the party
and builds the authentication



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'app/models/account.rb', line 268

def apply_omniauth(omniauth)
  logger.debug "apply_omniauth(omniauth): party.id: #{party.id}"
  logger.debug "apply_omniauth(omniauth): email: #{email}"
  logger.debug "apply_omniauth(omniauth): omniauth: #{omniauth.inspect}"
  if .blank?
    self. = Authentication.extract_email_from_omniauth_hash(omniauth)
    self.email = Authentication.extract_email_from_omniauth_hash(omniauth)
    logger.debug "apply_omniauth(omniauth): email: #{email}"
    logger.debug "apply_omniauth(omniauth): party.email: #{begin
      party.email
    rescue StandardError
      'n/a'
    end}"
  end

  if name.blank? || name.downcase.index('guest').present?
    omniauth_name = Authentication.extract_name_from_omniauth_hash(omniauth)
    logger.debug "apply_omniauth(omniauth): omniauth_name: #{omniauth_name}"
    self.name = omniauth_name if omniauth_name
    logger.debug "apply_omniauth(omniauth): SET name: #{name}"
    logger.debug "apply_omniauth(omniauth): party.name: #{begin
      party.name
    rescue StandardError
      'n/a'
    end}"
  end
  authentications.build(provider: omniauth['provider'], uid: omniauth['uid'])
end

#auth_token_required?Boolean

Returns:

  • (Boolean)


373
374
375
# File 'app/models/account.rb', line 373

def auth_token_required?
  .present? && authentications.empty? && encrypted_password.blank? && !is_employee?
end

#authentication_internal?Boolean

Returns:

  • (Boolean)


199
200
201
# File 'app/models/account.rb', line 199

def authentication_internal?
  authentication_mode == AUTH_INTERNAL
end

#authentication_methodsObject



305
306
307
308
309
310
311
312
313
# File 'app/models/account.rb', line 305

def authentication_methods
  auth_methods = []
  auth_methods << 'password' if encrypted_password.present?
  # auth_methods << "guest_email" if auth_token_required?
  authentications.each do |auth|
    auth_methods << auth.provider
  end
  auth_methods
end

#authentication_mode_nameObject



235
236
237
# File 'app/models/account.rb', line 235

def authentication_mode_name
  %w[Internal Google][authentication_mode]
end

#authenticationsActiveRecord::Relation<Authentication>

Returns:

See Also:



95
# File 'app/models/account.rb', line 95

has_many :authentications, dependent: :destroy

#can?Object

Alias for Ability#can?

Returns:

  • (Object)

    Ability#can?

See Also:



134
# File 'app/models/account.rb', line 134

delegate :can?, :cannot?, to: :ability

#can_access_mcp?Boolean

MCP (Model Context Protocol) access methods
Used for AI assistant integrations like Cursor/Claude

Returns:

  • (Boolean)


334
335
336
# File 'app/models/account.rb', line 334

def can_access_mcp?
  is_employee? && has_role?('mcp_access')
end

#can_impersonate?(customer_account) ⇒ Boolean

Returns:

  • (Boolean)


221
222
223
224
225
226
# File 'app/models/account.rb', line 221

def can_impersonate?()
  is_manager? ||
    is_customer_service_rep? ||
    party_id == .customer.primary_sales_rep_id ||
    party_id == .customer.secondary_sales_rep_id
end

#cannot?Object

Alias for Ability#cannot?

Returns:

  • (Object)

    Ability#cannot?

See Also:



134
# File 'app/models/account.rb', line 134

delegate :can?, :cannot?, to: :ability

#check_for_reset_instructionsObject (protected)



450
451
452
453
454
455
# File 'app/models/account.rb', line 450

def check_for_reset_instructions
  return unless email_reset_instructions == '1'

  self.email_password_reset_instructions = nil
  send_reset_password_instructions
end

#confirmation_required?Boolean (protected)

Returns:

  • (Boolean)


431
432
433
434
435
436
# File 'app/models/account.rb', line 431

def confirmation_required?
  return false if party&.is_employee?
  return false if authentications.present?

  true
end

#customerObject

Alias for Party#customer

Returns:

  • (Object)

    Party#customer

See Also:



203
# File 'app/models/account.rb', line 203

delegate :customer, to: :party

#employeeEmployee

Returns:

See Also:



92
# File 'app/models/account.rb', line 92

belongs_to :employee, class_name: 'Employee', foreign_key: :party_id, optional: true

#ensure_authentication_tokenObject



360
361
362
363
364
365
# File 'app/models/account.rb', line 360

def ensure_authentication_token
  return if authentication_token.present?

  self.authentication_token = generate_authentication_token
  save
end

#fetch_inherited_role_namesObject



421
422
423
# File 'app/models/account.rb', line 421

def fetch_inherited_role_names
  Role.where(id: inherited_role_ids).order(:name).pluck(:name)
end

#fully_enabled?Boolean

Returns:

  • (Boolean)


239
240
241
# File 'app/models/account.rb', line 239

def fully_enabled?
  .present? && (encrypted_password.present? || authentications.present?)
end

#generate_mcp_token!Object



338
339
340
341
342
# File 'app/models/account.rb', line 338

def generate_mcp_token!
  raise 'Account does not have MCP access' unless can_access_mcp?

  api_sign_in!
end

#has_role?(*roles_in_question, admin_check: true) ⇒ Boolean

Returns:

  • (Boolean)


228
229
230
231
232
233
# File 'app/models/account.rb', line 228

def has_role?(*roles_in_question, admin_check: true)
  # Handle both array and individual arguments
  roles_array = roles_in_question.flatten.map(&:to_s).map(&:downcase)
  (admin_check && inherited_role_names.include?('admin')) ||
    inherited_role_names.map(&:downcase).intersect?(roles_array)
end

#headers_for(_action) ⇒ Object



248
249
250
251
252
253
254
# File 'app/models/account.rb', line 248

def headers_for(_action)
  if party&.respond_to?(:primary_sales_rep?)
    { from: "'#{party.primary_sales_rep.name}' <#{party.primary_sales_rep.email}>" }
  else
    {}
  end
end

#inactive_messageObject



183
184
185
# File 'app/models/account.rb', line 183

def inactive_message
  'You account has been disabled, please contact us at (800) 875-5285'
end

#invitationsActiveRecord::Relation<Invitation>

Returns:

  • (ActiveRecord::Relation<Invitation>)

See Also:



94
# File 'app/models/account.rb', line 94

has_many :invitations, class_name: self.class.to_s, as: :invited_by

#is_admin?Boolean

Returns:

  • (Boolean)


213
214
215
# File 'app/models/account.rb', line 213

def is_admin?
  inherited_role_names.include?('admin')
end

#is_customer?Boolean

Returns:

  • (Boolean)


209
210
211
# File 'app/models/account.rb', line 209

def is_customer?
  party.is_a?(Customer)
end

#is_employee?Boolean

Returns:

  • (Boolean)


205
206
207
# File 'app/models/account.rb', line 205

def is_employee?
  party.is_a?(Employee)
end

#is_manager?Boolean

Returns:

  • (Boolean)


217
218
219
# File 'app/models/account.rb', line 217

def is_manager?
  is_admin? || inherited_role_names.any? { |rn| rn =~ /_manager$/ && rn != 'item_manager' }
end

#login_is_email?Boolean

Returns:

  • (Boolean)


174
175
176
# File 'app/models/account.rb', line 174

def 
   =~ RFC822::EMAIL
end

#notify(options) ⇒ Object



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'app/models/account.rb', line 344

def notify(options)
  mailers = []
  case options[:activity]
  when 'update_password'
    mailers << AccountMailer.password_changed(id)
  when 'update_email'
    # notify both old and new of the change if they are different
    mailers << AccountMailer.email_changed(id, options[:old_email], options[:old_login], options[:old_email]) if options[:old_email] != email
    mailers << AccountMailer.email_changed(id, options[:old_email], options[:old_login], email) if (options[:old_email] != email) || (options[:old_login] != )
  when 'created_account'
    mailers << AccountMailer.(id)
  end

  mailers.each { |m| m.deliver_later(wait: 10.seconds) }
end

#obfuscated_emailObject



425
426
427
# File 'app/models/account.rb', line 425

def obfuscated_email
  email&.gsub(/(?<=.{2}).*@.*(?=\S{2})/, '****@****')
end

#omniauth_provider_icon_basenamesObject



297
298
299
# File 'app/models/account.rb', line 297

def omniauth_provider_icon_basenames
  Authentication::PROVIDERS.select { |p, _h| authentications.find_by(provider: p) }.map { |_p, h| h[:icon] }
end

#partyParty

Returns:

See Also:



91
# File 'app/models/account.rb', line 91

belongs_to :party, optional: true

#password_required?Boolean

Returns:

  • (Boolean)


256
257
258
259
260
261
262
263
264
# File 'app/models/account.rb', line 256

def password_required?
  return false if authentication_mode == AUTH_GOOGLE

  res = true
  res = false if authentications.any?
  res = false if encrypted_password.blank?
  res = true if require_password
  res && (!persisted? || !password.nil? || !password_confirmation.nil?)
end

#pending_confirmation?Boolean

Returns:

  • (Boolean)


243
244
245
# File 'app/models/account.rb', line 243

def pending_confirmation?
  invitation_token.present?
end

#push_login_to_emailObject



416
417
418
419
# File 'app/models/account.rb', line 416

def 
  # Set the email to the login if login is an email, also fixes it if login changed
  self.email =  if email.blank? && 
end

#respond_to?(method_id, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


301
302
303
# File 'app/models/account.rb', line 301

def respond_to?(method_id, include_private = false)
  /^is_\w+?/.match?(method_id.to_s) || super
end

#restore_authentication_token!Object



367
368
369
370
371
# File 'app/models/account.rb', line 367

def restore_authentication_token!
  self.authentication_token = generate_authentication_token
  save
  super
end

#role_inheritanceObject (protected)



461
462
463
464
465
466
467
468
469
470
# File 'app/models/account.rb', line 461

def role_inheritance
  roles.each do |r|
    arids = r.ancestors_ids # Ancestors of this role
    # Any ancestor present? then we don't need to keep this role
    if (res = (role_ids & arids)).present?
      role_names = Role.where(id: res).pluck(:name)
      errors.add(:base, "Role #{r.name} is already inherited by role #{role_names.join(', ')} and cannot be assigned")
    end
  end
end

#rolesActiveRecord::Relation<Role>

Returns:

  • (ActiveRecord::Relation<Role>)

See Also:



98
# File 'app/models/account.rb', line 98

has_and_belongs_to_many :roles, after_add: :touch_me, after_remove: :touch_me, inverse_of: :accounts

#set_default_marketing_preferences(locale) ⇒ Object



187
188
189
190
191
192
193
# File 'app/models/account.rb', line 187

def set_default_marketing_preferences(locale)
  self. = if (locale == :'en-CA') || (locale == :'fr-CA')
                             false
                           else
                             true
                           end
end

#set_defaultsObject (protected)



438
439
440
441
# File 'app/models/account.rb', line 438

def set_defaults
  self.authentication_mode ||= AUTH_INTERNAL
  
end

#signed_login_url(target_url, purpose: :magic_login, expires_in: 7.days) ⇒ Object

Build a magic-login URL — the embedded login_token authenticates this
account when the URL is visited. Tokens are purpose-tagged via Rails'
signed_id so a token minted for one flow (e.g. cart recovery) cannot
be replayed against another (e.g. blog preview). TTL bound; no DB column.

Example:
account.signed_login_url(retrieve_my_cart_url(host: WEB_HOSTNAME))
account.signed_login_url(change_password_my_account_url(...), expires_in: 24.hours)



398
399
400
401
402
403
404
405
406
407
408
# File 'app/models/account.rb', line 398

def (target_url, purpose: :magic_login, expires_in: 7.days)
  uri = Addressable::URI.parse(target_url)
  new_query_ar = uri.query ? Addressable::URI.form_unencode(uri.query) : []
  # Drop any pre-existing `login_token` so we never emit a URL with two of
  # them (ambiguous to the auth strategy) and so re-signing an already-signed
  # URL doesn't leak the stale token alongside the fresh one.
  new_query_ar.reject! { |key, _| key == 'login_token' }
  new_query_ar << ['login_token', signed_id(purpose: purpose, expires_in: expires_in)]
  uri.query = Addressable::URI.form_encode(new_query_ar)
  uri.to_s
end

#timeout_inObject (protected)



484
485
486
487
# File 'app/models/account.rb', line 484

def timeout_in
  # puts "TIMEOUTS_IN: auth_token_required?: #{auth_token_required?}, Devise.timeout_in: #{Devise.timeout_in}"
  auth_token_required? ? 30.minutes : Devise.timeout_in
end

#touch_me(_role) ⇒ Object (protected)



457
458
459
# File 'app/models/account.rb', line 457

def touch_me(_role)
  touch unless new_record?
end

#update_matching_contact_point_if_neededObject (protected)



443
444
445
446
447
448
# File 'app/models/account.rb', line 443

def update_matching_contact_point_if_needed
  return unless email_changed? && valid?

  cp = party.contact_points.where(detail: email_was, category: 'email').first
  cp&.update_attribute(:detail, email)
end