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
-
.duplicate_addresses ⇒ Hash{Integer => Array<String>}
:reek:UtilityFunction Finds duplicate normalized addresses for recent non-guest customers.
-
.duplicate_contact_points ⇒ Hash{Integer => Array<Object>}
:reek:UtilityFunction Finds duplicate contact-point fingerprints for recent non-guest customers.
-
.statement_of_account(party_id) ⇒ Array<Object>
:reek:UtilityFunction Builds the statement-of-account rows for a party.
Instance Method Summary collapse
-
#account_on_hold? ⇒ Boolean
Whether this customer's account is on hold.
-
#all_rmas ⇒ ActiveRecord::Relation<Rma>
All RMAs for this customer or its original orders.
-
#all_terms ⇒ Array<String>
All applicable payment terms for this customer, delegating to the billing party.
-
#apc_available? ⇒ Boolean
Whether APC (authorization code) payment is available for this customer.
-
#approve_credit_request(amount, order = nil) ⇒ void
Approves a credit request by increasing available credit and notifying the customer.
-
#ar_listings(show_open_only: false) ⇒ ActiveRecord::Relation<ViewArListing>
:reek:BooleanParameter :reek:ControlParameter AR listings for this customer, optionally limited to open items only.
-
#auto_increase_available_credit(amount, order = nil) ⇒ Boolean
Attempts to automatically increase this customer's available credit.
-
#available_store_credit ⇒ BigDecimal
Net store credit available after open-order commitments.
-
#bad_debt ⇒ BigDecimal
Total amount written off across receipt details for this customer.
-
#billing_entity ⇒ Customer
The customer or party responsible for billing this customer.
-
#calculated_purchase_history ⇒ BigDecimal
Purchase history net of outstanding balance.
-
#combined_terms ⇒ String
Human-readable payment terms, including early-payment discount if present.
-
#credit_limit_verification(amount, quote = nil) ⇒ void
Verifies a credit limit and emails a denial notification when not approved.
-
#determine_receipt_billing_entity ⇒ Customer
Billing entity to use when creating receipts.
-
#locked_credit_amount ⇒ BigDecimal
Total credit currently locked by outstanding and pre-authorized amounts.
-
#open_amount_greater_than_a_month ⇒ BigDecimal
Total open AR balance that is more than 30 days past due.
-
#open_orders_total(exclude_order = nil) ⇒ BigDecimal
Total of open (non-credit) orders for this customer and its children.
-
#outstanding_amount ⇒ String
Outstanding AR balance formatted as currency.
-
#outstanding_amount_numeric ⇒ BigDecimal
Numeric outstanding AR balance.
-
#outstanding_ar_listings ⇒ ActiveRecord::Relation<ViewArListing>
AR listings with a non-zero open amount, ordered by document date.
-
#outstanding_balance ⇒ BigDecimal
Total unpaid invoice balance for this customer's billing entity.
-
#overdue_balance ⇒ BigDecimal
Total overdue invoice balance for this customer's billing entity.
-
#payment_options ⇒ Array<String>
Payment options available to this customer.
-
#pre_authorized_amount_numeric ⇒ BigDecimal
Numeric total of pre-authorized payments.
-
#pre_authorized_payments ⇒ ActiveRecord::Relation<Payment>
Authorized purchase-order payments tied to this customer's open orders.
-
#preferred_payment_method(limit: 10) ⇒ Customer::PreferredPayment
Determines this customer's preferred payment method from recent orders.
-
#purchase_history ⇒ BigDecimal
Total invoiced amount for this customer's billing entity.
-
#receipts_list ⇒ ActiveRecord::Relation<Receipt>
Receipts for this customer, sorted by application state and date.
-
#request_credit(amount, ignore_annual_revenue: false) ⇒ Hash{Symbol => Object}
:reek:BooleanParameter Evaluates whether this customer is eligible for the requested credit amount.
-
#sync_credit_limit ⇒ Hash{Symbol => Object}
Synchronizes available credit against the credit limit and locked amount.
-
#terms? ⇒ Boolean
Whether this customer has net payment terms.
-
#terms_credit_limit ⇒ BigDecimal
Available credit for customers with terms; zero otherwise.
-
#terms_in_days ⇒ Integer
Number of days until payment is due based on the customer's terms.
Class Method Details
.duplicate_addresses ⇒ Hash{Integer => Array<String>}
:reek:UtilityFunction
Finds duplicate normalized addresses for recent non-guest customers.
344 345 346 |
# File 'app/concerns/models/customer_financials.rb', line 344 def duplicate_addresses Maintenance::DuplicateCustomerAddresses.call end |
.duplicate_contact_points ⇒ Hash{Integer => Array<Object>}
:reek:UtilityFunction
Finds duplicate contact-point fingerprints for recent non-guest customers.
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.
336 337 338 |
# File 'app/concerns/models/customer_financials.rb', line 336 def statement_of_account(party_id) Query::CustomerStatementOfAccount.call(party_id) end |
Instance Method Details
#account_on_hold? ⇒ Boolean
Whether this customer's account is on hold.
317 318 319 |
# File 'app/concerns/models/customer_financials.rb', line 317 def account_on_hold? on_hold end |
#all_rmas ⇒ ActiveRecord::Relation<Rma>
All RMAs for this customer or its original orders.
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_terms ⇒ Array<String>
All applicable payment terms for this customer, delegating to the billing party.
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.
296 297 298 299 300 301 |
# File 'app/concerns/models/customer_financials.rb', line 296 def apc_available? bg = .corporate_account bg. && 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.
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.
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.
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_credit ⇒ BigDecimal
Net store credit available after open-order commitments.
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_debt ⇒ BigDecimal
Total amount written off across receipt details for this customer.
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_entity ⇒ Customer
The customer or party responsible for billing this customer.
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_history ⇒ BigDecimal
Purchase history net of outstanding balance.
36 37 38 |
# File 'app/concerns/models/customer_financials.rb', line 36 def calculated_purchase_history purchase_history - outstanding_balance end |
#combined_terms ⇒ String
Human-readable payment terms, including early-payment discount if present.
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.
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_entity ⇒ Customer
Billing entity to use when creating receipts.
Falls back to +self+ when the billing entity belongs to a different company.
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_amount ⇒ BigDecimal
Total credit currently locked by outstanding and pre-authorized amounts.
118 119 120 |
# File 'app/concerns/models/customer_financials.rb', line 118 def locked_credit_amount outstanding_amount_numeric + end |
#open_amount_greater_than_a_month ⇒ BigDecimal
Total open AR balance that is more than 30 days past due.
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.
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_amount ⇒ String
Outstanding AR balance formatted as currency.
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_numeric ⇒ BigDecimal
Numeric outstanding AR balance.
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_listings ⇒ ActiveRecord::Relation<ViewArListing>
AR listings with a non-zero open amount, ordered by document date.
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_balance ⇒ BigDecimal
Total unpaid invoice balance for this customer's billing entity.
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_balance ⇒ BigDecimal
Total overdue invoice balance for this customer's billing entity.
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_options ⇒ Array<String>
Payment options available to this customer.
324 325 326 327 328 |
# File 'app/concerns/models/customer_financials.rb', line 324 def = ['Credit Card'] << terms if terms != Customer::TERM_DUE end |
#pre_authorized_amount_numeric ⇒ BigDecimal
Numeric total of pre-authorized payments.
184 185 186 |
# File 'app/concerns/models/customer_financials.rb', line 184 def .sum(:amount) end |
#pre_authorized_payments ⇒ ActiveRecord::Relation<Payment>
Authorized purchase-order payments tied to this customer's open orders.
174 175 176 177 178 179 |
# File 'app/concerns/models/customer_financials.rb', line 174 def Payment.joins(:order).purchase_orders. .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.
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_history ⇒ BigDecimal
Total invoiced amount for this customer's billing entity.
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_list ⇒ ActiveRecord::Relation<Receipt>
Receipts for this customer, sorted by application state and date.
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.
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_limit ⇒ Hash{Symbol => Object}
Synchronizes available credit against the credit limit and locked amount.
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.
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_limit ⇒ BigDecimal
Available credit for customers with terms; zero otherwise.
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_days ⇒ Integer
Number of days until payment is due based on the customer's terms.
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 |