3scale/porta

View on GitHub
app/models/contract.rb

Summary

Maintainability
A
2 hrs
Test Coverage
 
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] invoice
Contract#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 do
Contract#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