Module: CustomerFinancials

Extended by:
ActiveSupport::Concern
Included in:
Customer
Defined in:
app/models/concerns/customer_financials.rb

Overview

Invoices, receipts, credit, AR, terms, store credit, payment methods.

rubocop:disable Metrics/ModuleLength -- AR/financial surface intentionally grouped

See Also:

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.duplicate_addressesObject

:reek:UtilityFunction



221
222
223
# File 'app/models/concerns/customer_financials.rb', line 221

def duplicate_addresses # :reek:UtilityFunction
  Maintenance::DuplicateCustomerAddresses.call
end

.duplicate_contact_pointsObject

:reek:UtilityFunction



225
226
227
# File 'app/models/concerns/customer_financials.rb', line 225

def duplicate_contact_points # :reek:UtilityFunction
  Maintenance::DuplicateCustomerContactPoints.call
end

.statement_of_account(party_id) ⇒ Object

:reek:UtilityFunction



217
218
219
# File 'app/models/concerns/customer_financials.rb', line 217

def (party_id) # :reek:UtilityFunction
  Query::CustomerStatementOfAccount.call(party_id)
end

Instance Method Details

#account_on_hold?Boolean

Returns:

  • (Boolean)


206
207
208
# File 'app/models/concerns/customer_financials.rb', line 206

def 
  on_hold
end

#all_rmasObject



64
65
66
67
# File 'app/models/concerns/customer_financials.rb', line 64

def all_rmas
  Rma.joins('left outer join orders on orders.id = rmas.original_order_id')
     .where('rmas.customer_id = :customer_id or orders.customer_id = :customer_id', customer_id: id)
end

#all_termsObject



198
199
200
201
202
203
204
# File 'app/models/concerns/customer_financials.rb', line 198

def all_terms
  if billing_address.present? && billing_address.party && (billing_address.party != self)
    billing_address.party.all_terms
  else
    build_local_all_terms
  end
end

#apc_available?Boolean

Returns:

  • (Boolean)


191
192
193
194
195
196
# File 'app/models/concerns/customer_financials.rb', line 191

def apc_available?
  bg = buying_group.
  bg.authorization_code_required && bg.terms?
rescue StandardError
  false
end

#approve_credit_request(amount, order = nil) ⇒ Object



50
51
52
53
54
55
# File 'app/models/concerns/customer_financials.rb', line 50

def approve_credit_request(amount, order = nil)
  self.available_credit += amount
  self.on_hold = false if on_hold?
  save!
  InternalMailer.credit_request_approved_notification(self, amount, order).deliver_later
end

#ar_listings(show_open_only: false) ⇒ Object

:reek:BooleanParameter :reek:ControlParameter



85
86
87
88
89
# File 'app/models/concerns/customer_financials.rb', line 85

def ar_listings(show_open_only: false) # :reek:BooleanParameter :reek:ControlParameter
  list = ViewArListing.where('billing_customer_id = ? or customer_id = ?', id, id)
  list = list.where(open_amount_is_zero: false) if show_open_only.present?
  list.order(Arel.sql('open_amount_is_zero,coalesce(due_date, document_date) DESC,line_type DESC,reference_number DESC'))
end

#auto_increase_available_credit(amount, order = nil) ⇒ Object

rubocop:disable Naming/PredicateMethod -- imperative API: performs side effects and notifications



41
42
43
# File 'app/models/concerns/customer_financials.rb', line 41

def auto_increase_available_credit(amount, order = nil)
  Credit::AutoIncrease.call(self, amount, order:)
end

#available_store_creditObject



129
130
131
132
133
134
135
136
# File 'app/models/concerns/customer_financials.rb', line 129

def available_store_credit
  ar_scope = ViewArListing.where('billing_customer_id = ? or customer_id = ?', id, id)
  totals = ar_scope.where(line_type: %w[CM INV]).pick(
    Arel.sql("COALESCE(SUM(CASE WHEN line_type = 'CM' THEN open_amount_calc * -1 ELSE 0 END), 0)"),
    Arel.sql("COALESCE(SUM(CASE WHEN line_type = 'INV' THEN open_amount_calc ELSE 0 END), 0)")
  )
  (totals[0] - totals[1]) - store_credit_used_on_open_orders
end

#bad_debtObject



32
33
34
# File 'app/models/concerns/customer_financials.rb', line 32

def bad_debt
  ReceiptDetail.joins(:receipt).where(receipts: { customer_id: billing_entity.id }).sum(:write_off)
end

#billing_entityObject



150
151
152
153
154
155
156
157
158
# File 'app/models/concerns/customer_financials.rb', line 150

def billing_entity
  return self if billing_address.nil? || (billing_address.party_id == id) || billing_address.party_id.nil?

  (begin
    billing_address.party
  rescue StandardError
    nil
  end) || self
end

#calculated_purchase_historyObject



24
25
26
# File 'app/models/concerns/customer_financials.rb', line 24

def calculated_purchase_history
  purchase_history - outstanding_balance
end

#combined_termsObject



166
167
168
169
170
171
172
# File 'app/models/concerns/customer_financials.rb', line 166

def combined_terms
  if early_payment_discount && early_payment_timescale
    "#{terms} - #{early_payment_discount}%/#{early_payment_timescale}"
  else
    terms
  end
end

#credit_limit_verification(amount, quote = nil) ⇒ Object



57
58
59
60
61
62
# File 'app/models/concerns/customer_financials.rb', line 57

def credit_limit_verification(amount, quote = nil)
  rc = request_credit(amount, ignore_annual_revenue: false)
  return unless rc[:approved] == false

  InternalMailer.credit_limit_verification_denied_notification(self, amount, rc[:fail_reasons], quote).deliver_later
end

#determine_receipt_billing_entityObject



160
161
162
163
164
# File 'app/models/concerns/customer_financials.rb', line 160

def determine_receipt_billing_entity
  return self if billing_entity == self

  billing_entity.company == company ? billing_entity : self
end

#locked_credit_amountObject



69
70
71
# File 'app/models/concerns/customer_financials.rb', line 69

def locked_credit_amount
  outstanding_amount_numeric + pre_authorized_amount_numeric
end

#open_amount_greater_than_a_monthObject



36
37
38
# File 'app/models/concerns/customer_financials.rb', line 36

def open_amount_greater_than_a_month
  outstanding_ar_listings.where('(now()::date - due_date::date) > 30').sum(:open_amount_calc)
end

#open_orders_total(exclude_order = nil) ⇒ Object



14
15
16
17
18
# File 'app/models/concerns/customer_financials.rb', line 14

def open_orders_total(exclude_order = nil)
  res = Order.non_credit.where(customer_id: billing_entity.self_and_children_ids).where.not(state: %w[cart invoiced cancelled fraudulent])
  res = res.where.not(id: exclude_order.id) if exclude_order
  res.sum(:total)
end

#outstanding_amountObject



101
102
103
# File 'app/models/concerns/customer_financials.rb', line 101

def outstanding_amount
  ActionController::Base.helpers.number_to_currency(outstanding_amount_numeric)
end

#outstanding_amount_numericObject



97
98
99
# File 'app/models/concerns/customer_financials.rb', line 97

def outstanding_amount_numeric
  outstanding_ar_listings.sum(:open_amount_calc)
end

#outstanding_ar_listingsObject



91
92
93
94
95
# File 'app/models/concerns/customer_financials.rb', line 91

def outstanding_ar_listings
  ViewArListing.where('billing_customer_id = :customer_id or customer_id = :customer_id', customer_id: id)
               .where.not(open_amount_calc: 0)
               .order(:document_date)
end

#outstanding_balanceObject



20
21
22
# File 'app/models/concerns/customer_financials.rb', line 20

def outstanding_balance
  Invoice.where(billing_customer_id: billing_entity.id).unpaid.sum(:total)
end

#overdue_balanceObject



28
29
30
# File 'app/models/concerns/customer_financials.rb', line 28

def overdue_balance
  Invoice.where(billing_customer_id: billing_entity.id).overdue.sum(:total)
end

#payment_optionsObject



210
211
212
213
214
# File 'app/models/concerns/customer_financials.rb', line 210

def payment_options
  options = ['Credit Card']
  options << terms if terms != Customer::TERM_DUE
  options
end

#pre_authorized_amount_numericObject



112
113
114
# File 'app/models/concerns/customer_financials.rb', line 112

def pre_authorized_amount_numeric
  pre_authorized_payments.sum(:amount)
end

#pre_authorized_paymentsObject



105
106
107
108
109
110
# File 'app/models/concerns/customer_financials.rb', line 105

def pre_authorized_payments
  Payment.joins(:order).purchase_orders.all_authorized
         .where.not(orders: { state: %w[invoiced cancelled fraudulent] })
         .where(orders: { customer_id: [customer_id, billing_entity&.id].compact.uniq })
         .order(:created_at)
end

#preferred_payment_method(limit: 10) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
# File 'app/models/concerns/customer_financials.rb', line 138

def preferred_payment_method(limit: 10)
  recent_categories = preferred_payment_categories(limit)
  return Customer::PreferredPayment.new if recent_categories.empty?

  counts = recent_categories.tally
  top_category, top_count = counts.max_by { |_k, v| v }
  return Customer::PreferredPayment.new unless top_category

  detail = preferred_payment_detail(top_category)
  Customer::PreferredPayment.new(category: top_category, count: top_count, total_orders: recent_categories.size, detail: detail)
end

#purchase_historyObject



10
11
12
# File 'app/models/concerns/customer_financials.rb', line 10

def purchase_history
  Invoice.where(billing_customer_id: billing_entity.id).sum(:total)
end

#receipts_listObject



116
117
118
119
# File 'app/models/concerns/customer_financials.rb', line 116

def receipts_list
  sort_order = "CASE state WHEN 'unapplied' THEN 1 WHEN 'partially_applied' THEN 2 WHEN 'fully_applied' THEN 3 WHEN 'voided' THEN 4 ELSE 5 END, receipt_date desc".sql_safe
  Receipt.where(customer_id: id).select('id,category,amount,reference,currency,card_type,gl_date,receipt_date,state').order(sort_order)
end

#request_credit(amount, ignore_annual_revenue: false) ⇒ Object

rubocop:enable Naming/PredicateMethod



46
47
48
# File 'app/models/concerns/customer_financials.rb', line 46

def request_credit(amount, ignore_annual_revenue: false) # :reek:BooleanParameter
  Credit::EligibilityEvaluator.call(self, amount, ignore_annual_revenue:)
end

#sync_credit_limitObject



73
74
75
76
77
78
79
80
81
82
83
# File 'app/models/concerns/customer_financials.rb', line 73

def sync_credit_limit
  logger.info "Synchronizing credit limit for Customer #{self}"
  self.credit_limit ||= 0
  new_credit = 0
  new_credit = [credit_limit - locked_credit_amount, 0].max if !on_hold? && credit_limit.positive?
  if update(available_credit: new_credit)
    { ok: true }
  else
    { message: errors_to_s }
  end
end

#terms?Boolean

Returns:

  • (Boolean)


187
188
189
# File 'app/models/concerns/customer_financials.rb', line 187

def terms?
  [Customer::TERM_NET15, Customer::TERM_NET30, Customer::TERM_NET37, Customer::TERM_NET45, Customer::TERM_NET60, Customer::TERM_NET90].include? terms
end

#terms_credit_limitObject



121
122
123
124
125
126
127
# File 'app/models/concerns/customer_financials.rb', line 121

def terms_credit_limit
  be = billing_entity
  return 0 unless be.respond_to?(:terms?) && be.terms?
  return 0 if be.respond_to?(:on_hold) && be.on_hold

  available_credit
end

#terms_in_daysObject



174
175
176
177
178
179
180
181
182
183
184
185
# File 'app/models/concerns/customer_financials.rb', line 174

def terms_in_days
  return 0 unless terms?

  case terms
  when Customer::TERM_NET90 then 90
  when Customer::TERM_NET60 then 60
  when Customer::TERM_NET45 then 45
  when Customer::TERM_NET37 then 37
  when Customer::TERM_NET15 then 15
  else 30
  end
end