Code Review
eCommerce checkout
July 25, 2017module BrandCruz
class Order < BrandCruz::Base
module Checkout
def self.included(klass)
klass.class_eval do
class_attribute :next_event_transitions
class_attribute :previous_states
class_attribute :checkout_flow
class_attribute :checkout_steps
class_attribute :removed_transitions
self.checkout_steps ||= {}
self.next_event_transitions ||= []
self.previous_states ||= [:cart]
self.removed_transitions ||= []
def self.checkout_flow(&block)
if block_given?
@checkout_flow = block
define_state_machine!
else
@checkout_flow
end
end
def self.define_state_machine!
self.checkout_steps = {}
self.next_event_transitions = []
self.previous_states = [:cart]
self.removed_transitions = []
# Build the checkout flow using the checkout_flow defined either
# within the Order class, or a decorator for that class.
#
# This method may be called multiple times depending on if the
# checkout_flow is re-defined in a decorator or not.
instance_eval(&checkout_flow)
klass = self
# To avoid multiple occurrences of the same transition being defined
# On first definition, state_machines will not be defined
state_machines.clear if respond_to?(:state_machines)
state_machine :state, initial: :cart, use_transactions: false, action: :save_state do
klass.next_event_transitions.each { |t| transition(t.merge(on: :next)) }
# Persist the state on the order
after_transition do |order, transition|
order.state = order.state
order.state_changes.create(
previous_state: transition.from,
next_state: transition.to,
name: 'order',
user_id: order.user_id
)
order.save
end
event :cancel do
transition to: :canceled, if: :allow_cancel?
end
event :return do
transition to: :returned,
from: [:complete, :awaiting_return, :canceled],
if: :all_inventory_units_returned?
end
event :resume do
transition to: :resumed, from: :canceled, if: :canceled?
end
event :authorize_return do
transition to: :awaiting_return
end
before_transition to: :complete, do: :ensure_line_item_variants_are_not_discontinued
before_transition to: :complete, do: :ensure_line_items_are_in_stock
if states[:payment]
before_transition to: :complete do |order|
# if order.payment_required? && order.payments.valid.empty?
# order.errors.add(:base, BrandCruz.t(:no_payment_found))
# false
# elsif order.payment_required?
# order.process_payments!
# end
unless order.user.credit_cards.present?
order.errors.add(:base, BrandCruz.t(:no_payment_found))
false
end
end
after_transition to: :complete, do: :persist_user_credit_card
before_transition to: :payment, do: :set_shipments_cost
before_transition to: :payment, do: :create_tax_charge!
end
before_transition from: :cart, do: :ensure_line_items_present
if states[:address]
before_transition from: :address, do: :update_line_item_prices!
before_transition from: :address, do: :create_tax_charge!
before_transition to: :address, do: :assign_default_addresses!
before_transition from: :address, do: :persist_user_address!
end
if states[:delivery]
before_transition to: :delivery, do: :create_proposed_shipments
before_transition to: :delivery, do: :ensure_available_shipping_rates
before_transition to: :delivery, do: :set_shipments_cost
before_transition from: :delivery, do: :apply_free_shipping_promotions
end
before_transition to: :resumed, do: :ensure_line_item_variants_are_not_discontinued
before_transition to: :resumed, do: :ensure_line_items_are_in_stock
after_transition to: :complete, do: :finalize!
after_transition to: :resumed, do: :after_resume
after_transition to: :canceled, do: :after_cancel
after_transition from: any - :cart, to: any - [:confirm, :complete] do |order|
order.update_totals
order.persist_totals
end
end
alias_method :save_state, :save
end
def self.go_to_state(name, options = {})
self.checkout_steps[name] = options
previous_states.each do |state|
add_transition({from: state, to: name}.merge(options))
end
if options[:if]
previous_states << name
else
self.previous_states = [name]
end
end
def self.insert_checkout_step(name, options = {})
before = options.delete(:before)
after = options.delete(:after) unless before
after = self.checkout_steps.keys.last unless before || after
cloned_steps = self.checkout_steps.clone
cloned_removed_transitions = self.removed_transitions.clone
checkout_flow do
cloned_steps.each_pair do |key, value|
go_to_state(name, options) if key == before
go_to_state(key, value)
go_to_state(name, options) if key == after
end
cloned_removed_transitions.each do |transition|
remove_transition(transition)
end
end
end
def self.remove_checkout_step(name)
cloned_steps = self.checkout_steps.clone
cloned_removed_transitions = self.removed_transitions.clone
checkout_flow do
cloned_steps.each_pair do |key, value|
go_to_state(key, value) unless key == name
end
cloned_removed_transitions.each do |transition|
remove_transition(transition)
end
end
end
def self.remove_transition(options = {})
self.removed_transitions << options
self.next_event_transitions.delete(find_transition(options))
end
def self.find_transition(options = {})
return nil if options.nil? || !options.include?(:from) || !options.include?(:to)
self.next_event_transitions.detect do |transition|
transition[options[:from].to_sym] == options[:to].to_sym
end
end
def self.checkout_step_names
self.checkout_steps.keys
end
def self.add_transition(options)
self.next_event_transitions << {options.delete(:from) => options.delete(:to)}.merge(options)
end
def checkout_steps
steps = (self.class.checkout_steps.each_with_object([]) do |(step, options), checkout_steps|
next if options.include?(:if) && !options[:if].call(self)
checkout_steps << step
end).map(&:to_s)
# Ensure there is always a complete step
steps << "complete" unless steps.include?("complete")
steps
end
def has_checkout_step?(step)
step.present? && self.checkout_steps.include?(step)
end
def passed_checkout_step?(step)
has_checkout_step?(step) && checkout_step_index(step) < checkout_step_index(state)
end
def checkout_step_index(step)
self.checkout_steps.index(step).to_i
end
def can_go_to_state?(state)
return false unless has_checkout_step?(self.state) && has_checkout_step?(state)
checkout_step_index(state) > checkout_step_index(self.state)
end
define_callbacks :updating_from_params, terminator: ->(_target, result) { result == false }
set_callback :updating_from_params, :before, :update_params_payment_source
def update_from_params(params, permitted_params, request_env = {})
success = false
@updating_params = params
run_callbacks :updating_from_params do
# Set existing card after setting permitted parameters because
# rails would slice parameters containg ruby objects, apparently
existing_card_id = @updating_params[:order] ? @updating_params[:order].delete(:existing_card) : nil
attributes = @updating_params[:order] ? @updating_params[:order].permit(permitted_params).delete_if { |_k, v| v.nil? } : {}
if existing_card_id.present?
credit_card = CreditCard.find existing_card_id
if credit_card.user_id != user_id || credit_card.user_id.blank?
raise Core::GatewayError.new BrandCruz.t(:invalid_credit_card)
end
credit_card.verification_value = params[:cvc_confirm] if params[:cvc_confirm].present?
attributes[:payments_attributes].first[:source] = credit_card
attributes[:payments_attributes].first[:payment_method_id] = credit_card.payment_method_id
attributes[:payments_attributes].first.delete :source_attributes
end
if attributes[:payments_attributes]
#attributes[:payments_attributes].first[:request_env] = request_env
payment_attributes = attributes.delete(:payments_attributes)
# payment_attributes = attributes[:payments_attributes]
# attributes[:payments_attributes] = {
# 0 => {
# amount: self.total,
# payment_method_id: BrandCruz::PaymentMethod.first.id,
# source_attributes: {
# name: payment_attributes[:name],
# number: payment_attributes[:number],
# expiry: payment_attributes[:expire],
# verification_value: payment_attributes[:cvv_response_code]
# }
# }
# }
end
success = update_attributes(attributes)
if success && payment_attributes.present?
store_user_card(payment_attributes)
end
# set_shipments_cost if shipments.any?
end
@updating_params = nil
success
end
def assign_default_addresses!
if user
#clone_billing
# Skip setting ship address if order doesn't have a delivery checkout step
# to avoid triggering validations on shipping address
clone_shipping #if checkout_steps.include?("delivery")
end
end
def clone_billing
if !bill_address_id && user.bill_address.try(:valid?)
self.bill_address = user.bill_address.try(:clone)
end
end
def clone_shipping
if !ship_address_id && user.ship_address.try(:valid?)
self.ship_address = user.ship_address.try(:clone)
end
end
def store_user_card(payment_attributes)
expire = payment_attributes.delete(:expire).split('/')
secret_key = SecureRandom.base64(32)
card_params = {
user_id: self.user_id,
month: expire.first.to_i,
year: expire.last.to_i,
last_digits: last_4digits(payment_attributes[:number]),
cc_type: payment_attributes[:cvv_response_code],
name: payment_attributes[:name],
order_id: self.id,
secret_key: secret_key,
card_number: encrypt_card_number(payment_attributes[:number], secret_key)
}
BrandCruz::CreditCard.create!(card_params)
end
def last_4digits(card_number)
card_number.split(//).last(4).join('')
end
def encrypt_card_number(number, key)
cipher = Gibberish::AES.new(key)
cipher.encrypt(number)
end
def persist_user_address!
# if !temporary_address && user && user.respond_to?(:persist_order_address) && bill_address_id
# user.persist_order_address(self)
# end
user.ship_address = self.ship_address
user.bill_address = self.bill_address
user.save
end
def persist_user_credit_card
if !temporary_credit_card && user_id && valid_credit_cards.present?
valid_credit_cards.first.update(user_id: user_id, default: true)
end
end
def assign_default_credit_card
if payments.from_credit_card.size == 0 && user_has_valid_default_card? && payment_required?
cc = user.default_credit_card
payments.create!(payment_method_id: cc.payment_method_id, source: cc, amount: total)
end
end
def user_has_valid_default_card?
user && user.default_credit_card.try(:valid?)
end
private
# For payment step, filter order parameters to produce the expected nested
# attributes for a single payment and its source, discarding attributes
# for payment methods other than the one selected
#
# In case a existing credit card is provided it needs to build the payment
# attributes from scratch so we can set the amount. example payload:
#
# {
# "order": {
# "existing_card": "2"
# }
# }
#
def update_params_payment_source
if @updating_params[:payment_source].present?
source_params = @updating_params.
delete(:payment_source)[@updating_params[:order][:payments_attributes].
first[:payment_method_id].to_s]
if source_params
@updating_params[:order][:payments_attributes].first[:source_attributes] = source_params
end
end
if @updating_params[:order] && (@updating_params[:order][:payments_attributes] ||
@updating_params[:order][:existing_card])
@updating_params[:order][:payments_attributes] ||= [{}]
@updating_params[:order][:payments_attributes][:amount] = order_total_after_store_credit
#@updating_params[:order][:payments_attributes].first[:amount] = order_total_after_store_credit
end
end
end
end
end
end
end
Like this article?
0