-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathpayment.rb
284 lines (238 loc) · 9.88 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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# 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
alias_attribute :identifier, :number
deprecate :identifier, :identifier=, deprecator: Spree::Deprecation
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
belongs_to :source, polymorphic: true
belongs_to :payment_method, -> { with_deleted }, class_name: 'Spree::PaymentMethod', inverse_of: :payments
has_many :offsets, -> { offset_payment }, class_name: "Spree::Payment", foreign_key: :source_id
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, ->(s) { where(state: s.to_s) }
# "offset" is reserved by activerecord
scope :offset_payment, -> { where("source_type = 'Spree::Payment' AND amount < 0 AND state = 'completed'") }
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, -> { where("avs_response IN (?) OR (cvv_response_code IS NOT NULL and cvv_response_code != 'M') OR state = 'failed'", RISKY_AVS_CODES) }
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))) }
# order state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
state_machine initial: :checkout do
# With card payments, happens before purchase or authorization happens
#
# Setting it after creating a profile and authorizing a full amount will
# prevent the payment from being authorized again once Order transitions
# to complete
event :started_processing do
transition from: [:checkout, :pending, :completed, :processing], to: :processing
end
# When processing during checkout fails
event :failure do
transition from: [:pending, :processing], to: :failed
end
# With card payments this represents authorizing the payment
event :pend do
transition from: [:checkout, :processing], to: :pending
end
# With card payments this represents completing a purchase or capture transaction
event :complete do
transition from: [:processing, :pending, :checkout], to: :completed
end
event :void do
transition from: [:pending, :processing, :completed, :checkout], to: :void
end
# when the card brand isnt supported
event :invalidate do
transition from: [:checkout], to: :invalid
end
after_transition do |payment, transition|
payment.state_changes.create!(
previous_state: transition.from,
next_state: transition.to,
name: 'payment'
)
end
end
# @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: 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 of the offsets (for old-style refunds) for this payment.
#
# @return [BigDecimal] the total amount of this payment's offsets
def offsets_total
offsets.pluck(:amount).sum
end
# The total amount this payment can be credited.
#
# @return [BigDecimal] the amount of this payment minus the offsets
# (old-style refunds) and refunds
def credit_allowed
amount - (offsets_total.abs + 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 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?
return false if cvv_response_message.present?
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 |field, error|
field_name = I18n.t("activerecord.attributes.#{source.class.to_s.underscore}.#{field}")
errors.add(I18n.t(source.class.to_s.demodulize.underscore, scope: 'spree'), "#{field_name} #{error}")
end
end
if errors.any?
throw :abort
end
end
def source_required?
payment_method.present? && payment_method.source_required?
end
def profiles_supported?
payment_method.respond_to?(:payment_profiles_supported?) && payment_method.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 => e
gateway_error e
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: 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_attributes!({
action: Spree::StoreCredit::ELIGIBLE_ACTION,
action_amount: amount,
action_authorization_code: response_code
})
end
end
end
end