3scale/porta

View on GitHub
app/models/account.rb

Summary

Maintainability
D
2 days
Test Coverage
# frozen_string_literal: true
 
Class `Account` has 56 methods (exceeds 20 allowed). Consider refactoring.
File `account.rb` has 405 lines of code (exceeds 250 allowed). Consider refactoring.
Account has at least 49 methods
class Account < ApplicationRecord
attribute :credit_card_expires_on, :date
self.ignored_columns = %i[proxy_configs_file_name proxy_configs_content_type proxy_configs_file_size
proxy_configs_updated_at proxy_configs_conf_file_name proxy_configs_conf_content_type
proxy_configs_conf_file_size proxy_configs_conf_updated_at]
 
# it has to be THE FIRST callback after create, so associations get the tenant id
after_create :update_tenant_id, if: :provider?, prepend: true
 
 
include Fields::Fields
required_fields_are :org_name
optional_fields_are :org_legaladdress, :org_legaladdress_cont,
:telephone_number, :vat_code, :vat_rate, :fiscal_code,
:state_region, :city, :country, :zip,
:primary_business, :business_category, :po_number
default_fields_are :org_name
internal_fields_are :billing_address
 
set_fields_account_source :self
include Fields::Provider
 
include MasterMethods
 
include Backend::ModelExtensions::Provider
include Logic::Buyer
include Logic::PlanChanges::Provider
include Logic::Contracting::Buyer
include Logic::Signup::Provider
include Logic::CMS::Provider
include Logic::ProviderSignup::Provider
include Logic::ProviderUpgrade::Provider
include Logic::RollingUpdates::Provider
include Logic::Contracting::Provider
include Logic::ProviderSettings
include Logic::ProviderConstraints
include ProviderMethods
include ServiceDiscovery::AuthenticationProviderSupport
 
include BuyerMethods
include Billing
include BillingAddress
include PaymentDetails
include CreditCard
include Gateway
include States
include ProviderDomains
include Indices::AccountIndex::ForAccount
 
self.background_deletion = [
:users,
:mail_dispatch_rules,
[:api_docs_services, { class_name: 'ApiDocs::Service' }],
:services,
:contracts,
:account_plans,
[:settings, { action: :destroy, class_name: 'Settings', has_many: false }],
[:payment_detail, { action: :destroy, has_many: false }],
[:buyer_accounts, { action: :destroy, class_name: 'Account' }],
[:payment_gateway_setting, { action: :destroy, has_many: false }],
[:profile, { action: :delete, has_many: false }],
[:templates, { action: :delete, class_name: 'CMS::Template' }],
[:sections, { action: :delete, class_name: 'CMS::Section' }],
[:provided_sections, { action: :delete, class_name: 'CMS::Section' }],
[:redirects, { action: :delete, class_name: 'CMS::Redirect' }],
[:files, { action: :delete, class_name: 'CMS::File' }],
[:builtin_pages, { action: :delete, class_name: 'CMS::BuiltinPage' }],
[:provided_groups, { action: :delete, class_name: 'CMS::Group' }]
].freeze
 
#TODO: this needs testing?
scope :providers, -> { where(provider: true) }
 
scope :providers_with_master, -> { where.has { (provider == true) | (master == true) } }
scope :tenants, -> { providers.not_master }
 
#OPTIMIZE: adding master boolean a default to false, then this could be done
# scope :buyers, :conditions => {:provider => false, :master => false}
scope :buyers, -> { where(provider: false, buyer: true) }
 
scope :not_master, -> { where.has { (master != true) | (master == nil) } }
 
scope :searchable, -> { not_master.without_to_be_deleted.includes(:users, :bought_cinstances) }
 
annotated
audited
 
# this is done in a callback because we want to do this AFTER the account is deleted
# otherwise the before_destroy admin check in the user will stop the deletion
after_destroy :destroy_all_users
after_destroy :destroy_all_contracts
 
include WebHooksHelpers #TODO: make this inclusion more dsl-ish
fires_human_web_hooks_on_events
 
before_validation(on: :create, if: :provider?) { generate_s3_prefix }
before_validation(on: :create, if: :provider?) { generate_domains }
before_create :generate_site_access_code
 
attr_accessible is recommended over attr_protected
attr_protected :master, :provider, :buyer, :from_email, :vat_rate, :sample_data, :default_service_id, :s3_prefix,
:provider_account_id, :paid_at, :paid, :signs_legal_terms, :tenant_id, :default_account_plan_id,
:default_service_id, :domain, :subdomain, :self_subdomain, :self_domain,:audit_ids, :partner,
:hosted_proxy_deployed_at
 
belongs_to :partner
has_many :users, inverse_of: :account, dependent: :destroy
has_many :admin_users, -> { admins }, class_name: 'User', inverse_of: :account
 
has_one :admin_user, -> { admins.but_impersonation_admin }, class_name: 'User', inverse_of: :account
 
has_many :features, as: :featurable
has_many :email_configurations
 
composed_of :address,
mapping: ThreeScale::Address.account_mapping,
class_name: 'ThreeScale::Address'
 
before_destroy :destroy_features
 
scope :free, ->(free_date) { where.has { not_exists Contract.have_paid_on(free_date).by_account(BabySqueel[:accounts].id).select(:id) } }
 
scope :lacks_cinstance_with_plan_system_name, ->(system_names) {
where.has do
not_exists Cinstance.by_account(BabySqueel[:accounts].id).by_plan_system_name(system_names).select(:id)
end
}
 
alias deleted? scheduled_for_deletion?
 
def destroy_features
features.destroy_all
end
 
def destroy_all_contracts
contracts.reload.destroy_all
end
 
def smart_destroy
Account tests 'master?' at least 3 times
return if master?
if buyer?
first_admin # needs to be cached before destroying
destroy
else
schedule_for_deletion
end
end
 
def schedule_backend_sync_worker
Account tests 'provider?' at least 4 times
BackendProviderSyncWorker.enqueue(id) if provider?
end
 
has_many :messages, -> { visible }, foreign_key: :sender_id, class_name: 'Message'
has_many :sent_messages, foreign_key: :sender_id, class_name: 'Message'
 
has_many :mail_dispatch_rules, dependent: :destroy, inverse_of: :account
has_many :system_operations, through: :mail_dispatch_rules
 
# Deleted received messages
has_many :hidden_messages, -> { latest_first.received.hidden }, as: :receiver, class_name: 'MessageRecipient'
has_many :received_messages, -> { latest_first.received.visible }, as: :receiver, class_name: 'MessageRecipient'
 
has_many :api_docs_services, class_name: 'ApiDocs::Service', dependent: :destroy
has_many :log_entries, foreign_key: 'provider_id'
 
has_many :events, class_name: 'EventStore::Event', foreign_key: :provider_id, inverse_of: :account
has_many :access_tokens, through: :users
has_many :sso_authorizations, through: :users
has_many :user_sessions, through: :users
 
alias_attribute :name, :org_name
 
has_one :onboarding
 
def trashed_messages
Message.where('id IN (:sent) OR id IN (:received)', sent: sent_messages.hidden.select(:id),
received: hidden_messages.pluck(:message_id))
end
 
def onboarding
super || Onboarding.null
end
 
def admins
users.admins.but_impersonation_admin
end
 
def first_admin
@_first_admin ||= admins.first
end
 
def first_admin!
@_first_admin ||= admins.first!
end
 
def has_impersonation_admin?
provider? && find_impersonation_admin
end
 
def find_impersonation_admin
users.admins.find_by(username: ThreeScale.config.impersonation_admin[:username])
end
 
# Users of this account + users of all buyer accounts of this account (if it is provider).
def managed_users
conditions = ['users.account_id = :id OR accounts.provider_account_id = :id', { id: id }]
User.where(conditions).joins(:account).readonly(false)
end
 
def build_forum(attributes = {})
Unprotected mass assignment
self.forum = Forum.new(attributes.reverse_merge(name: 'Forum'))
end
 
def create_forum(attributes = {})
build_forum(attributes).tap(&:save)
end
 
#TODO: check if the comment below still holds
# profile is using acts_as_audited and it will not work if :dependent => :destroy
has_one :profile, dependent: :delete
has_one :settings, dependent: :destroy, inverse_of: :account, autosave: true
lazy_initialization_for :profile, :settings, if: :should_not_be_deleted?
accepts_nested_attributes_for :profile
 
belongs_to :country
 
#TODO: test this one
def bought?(plan)
contracts.map(&:plan).include?(plan)
end
 
has_many :invitations
 
# XXX: This is hax is needed because of current cancan limitation.
#
# Basically, to allow cancan tests like this:
#
# can? :create, provider => Account
#
# there has to be method :account on the account, which return the provider account.
#
# TODO: Patch cancan to support parents with different names than it's class,
# or
# Split Account class into three classes: BuyerAccount, ProviderAccount and MasterAccount
#
alias account provider_account
 
#
# Searching
#
 
include Account::Search
 
#
# Validations
#
 
# TODO: multitenant. enable it?
# validates_uniqueness_of :s3_prefix
validates :org_name, presence: true, length: { maximum: 255 },
format: { with: /\A.*[a-zA-Z0-9]+.*\z/, message: :invalid_format }
 
 
validates :org_legaladdress, :domain, :telephone_number, :site_access_code,
:billing_address_name, :billing_address_address1, :billing_address_address2, :billing_address_city,
:billing_address_state, :billing_address_country, :billing_address_zip, :billing_address_phone,
:org_legaladdress_cont, :city, :state_region, :state, :timezone, :from_email, :primary_business,
:business_category, :zip, :self_domain, :s3_prefix, :support_email, :finance_support_email,
:billing_address_first_name, :billing_address_last_name, :po_number, :vat_code, :fiscal_code,
length: { maximum: 255 }
 
validates :extra_fields, :invoice_footnote, :vat_zero_text,
length: { maximum: 65535 }
 
validate :validate_timezone
validate :master_uniqueness, if: :master?
 
include Authentication
validates :support_email, format: { with: RE_EMAIL_OK, message: MSG_EMAIL_BAD,
allow_blank: true, unless: :buyer? }
validates :finance_support_email, format: { with: RE_EMAIL_OK, message: MSG_EMAIL_BAD,
allow_blank: true, unless: :buyer? }
#
# Other stuff
#
def config
@config ||= Configuration.new(self)
end
 
scope :created_before, ->(date) { where(['created_at <= ?', date]) }
scope :created_after, ->(date) { where(['created_at >= ?', date]) }
 
def self.attributes_for_destroy_list
%w[id org_name state org_legaladdress org_legaladdress_cont city state_region telephone_number vat_code vat_rate extra_fields created_at]
end
 
def self.master
find_by!(master: true)
end
 
def self.provider
find_by!(provider: true)
end
 
def self.master?
exists?(master: true)
end
 
def self.master_id
Rails.cache.fetch('master_account_id') { master.id }
end
 
def self.master_on_premises
master if ThreeScale.master_on_premises?
end
 
def country=(country_name)
self.country_id = if country_name.is_a? Country
country_name.id
elsif country_name
Country.find_by(name: country_name)&.id
end
end
 
def special_fields
%i[country annotations]
end
 
# Returns the id corresponding to an account with given api key. This function avoids
# database lookup if possible (uses cache), so it is super fast.
def self.id_from_api_key(api_key)
Rails.cache.fetch("account_ids/#{api_key}") do
Account.first_by_provider_key!(api_key).id # rubocop:disable Rails/DynamicFindBy
end
end
 
# TODO: Put the bulk approval back.
 
# #OPTIMIZE these bulk methods won't work if an unexisting id is passed!
 
# # Calls approve on an array of accounts
# def self.bulk_approve(ids)
# ids.each{|id| self.find(id).approve!}
# end
 
# # Calls reject on an array of accounts
# def self.bulk_reject(ids)
# ids.each{|id| self.find(id).reject!}
# end
 
# def self.to_csv
# end
 
def emails
admins.map(&:email).compact
end
 
def timezone
self[:timezone] || 'UTC'
end
 
# Currency associated with this account. Fallbacks to country's
# currency and further to DEFAULT_CURRENCY.
#
def currency
billing_strategy&.currency || country&.currency || DEFAULT_CURRENCY
end
 
# Tax rate associated with this account. It is taken from it's country of
# residence.
def tax_rate
country&.tax_rate || 0.0
end
 
# Return country name
def country_name
country ? country.name : ''
end
 
# Return short description (from profile)
def short_description
profile ? profile.oneline_description : ''
end
 
def full_address
[
org_legaladdress, org_legaladdress_cont, city, state_region
].map(&:presence).compact.join(', ')
end
 
def address_for_invoice
address.presence || billing_address
end
 
def provider?
self[:provider] || master?
end
 
def tenant?
provider && !master?
end
 
# @param [SystemOperation] operation
def fetch_dispatch_rule(operation)
Account#fetch_dispatch_rule has the variable name 'm'
MailDispatchRule.fetch_with_retry!(system_operation: operation, account: self) do |m|
m.dispatch = false if %w[weekly_reports daily_reports new_forum_post].include?(operation.ref)
m.emails = emails.first
end
end
 
# @param [SystemOperation] operation
def dispatch_rule_for(operation)
rule = fetch_dispatch_rule(operation)
 
migration = Notifications::NewNotificationSystemMigration.new(self)
 
if migration.enabled?
dispatch = rule.dispatch
overridden = rule.dispatch = migration.dispatch?(operation)
logger.info("Overriding dispatch rule for Account #{id} (#{name}) #{dispatch} => #{overridden} for operation #{operation.ref}")
end
 
rule
end
 
# Is the feature allowed for this account?
def feature_allowed?(feature)
if master?
master_feature_allowed?(feature)
else
if has_bought_cinstance?
#TODO: this only applies now to application plans, move the question to plan instead
bought_plan.features.exists?(system_name: feature.to_s)
else
# TODO: ask steve
feature.to_sym == :method_tracking
end
end
end
 
# Decides if the email sent from this provider should have the viral email footer appended.
def should_apply_email_engagement_footer?
return false if master?
if buyer? && !provider? # no idea what I'm doing.
provider_account.settings.skip_email_engagement_footer.denied?
else
settings.skip_email_engagement_footer.denied?
end
end
 
def reload(options = nil)
# TODO: there is a pattern emerging here. Abstract up!
@provided_cinstances = nil
@buyer_attribute_descriptors = nil
@signup_form_fields = nil
@_first_admin = nil
 
super
end
 
def backend_object
raise 'backend_object is only for provider accounts' unless provider?
 
@backend_object ||= BackendClient::Connection.new.provider(self)
end
 
# TODO: should be multiple_applications_enabled?
# don't freak out, this is a legacy naming
def multiple_applications_allowed?
return false unless settings
settings.multiple_applications.visible?
end
 
Method `to_xml` has a Cognitive Complexity of 22 (exceeds 5 allowed). Consider refactoring.
Method `to_xml` has 38 lines of code (exceeds 25 allowed). Consider refactoring.
Account#to_xml has approx 27 statements
def to_xml(options = {})
#TODO: use Nokogiri builder
xml = options[:builder] || ThreeScale::XML::Builder.new
 
xml.account do |xml|
unless new_record?
xml.id_ id
xml.created_at created_at.xmlschema
xml.updated_at updated_at.xmlschema
end
 
xml.state state
annotations_xml(:builder => xml)
xml.deletion_date deletion_date.xmlschema if scheduled_for_deletion? && deletion_date
 
if provider?
Similar blocks of code found in 2 locations. Consider refactoring.
xml.admin_domain admin_domain
xml.domain domain
xml.admin_base_url admin_base_url
xml.base_url base_url
xml.from_email from_email
xml.support_email support_email
xml.finance_support_email finance_support_email
xml.site_access_code site_access_code
end
 
unless destroyed?
fields_to_xml(xml)
extra_fields_to_xml(xml)
 
unless should_be_deleted?
xml.monthly_billing_enabled settings.monthly_billing_enabled
xml.monthly_charging_enabled settings.monthly_charging_enabled
end
 
xml.credit_card_stored credit_card_stored?
 
if credit_card_stored?
xml.credit_card_partial_number payment_detail.credit_card_partial_number
xml.credit_card_expires_on payment_detail.credit_card_expires_on
end
 
bought_plans.to_xml(builder: xml, root: 'plans')
users.to_xml(builder: xml, root: 'users')
 
bought_cinstances.to_xml(builder: xml, root: 'applications') if options.dig(:user_options, :with_apps)
end
end
 
xml.to_xml
end
 
def generate_s3_prefix
self.s3_prefix = if org_name
org_name.parameterize
else
# TODO: Add time zone
Digest::SHA1.hexdigest(Time.now.to_s)[0..20]
end
end
 
def paid?
Account#paid? has the variable name 'c'
contracts.any? { |c| c.paid? }
end
 
def on_trial?
Account#on_trial? has the variable name 'c'
contracts.all? { |c| c.trial? }
end
 
# Grabs the support_email if defined, otherwise falls back to the email of first admin. Dog.
def support_email
se = self[:support_email]
se.presence || first_admin&.email
end
 
def finance_support_email
self[:finance_support_email].presence || support_email
end
 
def provider_id_for_audits
if buyer?
provider_account&.provider_id_for_audits
else
id
end
end
 
def sections
# Filter out existing forum sections (builtin static pages) from the CMS sidebar and return 404
# if accessed via URL. TODO: Remove forums THREESCALE-6714
super.where.not(system_name: %i[forum categories posts topics user-topics])
end
 
private
 
def validate_timezone
tz = ActiveSupport::TimeZone.new(timezone)
 
unless ALLOWED_TIMEZONES.include?(tz)
errors.add(:timezone, "Timezone #{timezone} is not allowed")
end
end
 
def master_feature_allowed?(feature)
# HACK: have to hardcode the features here, because master account is not signed up
# to any plan, so there is no real list of features for it.
feature != :anonymous_clients
end
 
def generate_site_access_code
self.site_access_code ||= SecureRandom.hex(5) if provider?
end
 
def destroy_all_users
users.each(&:destroy)
end
 
def update_tenant_id
update_column(:tenant_id, id)
end
 
def master_uniqueness
scope = self.class.unscoped
scope = persisted? ? scope.where.not(id: id) : scope
 
errors.add :master, 'can be only one' if scope.exists?(master: true)
end
 
protected
end