-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathpayment.rb
238 lines (194 loc) · 7.85 KB
/
payment.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# frozen_string_literal: true
module Spree
# Manage and process a payment for an order, from a specific
# source (e.g. `Spree::CreditCard`) using a specific payment method (e.g
# `Solidus::Gateway::Braintree`).
#
class Payment < Spree::Base
include Spree::Payment::Processing
include Metadata
IDENTIFIER_CHARS = (('A'..'Z').to_a + ('0'..'9').to_a - %w(0 1 I O)).freeze
NON_RISKY_AVS_CODES = ['B', 'D', 'H', 'J', 'M', 'Q', 'T', 'V', 'X', 'Y'].freeze
RISKY_AVS_CODES = ['A', 'C', 'E', 'F', 'G', 'I', 'K', 'L', 'N', 'O', 'P', 'R', 'S', 'U', 'W', 'Z'].freeze
belongs_to :order, class_name: 'Spree::Order', touch: true, inverse_of: :payments, optional: true
belongs_to :source, polymorphic: true, optional: true
belongs_to :payment_method, -> { with_discarded }, class_name: 'Spree::PaymentMethod', inverse_of: :payments, optional: true
has_many :log_entries, as: :source
has_many :state_changes, as: :stateful
has_many :capture_events, class_name: 'Spree::PaymentCaptureEvent'
has_many :refunds, inverse_of: :payment
before_validation :validate_source, unless: :invalid?
before_create :set_unique_identifier
after_save :create_payment_profile, if: :profiles_supported?
# update the order totals, etc.
after_save :update_order
after_create :create_eligible_credit_event
# invalidate previously entered payments
after_create :invalidate_old_payments
attr_accessor :request_env
validates :amount, numericality: true
validates :source, presence: true, if: :source_required?
validates :payment_method, presence: true
default_scope -> { order(:created_at) }
scope :from_credit_card, -> { where(source_type: 'Spree::CreditCard') }
scope :with_state, ->(state) { where(state: state.to_s) }
scope :checkout, -> { with_state('checkout') }
scope :completed, -> { with_state('completed') }
scope :pending, -> { with_state('pending') }
scope :processing, -> { with_state('processing') }
scope :failed, -> { with_state('failed') }
scope :risky, -> { failed.or(where(avs_response: RISKY_AVS_CODES)).or(where.not(cvv_response_code: [nil, '', 'M'])) }
scope :valid, -> { where.not(state: %w(failed invalid void)) }
scope :store_credits, -> { where(source_type: Spree::StoreCredit.to_s) }
scope :not_store_credits, -> { where(arel_table[:source_type].not_eq(Spree::StoreCredit.to_s).or(arel_table[:source_type].eq(nil))) }
include ::Spree::Config.state_machines.payment
# @return [String] this payment's response code
def transaction_id
response_code
end
# @return [String] this payment's currency
delegate :currency, to: :order
# @return [Spree::Money] this amount of this payment as money object
def money
Spree::Money.new(amount, { currency: })
end
alias display_amount money
# Sets the amount, parsing it based on i18n settings if it is a string.
#
# @param amount [BigDecimal, String] the desired new amount
def amount=(amount)
self[:amount] =
case amount
when String
separator = I18n.t('number.currency.format.separator')
number = amount.delete("^0-9-#{separator}\.").tr(separator, '.')
number.to_d if number.present?
end || amount
end
# The total amount this payment can be credited.
#
# @return [BigDecimal] the amount of this payment minus refunds
def credit_allowed
amount - refunds.sum(:amount)
end
# @return [Boolean] true when this payment can be credited
def can_credit?
credit_allowed > 0
end
# @return [Boolean] true when this payment has been fully refunded
def fully_refunded?
refunds.map(&:amount).sum == amount
end
# @return [Array<String>] the actions available on this payment
def actions
sa = source_actions
sa |= ["failure"] if processing?
sa
end
# @return [Object] the source of ths payment
def payment_source
res = source.is_a?(Payment) ? source.source : source
res || payment_method
end
# @return [Boolean] true when this payment is risky
def risky?
is_avs_risky? || is_cvv_risky? || state == 'failed'
end
# @return [Boolean] true when this payment is risky based on address
def is_avs_risky?
return false if avs_response.blank? || NON_RISKY_AVS_CODES.include?(avs_response)
true
end
# @return [Boolean] true when this payment is risky based on cvv
def is_cvv_risky?
return false if cvv_response_code == "M"
return false if cvv_response_code.nil?
true
end
# @return [BigDecimal] the total amount captured on this payment
def captured_amount
capture_events.sum(:amount)
end
# @return [BigDecimal] the total amount left uncaptured on this payment
def uncaptured_amount
amount - captured_amount
end
# @return [Boolean] true when the payment method exists and is a store credit payment method
def store_credit?
payment_method.try!(:store_credit?)
end
private
def source_actions
return [] unless payment_source && payment_source.respond_to?(:actions)
payment_source.actions.select { |action| !payment_source.respond_to?("can_#{action}?") || payment_source.send("can_#{action}?", self) }
end
def validate_source
if source && !source.valid?
source.errors.each do |error|
field_name = I18n.t("activerecord.attributes.#{source.class.to_s.underscore}.#{error.attribute}")
errors.add(I18n.t(source.class.to_s.demodulize.underscore, scope: 'spree'), "#{field_name} #{error.message}")
end
end
if errors.any?
throw :abort
end
end
def source_required?
!!payment_method&.source_required?
end
def profiles_supported?
!!payment_method.try(:payment_profiles_supported?)
end
def create_payment_profile
# Don't attempt to create on bad payments.
return if %w(invalid failed).include?(state)
# Payment profile cannot be created without source
return unless source
# Imported payments shouldn't create a payment profile.
return if source.imported
payment_method.create_profile(self)
rescue ActiveMerchant::ConnectionError => error
gateway_error error
end
def invalidate_old_payments
if !store_credit? && !['invalid', 'failed'].include?(state)
order.payments.select { |payment|
payment.state == 'checkout' && !payment.store_credit? && payment.id != id
}.each(&:invalidate!)
end
end
def update_order
if order.completed? || completed? || void?
order.recalculate
end
end
# Necessary because some payment gateways will refuse payments with
# duplicate IDs. We *were* using the Order number, but that's set once and
# is unchanging. What we need is a unique identifier on a per-payment basis,
# and this is it. Related to https://github.com/spree/spree/issues/1998.
# See https://github.com/spree/spree/issues/1998#issuecomment-12869105
def set_unique_identifier
loop do
self.number = generate_identifier
break unless self.class.exists?(number:)
end
end
def generate_identifier
Array.new(8){ IDENTIFIER_CHARS.sample }.join
end
def create_eligible_credit_event
# When cancelling an order, a payment with the negative amount
# of the payment total is created to refund the customer. That
# payment has a source of itself (Spree::Payment) no matter the
# type of payment getting refunded, hence the additional check
# if the source is a store credit.
if store_credit? && source.is_a?(Spree::StoreCredit)
source.update!({
action: Spree::StoreCredit::ELIGIBLE_ACTION,
action_amount: amount,
action_authorization_code: response_code
})
end
end
end
end