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

Remove state machine gem from Spree::Payment (updated PR #2664) #3039

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
196 changes: 151 additions & 45 deletions core/app/models/spree/payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,20 @@ module Spree
class Payment < Spree::Base
include Spree::Payment::Processing

class InvalidStateChange < StandardError; end

alias_attribute :identifier, :number
deprecate :identifier, :identifier=, deprecator: Spree::Deprecation

PENDING = 'pending'
PROCESSING = 'processing'
CHECKOUT = 'checkout'
COMPLETED = 'completed'
INVALID = 'invalid'
VOID = 'void'
FAILED = 'failed'
DEFAULT_STATES = [PENDING, PROCESSING, CHECKOUT, COMPLETED, INVALID, VOID, FAILED]

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
Expand Down Expand Up @@ -43,6 +54,7 @@ class Payment < Spree::Base
validates :amount, numericality: true
validates :source, presence: true, if: :source_required?
validates :payment_method, presence: true
validate :is_valid_state?

default_scope -> { order(:created_at) }

Expand All @@ -51,55 +63,128 @@ class Payment < Spree::Base
# "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 :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)) }
scope :valid, -> { where.not(state: [FAILED, INVALID]) }

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
def started_processing!
started_processing || raise(InvalidStateChange)
end

after_transition do |payment, transition|
payment.state_changes.create!(
previous_state: transition.from,
next_state: transition.to,
name: 'payment'
)
end
def started_processing
return false unless can_started_processing?
change_state!(PROCESSING)
true
end

def processing?
state == PROCESSING
end

def can_started_processing?
checkout? || pending? || completed? || processing?
end

def failure!
failure || raise(InvalidStateChange)
end

def failure
return false unless can_failure?
change_state!(FAILED)
true
end

def failed?
state == FAILED
end

def can_failure?
pending? || processing?
end

def pend!
pend || raise(InvalidStateChange)
end

def pend
return false unless can_pend?
change_state!(PENDING)
true
end

def pending?
state == PENDING
end

def can_pend?
checkout? || processing?
end

def complete!
complete || raise(InvalidStateChange)
end

def complete
return false unless can_complete?
change_state!(COMPLETED)
true
end

def completed?
state == COMPLETED
end

def can_complete?
processing? || pending? || checkout?
end

def void!
void || raise(InvalidStateChange)
end

def void
return false unless can_void?
change_state!(VOID)
true
end

def void?
state == VOID
end

def can_void?
pending? || processing? || completed? || checkout?
end

def invalidate!
invalidate || raise(InvalidStateChange)
end

def invalidate
return false unless can_invalidate?
change_state!(INVALID)
true
end

def invalid?
state == INVALID
end

def can_invalidate?
checkout?
end

def checkout?
state == CHECKOUT
end

# @return [String] this payment's response code
Expand Down Expand Up @@ -225,7 +310,7 @@ def profiles_supported?

def create_payment_profile
# Don't attempt to create on bad payments.
return if %w(invalid failed).include?(state)
return if [INVALID, FAILED].include?(state)
# Payment profile cannot be created without source
return unless source
# Imported payments shouldn't create a payment profile.
Expand All @@ -237,9 +322,9 @@ def create_payment_profile
end

def invalidate_old_payments
if !store_credit? && !['invalid', 'failed'].include?(state)
if !store_credit? && ![INVALID, FAILED].include?(state)
order.payments.select { |payment|
payment.state == 'checkout' && !payment.store_credit? && payment.id != id
payment.state == CHECKOUT && !payment.store_credit? && payment.id != id
}.each(&:invalidate!)
end
end
Expand Down Expand Up @@ -280,5 +365,26 @@ def create_eligible_credit_event
})
end
end

def store_state_change(previous_state, new_state)
state_changes.create!(
previous_state: previous_state,
next_state: new_state,
name: 'payment'
)
end

def change_state!(new_state)
previous_state = state
return if new_state == previous_state
update!(state: new_state)
store_state_change(previous_state, new_state)
end

def is_valid_state?
unless DEFAULT_STATES.include?(state)
errors.add(:state, "Invalid state")
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddDefaultStateToPayment < ActiveRecord::Migration[5.1]
def change
change_column_default(:spree_payments, :state, 'checkout')
end
end
Loading