Class: Payment

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, PgSearch::Model
Defined in:
app/models/payment.rb

Overview

== Schema Information

Table name: payments
Database name: primary

id :bigint not null, primary key
account_holder_type :string
account_type :string
address_line1_check :string
address_zip_check :string
amazon_pay_status :string
amount :decimal(, )
approval_notification_sent :boolean
authorization_code :string
authorization_reference :string
authorization_type :string
billing_address_city :string
billing_address_country :string
billing_address_line1 :string
billing_address_line2 :string
billing_address_state :string
billing_address_zip :string
brand :string
bread_token :string
capture_before :datetime
card_country :string
card_expires_on :date
card_identifier :string
card_type :string
category :string
currency :string
cvc_check :string
date :date
email :string
exp_month :integer
exp_year :integer
first_name :string
fraud_review_done :boolean
http_accept_language :string
http_user_agent :string
issuer_number :string
last4 :string
last_name :string
name :string
payment_approved :boolean
paypal_email :string
paypal_metadata :jsonb
paypal_token :string
plaid_expected_settlement_date :date
plaid_public_token :string
po_number :string
radar_network_status :string
radar_reason :string
radar_risk_level :string
radar_seller_message :string
radar_type :string
reference :string
remote_ip_address :string
routing_number :string
send_authorization_email :boolean
shipping_address_city :string
shipping_address_country :string
shipping_address_line1 :string
shipping_address_line2 :string
shipping_address_name :string
shipping_address_state :string
shipping_address_zip :string
skip_auto_receipt :boolean default(FALSE)
skip_minfraud :boolean
state :string default("pending")
stripe_capabilities :jsonb
test :boolean
uploads_count :integer
zip_code :string
created_at :datetime not null
updated_at :datetime not null
account_id :integer
amazon_pay_charge_id :string
amazon_pay_charge_permission_id :string
amazon_pay_checkout_session_id :string
creator_id :integer
credit_memo_id :integer
customer_id :integer
delivery_id :integer
invoice_id :integer
order_id :integer
paypal_payer_id :string
paypal_transaction_id :string
plaid_account_id :string
plaid_transfer_id :string
plaid_transfer_intent_id :string
rma_id :integer
stripe_payment_intent_id :string
transaction_id :string
updater_id :integer
vault_id :string
vpo_contact_id :integer

Indexes

by_did_ctry_st (delivery_id,category,state)
by_iid_st_at (invoice_id,state,authorization_type)
by_oid_ctry_st (order_id,category,state)
idx_state_category (state,category)
index_payments_on_account_id (account_id)
index_payments_on_authorization_type_and_authorization_code (authorization_type,authorization_code) WHERE ((authorization_type IS NOT NULL) AND (authorization_code IS NOT NULL))
index_payments_on_creator_id (creator_id)
index_payments_on_credit_memo_id (credit_memo_id)
index_payments_on_customer_id (customer_id)
index_payments_on_order_id_and_state (order_id,state)
index_payments_on_paypal_payer_id (paypal_payer_id)
index_payments_on_paypal_transaction_id (paypal_transaction_id)
index_payments_on_rma_id (rma_id)
index_payments_on_stripe_payment_intent_id (stripe_payment_intent_id) WHERE (stripe_payment_intent_id IS NOT NULL)
index_payments_on_transaction_id (transaction_id)
index_payments_on_updater_id (updater_id)
index_payments_on_vault_id (vault_id)
index_payments_on_vpo_contact_id (vpo_contact_id)

Foreign Keys

payments_credit_memo_id_fkey (credit_memo_id => credit_memos.id)
payments_customer_id_fkey (customer_id => parties.id) ON DELETE => cascade
payments_delivery_id_fk (delivery_id => deliveries.id) ON DELETE => nullify
payments_invoice_id_fkey (invoice_id => invoices.id)
payments_order_id_fk (order_id => orders.id) ON DELETE => cascade

Defined Under Namespace

Classes: OrderProcessor, PaypalStatusResult, StrategyResolver

Constant Summary collapse

ADV_REPL =
'Advance Replacement'
CHECK =
'Check'
CREDIT_CARD =
'Credit Card'
CREDIT_CARD_TERMINAL =
'Credit Card Terminal'
BREAD =
'Bread'
PLAID =
'Plaid'
AMAZON_PAY =
'Amazon Pay'
PO =
'Purchase Order'
VPO =
'Verbal Purchase Order'
ECHECK =
'eCheck'
PAYPAL =
'PayPal'
PAYPAL_INVOICE =
'PayPal Invoice'
RMA_CREDIT =
'RMA Credit'
CASH =
'Cash'
STORE_CREDIT =
'Store Credit'
WIRE =
'Wire Transfer'
ACCOUNT_HOLDER_TYPES =
%w[personal business]
ACCOUNT_TYPES =
%w[checking savings]
PAYPAL_MIN_SIGNATURE_REQUIRED =
750.00
ECHECK_MIN_AMOUNT_WITHOUT_SUPERVISION =
2000.00
CATEGORIES_REQUIRING_REVIEW =
[PAYPAL_INVOICE, RMA_CREDIT, ECHECK, CHECK, CASH, WIRE]
CATEGORIES_NOT_ALLOWING_CAPTURE =
[PO, VPO, ECHECK, WIRE]
PAYPAL_OVER_CAPTURE_PERCENT =
BigDecimal('1.15')
PAYPAL_OVER_CAPTURE_MAX_INCREASE =
BigDecimal('75')
PAYPAL_HONOR_PERIOD =
3.days
PAYPAL_REAUTH_BUFFER =
12.hours
PAYPAL_MAX_AUTH_DAYS =
29
PAYPAL_REAUTH_EARLIEST_DAY =
4

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has one collapse

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#account_holder_typeObject (readonly)



185
# File 'app/models/payment.rb', line 185

validates :account_type, :account_holder_type, presence: { if: proc { |pp| pp.vault_id.blank? && pp.authorization_type == 'check' } }

#account_numberObject

Returns the value of attribute account_number.



211
212
213
# File 'app/models/payment.rb', line 211

def 
  @account_number
end

#account_typeObject (readonly)



185
# File 'app/models/payment.rb', line 185

validates :account_type, :account_holder_type, presence: { if: proc { |pp| pp.vault_id.blank? && pp.authorization_type == 'check' } }

#address_cityObject

Returns the value of attribute address_city.



211
212
213
# File 'app/models/payment.rb', line 211

def address_city
  @address_city
end

#address_countryObject

Returns the value of attribute address_country.



211
212
213
# File 'app/models/payment.rb', line 211

def address_country
  @address_country
end

#address_idObject

Returns the value of attribute address_id.



211
212
213
# File 'app/models/payment.rb', line 211

def address_id
  @address_id
end

#address_line1Object

Returns the value of attribute address_line1.



211
212
213
# File 'app/models/payment.rb', line 211

def address_line1
  @address_line1
end

#address_line2Object

Returns the value of attribute address_line2.



211
212
213
# File 'app/models/payment.rb', line 211

def address_line2
  @address_line2
end

#address_stateObject

Returns the value of attribute address_state.



211
212
213
# File 'app/models/payment.rb', line 211

def address_state
  @address_state
end

#address_zipObject

Returns the value of attribute address_zip.



211
212
213
# File 'app/models/payment.rb', line 211

def address_zip
  @address_zip
end

#amountObject (readonly)



181
# File 'app/models/payment.rb', line 181

validates :category, :amount, :currency, :state, presence: true

#amount_to_captureObject

Returns the value of attribute amount_to_capture.



211
212
213
# File 'app/models/payment.rb', line 211

def amount_to_capture
  @amount_to_capture
end

#bread_tokenObject

Returns the value of attribute bread_token.



211
212
213
# File 'app/models/payment.rb', line 211

def bread_token
  @bread_token
end

#card_tokenObject

Returns the value of attribute card_token.



211
212
213
# File 'app/models/payment.rb', line 211

def card_token
  @card_token
end

#categoryObject (readonly)



181
# File 'app/models/payment.rb', line 181

validates :category, :amount, :currency, :state, presence: true

Returns the value of attribute consent_channel.



211
212
213
# File 'app/models/payment.rb', line 211

def consent_channel
  @consent_channel
end

#currencyObject (readonly)



181
# File 'app/models/payment.rb', line 181

validates :category, :amount, :currency, :state, presence: true

#emailObject (readonly)



205
# File 'app/models/payment.rb', line 205

validates :email, email_format: true

#error_codesObject

Returns the value of attribute error_codes.



211
212
213
# File 'app/models/payment.rb', line 211

def error_codes
  @error_codes
end

#issuer_numberObject

Returns the value of attribute issuer_number.



211
212
213
# File 'app/models/payment.rb', line 211

def issuer_number
  @issuer_number
end

#last_responseObject

Returns the value of attribute last_response.



211
212
213
# File 'app/models/payment.rb', line 211

def last_response
  @last_response
end

#paypal_emailObject (readonly)



186
# File 'app/models/payment.rb', line 186

validates :paypal_email, presence: { if: proc { |pp| pp.authorization_type == 'paypal_invoice' } }

#po_numberObject (readonly)



183
# File 'app/models/payment.rb', line 183

validates :po_number, presence: { if: proc { |pp| pp.category == PO } }

#rma_idObject (readonly)



184
# File 'app/models/payment.rb', line 184

validates :rma_id, presence: { if: proc { |pp| pp.category == ADV_REPL } }

#stateObject (readonly)



181
# File 'app/models/payment.rb', line 181

validates :category, :amount, :currency, :state, presence: true

#store_addressObject

Returns the value of attribute store_address.



211
212
213
# File 'app/models/payment.rb', line 211

def store_address
  @store_address
end

#store_cardObject

Returns the value of attribute store_card.



211
212
213
# File 'app/models/payment.rb', line 211

def store_card
  @store_card
end

#store_card_nameObject

Returns the value of attribute store_card_name.



211
212
213
# File 'app/models/payment.rb', line 211

def store_card_name
  @store_card_name
end

#vpo_contact_idObject (readonly)



182
# File 'app/models/payment.rb', line 182

validates :vpo_contact_id, presence: { if: proc { |pp| pp.category == VPO } }

Class Method Details

.all_amazon_pay_capturedActiveRecord::Relation<Payment>

A relation of Payments that are all amazon pay captured. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



233
# File 'app/models/payment.rb', line 233

scope :all_amazon_pay_captured, -> { all_captured.where(authorization_type: 'amazon_pay') }

.all_authorizedActiveRecord::Relation<Payment>

A relation of Payments that are all authorized. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



236
# File 'app/models/payment.rb', line 236

scope :all_authorized, -> { where(state: 'authorized') }

.all_capturedActiveRecord::Relation<Payment>

A relation of Payments that are all captured. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



228
# File 'app/models/payment.rb', line 228

scope :all_captured, -> { where(state: 'captured') }

.all_cc_capturedActiveRecord::Relation<Payment>

A relation of Payments that are all cc captured. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



230
# File 'app/models/payment.rb', line 230

scope :all_cc_captured, -> { all_captured.where(authorization_type: %w[credit_card paypal]) }

.all_check_capturedActiveRecord::Relation<Payment>

A relation of Payments that are all check captured. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



231
# File 'app/models/payment.rb', line 231

scope :all_check_captured, -> { all_captured.where(authorization_type: 'check') }

.all_collect_capturedActiveRecord::Relation<Payment>

A relation of Payments that are all collect captured. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



235
# File 'app/models/payment.rb', line 235

scope :all_collect_captured, -> { all_captured.where(authorization_type: 'credit_card', reference: 'Collect Card Reader') }

.all_paypal_invoice_capturedActiveRecord::Relation<Payment>

A relation of Payments that are all paypal invoice captured. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



234
# File 'app/models/payment.rb', line 234

scope :all_paypal_invoice_captured, -> { all_captured.where(authorization_type: 'paypal_invoice') }

.all_plaid_capturedActiveRecord::Relation<Payment>

A relation of Payments that are all plaid captured. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



232
# File 'app/models/payment.rb', line 232

scope :all_plaid_captured, -> { all_captured.where(authorization_type: 'plaid') }

.amazon_paymentsActiveRecord::Relation<Payment>

A relation of Payments that are amazon payments. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



218
# File 'app/models/payment.rb', line 218

scope :amazon_payments, -> { where(category: AMAZON_PAY) }

.bread_paymentsActiveRecord::Relation<Payment>

A relation of Payments that are bread payments. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



217
# File 'app/models/payment.rb', line 217

scope :bread_payments, -> { where(category: BREAD) }

.can_be_refundedActiveRecord::Relation<Payment>

A relation of Payments that are can be refunded. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



237
# File 'app/models/payment.rb', line 237

scope :can_be_refunded, -> { where(state: %w[captured partially_refunded]) }

.cc_paypal_bread_amazonActiveRecord::Relation<Payment>

A relation of Payments that are cc paypal bread amazon. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



229
# File 'app/models/payment.rb', line 229

scope :cc_paypal_bread_amazon, -> { where(authorization_type: %w[credit_card paypal bread, amazon_pay]) }

.check_paymentsActiveRecord::Relation<Payment>

A relation of Payments that are check payments. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



220
# File 'app/models/payment.rb', line 220

scope :check_payments, -> { where(category: CHECK) }

.credit_cardsActiveRecord::Relation<Payment>

A relation of Payments that are credit cards. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



224
# File 'app/models/payment.rb', line 224

scope :credit_cards, -> { where(category: CREDIT_CARD) }

.echeck_payment_review(amount, customer, order) ⇒ Object



699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/payment.rb', line 699

def self.echeck_payment_review(amount, customer, order)
  res = {}
  fails = 0
  fail_reasons = []
  pass_reasons = []

  if Payment.where(category: ECHECK).all_authorized.where('payments.created_at > ?', Time.current.at_midnight).count > 5
    fails += 1
    fail_reasons << 'Limit exceeded for number of automatic eCheck approvals in one day, company wide (5).'
  end
  if Payment.joins(:order).where(category: ECHECK, orders: { customer_id: order.customer_id }).all_authorized.where('payments.created_at > ?',
                                                                                                                    Time.current.at_midnight).count > 2
    fails += 1
    fail_reasons << 'Limit exceeded for number of automatic eCheck approvals in one day, per customer (2).'
  end

  if amount < 100
    pass_reasons = ['Amount less than $100']
  else
    if amount > 10_000
      fails += 1
      fail_reasons << 'Amount greater than $10,000'
    end
    rc_res = customer.request_credit(amount)
    if rc_res[:approved] == false
      fails += rc_res[:fails]
      fail_reasons += rc_res[:fail_reasons]
    else
      pass_reasons = rc_res[:pass_reasons]
    end
  end
  if fails > 0
    res[:required] = true
    res[:fail_reasons] = fail_reasons
  else
    res[:required] = false
    res[:pass_reasons] = pass_reasons
  end
  res
end

.expiredActiveRecord::Relation<Payment>

A relation of Payments that are expired. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



226
# File 'app/models/payment.rb', line 226

scope :expired, -> { where(state: 'expired') }

.non_voidedActiveRecord::Relation<Payment>

A relation of Payments that are non voided. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



238
# File 'app/models/payment.rb', line 238

scope :non_voided, -> { where.not(state: %w[voided expired]) }

.orderObject



937
938
939
# File 'app/models/payment.rb', line 937

def self.order
  legacy_order || order
end

.payment_options(customer, order = nil, currency = nil) ⇒ Object



317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'app/models/payment.rb', line 317

def self.payment_options(customer, order = nil, currency = nil)
  payment_options = [CHECK, WIRE, CREDIT_CARD, PAYPAL, AMAZON_PAY, PAYPAL_INVOICE, CASH]
  if customer.has_terms?
    payment_options << PO
    # Use exists? instead of any? for better performance (single COUNT query vs loading records)
    payment_options << VPO if customer.contacts.verbal_po_contacts.exists?
  end
  payment_options << ADV_REPL if order && order.rma.present? && order.rma.credit_available > 0
  payment_options << RMA_CREDIT if order && order.precreate_rma?
  payment_options << ECHECK if (order && order.currency == 'USD') || currency == 'USD'
  # Use exists? and check store credit in single condition to avoid loading all credit memos
  payment_options << STORE_CREDIT if customer.available_store_credit.to_d > 0 && customer.credit_memos.available_to_apply.exists?
  payment_options.sort
end

.paypal_invoicesActiveRecord::Relation<Payment>

A relation of Payments that are paypal invoices. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



225
# File 'app/models/payment.rb', line 225

scope :paypal_invoices, -> { where(category: PAYPAL_INVOICE) }

.paypal_paymentsActiveRecord::Relation<Payment>

A relation of Payments that are paypal payments. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



222
# File 'app/models/payment.rb', line 222

scope :paypal_payments, -> { where(category: PAYPAL) }

.plaid_paymentsActiveRecord::Relation<Payment>

A relation of Payments that are plaid payments. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



219
# File 'app/models/payment.rb', line 219

scope :plaid_payments, -> { where(category: PLAID) }

.po_searchActiveRecord::Relation<Payment>

A relation of Payments that are po search. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



227
# File 'app/models/payment.rb', line 227

scope :po_search, ->(term) { where(Payment[:po_number].matches("%#{term}%")) }

.purchase_ordersActiveRecord::Relation<Payment>

A relation of Payments that are purchase orders. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



223
# File 'app/models/payment.rb', line 223

scope :purchase_orders, -> { where.not(order_id: nil).where(category: [PO, VPO]) }

.wire_paymentsActiveRecord::Relation<Payment>

A relation of Payments that are wire payments. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



221
# File 'app/models/payment.rb', line 221

scope :wire_payments, -> { where(category: WIRE) }

Instance Method Details

#accountAccount

Returns:

See Also:



165
# File 'app/models/payment.rb', line 165

belongs_to :account, optional: true

#all_pi_siblingsObject



373
374
375
376
377
378
# File 'app/models/payment.rb', line 373

def all_pi_siblings
  return Payment.none unless stripe_payment_intent_id.present?

  Payment.where(stripe_payment_intent_id: stripe_payment_intent_id)
         .where.not(id: id)
end

#amount_captured_on_paypalObject



930
931
932
933
934
935
# File 'app/models/payment.rb', line 930

def amount_captured_on_paypal
  return false if authorization_type != 'paypal'

  auth_details = Payment::Apis::Paypal.get_authorization_details(self).parse
  auth_details["amount"]["value"].to_f
end

#amount_captured_on_stripeObject



909
910
911
912
913
914
915
916
917
918
919
920
# File 'app/models/payment.rb', line 909

def amount_captured_on_stripe
  return false if authorization_type != 'credit_card'

  obj = stripe_payment_object
  return false unless obj

  if Payment::Apis::Stripe.payment_intent?(authorization_code)
    obj.amount_received.to_f
  else
    obj.try(:amount_captured).to_f rescue false
  end
end

#attempt_paypal_reauthorization_if_needed(auth_expiration) ⇒ Object



536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'app/models/payment.rb', line 536

def attempt_paypal_reauthorization_if_needed(auth_expiration)
  if auth_expiration.present? && auth_expiration > PAYPAL_REAUTH_BUFFER.from_now
    sync_capture_before_from_paypal_expiry(auth_expiration)
    hours_left = ((auth_expiration - Time.current) / 1.hour).round(1)
    return PaypalStatusResult.new(status: :still_valid, message: "PayPal authorization still valid (#{hours_left} hours remaining, expires #{auth_expiration.strftime('%b %d %H:%M %Z')})")
  end

  honor_deadline = capture_before || (created_at + PAYPAL_HONOR_PERIOD)
  if honor_deadline > PAYPAL_REAUTH_BUFFER.from_now
    hours_left = ((honor_deadline - Time.current) / 1.hour).round(1)
    return PaypalStatusResult.new(status: :still_valid, message: "Honor period still valid (#{hours_left} hours remaining, capture by #{honor_deadline.strftime('%b %d %H:%M %Z')})")
  end

  if created_at < PAYPAL_MAX_AUTH_DAYS.days.ago
    logger.error("Payment #{id}: PayPal authorization past #{PAYPAL_MAX_AUTH_DAYS}-day limit, cannot reauthorize")
    Mailer.generic_mailer(
      from: ADMINISTRATOR_EMAIL,
      to: "#{ADMINISTRATOR_EMAIL},#{ACCOUNTS_RECEIVABLE_EMAIL}",
      subject: "PAYPAL PAYMENT ##{id} AUTHORIZATION EXPIRED",
      message: "PayPal payment id: #{id} has exceeded the 29-day authorization period and cannot be reauthorized. Please take action to ensure all funds are captured or a new payment is collected.",
      no_verbage: true
    ).deliver
    order.cr_hold if order.present?
    payment_expired!
    return PaypalStatusResult.new(status: :expired, message: "Authorization past #{PAYPAL_MAX_AUTH_DAYS}-day limit")
  end

  days_since_auth = ((Time.current - created_at) / 1.day).floor
  if days_since_auth < PAYPAL_REAUTH_EARLIEST_DAY
    return PaypalStatusResult.new(status: :still_valid, message: "Too early to reauthorize (day #{days_since_auth} of #{PAYPAL_REAUTH_EARLIEST_DAY} minimum). Authorization is still capturable.")
  end

  res = Payment::Gateways::Paypal.new(self).reauthorize(amount)
  if res.success
    PaypalStatusResult.new(status: :reauthorized, message: 'Successfully reauthorized')
  else
    Mailer.generic_mailer(
      from: ADMINISTRATOR_EMAIL,
      to: "#{ADMINISTRATOR_EMAIL},#{ACCOUNTS_RECEIVABLE_EMAIL}",
      subject: "PAYPAL PAYMENT ##{id} REAUTHORIZATION ERROR",
      message: "Unable to reauthorize paypal payment id: #{id}. Please take action to ensure all funds are captured or applied.",
      no_verbage: true
    ).deliver
    logger.error("#{Time.current}: PAYPAL REAUTHORIZATION ERROR: Problem reauthorizing paypal payment.")
    PaypalStatusResult.new(status: :reauth_failed, message: res.message || 'Reauthorization failed')
  end
end

#auth_codeObject



845
846
847
848
# File 'app/models/payment.rb', line 845

def auth_code
  capture = transactions.where(action: 'capture', success: true).first
  capture.nil? ? nil : capture&.params&.[]('auth_code')
end

#auth_urlObject



1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
# File 'app/models/payment.rb', line 1106

def auth_url
  return nil if authorization_code.nil?

  if authorization_type == 'paypal'
    host = test? ? "www.sandbox.paypal.com" : "www.paypal.com"
    "https://#{host}/activity/payment/#{authorization_code}"
  elsif test?
    "https://dashboard.stripe.com/test/payments/#{authorization_code}"
  else
    "https://dashboard.stripe.com/payments/#{authorization_code}"
  end
end

#authorization_reviewObject



429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'app/models/payment.rb', line 429

def authorization_review
  res = {}
  case category
  when PAYPAL_INVOICE, RMA_CREDIT
    res[:required] = true
    res[:fail_reasons] = ["#{category} payment requires Accounting approval"]
  when ECHECK
    echeck_total = order.payments.all_authorized.where(category: ECHECK).sum(:amount)
    echeck_res = Payment.echeck_payment_review(echeck_total, order.customer, order)
    res[:required] = echeck_res[:required]
    res[:fail_reasons] = echeck_res[:fail_reasons]
    res[:pass_reasons] = echeck_res[:pass_reasons]
  when CHECK, WIRE
    res[:required] = true
    res[:fail_reasons] = ['All Check/Wire payments requires Accounting approval']
  when CASH
    if order.is_warehouse_pickup?
      if amount > 100
        # pickups where cash amount is greater than $100 require authorization
        res[:required] = true
        res[:fail_reasons] = ["Pickup order with #{category} payment more than $100 requires Accounting approval"]
      else
        res[:required] = false
        res[:pass_reasons] = ["Pickup order with #{category} payment more than $100 requires Accounting approval"]
      end
    else
      # if it's not a pickup, then cash always require authorization
      res[:required] = true
      res[:fail_reasons] = ["Non-pickup order with #{category} payment requires Accounting approval"]
    end
  else
    # any other payment method doesn't require authorization
    res[:required] = false
  end
  res
end

#authorization_review_required?Boolean

Returns:

  • (Boolean)


425
426
427
# File 'app/models/payment.rb', line 425

def authorization_review_required?
  CATEGORIES_REQUIRING_REVIEW.include?(category)
end

#automatically_authorized?Boolean

Returns:

  • (Boolean)


466
467
468
# File 'app/models/payment.rb', line 466

def automatically_authorized?
  authorization_review_required? and authorization_review[:required] == false
end

#available_to_refundObject



941
942
943
# File 'app/models/payment.rb', line 941

def available_to_refund
  total_captured - total_refunded
end

#billing_addressObject



1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
# File 'app/models/payment.rb', line 1124

def billing_address
  Address.new(
    street1: billing_address_line1,
    street2: billing_address_line2,
    city: billing_address_city,
    zip: billing_address_zip,
    state_code: State.code_for_string(billing_address_state),
    country_iso3: Country.iso3_for_string(billing_address_country)
  )
end

#can_be_voided?Boolean

Returns:

  • (Boolean)


361
362
363
# File 'app/models/payment.rb', line 361

def can_be_voided?
  (authorized? and !order.editing_locked? and category != 'Credit Card Terminal')
end

#capture_deadlineObject



357
358
359
# File 'app/models/payment.rb', line 357

def capture_deadline
  stripe_resolver.authorization_deadline
end

#captured_on_paypal?Boolean

Returns:

  • (Boolean)


922
923
924
925
926
927
928
# File 'app/models/payment.rb', line 922

def captured_on_paypal?
  return false if authorization_type != 'paypal'

  auth_details = Payment::Apis::Paypal.get_authorization_details(self).parse
  auth_status = auth_details['status']
  auth_status == 'CAPTURED'
end

#captured_on_stripe?Boolean

Returns:

  • (Boolean)


896
897
898
899
900
901
902
903
904
905
906
907
# File 'app/models/payment.rb', line 896

def captured_on_stripe?
  return false if authorization_type != 'credit_card'

  obj = stripe_payment_object
  return false unless obj

  if Payment::Apis::Stripe.payment_intent?(authorization_code)
    obj.status == 'succeeded'
  else
    obj.try(:captured?) rescue false
  end
end

#check_cc_payment_statusObject



591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
# File 'app/models/payment.rb', line 591

def check_cc_payment_status
  return unless authorized?
  return unless authorization_type == 'credit_card'

  obj = stripe_payment_object
  return unless obj

  pi_status = Payment::Apis::Stripe.payment_intent?(authorization_code) ? obj.status : nil

  case pi_status
  when 'canceled'
    # PI was voided externally (e.g. from Stripe dashboard)
    logger.info("Payment #{id}: PI #{stripe_payment_intent_id} canceled on Stripe, voiding")
    payment_voided!

  when 'succeeded'
    # PI was finalized — either captured or released externally
    sync_external_capture(obj)

  when 'requires_capture'
    # PI still open — check if approaching reauth deadline
    if stripe_resolver.needs_reauthorization?.reauth_needed
      Payment::Gateways::CreditCard.new(self).reauthorize
    end

  else
    # Legacy charge objects or unknown status — fall back to old behavior
    if obj.try(:captured?)
      transaction = OrderTransaction.new(
        amount: obj.try(:amount_captured).to_f,
        action: 'capture',
        success: true,
        reference: authorization_code,
        message: "THIS PAYMENT WAS MANUALLY CAPTURED ON THE PAYMENT PLATFORM",
        params: obj.to_hash,
        test: false
      )
      transactions.push(transaction)
      payment_captured!
    elsif stripe_resolver.needs_reauthorization?.reauth_needed
      Payment::Gateways::CreditCard.new(self).reauthorize
    end
  end
end

#check_paypal_invoice_payment_statusObject



669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
# File 'app/models/payment.rb', line 669

def check_paypal_invoice_payment_status
  if authorized?
    response = Payment::Apis::Paypal.get_invoice_details(authorization_code)
    res = JSON.parse(response.to_s)
    invoice_status = res['status']
    if invoice_status.present? && invoice_status == 'PAID'
      Payment::Gateways::PaypalInvoice.new(self).capture(res)
      order.reload
      order.release_order if order.payments.paypal_invoices.all?(&:captured?)
      true
    else
      # Paypal invoice has not been paid yet
      false
    end
  elsif captured?
    # If the payment is captured do nothing. We could verify that the receipt has been created, etc.
    true
  end
end

#check_paypal_payment_statusObject



479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'app/models/payment.rb', line 479

def check_paypal_payment_status
  return PaypalStatusResult.new(status: :not_authorized, message: 'Payment is not in authorized state') unless authorized?

  response = Payment::Apis::Paypal.get_authorization(authorization_code)
  auth_status = response['status']
  auth_expiration = response['expiration_time']&.to_time

  sync_paypal_authorization_expiry(auth_expiration)

  case auth_status
  when 'CAPTURED'
    sync_paypal_external_capture(response)
    PaypalStatusResult.new(status: :captured, message: 'Payment was captured externally on PayPal')
  when 'VOIDED'
    logger.info("Payment #{id}: PayPal authorization voided externally")
    payment_voided!
    PaypalStatusResult.new(status: :voided, message: 'Authorization was voided on PayPal')
  when 'EXPIRED'
    logger.info("Payment #{id}: PayPal authorization expired")
    payment_expired!
    PaypalStatusResult.new(status: :expired, message: 'Authorization has expired')
  when 'DENIED'
    logger.info("Payment #{id}: PayPal authorization denied")
    transaction_declined!
    PaypalStatusResult.new(status: :denied, message: 'Authorization was denied')
  when 'CREATED', 'PENDING'
    attempt_paypal_reauthorization_if_needed(auth_expiration)
  else
    PaypalStatusResult.new(status: :unknown, message: "Unknown PayPal status: #{auth_status}")
  end
end

#communication_resourceOrder

Returns:

See Also:

  • http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html ActiveRecord::Associations


315
# File 'app/models/payment.rb', line 315

belongs_to :order, optional: true

#credit_card_vaultCreditCardVault



163
# File 'app/models/payment.rb', line 163

belongs_to :credit_card_vault, primary_key: 'vault_id', foreign_key: 'vault_id', optional: true

#credit_memoCreditMemo

Returns:

See Also:



164
# File 'app/models/payment.rb', line 164

belongs_to :credit_memo, optional: true


820
821
822
823
824
# File 'app/models/payment.rb', line 820

def crm_link
  UrlHelper.instance.order_path(order)
rescue StandardError
  ''
end

#currency_symbolObject



945
946
947
# File 'app/models/payment.rb', line 945

def currency_symbol
  Money::Currency.new(currency).symbol
end

#customerCustomer

Returns:

See Also:



160
# File 'app/models/payment.rb', line 160

belongs_to :customer, optional: true

#deep_dupObject



311
312
313
# File 'app/models/payment.rb', line 311

def deep_dup
  deep_clone(except: :delivery_id)
end

#default_cc_options(ip_address = nil, email = nil) ⇒ Object



1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
# File 'app/models/payment.rb', line 1070

def default_cc_options(ip_address = nil, email = nil)
  options = {
    description: (order.reference_number.present? ? "Order #{order.reference_number}" : "Order ID #{order.id}"),
    statement_description: "WarmlyYours #{order.reference_number}",
    currency:,
    shipping_address: order.shipping_address.format_for_payment_gateway(true),
    metadata: {
      email:,
      ip: ip_address,
      order_id: order.id,
      payment_id: id
    }
  }
  # only pass the customer as an option if we detect a vault_id, otherwise is will just charge the first card stored on the customer account
  options[:customer] = order.customer.stripe_customer_id if vault_id.present?
  options
end

#default_echeck_options(_ip_address = nil) ⇒ Object



1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
# File 'app/models/payment.rb', line 1094

def default_echeck_options(_ip_address = nil)
  {
    # customer: order.customer_id,
    # ip: ip_address,
    # email: self.authorization.email,
    # shipping_address: order.shipping_address.format_for_payment_gateway(true),
    billing_address: order.billing_address.format_for_forte(true),
    order_id: order.reference_number
    # description: "Order #{order.reference_number}"
  }
end

#default_paypal_optionsObject



1088
1089
1090
1091
1092
# File 'app/models/payment.rb', line 1088

def default_paypal_options
  {
    currency: currency
  }
end

#deliveryDelivery

Returns:

See Also:



166
# File 'app/models/payment.rb', line 166

belongs_to :delivery, inverse_of: :payments, optional: true

#detect_fraud(force_new_report: false) ⇒ Object



826
827
828
829
830
831
832
833
834
835
836
837
# File 'app/models/payment.rb', line 826

def detect_fraud(force_new_report: false)
  return nil if order.nil?
  return nil unless category.in?(['Credit Card', 'PayPal', 'eCheck'])
  return nil if order.belongs_to_smartservice_group?

  begin
    Order::FraudDetector.new(order, self).process(force_new_report:)
  rescue StandardError => e
    ErrorReporting.error(e, "FraudDetector Error - Unable to process new FraudDetector request for Payment ID:#{id}")
    nil
  end
end

#does_not_allow_capture?Boolean

Returns:

  • (Boolean)


808
809
810
# File 'app/models/payment.rb', line 808

def does_not_allow_capture?
  category.in?(CATEGORIES_NOT_ALLOWING_CAPTURE)
end

#email_collection_for_selectObject



858
859
860
861
862
# File 'app/models/payment.rb', line 858

def email_collection_for_select
  customer_for_collection = customer
  customer_for_collection ||= order&.customer
  [customer_for_collection&.all_emails, email].flatten.compact.uniq
end

#fraud_reportFraudReport



169
# File 'app/models/payment.rb', line 169

has_one :fraud_report

#full_card_numberObject



864
865
866
867
868
869
870
# File 'app/models/payment.rb', line 864

def full_card_number
  if issuer_number and last4
    "#{issuer_number}xxxxxx#{last4}"
  elsif last4
    "xxxxxxxxxxxx#{last4}"
  end
end

#full_nameObject



949
950
951
# File 'app/models/payment.rb', line 949

def full_name
  [first_name, last_name].compact.join(' ')
end

#full_name=(value) ⇒ Object



953
954
955
956
957
# File 'app/models/payment.rb', line 953

def full_name=(value)
  pnp = PersonNameParser.new(value)
  self.first_name = pnp.first
  self.last_name = pnp.last
end

#funds_fully_refunded?Boolean

Returns:

  • (Boolean)


850
851
852
# File 'app/models/payment.rb', line 850

def funds_fully_refunded?
  total_refunded == total_captured
end

#funds_partially_refunded?Boolean

Returns:

  • (Boolean)


854
855
856
# File 'app/models/payment.rb', line 854

def funds_partially_refunded?
  total_refunded.positive? and total_refunded < total_captured
end

#gateway_classObject



332
333
334
335
336
337
# File 'app/models/payment.rb', line 332

def gateway_class
  class_name = category.parameterize(separator: '_')
  # Map non-conventional class names
  class_name = { VPO: 'VerbalPurchaseOrder', PO: 'PurchaseOrder' }[class_name] || class_name
  "Payment::Gateways::#{class_name.classify}".safe_constantize || Payment::Gateways::Default
end

#invoiceInvoice

Returns:

See Also:



162
# File 'app/models/payment.rb', line 162

belongs_to :invoice, optional: true

#is_advanced_replacement?Boolean

Returns:

  • (Boolean)


748
749
750
# File 'app/models/payment.rb', line 748

def is_advanced_replacement?
  category == ADV_REPL
end

#is_amazon_pay?Boolean

Returns:

  • (Boolean)


760
761
762
# File 'app/models/payment.rb', line 760

def is_amazon_pay?
  category == AMAZON_PAY
end

#is_crm_legacy_vault?Boolean

Returns:

  • (Boolean)


1066
1067
1068
# File 'app/models/payment.rb', line 1066

def is_crm_legacy_vault?
  order&.order_reception_type == 'CRM' && (vault_id && credit_card_vault&.address_line1.blank?)
end

#is_plaid?Boolean

Returns:

  • (Boolean)


756
757
758
# File 'app/models/payment.rb', line 756

def is_plaid?
  category == PLAID
end

#is_po?Boolean

Returns:

  • (Boolean)


740
741
742
# File 'app/models/payment.rb', line 740

def is_po?
  category.in?([PO, VPO])
end

#is_receipt_skippable?Boolean

Returns:

  • (Boolean)


1062
1063
1064
# File 'app/models/payment.rb', line 1062

def is_receipt_skippable?
  authorization_type.in?(%w[paypal_invoice check]) and state == 'captured' and invoice.nil?
end

#is_rma_credit?Boolean

Returns:

  • (Boolean)


744
745
746
# File 'app/models/payment.rb', line 744

def is_rma_credit?
  category == RMA_CREDIT
end

#is_store_credit?Boolean

Returns:

  • (Boolean)


752
753
754
# File 'app/models/payment.rb', line 752

def is_store_credit?
  category == STORE_CREDIT
end

#is_www_apple_pay?Boolean

Returns:

  • (Boolean)


1039
1040
1041
# File 'app/models/payment.rb', line 1039

def is_www_apple_pay?
  order&.order_reception_type == 'Online' && transactions.any? { |t| t.params&.dig('source')&.dig('tokenization_method') == 'apple_pay' }
end

#last_authorization_messageObject



812
813
814
# File 'app/models/payment.rb', line 812

def last_authorization_message
  transactions&.first&.message
end

#legacy_orderOrder

Returns:

See Also:



161
# File 'app/models/payment.rb', line 161

belongs_to :legacy_order, class_name: 'Order', foreign_key: 'order_id', optional: true

#orderOrder

Returns:

See Also:



158
# File 'app/models/payment.rb', line 158

belongs_to :order, optional: true

#paypal_over_capture_headroomObject



415
416
417
418
419
420
421
422
423
# File 'app/models/payment.rb', line 415

def paypal_over_capture_headroom
  limit = paypal_over_capture_limit
  return BigDecimal('0') unless limit

  all_committed = Payment.where(authorization_code: authorization_code, authorization_type: 'paypal')
                         .where.not(state: %w[voided declined expired])
                         .sum(:amount)
  [limit - all_committed, BigDecimal('0')].max
end

#paypal_over_capture_limitObject



408
409
410
411
412
413
# File 'app/models/payment.rb', line 408

def paypal_over_capture_limit
  auth_total = paypal_shared_auth_total
  return nil unless auth_total&.positive?

  [auth_total * PAYPAL_OVER_CAPTURE_PERCENT, auth_total + PAYPAL_OVER_CAPTURE_MAX_INCREASE].min
end

#paypal_shared_auth_totalObject



402
403
404
405
406
# File 'app/models/payment.rb', line 402

def paypal_shared_auth_total
  BigDecimal(&.dig('shared_authorization_total').to_s)
rescue ArgumentError
  nil
end

#pending_release_authorization?Boolean

Returns:

  • (Boolean)


764
765
766
# File 'app/models/payment.rb', line 764

def pending_release_authorization?
  !payment_approved? && authorization_review[:required] == true
end

#po_uploadObject



816
817
818
# File 'app/models/payment.rb', line 816

def po_upload
  uploads.in_category('purchase_order').valid.first
end

#process_tx_results(tx) ⇒ Object



959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
# File 'app/models/payment.rb', line 959

def process_tx_results(tx)
  if vault_id.present? and vault = CreditCardVault.find_by(vault_id:)
    self.issuer_number = vault.issuer_number
    self.card_type = vault.card_type
    self.name = vault.name
    self.exp_month = vault.exp_month
    self.exp_year = vault.exp_year
    self.reference = self.last4 = vault.number
    self.address_line1_check = vault.address_line1_check
    self.address_zip_check = vault.address_zip_check
    self.cvc_check = vault.cvc_check
    self.card_identifier = vault.vault_id
    self.card_country = nil
    self.billing_address_line1 = vault.address_line1
    self.billing_address_line2 = vault.address_line2
    self.billing_address_city = vault.address_city
    self.billing_address_state = vault.address_state
    self.billing_address_zip = vault.zip_code
    self.billing_address_country = vault.address_country
  end
  if (shipping_atts = order&.ship_to_attributes).present?
    shipping = shipping_atts[:address]
    self.shipping_address_name = shipping_atts[:attention_name] unless shipping_atts[:attention_name].blank? || shipping.is_placeholder
    unless shipping_atts[:name].blank? || shipping.is_placeholder || shipping_atts[:attention_name] == shipping_atts[:name]
      self.shipping_address_name = shipping_atts[:name]
    end
    self.shipping_address_line1 = shipping.street1
    self.shipping_address_line2 = shipping.street2
    self.shipping_address_city = shipping.city
    self.shipping_address_state = shipping.state&.name
    self.shipping_address_zip = shipping.zip
    self.shipping_address_country = shipping.country&.iso
  end
  if tx_source = tx.params['source']
    self.card_type = tx_source['brand']
    self.name = tx_source['name']
    self.exp_month = tx_source['exp_month']
    self.exp_year = tx_source['exp_year']
    self.reference = "....#{tx_source['last4']}"
    self.last4 = tx_source['last4']
    self.address_line1_check = tx_source['address_line1_check']
    self.address_zip_check = tx_source['address_zip_check']
    self.cvc_check = tx_source['cvc_check']
    self.card_identifier = tx_source['id']
    self.card_country = tx_source['country']
    self.billing_address_line1 = tx_source['address_line1']
    self.billing_address_line2 = tx_source['address_line2']
    self.billing_address_city = tx_source['address_city']
    self.billing_address_state = tx_source['address_state']
    self.billing_address_zip = tx_source['address_zip']
    self.billing_address_country = tx_source['address_country']
  end
  # Removing this after disconnecting David's active merchant branch on July 2020
  # if tx.params["shipping"]
  #   self.shipping_address_name = tx.params["shipping"]["name"]
  #   if tx_shipping = tx.params["shipping"]["address"]
  #     self.shipping_address_line1 = tx_shipping["line1"]
  #     self.shipping_address_line2 = tx_shipping["line2"]
  #     self.shipping_address_city = tx_shipping["city"]
  #     self.shipping_address_state = tx_shipping["state"]
  #     self.shipping_address_zip = tx_shipping["postal_code"]
  #     self.shipping_address_country = tx_shipping["country"]
  #   end
  # end
  if tx_outcome = tx.params['outcome']
    self.radar_network_status = tx_outcome['network_status']
    self.radar_reason = tx_outcome['reason']
    self.radar_risk_level = tx_outcome['risk_level']
    self.radar_seller_message = tx_outcome['seller_message']
    self.radar_type = tx_outcome['type']
  end
  return unless livemode = tx.params['livemode']

  self.test = livemode == false
end

#receiptsActiveRecord::Relation<Receipt>

Returns:

  • (ActiveRecord::Relation<Receipt>)

See Also:



173
# File 'app/models/payment.rb', line 173

has_many :receipts

#refunded_on_stripe?Boolean

Returns:

  • (Boolean)


883
884
885
886
887
888
889
890
891
892
893
894
# File 'app/models/payment.rb', line 883

def refunded_on_stripe?
  return false if authorization_type != 'credit_card'

  obj = stripe_payment_object
  return false unless obj

  if Payment::Apis::Stripe.payment_intent?(authorization_code)
    obj.status == 'canceled'
  else
    obj.try(:refunded?) rescue false
  end
end

#resend_paypal_invoiceObject



690
691
692
693
694
695
696
697
# File 'app/models/payment.rb', line 690

def resend_paypal_invoice
  response = Payment::Apis::Paypal.remind_invoice(authorization_code)
  if response['_http_success']
    { success: true }
  else
    { success: false, message: 'Something went wrong with Paypal reminder.' }
  end
end

#rmaRma

Returns:

See Also:



159
# File 'app/models/payment.rb', line 159

belongs_to :rma, optional: true

#send_authorization_email_notificationObject



768
769
770
771
772
773
774
775
776
777
778
779
780
781
# File 'app/models/payment.rb', line 768

def send_authorization_email_notification
  if category == CREDIT_CARD and email.present? and send_authorization_email == true
    sender = order.customer.try(:primary_sales_rep)
    CommunicationBuilder.new(
      resource: self,
      sender_party: sender,
      sender: (sender.nil? ? INFO_EMAIL : nil),
      emails: email,
      bcc: (sender.nil? ? nil : sender.email)
    ).create
  else
    'Unable to send authorization email'
  end
end

#send_wire_info_emailObject



783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
# File 'app/models/payment.rb', line 783

def send_wire_info_email
  if category == WIRE and email.present?
    sender = order.customer.try(:primary_sales_rep)
    comm = CommunicationBuilder.new(
      resource: order,
      sender_party: sender,
      sender: (sender.nil? ? INFO_EMAIL : nil),
      emails: email,
      merge_options: {
        order_reference: order.cart_identifier,
        order_total: amount,
        country_instructions: (order.catalog.id == 1 ? 'usa' : 'ca')
      },
      bcc: (sender.nil? ? nil : sender.email),
      template: EmailTemplate.find_by(system_code: 'WIRE_TRANSFER_INFO')
    ).create
  else
    'Unable to send wire info email'
  end
end

#shared_paypal_auth?Boolean

Returns:

  • (Boolean)


393
394
395
396
397
# File 'app/models/payment.rb', line 393

def shared_paypal_auth?
  authorization_type == 'paypal' && authorization_code.present? &&
    Payment.where(authorization_code: authorization_code, authorization_type: 'paypal')
           .where.not(id: id).exists?
end

#shared_paypal_auth_siblingsObject



385
386
387
388
389
390
391
# File 'app/models/payment.rb', line 385

def shared_paypal_auth_siblings
  return Payment.none unless authorization_type == 'paypal' && authorization_code.present?

  Payment.where(authorization_code: authorization_code, authorization_type: 'paypal')
         .where.not(id: id)
         .where(state: 'authorized')
end

#shared_pi?Boolean

Returns:

  • (Boolean)


380
381
382
383
# File 'app/models/payment.rb', line 380

def shared_pi?
  stripe_payment_intent_id.present? &&
    Payment.where(stripe_payment_intent_id: stripe_payment_intent_id).where.not(id: id).exists?
end

#shared_pi_siblingsObject



365
366
367
368
369
370
371
# File 'app/models/payment.rb', line 365

def shared_pi_siblings
  return Payment.none unless stripe_payment_intent_id.present?

  Payment.where(stripe_payment_intent_id: stripe_payment_intent_id)
         .where.not(id: id)
         .where(state: %w[authorized])
end

#shipping_addressObject



1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
# File 'app/models/payment.rb', line 1135

def shipping_address
  Address.new(
    street1: shipping_address_line1,
    street2: shipping_address_line2,
    city: shipping_address_city,
    zip: shipping_address_zip,
    state_code: State.code_for_string(shipping_address_state),
    country_iso3: Country.iso3_for_string(shipping_address_country)
  )
end

#stripe_api_keyObject



1035
1036
1037
# File 'app/models/payment.rb', line 1035

def stripe_api_key
  Payment::Apis::Stripe.api_key(currency)
end

#stripe_payment_objectObject Also known as: stripe_charge



872
873
874
875
876
877
878
# File 'app/models/payment.rb', line 872

def stripe_payment_object
  return false if authorization_code.blank?

  Payment::Apis::Stripe.retrieve_payment_object(authorization_code, currency: currency)
rescue ::Stripe::StripeError
  false
end

#stripe_resolverObject



339
340
341
# File 'app/models/payment.rb', line 339

def stripe_resolver
  @stripe_resolver ||= Payment::StrategyResolver.new(self)
end

#supports_extended_authorization?Boolean

Returns:

  • (Boolean)


353
354
355
# File 'app/models/payment.rb', line 353

def supports_extended_authorization?
  stripe_resolver.supports_extended_authorization?
end

#supports_incremental_authorization?Boolean

Returns:

  • (Boolean)


349
350
351
# File 'app/models/payment.rb', line 349

def supports_incremental_authorization?
  stripe_resolver.supports_incremental_authorization?
end

#supports_multicapture?Boolean

Returns:

  • (Boolean)


343
344
345
346
347
# File 'app/models/payment.rb', line 343

def supports_multicapture?
  return true if authorization_type == 'paypal'

  stripe_resolver.supports_multicapture?
end

#sync_capture_before_from_paypal_expiry(auth_expiration) ⇒ Object



584
585
586
587
588
589
# File 'app/models/payment.rb', line 584

def sync_capture_before_from_paypal_expiry(auth_expiration)
  effective_capture_before = auth_expiration - PAYPAL_HONOR_PERIOD
  if capture_before.nil? || capture_before < effective_capture_before
    update_column(:capture_before, effective_capture_before)
  end
end

#sync_external_capture(pi) ⇒ Object



636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
# File 'app/models/payment.rb', line 636

def sync_external_capture(pi)
  pi_received_cents = pi.amount_received.to_i
  sibling_captured_cents = all_pi_siblings
    .where.not(state: %w[declined expired])
    .sum { |s| (s.total_captured * 100).to_i }

  unaccounted_cents = pi_received_cents - sibling_captured_cents - (total_captured * 100).to_i

  if unaccounted_cents.positive?
    capture_amount_cents = [unaccounted_cents, (amount * 100).to_i].min
    logger.info("Payment #{id}: PI finalized on Stripe, syncing external capture of #{capture_amount_cents} cents")
    transactions.create!(
      amount: capture_amount_cents,
      action: 'capture',
      success: true,
      reference: authorization_code,
      message: "THIS PAYMENT WAS CAPTURED EXTERNALLY ON STRIPE",
      params: pi.to_hash,
      test: false
    )
    payment_captured!
  else
    # PI finalized but no funds captured for this payment — remaining auth was released
    logger.info("Payment #{id}: PI finalized on Stripe with no capture for this payment, voiding")
    if total_captured.positive?
      # Had partial captures before — void remaining authorization
      payment_voided!
    else
      payment_voided!
    end
  end
end

#sync_paypal_authorization_expiry(auth_expiration) ⇒ Object



511
512
513
514
515
516
517
# File 'app/models/payment.rb', line 511

def sync_paypal_authorization_expiry(auth_expiration)
  return unless auth_expiration.present?

   =  || {}
  ['authorization_expiry'] = auth_expiration.iso8601
  update_column(:paypal_metadata, )
end

#sync_paypal_external_capture(auth_response) ⇒ Object



519
520
521
522
523
524
525
526
527
528
529
530
531
532
# File 'app/models/payment.rb', line 519

def sync_paypal_external_capture(auth_response)
  captured_amount_dollars = auth_response.dig("amount", "value").to_f
  transaction = OrderTransaction.new(
    amount: (captured_amount_dollars * 100).to_i,
    action: 'capture',
    success: true,
    reference: auth_response['id'] || authorization_code,
    message: "THIS PAYMENT WAS MANUALLY CAPTURED ON THE PAYMENT PLATFORM",
    params: auth_response.except('_http_status', '_http_success', '_raw_body'),
    test: false
  )
  transactions.push(transaction)
  payment_captured!
end

#to_sObject



804
805
806
# File 'app/models/payment.rb', line 804

def to_s
  "Payment #{id} (#{order&.reference_number})"
end

#total_authorizedObject



1043
1044
1045
1046
1047
1048
1049
1050
# File 'app/models/payment.rb', line 1043

def total_authorized
  latest_increment = transactions.select { |t| t.success && t.action == 'incremental_authorization' }.max_by(&:created_at)
  if latest_increment
    (latest_increment.amount.to_f / 100).round(2)
  else
    transactions.select { |t| t.success && t.action.in?(%w[authorization authorize]) }.sum { |t| t.amount.to_f / 100 }.to_f.round(2)
  end
end

#total_capturedObject



1052
1053
1054
1055
1056
# File 'app/models/payment.rb', line 1052

def total_captured
  transactions.select do |t|
    t.success and (t.action == 'capture' or t.action == 'purchase' or t.action == 'settle')
  end.sum { |t| t.amount.to_f / 100 }.to_f.round(2)
end

#total_refundedObject



1058
1059
1060
# File 'app/models/payment.rb', line 1058

def total_refunded
  transactions.select { |t| t.success and t.action == 'refund' }.sum { |t| t.amount.to_f / 100 }.to_f.round(2)
end

#transaction_reference(action) ⇒ Object



839
840
841
842
843
# File 'app/models/payment.rb', line 839

def transaction_reference(action)
  return unless payment = transactions.where(action:).where(success: true).order(:id).first

  payment.reference
end

#transactionsActiveRecord::Relation<OrderTransaction>

Returns:

See Also:



171
# File 'app/models/payment.rb', line 171

has_many :transactions, class_name: 'OrderTransaction', dependent: :destroy

#update_authorization_code(action) ⇒ Object



1119
1120
1121
1122
# File 'app/models/payment.rb', line 1119

def update_authorization_code(action)
  update(authorization_code: transaction_reference(action))
  payment.update(amount:)
end

#uploadsActiveRecord::Relation<Upload>

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



172
# File 'app/models/payment.rb', line 172

has_many :uploads, as: :resource, dependent: :destroy

#vpo_contactContact

Returns:

See Also:



167
# File 'app/models/payment.rb', line 167

belongs_to :vpo_contact, class_name: 'Contact', optional: true