Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring in payment model #5678

Closed
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
06aa561
Bring in Payment model from Spree
sauloperez Jun 26, 2020
abacd06
Fix credit card instance in specs
sauloperez Jun 26, 2020
e1ea5db
Fix all but the 7 last payment specs
sauloperez Jun 26, 2020
34de219
Bring in missing translation
sauloperez Jun 26, 2020
a01f601
Fix yet another spec
sauloperez Jun 26, 2020
0ad8dcc
Fix payment log entries specs
sauloperez Jun 26, 2020
9935df9
Move Pin payment method from decorator into model
sauloperez Jun 26, 2020
31d0d4b
Fix error "no parent is saved"
sauloperez Jun 26, 2020
eafaa97
Temporarily skip spec
sauloperez Jun 26, 2020
322c4d0
Move decorator's callbacks to model
sauloperez Jun 26, 2020
6d9a518
Move method from decorator to model
sauloperez Jun 26, 2020
48910ae
Move #refund! to the processing.rb
sauloperez Jun 26, 2020
8617262
Move localize_number from decorator to model
sauloperez Jun 26, 2020
3fb6193
Move adjustments logic from decorator into model
sauloperez Jun 26, 2020
cf6138d
Replace model method with its decorated version
sauloperez Jun 26, 2020
d49068c
Move method delegation from decorator to model
sauloperez Jun 26, 2020
d8b748a
Merge alias_method method and its original version
sauloperez Jun 26, 2020
8fbbb0b
Bring back our card factory modification
sauloperez Jul 10, 2020
562f397
Isolate Spree's specs into their own context
sauloperez Jul 10, 2020
2f46483
Merge decorator specs with Spree's ones
sauloperez Jul 10, 2020
6837946
Rename spec file
sauloperez Jul 10, 2020
55d52b8
Run rubocop autocorrect on payment model
sauloperez Jul 10, 2020
cf64d3a
Merge skipped callback from decorator into model
sauloperez Jul 10, 2020
3435d5a
Fix Rubocop non-metrics issues in payment model
sauloperez Jul 10, 2020
66dbd85
Run rubocop autocorrect on payment/processing.rb
sauloperez Jul 10, 2020
42658b5
Refactor `#process!` nested ifs to guard clauses
sauloperez Jul 13, 2020
a8af3a2
Fix all but Metrics Rubocop cops in processing.rb
sauloperez Jul 13, 2020
3a64cc4
Reuse #calculate_refund_amount method
sauloperez Jul 13, 2020
70afcee
Fix Spree's spec clashing with a customization
sauloperez Jul 15, 2020
dd5e679
Address code review comments
sauloperez Jul 16, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions app/models/spree/payment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
module Spree
class Payment < ActiveRecord::Base
include Spree::Payment::Processing

IDENTIFIER_CHARS = (('A'..'Z').to_a + ('0'..'9').to_a - %w(0 1 I O)).freeze

belongs_to :order, class_name: 'Spree::Order'
belongs_to :source, polymorphic: true
belongs_to :payment_method, class_name: 'Spree::PaymentMethod'

has_many :offsets, -> { where("source_type = 'Spree::Payment' AND amount < 0 AND state = 'completed'") },
class_name: "Spree::Payment", foreign_key: :source_id
has_many :log_entries, as: :source

before_validation :validate_source
before_save :set_unique_identifier

after_save :create_payment_profile, if: :profiles_supported?

# update the order totals, etc.
after_save :ensure_correct_adjustment, :update_order
# invalidate previously entered payments
after_create :invalidate_old_payments

attr_accessor :source_attributes
after_initialize :build_source

scope :from_credit_card, -> { where(source_type: 'Spree::CreditCard') }
scope :with_state, ->(s) { where(state: s.to_s) }
scope :completed, -> { with_state('completed') }
scope :pending, -> { with_state('pending') }
scope :failed, -> { with_state('failed') }
scope :valid, -> { where('state NOT IN (?)', %w(failed invalid)) }

after_rollback :persist_invalid

def persist_invalid
return unless ['failed', 'invalid'].include?(state)
state_will_change!
save
end

# 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
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, :completed, :checkout], to: :void
end
# when the card brand isnt supported
event :invalidate do
transition from: [:checkout], to: :invalid
end
end

def currency
order.currency
end

def money
Spree::Money.new(amount, { currency: currency })
end
alias display_amount money

def offsets_total
offsets.pluck(:amount).sum
end

def credit_allowed
amount - offsets_total
end

def can_credit?
credit_allowed > 0
end

# see https://github.com/spree/spree/issues/981
#
# Import from future Spree v.2.3.0 d470b31798f37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just remove these 2 comments. It's just history, I dont see them as useful for the future dev.

def build_source
return if source_attributes.nil?
return unless payment_method.andand.payment_source_class

self.source = payment_method.payment_source_class.new(source_attributes)
source.payment_method_id = payment_method.id
source.user_id = order.user_id if order
end

def actions
return [] unless payment_source and payment_source.respond_to? :actions
payment_source.actions.select { |action| !payment_source.respond_to?("can_#{action}?") or payment_source.send("can_#{action}?", self) }
end

# Pin payments lacks void and credit methods, but it does have refund
# Here we swap credit out for refund and remove void as a possible action
def actions_with_pin_payment_adaptations
actions = actions_without_pin_payment_adaptations
if payment_method.is_a? Gateway::Pin
actions << 'refund' if actions.include? 'credit'
actions.reject! { |a| ['credit', 'void'].include? a }
end
actions
end
alias_method_chain :actions, :pin_payment_adaptations

def payment_source
res = source.is_a?(Payment) ? source.source : source
res || payment_method
end

private

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}")
self.errors.add(Spree.t(source.class.to_s.demodulize.underscore), "#{field_name} #{error}")
end
end
return !errors.present?
end

def profiles_supported?
payment_method.respond_to?(:payment_profiles_supported?) && payment_method.payment_profiles_supported?
end

def create_payment_profile
return unless source.is_a?(CreditCard) && source.number && !source.has_payment_profile?
payment_method.create_profile(self)
rescue ActiveMerchant::ConnectionError => e
gateway_error e
end

def invalidate_old_payments
order.payments.with_state('checkout').where("id != ?", self.id).each do |payment|
payment.invalidate!
end
end

def update_order
order.payments.reload
order.update!
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 #1998.
# See https://github.com/spree/spree/issues/1998#issuecomment-12869105
def set_unique_identifier
begin
self.identifier = generate_identifier
end while self.class.exists?(identifier: self.identifier)
end

def generate_identifier
Array.new(8){ IDENTIFIER_CHARS.sample }.join
end
end
end
210 changes: 210 additions & 0 deletions app/models/spree/payment/processing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
module Spree
class Payment < ActiveRecord::Base
module Processing
def process!
if payment_method && payment_method.source_required?
if source
if !processing?
if payment_method.supports?(source)
if payment_method.auto_capture?
purchase!
else
authorize!
end
else
invalidate!
raise Core::GatewayError.new(Spree.t(:payment_method_not_supported))
end
end
else
raise Core::GatewayError.new(Spree.t(:payment_processing_failed))
end
end
sauloperez marked this conversation as resolved.
Show resolved Hide resolved
end

def authorize!
started_processing!
gateway_action(source, :authorize, :pend)
end

def purchase!
started_processing!
gateway_action(source, :purchase, :complete)
end

def capture!
return true if completed?
started_processing!
sauloperez marked this conversation as resolved.
Show resolved Hide resolved
protect_from_connection_error do
check_environment

if payment_method.payment_profiles_supported?
# Gateways supporting payment profiles will need access to credit card object because this stores the payment profile information
# so supply the authorization itself as well as the credit card, rather than just the authorization code
response = payment_method.capture(self, source, gateway_options)
else
# Standard ActiveMerchant capture usage
response = payment_method.capture(money.money.cents,
response_code,
gateway_options)
end

handle_response(response, :complete, :failure)
end
end

def void_transaction!
return true if void?
protect_from_connection_error do
check_environment

if payment_method.payment_profiles_supported?
# Gateways supporting payment profiles will need access to credit card object because this stores the payment profile information
# so supply the authorization itself as well as the credit card, rather than just the authorization code
response = payment_method.void(self.response_code, source, gateway_options)
else
# Standard ActiveMerchant void usage
response = payment_method.void(self.response_code, gateway_options)
end
record_response(response)

if response.success?
self.response_code = response.authorization
self.void
else
gateway_error(response)
end
end
end

def credit!(credit_amount=nil)
protect_from_connection_error do
check_environment

credit_amount ||= credit_allowed >= order.outstanding_balance.abs ? order.outstanding_balance.abs : credit_allowed.abs
credit_amount = credit_amount.to_f

if payment_method.payment_profiles_supported?
response = payment_method.credit((credit_amount * 100).round, source, response_code, gateway_options)
else
response = payment_method.credit((credit_amount * 100).round, response_code, gateway_options)
end

record_response(response)

if response.success?
self.class.create(
:order => order,
:source => self,
:payment_method => payment_method,
:amount => credit_amount.abs * -1,
:response_code => response.authorization,
:state => 'completed'
)
else
gateway_error(response)
end
end
end

def partial_credit(amount)
return if amount > credit_allowed
started_processing!
credit!(amount)
end

def gateway_options
options = { :email => order.email,
:customer => order.email,
:ip => order.last_ip_address,
# Need to pass in a unique identifier here to make some
# payment gateways happy.
#
# For more information, please see Spree::Payment#set_unique_identifier
:order_id => gateway_order_id }

options.merge!({ :shipping => order.ship_total * 100,
:tax => order.tax_total * 100,
:subtotal => order.item_total * 100,
:discount => order.promo_total * 100,
:currency => currency })

options.merge!({ :billing_address => order.bill_address.try(:active_merchant_hash),
:shipping_address => order.ship_address.try(:active_merchant_hash) })

options
end

private

def gateway_action(source, action, success_state)
protect_from_connection_error do
check_environment

response = payment_method.send(action, (amount * 100).round,
source,
gateway_options)
handle_response(response, success_state, :failure)
end
end

def handle_response(response, success_state, failure_state)
record_response(response)

if response.success?
unless response.authorization.nil?
self.response_code = response.authorization
self.avs_response = response.avs_result['code']

if response.cvv_result
self.cvv_response_code = response.cvv_result['code']
self.cvv_response_message = response.cvv_result['message']
end
end
self.send("#{success_state}!")
else
self.send(failure_state)
gateway_error(response)
end
end

def record_response(response)
log_entries.create(:details => response.to_yaml)
end

def protect_from_connection_error
begin
yield
rescue ActiveMerchant::ConnectionError => e
gateway_error(e)
end
end

def gateway_error(error)
if error.is_a? ActiveMerchant::Billing::Response
text = error.params['message'] || error.params['response_reason_text'] || error.message
elsif error.is_a? ActiveMerchant::ConnectionError
text = Spree.t(:unable_to_connect_to_gateway)
else
text = error.to_s
end
logger.error(Spree.t(:gateway_error))
logger.error(" #{error.to_yaml}")
raise Core::GatewayError.new(text)
end

# Saftey check to make sure we're not accidentally performing operations on a live gateway.
# Ex. When testing in staging environment with a copy of production data.
def check_environment
return if payment_method.environment == Rails.env
message = Spree.t(:gateway_config_unavailable) + " - #{Rails.env}"
raise Core::GatewayError.new(message)
end

# The unique identifier to be passed in to the payment gateway
def gateway_order_id
"#{order.number}-#{self.identifier}"
end
end
end
end
Loading