Module: Models::CustomerFinancials

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

Overview

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

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.duplicate_addressesHash{Integer => Array<String>}

:reek:UtilityFunction
Finds duplicate normalized addresses for recent non-guest customers.

Returns:

  • (Hash{Integer => Array<String>})

    customer IDs mapped to duplicate addresses



344
345
346
# File 'app/concerns/models/customer_financials.rb', line 344

def duplicate_addresses
  Maintenance::DuplicateCustomerAddresses.call
end

.duplicate_contact_pointsHash{Integer => Array<Object>}

:reek:UtilityFunction
Finds duplicate contact-point fingerprints for recent non-guest customers.

Returns:

  • (Hash{Integer => Array<Object>})

    customer IDs mapped to duplicate fingerprints



352
353
354
# File 'app/concerns/models/customer_financials.rb', line 352

def duplicate_contact_points
  Maintenance::DuplicateCustomerContactPoints.call
end

.statement_of_account(party_id) ⇒ Array<Object>

:reek:UtilityFunction
Builds the statement-of-account rows for a party.

Parameters:

  • party_id (Integer)

    party to report on

Returns:

  • (Array<Object>)

    statement rows sorted by due date



336
337
338
# File 'app/concerns/models/customer_financials.rb', line 336

def (party_id)
  Query::CustomerStatementOfAccount.call(party_id)
end

Instance Method Details

#account_on_hold?Boolean

Whether this customer's account is on hold.

Returns:

  • (Boolean)

    true when the account is on hold



317
318
319
# File 'app/concerns/models/customer_financials.rb', line 317

def 
  on_hold
end

#all_rmasActiveRecord::Relation<Rma>

All RMAs for this customer or its original orders.

Returns:

  • (ActiveRecord::Relation<Rma>)

    relation of matching RMAs



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

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_termsArray<String>

All applicable payment terms for this customer, delegating to the billing party.

Returns:

  • (Array<String>)

    list of term identifiers



306
307
308
309
310
311
312
# File 'app/concerns/models/customer_financials.rb', line 306

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

Whether APC (authorization code) payment is available for this customer.

Returns:

  • (Boolean)

    true when the buying group requires auth codes and has terms



296
297
298
299
300
301
# File 'app/concerns/models/customer_financials.rb', line 296

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

#approve_credit_request(amount, order = nil) ⇒ void

This method returns an undefined value.

Approves a credit request by increasing available credit and notifying the customer.

Parameters:

  • amount (BigDecimal)

    the approved credit amount

  • order (Order, nil) (defaults to: nil)

    optional order associated with the approval



88
89
90
91
92
93
# File 'app/concerns/models/customer_financials.rb', line 88

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

#ar_listings(show_open_only: false) ⇒ ActiveRecord::Relation<ViewArListing>

:reek:BooleanParameter :reek:ControlParameter
AR listings for this customer, optionally limited to open items only.

Parameters:

  • show_open_only (Boolean) (defaults to: false)

    when true, only include rows with a non-zero open amount

Returns:

  • (ActiveRecord::Relation<ViewArListing>)

    ordered AR listings



142
143
144
145
146
# File 'app/concerns/models/customer_financials.rb', line 142

def ar_listings(show_open_only: false)
  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) ⇒ Boolean

Attempts to automatically increase this customer's available credit.

Parameters:

  • amount (BigDecimal)

    the requested increase amount

  • order (Order, nil) (defaults to: nil)

    optional order that triggered the increase

Returns:

  • (Boolean)

    true if the increase was approved, false if routed for review



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

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

#available_store_creditBigDecimal

Net store credit available after open-order commitments.

Returns:

  • (BigDecimal)

    available store credit



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

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_debtBigDecimal

Total amount written off across receipt details for this customer.

Returns:

  • (BigDecimal)

    summed write-off amounts



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

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

#billing_entityCustomer

The customer or party responsible for billing this customer.

Returns:

  • (Customer)

    billing entity (may be +self+)



238
239
240
241
242
243
244
245
246
# File 'app/concerns/models/customer_financials.rb', line 238

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_historyBigDecimal

Purchase history net of outstanding balance.

Returns:

  • (BigDecimal)

    calculated net purchases



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

def calculated_purchase_history
  purchase_history - outstanding_balance
end

#combined_termsString

Human-readable payment terms, including early-payment discount if present.

Returns:

  • (String)

    combined terms string



262
263
264
265
266
267
268
# File 'app/concerns/models/customer_financials.rb', line 262

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) ⇒ void

This method returns an undefined value.

Verifies a credit limit and emails a denial notification when not approved.

Parameters:

  • amount (BigDecimal)

    the credit amount being verified

  • quote (Quote, nil) (defaults to: nil)

    optional quote associated with the verification



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

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

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

#determine_receipt_billing_entityCustomer

Billing entity to use when creating receipts.

Falls back to +self+ when the billing entity belongs to a different company.

Returns:



253
254
255
256
257
# File 'app/concerns/models/customer_financials.rb', line 253

def determine_receipt_billing_entity
  return self if billing_entity == self

  billing_entity.company == company ? billing_entity : self
end

#locked_credit_amountBigDecimal

Total credit currently locked by outstanding and pre-authorized amounts.

Returns:

  • (BigDecimal)

    summed locked credit



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

def locked_credit_amount
  outstanding_amount_numeric + pre_authorized_amount_numeric
end

#open_amount_greater_than_a_monthBigDecimal

Total open AR balance that is more than 30 days past due.

Returns:

  • (BigDecimal)

    summed open amount for overdue listings



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

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) ⇒ BigDecimal

Total of open (non-credit) orders for this customer and its children.

Parameters:

  • exclude_order (Order, nil) (defaults to: nil)

    optional order to exclude from the total

Returns:

  • (BigDecimal)

    summed order totals



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

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.excluding(exclude_order) if exclude_order
  res.sum(:total)
end

#outstanding_amountString

Outstanding AR balance formatted as currency.

Returns:

  • (String)

    formatted currency string



167
168
169
# File 'app/concerns/models/customer_financials.rb', line 167

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

#outstanding_amount_numericBigDecimal

Numeric outstanding AR balance.

Returns:

  • (BigDecimal)

    summed open amount



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

def outstanding_amount_numeric
  outstanding_ar_listings.sum(:open_amount_calc)
end

#outstanding_ar_listingsActiveRecord::Relation<ViewArListing>

AR listings with a non-zero open amount, ordered by document date.

Returns:

  • (ActiveRecord::Relation<ViewArListing>)

    outstanding AR listings



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

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_balanceBigDecimal

Total unpaid invoice balance for this customer's billing entity.

Returns:

  • (BigDecimal)

    summed unpaid invoice totals



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

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

#overdue_balanceBigDecimal

Total overdue invoice balance for this customer's billing entity.

Returns:

  • (BigDecimal)

    summed overdue invoice totals



43
44
45
# File 'app/concerns/models/customer_financials.rb', line 43

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

#payment_optionsArray<String>

Payment options available to this customer.

Returns:

  • (Array<String>)

    available payment option labels



324
325
326
327
328
# File 'app/concerns/models/customer_financials.rb', line 324

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

#pre_authorized_amount_numericBigDecimal

Numeric total of pre-authorized payments.

Returns:

  • (BigDecimal)

    summed pre-authorized amount



184
185
186
# File 'app/concerns/models/customer_financials.rb', line 184

def pre_authorized_amount_numeric
  pre_authorized_payments.sum(:amount)
end

#pre_authorized_paymentsActiveRecord::Relation<Payment>

Authorized purchase-order payments tied to this customer's open orders.

Returns:

  • (ActiveRecord::Relation<Payment>)

    pre-authorized payments



174
175
176
177
178
179
# File 'app/concerns/models/customer_financials.rb', line 174

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) ⇒ Customer::PreferredPayment

Determines this customer's preferred payment method from recent orders.

Parameters:

  • limit (Integer) (defaults to: 10)

    number of recent payments to consider

Returns:



223
224
225
226
227
228
229
230
231
232
233
# File 'app/concerns/models/customer_financials.rb', line 223

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_historyBigDecimal

Total invoiced amount for this customer's billing entity.

Returns:

  • (BigDecimal)

    summed invoice totals



12
13
14
# File 'app/concerns/models/customer_financials.rb', line 12

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

#receipts_listActiveRecord::Relation<Receipt>

Receipts for this customer, sorted by application state and date.

Returns:

  • (ActiveRecord::Relation<Receipt>)

    selectable receipt relation



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

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) ⇒ Hash{Symbol => Object}

:reek:BooleanParameter
Evaluates whether this customer is eligible for the requested credit amount.

Parameters:

  • amount (BigDecimal)

    the requested credit amount

  • ignore_annual_revenue (Boolean) (defaults to: false)

    whether to skip the annual revenue check

Returns:

  • (Hash{Symbol => Object})

    eligibility result with approval status and reasons



79
80
81
# File 'app/concerns/models/customer_financials.rb', line 79

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

#sync_credit_limitHash{Symbol => Object}

Synchronizes available credit against the credit limit and locked amount.

Returns:

  • (Hash{Symbol => Object})

    result with either +:ok+ or +:message+



125
126
127
128
129
130
131
132
133
134
135
# File 'app/concerns/models/customer_financials.rb', line 125

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

Whether this customer has net payment terms.

Returns:

  • (Boolean)

    true when terms are a recognized net term



289
290
291
# File 'app/concerns/models/customer_financials.rb', line 289

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_limitBigDecimal

Available credit for customers with terms; zero otherwise.

Returns:

  • (BigDecimal)

    usable terms credit limit



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

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_daysInteger

Number of days until payment is due based on the customer's terms.

Returns:

  • (Integer)

    terms in days



273
274
275
276
277
278
279
280
281
282
283
284
# File 'app/concerns/models/customer_financials.rb', line 273

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