app/models/finance/billing_strategy.rb
# frozen_string_literal: true Class `BillingStrategy` has 37 methods (exceeds 20 allowed). Consider refactoring.
File `billing_strategy.rb` has 305 lines of code (exceeds 250 allowed). Consider refactoring.
Finance::BillingStrategy has at least 28 methods
Finance::BillingStrategy assumes too much for instance variable '@failed_buyers'class Finance::BillingStrategy < ApplicationRecord module NonAuditedColumns def non_audited_columns super - [inheritance_column] end end class << self prepend NonAuditedColumns end audited :allow_mass_assignment => true attr_reader :failed_buyers CURRENCIES = CurrenciesLoader.load_config.freeze belongs_to :account alias_attribute :provider, :account attr_accessible is recommended over attr_protected attr_protected :account_id, :tenant_id, :audit_ids accepts_nested_attributes_for :account validates :currency, inclusion: { in: CURRENCIES.values, message: :invalid } # TODO: uncomment when factories are fixed # validates_presence_of :account validates :numbering_period, presence: true validates :numbering_period, inclusion: { :in => %w(monthly yearly).freeze }, length: { maximum: 255 } validates :currency, :type, length: { maximum: 255 } def self.daily_canaries return if canaries.blank? daily_async(where("account_id in (?)", canaries)) end def self.daily_rest if canaries.blank? daily_async(all) else daily_async(where("account_id not in (?)", canaries)) end end def self.daily_async(scope, options = {}) now = options[:now] || Time.now.utc scope.select([:id, :account_id]).includes(:account).find_each do |billing_strategy| Finance::BillingService.async_call(billing_strategy.account, now) end end def self.canaries ThreeScale.config.payments.billing_canaries || [] end # Supported options # # :now - time when the billing is happening # :only - run billing only for providers with those IDs # :exclude - run billing for all providers, but exclude those IDs # # This is usually called by BillingService#call with one provider in `only` and a single buyer in `buyer_ids`Method `daily` has a Cognitive Complexity of 14 (exceeds 5 allowed). Consider refactoring.
Method `daily` has 33 lines of code (exceeds 25 allowed). Consider refactoring.
Finance::BillingStrategy#self.daily has approx 25 statements def self.daily(options = {}) raise 'Options must be a hash' unless options.is_a?(Hash) Finance::BillingStrategy#self.daily calls 'Rails.logger' 3 times Rails.logger.info("Finance::BillingStrategy.daily started for options #{options}") now = options[:now] || Time.now.utc skip_notifications = options[:skip_notifications] scope = if options.has_key?(:only) where("account_id in (?)", options[:only]) elsif options.has_key?(:exclude) where("account_id not in (?)", options[:exclude]) else all end results = Finance::BillingStrategy::Results.new(now) scope.find_each(:batch_size => 5) do |billing_strategy| begin unless billing_strategy.active? next results.skip(billing_strategy) end results.start(billing_strategy) ignoring_find_each_scope { billing_strategy.daily(now: now, buyer_ids: options[:buyer_ids], skip_notifications: skip_notifications) } results.success(billing_strategy)Finance::BillingStrategy#self.daily has the variable name 'e' rescue => e results.failure(billing_strategy) name = billing_strategy.provider.try!(:name) id = billing_strategy.id message = "BillingStrategy #{id}(#{name}) failed utterly" Rails.logger.error(message) System::ErrorReporting.report_error(e, :error_message => message, :error_class => 'BillingError') raise e end end Rails.logger.info("Finance::BillingStrategy.daily finished for options #{options}, with results: #{results.inspect_all_things}") notify_billing_results(results) unless skip_notifications results end Method `notify_billing_results` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring. def self.notify_billing_results(results) BillingMailer.billing_finished(results).deliver_now unless results.successful? rescue => error System::ErrorReporting.report_error(error) env = Rails.env raise if env.test? || env.development? end delegate :notify_billing_results, to: :class def self.account_currency(account_id) Rails.cache.fetch("account:#{account_id}:billing_strategy:currency") do where(:account_id => account_id).pluck(:currency).first || false end || nil # the false trick is there for store it in memcache, because nil is not stored end def notify_billing_finished(now) only_on_days(now, 1) { notify_about_finalized_invoices } notify_about_expired_credit_cards(now) end def active? provider.approved? end def build_invoice(opts = {}) options = opts.reverse_merge(period: Month.new(Time.now.utc.to_date)) creation_type = options.delete(:creation_type) create_invoice_counter(options[:period]) provider.buyer_invoices.new(options) do |new_invoice| new_invoice.creation_type = creation_type if creation_type.present? end end def create_invoice!(opts = {}) invoice = build_invoice(opts) Invoice.transaction { invoice.save! } invoice end def create_invoice(opts = {}) invoice = build_invoice(opts) Invoice.transaction { invoice.save } invoice end def create_invoice_counter(period) # The invoice prefix is either the 7 (0..6) or the 4 (0..3) initial characters of the period stringified, # respectively to monthly billing friendly ids (YYYY-MM-########) and to yearly billing friendly ids (YYYY-########) invoice_prefix = period.to_param[0..(billing_monthly? ? 6 : 3)] InvoiceCounter.transaction(requires_new: true) do InvoiceCounter.create(provider_account: account, invoice_prefix: invoice_prefix, invoice_count: 0) end rescue ActiveRecord::RecordNotUnique InvoiceCounter.find_by(provider_account: account, invoice_prefix: invoice_prefix) end # TODO: Remove. See https://github.com/3scale/system/pull/9360Method `next_available_friendly_id` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring. def next_available_friendly_id(month, step = 1) return unless month id_prefix = billing_monthly? ? month : month.begin.year last_of_period = Invoice.by_provider(account) .with_normalized_friendly_id(numbering_period, month) .first order = if last_of_period last_of_period.friendly_id.split('-').last else 0 end "#{id_prefix.to_param}-#{'%08d' % (order.to_i + step)}" end # TODO: rename to invoice_numbering_monthly? def billing_monthly? numbering_period == 'monthly' end # TODO: rename to invoice_numbering_yearly? def billing_yearly? numbering_period == 'yearly' end def change_mode(bs_mode) return if bs_mode == self.type new_name = (name == 'prepaid') ? 'postpaid' : 'prepaid' self.update_attribute(:type, bs_mode.to_s) warning("Billing mode changed to #{new_name}") end Finance::BillingStrategy takes parameters ['buyer', 'txt'] to 3 methods def warning(txt, buyer = nil) LogEntry.log(:warning, txt, self.account_id, buyer) end def error(txt, buyer = nil) LogEntry.log(:error, txt, self.account_id, buyer) end def info(txt, buyer = nil) LogEntry.log(:info, txt, self.account_id, buyer) end protected delegate :provider_id_for_audits, :to => :account, :allow_nil => true def only_on_days(*days, &block) now = days.shift yield if days.include?(now.day) end Finance::BillingStrategy takes parameters ['buyer', 'now'] to 6 methods def bill_expired_trials(buyer, now) buyer.billable_contracts_with_trial_period_expired(now - 1.day).find_each(batch_size: 50) do |contract|Finance::BillingStrategy#bill_expired_trials refers to 'contract' more than self (maybe move it to another class?)
Finance::BillingStrategy#bill_expired_trials calls 'contract.plan' 3 times plan_type = contract.plan.class.model_name.human.downcase info("#{log_prefix(buyer)} for #{plan_type} #{contract.plan.id} (#{contract.plan.name}) - just signed up or trial period expired", buyer) contract.bill_for(Month.new(now), invoice_for(buyer, now)) end end # TODO: DRY # TODO: cover it by unit tests #Similar blocks of code found in 2 locations. Consider refactoring. def bill_fixed_costs(buyer, now = Time.now.utc) info("#{log_prefix(buyer)} billing fixed costs at #{now}", buyer) buyer.billable_contracts.find_each(batch_size: 50) do |contract| contract.bill_for(Month.new(now), invoice_for(buyer, now)) end end # Finalize all invoices that are in open but belong to # a period (month) that is already over. # def finalize_invoices_of(buyer, now = Time.now.utc) invoices_to_finalize_of(buyer, now).find_each(:batch_size => 20) do |invoice| info("#{log_prefix(buyer)} finalizing invoices for period #{invoice.period}", buyer) invoice.finalize! end end def issue_invoices_of(buyer, now = Time.now.utc) to_issue = self.provider.buyer_invoices.by_buyer(buyer).finalized_before(now - 1.day - 22.hours) to_issue.find_each(batch_size: 100) do |invoice| info("#{log_prefix(buyer)} issuing invoice #{invoice.id} for period #{invoice.period}", buyer) invoice.issue_and_pay_if_free! # TODO: extract to overloaded method? # TODO: Decouple the notification to observer if charging_enabled? && invoice.cost.nonzero? && invoice.buyer_account.paying_monthly? InvoiceMessenger.upcoming_charge_notification(invoice).deliver end end end def invoice_for_cinstance(contract) buyer = contract.buyer_account invoice_for(buyer, Time.now.utc) end def notify_about_finalized_invoices if provider.buyer_line_items.sum_by_invoice_state(:finalized) > 0 info('Notifying about finalized invoices with non-zero cost') # do not send email if provider's using new notification system unless provider.provider_can_use?(:new_notification_system) AccountMessenger.invoices_to_review(provider).deliver end event = Invoices::InvoicesToReviewEvent.create(provider) Rails.application.config.event_store.publish_event(event) elsif provider.buyer_invoices.finalized.count > 0 info('All finalized invoices have zero cost - notification not sent') end end Finance::BillingStrategy#notify_about_expired_credit_cards has approx 8 statements def notify_about_expired_credit_cards(now) expiry_date = now.to_date + 10.days provider.buyers.expired_credit_card(expiry_date).find_each(:batch_size => 20) do |buyer| ignoring_find_each_scope do AccountMessenger.expired_credit_card_notification_for_buyer(buyer).deliver # do not send email if provider's using new notification system unless provider.provider_can_use?(:new_notification_system) AccountMessenger.expired_credit_card_notification_for_provider(buyer).deliver end event = Accounts::ExpiredCreditCardProviderEvent.create(buyer) Rails.application.config.event_store.publish_event(event) info('Notifying about expiring credit card', buyer) end end end def invoice_for(buyer, now) month = Month.new(now) Finance::InvoiceProxy.new(buyer, month) end def charge_invoices(buyer, now = Time.now.utc) if charging_enabled?Finance::BillingStrategy#charge_invoices performs a nil-check if self.currency.nil? raise "BillingStrategy(#{self.id}) is trying to charge without currency settings" end buyer.invoices.chargeable(now).find_each(batch_size: 50) do |invoice|Finance::BillingStrategy#charge_invoices calls 'Rails.logger' 2 times
Finance::BillingStrategy#charge_invoices calls 'log_prefix(buyer)' 2 times Rails.logger.info("#{log_prefix(buyer)} trying to charge invoice #{invoice.id}") invoice.charge! end else Rails.logger.info("#{log_prefix(buyer)} charging not enabled for provider #{account.org_name} - bypassing") end end def needs_credit_card? charging_enabled? end def log_prefix(buyer) "[billing] provider #{buyer.provider_account_id} buyer #{buyer.id}:" end public :needs_credit_card? private # Yields a block for each buyer, passing it as a parameter. If an # exception occurs meanwhile, catches and reports it. #Method `bill_and_charge_each` has 27 lines of code (exceeds 25 allowed). Consider refactoring.
Finance::BillingStrategy#bill_and_charge_each has approx 20 statements
Method `bill_and_charge_each` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring. def bill_and_charge_each(options = {}) buyer_ids = options[:buyer_ids] @failed_buyers = [] Finance::BillingStrategy#bill_and_charge_each performs a nil-check if provider.nil? message = "WARNING: tried to use billing strategy #{self.id} which has no account" exception = ::Finance::BillingError.new message System::ErrorReporting.report_error(exception, :error_message => message, :error_class => 'InvalidData') return end buyer_accounts = provider.buyer_accounts buyer_accounts = buyer_accounts.where(id: buyer_ids) if buyer_ids.present? buyer_accounts.find_each(:batch_size => 20) do |buyer| begin ignoring_find_each_scope { yield(buyer) } rescue => exception name = buyer.name buyer_id = buyer.id provider_id = provider.id msg = "Failed to bill or charge #{name}(#{buyer_id}) of provider(#{provider_id}): #{exception.message}\n" error(msg, buyer) System::ErrorReporting.report_error(exception, :error_message => msg, :error_class => 'BillingError', :parameters => { :buyer_id => buyer_id, :provider_id => provider_id } ) @failed_buyers << buyer_id raise if Rails.env.test? end end end Finance::BillingStrategy#add_plan_cost has 4 parameters
Method `add_plan_cost` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring. def add_plan_cost(action, contract, plan, period)Finance::BillingStrategy#add_plan_cost refers to 'plan' more than self (maybe move it to another class?) cost = plan.cost_for_period(period) Finance::BillingStrategy#add_plan_cost refers to 'cost' more than self (maybe move it to another class?) if cost.nonzero?Finance::BillingStrategy#add_plan_cost is controlled by argument 'action'
Finance::BillingStrategy#add_plan_cost refers to 'action' more than self (maybe move it to another class?)
Finance::BillingStrategy#add_plan_cost calls 'action == :refund' 2 times sign = action == :refund ? -1 : 1 reason = action == :refund ? 'Refund' : 'Fixed fee' add_cost(contract, "#{reason} ('#{plan.name}')", period.to_s, cost * sign, plan) end end Finance::BillingStrategy#add_cost has 5 parameters
Method `add_cost` has 5 arguments (exceeds 4 allowed). Consider refactoring. def add_cost(contract, name, description, cost, plan = contract.plan) invoice = invoice_for_cinstance(contract) Finance::BackgroundBilling.new(invoice).create_line_item!( { contract: contract, plan_id: plan.id, name: name, description: description, quantity: 1, cost: cost, type: LineItem::PlanCost } ) end delegate :report_error, to: 'System::ErrorReporting' module FindEachFix # HACK: to overcome find_each scoping, we reset the scopeFinance::BillingStrategy::FindEachFix#ignoring_find_each_scope doesn't depend on instance state (maybe move it to another class?) def ignoring_find_each_scope(&block) Account.unscoped.scoping(&block) end end extend FindEachFix include FindEachFixend