core/app/models/spree/store_credit.rb
module Spree
class StoreCredit < Spree::Base
include Spree::SingleStoreResource
include Spree::Metadata
if defined?(Spree::Webhooks::HasWebhooks)
include Spree::Webhooks::HasWebhooks
end
acts_as_paranoid
VOID_ACTION = 'void'.freeze
CANCEL_ACTION = 'cancel'.freeze
CREDIT_ACTION = 'credit'.freeze
CAPTURE_ACTION = 'capture'.freeze
ELIGIBLE_ACTION = 'eligible'.freeze
AUTHORIZE_ACTION = 'authorize'.freeze
ALLOCATION_ACTION = 'allocation'.freeze
DEFAULT_CREATED_BY_EMAIL = 'spree@example.com'.freeze
belongs_to :user, class_name: "::#{Spree.user_class}", foreign_key: 'user_id'
belongs_to :category, class_name: 'Spree::StoreCreditCategory'
belongs_to :created_by, class_name: Spree.admin_user_class.to_s, foreign_key: 'created_by_id'
belongs_to :credit_type, class_name: 'Spree::StoreCreditType', foreign_key: 'type_id'
belongs_to :store, class_name: 'Spree::Store'
has_many :store_credit_events, class_name: 'Spree::StoreCreditEvent'
validates :user, :category, :credit_type, :created_by, :currency, :store, presence: true
validates :amount, numericality: { greater_than: 0 }
validates :amount_used, numericality: { greater_than_or_equal_to: 0 }
validate :amount_used_less_than_or_equal_to_amount
validate :amount_authorized_less_than_or_equal_to_amount
delegate :name, to: :category, prefix: true
delegate :email, to: :created_by, prefix: true
scope :order_by_priority, -> { includes(:credit_type).order('spree_store_credit_types.priority ASC') }
before_validation :associate_credit_type
after_save :store_event
before_destroy :validate_no_amount_used
attr_accessor :action, :action_amount, :action_originator, :action_authorization_code
extend Spree::DisplayMoney
money_methods :amount, :amount_used
self.whitelisted_ransackable_attributes = %w[user_id created_by_id amount currency type_id]
self.whitelisted_ransackable_associations = %w[type user created_by]
def amount_remaining
amount - amount_used - amount_authorized
end
def authorize(amount, order_currency, options = {})
authorization_code = options[:action_authorization_code]
if authorization_code
if store_credit_events.find_by(action: AUTHORIZE_ACTION, authorization_code: authorization_code)
# Don't authorize again on capture
return true
end
else
authorization_code = generate_authorization_code
end
if validate_authorization(amount, order_currency)
update!(
action: AUTHORIZE_ACTION,
action_amount: amount,
action_originator: options[:action_originator],
action_authorization_code: authorization_code,
amount_authorized: amount_authorized + amount
)
authorization_code
else
errors.add(:base, Spree.t('store_credit_payment_method.insufficient_authorized_amount'))
false
end
end
def validate_authorization(amount, order_currency)
if BigDecimal(amount_remaining, 3) < BigDecimal(amount, 3)
errors.add(:base, Spree.t('store_credit_payment_method.insufficient_funds'))
elsif currency != order_currency
errors.add(:base, Spree.t('store_credit_payment_method.currency_mismatch'))
end
errors.blank?
end
def capture(amount, authorization_code, order_currency, options = {})
return false unless authorize(amount, order_currency, action_authorization_code: authorization_code)
if amount <= amount_authorized
if currency != order_currency
errors.add(:base, Spree.t('store_credit_payment_method.currency_mismatch'))
false
else
update!(
action: CAPTURE_ACTION,
action_amount: amount,
action_originator: options[:action_originator],
action_authorization_code: authorization_code,
amount_used: amount_used + amount,
amount_authorized: amount_authorized - amount
)
authorization_code
end
else
errors.add(:base, Spree.t('store_credit_payment_method.insufficient_authorized_amount'))
false
end
end
def void(authorization_code, options = {})
if auth_event = store_credit_events.find_by(action: AUTHORIZE_ACTION, authorization_code: authorization_code)
update!(
action: VOID_ACTION,
action_amount: auth_event.amount,
action_authorization_code: authorization_code,
action_originator: options[:action_originator],
amount_authorized: amount_authorized - auth_event.amount
)
true
else
errors.add(:base, Spree.t('store_credit_payment_method.unable_to_void', auth_code: authorization_code))
false
end
end
def credit(amount, authorization_code, order_currency, options = {})
# Find the amount related to this authorization_code in order to add the store credit back
capture_event = store_credit_events.find_by(action: CAPTURE_ACTION, authorization_code: authorization_code)
if currency != order_currency # sanity check to make sure the order currency hasn't changed since the auth
errors.add(:base, Spree.t('store_credit_payment_method.currency_mismatch'))
false
elsif capture_event && amount <= capture_event.amount
action_attributes = {
action: CREDIT_ACTION,
action_amount: amount,
action_originator: options[:action_originator],
action_authorization_code: authorization_code
}
create_credit_record(amount, action_attributes)
true
else
errors.add(:base, Spree.t('store_credit_payment_method.unable_to_credit', auth_code: authorization_code))
false
end
end
def actions
[CAPTURE_ACTION, VOID_ACTION, CREDIT_ACTION]
end
def can_capture?(payment)
payment.pending? || payment.checkout?
end
def can_void?(payment)
payment.pending? || (payment.checkout? && !payment.order.completed?)
end
def can_credit?(payment)
payment.completed? && payment.credit_allowed > 0
end
def generate_authorization_code
"#{id}-SC-#{Time.now.utc.strftime('%Y%m%d%H%M%S%6N')}"
end
class << self
def default_created_by
Spree.user_class.find_by(email: DEFAULT_CREATED_BY_EMAIL)
end
end
private
def create_credit_record(amount, action_attributes = {})
# Setting credit_to_new_allocation to true will create a new allocation anytime #credit is called
# If it is not set, it will update the store credit's amount in place
credit = if Spree::Config.credit_to_new_allocation
Spree::StoreCredit.new(create_credit_record_params(amount))
else
self.amount_used = amount_used - amount
self
end
credit.assign_attributes(action_attributes)
credit.save!
end
def create_credit_record_params(amount)
{
amount: amount,
user_id: user_id,
category_id: category_id,
created_by_id: created_by_id,
currency: currency,
type_id: type_id,
memo: credit_allocation_memo,
store: store
}
end
def credit_allocation_memo
"This is a credit from store credit ID #{id}"
end
def store_event
return unless saved_change_to_amount? ||
saved_change_to_amount_used? ||
saved_change_to_amount_authorized? ||
action == ELIGIBLE_ACTION
event = if action
store_credit_events.build(action: action)
else
store_credit_events.where(action: ALLOCATION_ACTION).first_or_initialize
end
event.update!(
amount: action_amount || amount,
authorization_code: action_authorization_code || event.authorization_code || generate_authorization_code,
user_total_amount: user.total_available_store_credit,
originator: action_originator
)
end
def amount_used_less_than_or_equal_to_amount
return true if amount_used.nil?
if amount_used > amount
errors.add(:amount_used, :cannot_be_greater_than_amount)
false
end
end
def amount_authorized_less_than_or_equal_to_amount
if (amount_used + amount_authorized) > amount
errors.add(:amount_authorized, :exceeds_total_credits)
false
end
end
def validate_no_amount_used
if amount_used > 0
errors.add(:amount_used, :greater_than_zero_restrict_delete)
throw(:abort)
end
end
def associate_credit_type
unless type_id
credit_type_name = category.try(:non_expiring?) ? 'Non-expiring' : 'Expiring'
self.credit_type = Spree::StoreCreditType.find_or_create_by(name: credit_type_name)
end
end
end
end