## Code Context **Query:** account management, user authentication, profile, roles, permissions, SAML SSO, account_user, custom_role, company ### Entry Points - **Saml::UpdateAccountUsersProviderJob** (class) - enterprise/app/jobs/saml/update_account_users_provider_job.rb:1 - **AddAgentCapacityPolicyToAccountUsers** (class) - db/migrate/20250806140004_add_agent_capacity_policy_to_account_users.rb:3 - **Company** (class) - enterprise/app/models/company.rb:23 ### Related Symbols - app/jobs/application_job.rb: ApplicationJob:1 - app/models/application_record.rb: ApplicationRecord:1 - enterprise/app/jobs/saml/update_account_users_provider_job.rb: perform:6, should_update_user_provider?:22, user_has_other_saml_accounts?:30 - enterprise/app/models/account_saml_settings.rb: update_account_users_provider:64, reset_account_users_provider:68 - db/migrate/20250806140004_add_agent_capacity_policy_to_account_users.rb: change:4 - enterprise/app/models/company.rb: record_activity_at!:63, prepare_jsonb_attributes:71 ### Code #### Saml::UpdateAccountUsersProviderJob (enterprise/app/jobs/saml/update_account_users_provider_job.rb:1) ```ruby class Saml::UpdateAccountUsersProviderJob < ApplicationJob queue_as :default # Updates the authentication provider for users in an account # This job is triggered when SAML settings are created or destroyed def perform(account_id, provider) account = Account.find(account_id) account.users.find_each(batch_size: 1000) do |user| next unless should_update_user_provider?(user, provider) # rubocop:disable Rails/SkipsModelValidations user.update_column(:provider, provider) # rubocop:enable Rails/SkipsModelValidations end end private # Determines if a user's provider should be updated based on their multi-account status # When resetting to 'email', only update users who don't have SAML enabled on other accounts # This prevents breaking SAML authentication for users who belong to multiple accounts def should_update_user_provider?(user, provider) return !user_has_other_saml_accounts?(user) if provider == 'email' true end # Checks if the user belongs to any other accounts that have SAML configured # Used to preserve SAML authentication when one account disables SAML but others still use it def user_has_other_saml_accounts?(user) user.accounts.joins(:saml_settings).exists? end end ``` #### AddAgentCapacityPolicyToAccountUsers (db/migrate/20250806140004_add_agent_capacity_policy_to_account_users.rb:3) ```ruby class AddAgentCapacityPolicyToAccountUsers < ActiveRecord::Migration[7.1] def change add_reference :account_users, :agent_capacity_policy, null: true, index: true end end ``` #### Company (enterprise/app/models/company.rb:23) ```ruby class Company < ApplicationRecord include Avatarable ACTIVITY_ROLLUP_INTERVAL = 5.minutes validates :account_id, presence: true validates :name, presence: true, length: { maximum: Limits::COMPANY_NAME_LENGTH_LIMIT } validates :domain, allow_blank: true, format: { with: /\A[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+\z/, message: I18n.t('errors.companies.domain.invalid') } validates :domain, uniqueness: { scope: :account_id }, if: -> { domain.present? } validates :description, length: { maximum: Limits::COMPANY_DESCRIPTION_LENGTH_LIMIT } validates :custom_attributes, jsonb_attributes_length: true belongs_to :account has_many :contacts, dependent: :nullify before_validation :prepare_jsonb_attributes after_create_commit :fetch_favicon, if: -> { domain.present? } scope :ordered_by_name, -> { order(:name) } scope :search_by_name_or_domain, lambda { |query| where('name ILIKE :search OR domain ILIKE :search', search: "%#{query.strip}%") } scope :order_on_contacts_count, lambda { |direction| order( Arel::Nodes::SqlLiteral.new( sanitize_sql_for_order("\"companies\".\"contacts_count\" #{direction} NULLS LAST") ) ) } scope :order_on_last_activity_at, lambda { |direction| order( Arel::Nodes::SqlLiteral.new( sanitize_sql_for_order("\"companies\".\"last_activity_at\" #{direction} NULLS LAST") ) ) } def record_activity_at!(activity_at) // ... truncated ... ``` #### perform (enterprise/app/jobs/saml/update_account_users_provider_job.rb:6) ```ruby def perform(account_id, provider) account = Account.find(account_id) account.users.find_each(batch_size: 1000) do |user| next unless should_update_user_provider?(user, provider) # rubocop:disable Rails/SkipsModelValidations user.update_column(:provider, provider) # rubocop:enable Rails/SkipsModelValidations end end ``` #### should_update_user_provider? (enterprise/app/jobs/saml/update_account_users_provider_job.rb:22) ```ruby def should_update_user_provider?(user, provider) return !user_has_other_saml_accounts?(user) if provider == 'email' true end ``` #### user_has_other_saml_accounts? (enterprise/app/jobs/saml/update_account_users_provider_job.rb:30) ```ruby def user_has_other_saml_accounts?(user) user.accounts.joins(:saml_settings).exists? end ``` #### update_account_users_provider (enterprise/app/models/account_saml_settings.rb:64) ```ruby def update_account_users_provider Saml::UpdateAccountUsersProviderJob.perform_later(account_id, 'saml') end ``` #### reset_account_users_provider (enterprise/app/models/account_saml_settings.rb:68) ```ruby def reset_account_users_provider Saml::UpdateAccountUsersProviderJob.perform_later(account_id, 'email') end ``` #### change (db/migrate/20250806140004_add_agent_capacity_policy_to_account_users.rb:4) ```ruby def change add_reference :account_users, :agent_capacity_policy, null: true, index: true end ``` #### record_activity_at! (enterprise/app/models/company.rb:63) ```ruby def record_activity_at!(activity_at) return if last_activity_at.present? && last_activity_at > activity_at - ACTIVITY_ROLLUP_INTERVAL update!(last_activity_at: activity_at) end ``` #### prepare_jsonb_attributes (enterprise/app/models/company.rb:71) ```ruby def prepare_jsonb_attributes self.additional_attributes = {} unless additional_attributes.is_a?(Hash) self.custom_attributes = {} unless custom_attributes.is_a?(Hash) end ``` #### fetch_favicon (enterprise/app/models/company.rb:76) ```ruby def fetch_favicon Avatar::AvatarFromFaviconJob.set(wait: 5.seconds).perform_later(self) end ``` #### perform (app/jobs/migration/backfill_companies_contacts_count_job.rb:4) ```ruby def perform return unless ChatwootApp.enterprise? Company.find_in_batches(batch_size: 100) do |company_batch| company_batch.each do |company| Company.reset_counters(company.id, :contacts) end end end ``` #### find_or_create_company (enterprise/app/jobs/migration/company_account_batch_job.rb:32) ```ruby def find_or_create_company(contact, account) domain = extract_domain(contact.email) company_name = derive_company_name(contact, domain) Company.find_or_create_by!(account: account, domain: domain) do |company| company.name = company_name end rescue ActiveRecord::RecordNotUnique # Race condition: Another job created it between our check and create # just find the one that was created Company.find_by(account: account, domain: domain) end ``` #### associate_company_from_email (enterprise/app/services/contacts/company_association_service.rb:2) ```ruby def associate_company_from_email(contact) return nil if skip_association?(contact) company = find_or_create_company(contact) if company # rubocop:disable Rails/SkipsModelValidations # Using update_column and increment_counter to avoid triggering callbacks while maintaining counter cache contact.update_column(:company_id, company.id) Company.increment_counter(:contacts_count, company.id) # rubocop:enable Rails/SkipsModelValidations company.record_activity_at!(contact.last_activity_at) if contact.last_activity_at.present? end company end ``` #### find_or_create_company (enterprise/app/services/contacts/company_association_service.rb:29) ```ruby def find_or_create_company(contact) domain = extract_domain(contact.email) company_name = derive_company_name(contact, domain) Company.find_or_create_by!(account: contact.account, domain: domain) do |company| company.name = company_name end rescue ActiveRecord::RecordNotUnique # If another process created it first, just find that Company.find_by(account: contact.account, domain: domain) end ``` #### ApplicationJob (app/jobs/application_job.rb:1) ```ruby class ApplicationJob < ActiveJob::Base # https://api.rubyonrails.org/v5.2.1/classes/ActiveJob/Exceptions/ClassMethods.html discard_on ActiveJob::DeserializationError do |job, error| Rails.logger.info("Skipping #{job.class} with #{ job.instance_variable_get(:@serialized_arguments) } because of ActiveJob::DeserializationError (#{error.message})") end end ``` #### ApplicationRecord (app/models/application_record.rb:1) ```ruby class ApplicationRecord < ActiveRecord::Base include Events::Types self.abstract_class = true before_validation :validates_column_content_length # the models that exposed in email templates through liquid def droppables %w[Account Channel Conversation Inbox User Message] end # ModelDrop class should exist in app/drops def to_drop return unless droppables.include?(self.class.name) "#{self.class.name}Drop".constantize.new(self) end private # Generic validation for all columns of type string and text # Validates the length of the column to prevent DOS via large payloads # if a custom length validation is already present, skip the validation def validates_column_content_length self.class.columns.each do |column| check_and_validate_content_length(column) if column_of_type_string_or_text?(column) end end def column_of_type_string_or_text?(column) %i[string text].include?(column.type) end def check_and_validate_content_length(column) length_validator = self.class.validators_on(column.name).find { |v| v.kind == :length } validate_content_length(column) if length_validator.blank? end def validate_content_length(column) max_length = column.type == :text ? 20_000 : 255 return if self[column.name].nil? || self[column.name].length <= max_length errors.add(column.name.to_sym, "is too long (maximum is #{max_length} characters)") end def normalize_empty_string_to_nil(attrs = []) attrs // ... truncated ... ```