app/models/contract.rb
Class `Contract` has 22 methods (exceeds 20 allowed). Consider refactoring.
Contract has at least 19 methods
Contract assumes too much for instance variable '@old_plan'class Contract < ApplicationRecord # Need to define table_name before audited because of # https://github.com/collectiveidea/audited/blob/f03c5b5d1717f2ebec64032d269316dc74476056/lib/audited/auditor.rb#L305-L311 self.table_name = 'cinstances' audited allow_mass_assignment: true # FIXME: This class should be an abstract class I think, but doing so makes plenty of tests fail # self.abstract_class = true include States include Billing include Trial include CounterCacheCallbacks include Finance::FixedFee include Finance::SetupFee include Finance::NoVariableCost include Logic::PlanChanges::Contract after_destroy :destroy_customized_plan after_commit :notify_plan_changed, if: :saved_change_to_plan_id? belongs_to :plan, inverse_of: :contracts validate :correct_plan_subclass? # this breaks nested saving of records, when validating there is no user_account yet, its new record # validates_presence_of :user_account validates :description, :redirect_url, :extra_fields, length: { maximum: 65535 } validates :provider_public_key, :state, :application_id, :name, :type, :create_origin, length: { maximum: 255 } validates :user_key, length: { maximum: 256 } # TODO: rename to buyer_account and remove alias belongs_to :user_account, class_name: 'Account', autosave: false, inverse_of: :contracts alias buyer_account user_account alias buyer user_account alias account user_account delegate :provider_account, to: :plan, allow_nil: true delegate :id, to: :provider_account, allow_nil: true, prefix: true delegate :id, to: :old_plan, prefix: true, allow_nil: true # TODO: remove with Rails 3 attr_reader :old_plan, :accepted_on_create attr_accessible is recommended over attr_protected attr_protected :plan_id, :state, :provider_public_key, :paid_until, :trial_period_expires_at, :setup_fee, :type, :variable_cost_paid_until, :application_id, :user_key, :user_account_id, :tenant_id, :audit_ids # TODO: unit test this scope def self.provided_by(account) where.has do plan_id.in(Plan.provided_by(account).select(:id)) end end def self.issued_by(issuer, *ids) scope = Plan.issued_by(issuer, *ids).select(:id) where.has { plan_id.in( scope ) } end scope :permitted_for, ->(user) { case user.permitted_services_status when :none none when :all merge(provided_by(user.account)) else merge(where(service_id: user.member_permission_service_ids)) end } # Return contracts bought by given account. scope :bought_by, ->(account) { where({:user_account_id => account.id}) } scope :with_account, -> { includes([:user_account])} scope :by_type, ->(contract_type) { where({ :type => contract_type.to_s }) } # SEARCH SCOPES scope :by_plan_id, ->(plan_id) { where(plan_id: plan_id.to_i) } scope :by_name, ->(text) { # replace start and end of string with % unless already has % pattern = text.sub(/(^[^%])/, '%\\1').sub( /([^%]$)/, '\\1%') collate = { oracle: 'GENERIC_M_CI', postgres: '"und-x-icu"', mysql: 'UTF8_GENERAL_CI' }.fetch(System::Database.adapter.to_sym) where.has { name.op('COLLATE', sql(collate)).matches(pattern)} } scope :by_account, ->(account_id) { where.has { user_account_id == account_id } } scope :by_account_query, ->(query) { where( { :user_account_id => Account.buyers.search_ids(query) } ) } scope :have_paid_on, ->(paid_date) { where.has { (paid_until >= paid_date) | (variable_cost_paid_until >= paid_date) } } Contract#self.by_plan_type has approx 9 statements def self.by_plan_type(type) plans = Plan.unscoped.distinct.joins { pricing_rules.outer } plan_type = case type.to_s when 'free'Contract#self.by_plan_type calls 'pricing_rules.id' 2 times
Contract#self.by_plan_type calls 'plans.where' 2 times
Contract#self.by_plan_type performs a nil-check plans.where { (cost_per_month == 0) & (setup_fee == 0) & (pricing_rules.id == nil) } # rubocop:disable Style/NumericPredicate,Style/NilComparison when 'paid' plans.where { (cost_per_month != 0) | (setup_fee != 0) | (pricing_rules.id != nil) } # rubocop:disable Style/NumericPredicate,Style/NonNilCheck else return all end where{ plan_id.in plan_type.select(:id) } end delegate :paid?, :to => :plan def messenger "#{self.class.name}Messenger".constantize end # TODO: rename service_id field to issuer_id on plan def issuer plan && plan.issuer end # TODO: remove this when also Account states (pending, aproved ...) are handled on an # account contract. # def has_lifecycle? true end # TODO: DRY the multiple ways to reach provider_account from # contract. The other way is user_account.provider_account def provider_account plan.try! :provider_account end def paid_until self[:paid_until] || accepted_at || trial_period_expires_at || created_at end # Using `read_attribute` because the getter method is overloaded # Meaning changing plan the same day of the creation of the contract # Useful for prepaid billing. See PrepaidBillingStrategy#bill_plan_change_safely def never_billed? self[:paid_until].blank? end # Returns boolean, indicating if something was billed. # # Note: trial period is correctly handled thanks to +paid_until+ # method implementation which takes it into account. # # TODO: create bill_for! method # TODO: logging - the reasons why it billed/skipped billing # # @param [Month] period # @param [Invoice] invoiceContract#bill_for has approx 8 statements def bill_for(period, invoice, plan = self.plan) # TODO: this makes the bill_for method dependent on Time.zone.now # so it should be handled differently # return false if trial? transaction doContract#bill_for calls 'period.end' 2 times if paid_until.to_date < period.end.to_date period = intersect_with_unpaid_period(period, paid_until) bill_fixed_fee_for(period, invoice, plan) self.paid_until = period.end end bill_setup_fee_for(period, invoice, plan) # no validation because our DB has broken data # TODO: cleanup DB and add validations?Contract#bill_for calls 'invoice.used?' 2 times self.save(:validate => false) if invoice.used? end invoice.used? end # this is remaining now here for service_contracts as of now # TODO: should be, but breaks a lot of it... # private :plan= # Changes plan by calling protected method to change plan # passed block is executed in transaction and can abort it # # TODO: test these change plan methods! # def change_plan!(new_plan) changed = change_plan_internal(new_plan) do self.save! end changed && self.plan end def change_plan(new_plan) changed = change_plan_internal(new_plan) do self.save or raise ActiveRecord::Rollback end changed && self.plan end # Customize plan this contract is assigned to. If the plan is already customized, it does # nothing. If not, if will create a new plan, copying all it's properties from the # original plan, then reassigning this contract to this new plan. # # This method will try to save the customized plan and this contract. #Contract#customize_plan! has approx 8 statements
Contract has missing safe method 'customize_plan!' def customize_plan!(attrs = {}) unless plan.customized? transaction do #TODO: this needs testing custom = plan.customize(attrs) if custom.persisted? old_plan = plan update_attribute(:plan, custom) old_plan.reset_contracts_counter end custom end end plan.reset_contracts_counter plan end # If the cinstance is on customized plan, revert it back to stock plan.Contract has missing safe method 'decustomize_plan!' def decustomize_plan! if plan.customized? transaction do custom_plan = plan self.plan = custom_plan.original save! custom_plan.destroy plan.reset_contracts_counter end end plan end protected def correct_plan_subclass? if plan && (not plan.is_a?(Plan)) errors.add(:plan, 'wrong plan subclass') end end # # Internal method which creates transaction # and inside transaction changes plan # and runs passed block # # passed block is expected to save the record # # this method can be (and is) overriden in children # to run something after successful trnsaction #Contract#change_plan_internal has approx 10 statements
Method `change_plan_internal` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring. def change_plan_internal(new_plan, &block)Contract#change_plan_internal calls 'self.plan' 2 times
Contract#change_plan_internal performs a nil-check return if self.plan == new_plan || new_plan.nil? raise 'change_plan_internal must be called with a block' unless block_given? transaction do # workaround - remove with Rails 3 @old_plan = self.plan self.plan = new_plan # TODO: change to notify_observers and add old/new params after # migration to Rails 3 res = yield new_plan.reset_contracts_counter @old_plan.customized? ? @old_plan.destroy : @old_plan.reset_contracts_counter res end end private def reset_counter_cache_for [:plan].freeze end def update_counter_cache?(association_name)Contract#update_counter_cache? is controlled by argument 'association_name' case association_name when :plan !provider_account&.scheduled_for_deletion? && !issuer&.deleted? else true end end def notify_plan_changed if @old_plan notify_observers(:bill_variable_for_plan_changed, @old_plan) notify_observers(:plan_changed) if plan.cost_per_month < @old_plan.cost_per_month plan.notify_observers(:plan_downgraded, @old_plan, self) end @old_plan = nil end end def destroy_customized_plan return if !plan || !plan.customized? || plan.scheduled_for_deletion? plan.destroy end def accept_on_create # RAILS3: not sure if we have to do this fancyness with webhooks # accept! if can_accept? and not plan.approval_required? # or service.plan.approval_required? return if plan.approval_required? # or service.plan.approval_required? # this skips saving the record # unfortunately it creates empty transaction @accepted_on_create = true fire_events!(:accept, false) end def intersect_with_unpaid_period(period, paid_end) if period.is_a?(BillingObserver::RangeForVariableCost)Contract#intersect_with_unpaid_period calls 'period.begin' 2 times
Contract#intersect_with_unpaid_period calls 'period.end' 2 times period = period.begin..(period.end - 1.second) end from = [ period.begin.to_date, paid_end ].max.to_date to = [ period.end.to_date, from ].max.to_date from.to_time..to.to_time.end_of_day end end