mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-03 02:21:33 +00:00
Merge pull request #5806 from openfoodfoundation/bring-in-payment-model
Bring in payment model
This commit is contained in:
@@ -61,7 +61,7 @@ module Spree
|
||||
else
|
||||
flash[:error] = t(:cannot_perform_operation)
|
||||
end
|
||||
rescue Spree::Core::GatewayError => e
|
||||
rescue StandardError => e
|
||||
flash[:error] = e.message
|
||||
ensure
|
||||
redirect_to request.referer
|
||||
|
||||
216
app/models/spree/payment.rb
Normal file
216
app/models/spree/payment.rb
Normal file
@@ -0,0 +1,216 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class Payment < ActiveRecord::Base
|
||||
include Spree::Payment::Processing
|
||||
extend Spree::LocalizedNumber
|
||||
|
||||
localize_number :amount
|
||||
|
||||
IDENTIFIER_CHARS = (('A'..'Z').to_a + ('0'..'9').to_a - %w(0 1 I O)).freeze
|
||||
|
||||
delegate :line_items, to: :order
|
||||
delegate :currency, to: :order
|
||||
|
||||
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").completed },
|
||||
class_name: "Spree::Payment", foreign_key: :source_id
|
||||
has_many :log_entries, as: :source, dependent: :destroy
|
||||
|
||||
has_one :adjustment, as: :source, dependent: :destroy
|
||||
|
||||
validate :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
|
||||
|
||||
# Skips the validation of the source (for example, credit card) of the payment.
|
||||
#
|
||||
# This is used in refunds as the validation of the card can fail but the refund can go through,
|
||||
# we trust the payment gateway in these cases. For example, Stripe is accepting refunds with
|
||||
# source cards that were valid when the payment was placed but are now expired, and we
|
||||
# consider them invalid.
|
||||
attr_accessor :skip_source_validation
|
||||
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)) }
|
||||
|
||||
# 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 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.positive?
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
return [] unless payment_source&.respond_to?(:actions)
|
||||
|
||||
actions = payment_source.actions.select do |action|
|
||||
!payment_source.respond_to?("can_#{action}?") ||
|
||||
payment_source.__send__("can_#{action}?", self)
|
||||
end
|
||||
|
||||
if payment_method.is_a? Gateway::Pin
|
||||
actions << 'refund' if actions.include? 'credit'
|
||||
actions.reject! { |a| ['credit', 'void'].include? a }
|
||||
end
|
||||
|
||||
actions
|
||||
end
|
||||
|
||||
def payment_source
|
||||
res = source.is_a?(Payment) ? source.source : source
|
||||
res || payment_method
|
||||
end
|
||||
|
||||
def ensure_correct_adjustment
|
||||
revoke_adjustment_eligibility if ['failed', 'invalid'].include?(state)
|
||||
return if adjustment.try(:finalized?)
|
||||
|
||||
if adjustment
|
||||
adjustment.originator = payment_method
|
||||
adjustment.label = adjustment_label
|
||||
adjustment.save
|
||||
else
|
||||
payment_method.create_adjustment(adjustment_label, order, self, true)
|
||||
association(:adjustment).reload
|
||||
end
|
||||
end
|
||||
|
||||
def adjustment_label
|
||||
I18n.t('payment_method_fee')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Don't charge fees for invalid or failed payments.
|
||||
# This is called twice for failed payments, because the persistence of the 'failed'
|
||||
# state is acheived through some trickery using an after_rollback callback on the
|
||||
# payment model. See Spree::Payment#persist_invalid
|
||||
def revoke_adjustment_eligibility
|
||||
return unless adjustment.try(:reload)
|
||||
return if adjustment.finalized?
|
||||
|
||||
adjustment.update_attribute(:eligible, false)
|
||||
adjustment.finalize!
|
||||
end
|
||||
|
||||
def validate_source
|
||||
if source && !skip_source_validation && !source.valid?
|
||||
source.errors.each do |field, error|
|
||||
field_name = I18n.t("activerecord.attributes.#{source.class.to_s.underscore}.#{field}")
|
||||
errors.add(Spree.t(source.class.to_s.demodulize.underscore), "#{field_name} #{error}")
|
||||
end
|
||||
end
|
||||
errors.blank?
|
||||
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)
|
||||
return unless source.try(:save_requested_by_customer?)
|
||||
return unless source.number || source.gateway_payment_profile_id
|
||||
return unless source.gateway_customer_profile_id.nil?
|
||||
|
||||
payment_method.create_profile(self)
|
||||
rescue ActiveMerchant::ConnectionError => e
|
||||
gateway_error e
|
||||
end
|
||||
|
||||
# Makes newly entered payments invalidate previously entered payments so the most recent payment
|
||||
# is used by the gateway.
|
||||
def invalidate_old_payments
|
||||
order.payments.with_state('checkout').where.not(id: id).each do |payment|
|
||||
# Using update_column skips validations and so it skips validate_source. As we are just
|
||||
# invalidating past payments here, we don't want to validate all of them at this stage.
|
||||
payment.update_column(:state, 'invalid')
|
||||
payment.ensure_correct_adjustment
|
||||
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
|
||||
self.identifier = generate_identifier while self.class.exists?(identifier: identifier)
|
||||
end
|
||||
|
||||
def generate_identifier
|
||||
Array.new(8){ IDENTIFIER_CHARS.sample }.join
|
||||
end
|
||||
end
|
||||
end
|
||||
277
app/models/spree/payment/processing.rb
Normal file
277
app/models/spree/payment/processing.rb
Normal file
@@ -0,0 +1,277 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class Payment < ActiveRecord::Base
|
||||
module Processing
|
||||
def process!
|
||||
return unless payment_method&.source_required?
|
||||
|
||||
raise Core::GatewayError, Spree.t(:payment_processing_failed) unless source
|
||||
|
||||
return if processing?
|
||||
|
||||
unless payment_method.supports?(source)
|
||||
invalidate!
|
||||
raise Core::GatewayError, Spree.t(:payment_method_not_supported)
|
||||
end
|
||||
|
||||
if payment_method.auto_capture?
|
||||
purchase!
|
||||
else
|
||||
authorize!
|
||||
end
|
||||
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!
|
||||
protect_from_connection_error do
|
||||
check_environment
|
||||
|
||||
response = 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
|
||||
payment_method.capture(self, source, gateway_options)
|
||||
else
|
||||
# Standard ActiveMerchant capture usage
|
||||
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
|
||||
|
||||
response = 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
|
||||
payment_method.void(response_code, source, gateway_options)
|
||||
else
|
||||
# Standard ActiveMerchant void usage
|
||||
payment_method.void(response_code, gateway_options)
|
||||
end
|
||||
|
||||
record_response(response)
|
||||
|
||||
if response.success?
|
||||
self.response_code = response.authorization
|
||||
void
|
||||
else
|
||||
gateway_error(response)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def credit!(credit_amount = nil)
|
||||
protect_from_connection_error do
|
||||
check_environment
|
||||
|
||||
credit_amount = calculate_refund_amount(credit_amount)
|
||||
|
||||
response = if payment_method.payment_profiles_supported?
|
||||
payment_method.credit(
|
||||
(credit_amount * 100).round,
|
||||
source,
|
||||
response_code,
|
||||
gateway_options
|
||||
)
|
||||
else
|
||||
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',
|
||||
skip_source_validation: true
|
||||
)
|
||||
else
|
||||
gateway_error(response)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def refund!(refund_amount = nil)
|
||||
protect_from_connection_error do
|
||||
check_environment
|
||||
|
||||
refund_amount = calculate_refund_amount(refund_amount)
|
||||
|
||||
response = if payment_method.payment_profiles_supported?
|
||||
payment_method.refund(
|
||||
(refund_amount * 100).round,
|
||||
source,
|
||||
response_code,
|
||||
gateway_options
|
||||
)
|
||||
else
|
||||
payment_method.refund(
|
||||
(refund_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: refund_amount.abs * -1,
|
||||
response_code: response.authorization,
|
||||
state: 'completed',
|
||||
skip_source_validation: true
|
||||
)
|
||||
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 calculate_refund_amount(refund_amount = nil)
|
||||
refund_amount ||= if credit_allowed >= order.outstanding_balance.abs
|
||||
order.outstanding_balance.abs
|
||||
else
|
||||
credit_allowed.abs
|
||||
end
|
||||
refund_amount.to_f
|
||||
end
|
||||
|
||||
def gateway_action(source, action, success_state)
|
||||
protect_from_connection_error do
|
||||
check_environment
|
||||
|
||||
response = payment_method.public_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
|
||||
__send__("#{success_state}!")
|
||||
else
|
||||
__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
|
||||
yield
|
||||
rescue ActiveMerchant::ConnectionError => e
|
||||
gateway_error(e)
|
||||
end
|
||||
|
||||
def gateway_error(error)
|
||||
text = if error.is_a? ActiveMerchant::Billing::Response
|
||||
error.params['message'] || error.params['response_reason_text'] || error.message
|
||||
elsif error.is_a? ActiveMerchant::ConnectionError
|
||||
Spree.t(:unable_to_connect_to_gateway)
|
||||
else
|
||||
error.to_s
|
||||
end
|
||||
logger.error(Spree.t(:gateway_error))
|
||||
logger.error(" #{error.to_yaml}")
|
||||
raise Core::GatewayError, 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, message
|
||||
end
|
||||
|
||||
# The unique identifier to be passed in to the payment gateway
|
||||
def gateway_order_id
|
||||
"#{order.number}-#{identifier}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,117 +0,0 @@
|
||||
require 'spree/localized_number'
|
||||
|
||||
module Spree
|
||||
Payment.class_eval do
|
||||
extend Spree::LocalizedNumber
|
||||
|
||||
delegate :line_items, to: :order
|
||||
|
||||
has_one :adjustment, as: :source, dependent: :destroy
|
||||
|
||||
after_save :ensure_correct_adjustment, :update_order
|
||||
|
||||
localize_number :amount
|
||||
|
||||
# We bypass this after_rollback callback that is setup in Spree::Payment
|
||||
# The issues the callback fixes are not experienced in OFN:
|
||||
# if a payment fails on checkout the state "failed" is persisted correctly
|
||||
def persist_invalid; end
|
||||
|
||||
def ensure_correct_adjustment
|
||||
revoke_adjustment_eligibility if ['failed', 'invalid'].include?(state)
|
||||
return if adjustment.try(:finalized?)
|
||||
|
||||
if adjustment
|
||||
adjustment.originator = payment_method
|
||||
adjustment.label = adjustment_label
|
||||
adjustment.save
|
||||
else
|
||||
payment_method.create_adjustment(adjustment_label, order, self, true)
|
||||
association(:adjustment).reload
|
||||
end
|
||||
end
|
||||
|
||||
def adjustment_label
|
||||
I18n.t('payment_method_fee')
|
||||
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 refund!(refund_amount = nil)
|
||||
protect_from_connection_error do
|
||||
check_environment
|
||||
|
||||
refund_amount = calculate_refund_amount(refund_amount)
|
||||
|
||||
if payment_method.payment_profiles_supported?
|
||||
response = payment_method.refund((refund_amount * 100).round, source, response_code, gateway_options)
|
||||
else
|
||||
response = payment_method.refund((refund_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: refund_amount.abs * -1,
|
||||
response_code: response.authorization,
|
||||
state: 'completed')
|
||||
else
|
||||
gateway_error(response)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Import from future Spree v.2.3.0 d470b31798f37
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
def calculate_refund_amount(refund_amount = nil)
|
||||
refund_amount ||= credit_allowed >= order.outstanding_balance.abs ? order.outstanding_balance.abs : credit_allowed.abs
|
||||
refund_amount.to_f
|
||||
end
|
||||
|
||||
def create_payment_profile
|
||||
return unless source.is_a?(CreditCard)
|
||||
return unless source.try(:save_requested_by_customer?)
|
||||
return unless source.number || source.gateway_payment_profile_id
|
||||
return unless source.gateway_customer_profile_id.nil?
|
||||
|
||||
payment_method.create_profile(self)
|
||||
rescue ActiveMerchant::ConnectionError => e
|
||||
gateway_error e
|
||||
end
|
||||
|
||||
# Don't charge fees for invalid or failed payments.
|
||||
# This is called twice for failed payments, because the persistence of the 'failed'
|
||||
# state is acheived through some trickery using an after_rollback callback on the
|
||||
# payment model. See Spree::Payment#persist_invalid
|
||||
def revoke_adjustment_eligibility
|
||||
return unless adjustment.try(:reload)
|
||||
return if adjustment.finalized?
|
||||
|
||||
adjustment.update_attribute(:eligible, false)
|
||||
adjustment.finalize!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -34,6 +34,8 @@ en:
|
||||
email: Customer E-Mail
|
||||
spree/payment:
|
||||
amount: Amount
|
||||
state: State
|
||||
source: Source
|
||||
spree/product:
|
||||
primary_taxon: "Product Category"
|
||||
supplier: "Supplier"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::Admin::PaymentsController, type: :controller do
|
||||
@@ -10,7 +12,7 @@ describe Spree::Admin::PaymentsController, type: :controller do
|
||||
allow(controller).to receive(:spree_current_user) { user }
|
||||
end
|
||||
|
||||
context "#create" do
|
||||
describe "#create" do
|
||||
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
|
||||
let(:params) { { amount: order.total, payment_method_id: payment_method.id } }
|
||||
|
||||
@@ -138,4 +140,92 @@ describe Spree::Admin::PaymentsController, type: :controller do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fire' do
|
||||
let(:payment_method) do
|
||||
create(
|
||||
:stripe_sca_payment_method,
|
||||
distributor_ids: [create(:distributor_enterprise).id],
|
||||
preferred_enterprise_id: create(:enterprise).id
|
||||
)
|
||||
end
|
||||
let(:order) { create(:order, state: 'complete') }
|
||||
let(:payment) do
|
||||
create(:payment, order: order, payment_method: payment_method, amount: order.total)
|
||||
end
|
||||
|
||||
let(:successful_response) { ActiveMerchant::Billing::Response.new(true, "Yay!") }
|
||||
|
||||
context 'on credit event' do
|
||||
let(:params) { { e: 'credit', order_id: order.number, id: payment.id } }
|
||||
|
||||
before do
|
||||
allow(request).to receive(:referer) { 'http://foo.com' }
|
||||
allow(Spree::Payment).to receive(:find).with(payment.id.to_s) { payment }
|
||||
end
|
||||
|
||||
it 'handles gateway errors' do
|
||||
allow(payment.payment_method)
|
||||
.to receive(:credit).and_raise(Spree::Core::GatewayError, 'error message')
|
||||
|
||||
spree_put :fire, params
|
||||
|
||||
expect(flash[:error]).to eq('error message')
|
||||
expect(response).to redirect_to('http://foo.com')
|
||||
end
|
||||
|
||||
it 'handles validation errors' do
|
||||
allow(payment).to receive(:credit!).and_raise(StandardError, 'validation error')
|
||||
|
||||
spree_put :fire, params
|
||||
|
||||
expect(flash[:error]).to eq('validation error')
|
||||
expect(response).to redirect_to('http://foo.com')
|
||||
end
|
||||
|
||||
it 'displays a success message and redirects to the referer' do
|
||||
allow(payment_method).to receive(:credit) { successful_response }
|
||||
|
||||
spree_put :fire, params
|
||||
|
||||
expect(flash[:success]).to eq(I18n.t(:payment_updated))
|
||||
end
|
||||
end
|
||||
|
||||
context 'on refund event' do
|
||||
let(:params) { { e: 'refund', order_id: order.number, id: payment.id } }
|
||||
|
||||
before do
|
||||
allow(request).to receive(:referer) { 'http://foo.com' }
|
||||
allow(Spree::Payment).to receive(:find).with(payment.id.to_s) { payment }
|
||||
end
|
||||
|
||||
it 'handles gateway errors' do
|
||||
allow(payment.payment_method)
|
||||
.to receive(:refund).and_raise(Spree::Core::GatewayError, 'error message')
|
||||
|
||||
spree_put :fire, params
|
||||
|
||||
expect(flash[:error]).to eq('error message')
|
||||
expect(response).to redirect_to('http://foo.com')
|
||||
end
|
||||
|
||||
it 'handles validation errors' do
|
||||
allow(payment).to receive(:refund!).and_raise(StandardError, 'validation error')
|
||||
|
||||
spree_put :fire, params
|
||||
|
||||
expect(flash[:error]).to eq('validation error')
|
||||
expect(response).to redirect_to('http://foo.com')
|
||||
end
|
||||
|
||||
it 'displays a success message and redirects to the referer' do
|
||||
allow(payment_method).to receive(:refund) { successful_response }
|
||||
|
||||
spree_put :fire, params
|
||||
|
||||
expect(flash[:success]).to eq(I18n.t(:payment_updated))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,7 @@ require 'spree/testing_support/factories'
|
||||
# * order_with_inventory_unit_shipped
|
||||
# * completed_order_with_totals
|
||||
#
|
||||
|
||||
FactoryBot.define do
|
||||
factory :classification, class: Spree::Classification do
|
||||
end
|
||||
|
||||
@@ -1,7 +1,687 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Payment do
|
||||
describe Spree::Payment do
|
||||
context 'original specs from Spree' do
|
||||
let(:order) { create(:order) }
|
||||
let(:gateway) do
|
||||
gateway = Spree::Gateway::Bogus.new(:environment => 'test', :active => true)
|
||||
gateway.stub :source_required => true
|
||||
gateway
|
||||
end
|
||||
|
||||
let(:card) { create(:credit_card) }
|
||||
|
||||
before { allow(card).to receive(:has_payment_profile?).and_return(true) }
|
||||
|
||||
let(:payment) do
|
||||
payment = create(:payment)
|
||||
payment.source = card
|
||||
payment.order = order
|
||||
payment.payment_method = gateway
|
||||
payment
|
||||
end
|
||||
|
||||
let(:amount_in_cents) { payment.amount.to_f * 100 }
|
||||
|
||||
let!(:success_response) do
|
||||
double('success_response', :success? => true,
|
||||
:authorization => '123',
|
||||
:avs_result => { 'code' => 'avs-code' },
|
||||
:cvv_result => { 'code' => 'cvv-code', 'message' => "CVV Result"})
|
||||
end
|
||||
|
||||
let(:failed_response) { double('gateway_response', :success? => false) }
|
||||
|
||||
before(:each) do
|
||||
# So it doesn't create log entries every time a processing method is called
|
||||
allow(payment).to receive(:record_response)
|
||||
end
|
||||
|
||||
context "extends LocalizedNumber" do
|
||||
it_behaves_like "a model using the LocalizedNumber module", [:amount]
|
||||
end
|
||||
|
||||
context 'validations' do
|
||||
it "returns useful error messages when source is invalid" do
|
||||
payment.source = Spree::CreditCard.new
|
||||
expect(payment).not_to be_valid
|
||||
cc_errors = payment.errors['Credit Card']
|
||||
expect(cc_errors).to include("Number can't be blank")
|
||||
expect(cc_errors).to include("Month is not a number")
|
||||
expect(cc_errors).to include("Year is not a number")
|
||||
expect(cc_errors).to include("Verification Value can't be blank")
|
||||
end
|
||||
end
|
||||
|
||||
# Regression test for https://github.com/spree/spree/pull/2224
|
||||
context 'failure' do
|
||||
it 'should transition to failed from pending state' do
|
||||
payment.state = 'pending'
|
||||
payment.failure
|
||||
expect(payment.state).to eql('failed')
|
||||
end
|
||||
|
||||
it 'should transition to failed from processing state' do
|
||||
payment.state = 'processing'
|
||||
payment.failure
|
||||
expect(payment.state).to eql('failed')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'invalidate' do
|
||||
it 'should transition from checkout to invalid' do
|
||||
payment.state = 'checkout'
|
||||
payment.invalidate
|
||||
expect(payment.state).to eq('invalid')
|
||||
end
|
||||
end
|
||||
|
||||
context "processing" do
|
||||
before do
|
||||
payment.stub(:update_order)
|
||||
payment.stub(:create_payment_profile)
|
||||
end
|
||||
|
||||
context "#process!" do
|
||||
it "should purchase if with auto_capture" do
|
||||
payment.payment_method.should_receive(:auto_capture?).and_return(true)
|
||||
payment.should_receive(:purchase!)
|
||||
payment.process!
|
||||
end
|
||||
|
||||
it "should authorize without auto_capture" do
|
||||
payment.payment_method.should_receive(:auto_capture?).and_return(false)
|
||||
payment.should_receive(:authorize!)
|
||||
payment.process!
|
||||
end
|
||||
|
||||
it "should make the state 'processing'" do
|
||||
payment.should_receive(:started_processing!)
|
||||
payment.process!
|
||||
end
|
||||
|
||||
it "should invalidate if payment method doesnt support source" do
|
||||
payment.payment_method.should_receive(:supports?).with(payment.source).and_return(false)
|
||||
expect { payment.process!}.to raise_error(Spree::Core::GatewayError)
|
||||
expect(payment.state).to eq('invalid')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#authorize" do
|
||||
it "should call authorize on the gateway with the payment amount" do
|
||||
payment.payment_method.should_receive(:authorize).with(amount_in_cents,
|
||||
card,
|
||||
anything).and_return(success_response)
|
||||
payment.authorize!
|
||||
end
|
||||
|
||||
it "should call authorize on the gateway with the currency code" do
|
||||
payment.stub :currency => 'GBP'
|
||||
payment.payment_method.should_receive(:authorize).with(amount_in_cents,
|
||||
card,
|
||||
hash_including({:currency => "GBP"})).and_return(success_response)
|
||||
payment.authorize!
|
||||
end
|
||||
|
||||
it "should log the response" do
|
||||
payment.authorize!
|
||||
expect(payment).to have_received(:record_response)
|
||||
end
|
||||
|
||||
context "when gateway does not match the environment" do
|
||||
it "should raise an exception" do
|
||||
gateway.stub :environment => "foo"
|
||||
expect { payment.authorize! }.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
|
||||
context "if successful" do
|
||||
before do
|
||||
payment.payment_method.should_receive(:authorize).with(amount_in_cents,
|
||||
card,
|
||||
anything).and_return(success_response)
|
||||
end
|
||||
|
||||
it "should store the response_code, avs_response and cvv_response fields" do
|
||||
payment.authorize!
|
||||
expect(payment.response_code).to eq('123')
|
||||
expect(payment.avs_response).to eq('avs-code')
|
||||
expect(payment.cvv_response_code).to eq('cvv-code')
|
||||
expect(payment.cvv_response_message).to eq('CVV Result')
|
||||
end
|
||||
|
||||
it "should make payment pending" do
|
||||
payment.should_receive(:pend!)
|
||||
payment.authorize!
|
||||
end
|
||||
end
|
||||
|
||||
context "if unsuccessful" do
|
||||
it "should mark payment as failed" do
|
||||
gateway.stub(:authorize).and_return(failed_response)
|
||||
payment.should_receive(:failure)
|
||||
payment.should_not_receive(:pend)
|
||||
expect {
|
||||
payment.authorize!
|
||||
}.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "purchase" do
|
||||
it "should call purchase on the gateway with the payment amount" do
|
||||
gateway.should_receive(:purchase).with(amount_in_cents, card, anything).and_return(success_response)
|
||||
payment.purchase!
|
||||
end
|
||||
|
||||
it "should log the response" do
|
||||
payment.purchase!
|
||||
expect(payment).to have_received(:record_response)
|
||||
end
|
||||
|
||||
context "when gateway does not match the environment" do
|
||||
it "should raise an exception" do
|
||||
gateway.stub :environment => "foo"
|
||||
expect { payment.purchase! }.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
|
||||
context "if successful" do
|
||||
before do
|
||||
payment.payment_method.should_receive(:purchase).with(amount_in_cents,
|
||||
card,
|
||||
anything).and_return(success_response)
|
||||
end
|
||||
|
||||
it "should store the response_code and avs_response" do
|
||||
payment.purchase!
|
||||
expect(payment.response_code).to eq('123')
|
||||
expect(payment.avs_response).to eq('avs-code')
|
||||
end
|
||||
|
||||
it "should make payment complete" do
|
||||
payment.should_receive(:complete!)
|
||||
payment.purchase!
|
||||
end
|
||||
end
|
||||
|
||||
context "if unsuccessful" do
|
||||
it "should make payment failed" do
|
||||
gateway.stub(:purchase).and_return(failed_response)
|
||||
payment.should_receive(:failure)
|
||||
payment.should_not_receive(:pend)
|
||||
expect { payment.purchase! }.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#capture" do
|
||||
before do
|
||||
payment.stub(:complete).and_return(true)
|
||||
end
|
||||
|
||||
context "when payment is pending" do
|
||||
before do
|
||||
payment.state = 'pending'
|
||||
end
|
||||
|
||||
context "if successful" do
|
||||
before do
|
||||
payment.payment_method.should_receive(:capture).with(payment, card, anything).and_return(success_response)
|
||||
end
|
||||
|
||||
it "should make payment complete" do
|
||||
payment.should_receive(:complete)
|
||||
payment.capture!
|
||||
end
|
||||
|
||||
it "should store the response_code" do
|
||||
gateway.stub :capture => success_response
|
||||
payment.capture!
|
||||
expect(payment.response_code).to eq('123')
|
||||
end
|
||||
end
|
||||
|
||||
context "if unsuccessful" do
|
||||
it "should not make payment complete" do
|
||||
gateway.stub :capture => failed_response
|
||||
payment.should_receive(:failure)
|
||||
payment.should_not_receive(:complete)
|
||||
expect { payment.capture! }.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Regression test for #2119
|
||||
context "when payment is completed" do
|
||||
before do
|
||||
payment.state = 'completed'
|
||||
end
|
||||
|
||||
it "should do nothing" do
|
||||
payment.should_not_receive(:complete)
|
||||
payment.payment_method.should_not_receive(:capture)
|
||||
payment.log_entries.should_not_receive(:create)
|
||||
payment.capture!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#void" do
|
||||
before do
|
||||
payment.response_code = '123'
|
||||
payment.state = 'pending'
|
||||
end
|
||||
|
||||
context "when profiles are supported" do
|
||||
it "should call payment_gateway.void with the payment's response_code" do
|
||||
gateway.stub :payment_profiles_supported? => true
|
||||
gateway.should_receive(:void).with('123', card, anything).and_return(success_response)
|
||||
payment.void_transaction!
|
||||
end
|
||||
end
|
||||
|
||||
context "when profiles are not supported" do
|
||||
it "should call payment_gateway.void with the payment's response_code" do
|
||||
gateway.stub :payment_profiles_supported? => false
|
||||
gateway.should_receive(:void).with('123', anything).and_return(success_response)
|
||||
payment.void_transaction!
|
||||
end
|
||||
end
|
||||
|
||||
it "should log the response" do
|
||||
payment.void_transaction!
|
||||
expect(payment).to have_received(:record_response)
|
||||
end
|
||||
|
||||
context "when gateway does not match the environment" do
|
||||
it "should raise an exception" do
|
||||
gateway.stub :environment => "foo"
|
||||
expect { payment.void_transaction! }.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
|
||||
context "if successful" do
|
||||
it "should update the response_code with the authorization from the gateway" do
|
||||
# Change it to something different
|
||||
payment.response_code = 'abc'
|
||||
payment.void_transaction!
|
||||
expect(payment.response_code).to eq('12345')
|
||||
end
|
||||
end
|
||||
|
||||
context "if unsuccessful" do
|
||||
it "should not void the payment" do
|
||||
gateway.stub :void => failed_response
|
||||
payment.should_not_receive(:void)
|
||||
expect { payment.void_transaction! }.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
|
||||
# Regression test for #2119
|
||||
context "if payment is already voided" do
|
||||
before do
|
||||
payment.state = 'void'
|
||||
end
|
||||
|
||||
it "should not void the payment" do
|
||||
payment.payment_method.should_not_receive(:void)
|
||||
payment.void_transaction!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#credit" do
|
||||
before do
|
||||
payment.state = 'completed'
|
||||
payment.response_code = '123'
|
||||
end
|
||||
|
||||
context "when outstanding_balance is less than payment amount" do
|
||||
before do
|
||||
payment.order.stub :outstanding_balance => 10
|
||||
payment.stub :credit_allowed => 1000
|
||||
end
|
||||
|
||||
it "should call credit on the gateway with the credit amount and response_code" do
|
||||
gateway.should_receive(:credit).with(1000, card, '123', anything).and_return(success_response)
|
||||
payment.credit!
|
||||
end
|
||||
end
|
||||
|
||||
context "when outstanding_balance is equal to payment amount" do
|
||||
before do
|
||||
payment.order.stub :outstanding_balance => payment.amount
|
||||
end
|
||||
|
||||
it "should call credit on the gateway with the credit amount and response_code" do
|
||||
gateway.should_receive(:credit).with(amount_in_cents, card, '123', anything).and_return(success_response)
|
||||
payment.credit!
|
||||
end
|
||||
end
|
||||
|
||||
context "when outstanding_balance is greater than payment amount" do
|
||||
before do
|
||||
payment.order.stub :outstanding_balance => 101
|
||||
end
|
||||
|
||||
it "should call credit on the gateway with the original payment amount and response_code" do
|
||||
gateway.should_receive(:credit).with(amount_in_cents.to_f, card, '123', anything).and_return(success_response)
|
||||
payment.credit!
|
||||
end
|
||||
end
|
||||
|
||||
it "should log the response" do
|
||||
payment.credit!
|
||||
expect(payment).to have_received(:record_response)
|
||||
end
|
||||
|
||||
context "when gateway does not match the environment" do
|
||||
it "should raise an exception" do
|
||||
gateway.stub :environment => "foo"
|
||||
expect { payment.credit! }.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
|
||||
context "when response is successful" do
|
||||
it "should create an offsetting payment" do
|
||||
expect(Spree::Payment).to receive(:create!)
|
||||
payment.credit!
|
||||
end
|
||||
|
||||
it "resulting payment should have correct values" do
|
||||
allow(payment.order).to receive(:outstanding_balance) { 100 }
|
||||
allow(payment).to receive(:credit_allowed) { 10 }
|
||||
|
||||
offsetting_payment = payment.credit!
|
||||
expect(offsetting_payment.amount.to_f).to eq(-10)
|
||||
expect(offsetting_payment).to be_completed
|
||||
expect(offsetting_payment.response_code).to eq('12345')
|
||||
expect(offsetting_payment.source).to eq(payment)
|
||||
end
|
||||
|
||||
context 'and the source payment card is expired' do
|
||||
let(:card) do
|
||||
Spree::CreditCard.new(month: 12, year: 1995, number: '4111111111111111')
|
||||
end
|
||||
|
||||
let(:successful_response) do
|
||||
ActiveMerchant::Billing::Response.new(true, "Yay!")
|
||||
end
|
||||
|
||||
it 'lets the new payment to be saved' do
|
||||
allow(payment.order).to receive(:outstanding_balance) { 100 }
|
||||
allow(payment).to receive(:credit_allowed) { 10 }
|
||||
|
||||
offsetting_payment = payment.credit!
|
||||
|
||||
expect(offsetting_payment).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when response is unsuccessful" do
|
||||
it "should not create a payment" do
|
||||
gateway.stub :credit => failed_response
|
||||
Spree::Payment.should_not_receive(:create)
|
||||
expect { payment.credit! }.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
|
||||
context "when already processing" do
|
||||
it "should return nil without trying to process the source" do
|
||||
payment.state = 'processing'
|
||||
|
||||
payment.should_not_receive(:authorize!)
|
||||
payment.should_not_receive(:purchase!)
|
||||
expect(payment.process!).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "with source required" do
|
||||
context "raises an error if no source is specified" do
|
||||
before do
|
||||
payment.source = nil
|
||||
end
|
||||
|
||||
specify do
|
||||
expect { payment.process! }.to raise_error(Spree::Core::GatewayError, Spree.t(:payment_processing_failed))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with source optional" do
|
||||
context "raises no error if source is not specified" do
|
||||
before do
|
||||
payment.source = nil
|
||||
payment.payment_method.stub(:source_required? => false)
|
||||
end
|
||||
|
||||
specify do
|
||||
expect { payment.process! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#credit_allowed" do
|
||||
it "is the difference between offsets total and payment amount" do
|
||||
payment.amount = 100
|
||||
payment.stub(:offsets_total).and_return(0)
|
||||
expect(payment.credit_allowed).to eq(100)
|
||||
payment.stub(:offsets_total).and_return(80)
|
||||
expect(payment.credit_allowed).to eq(20)
|
||||
end
|
||||
end
|
||||
|
||||
context "#can_credit?" do
|
||||
it "is true if credit_allowed > 0" do
|
||||
payment.stub(:credit_allowed).and_return(100)
|
||||
expect(payment.can_credit?).to be true
|
||||
end
|
||||
it "is false if credit_allowed is 0" do
|
||||
payment.stub(:credit_allowed).and_return(0)
|
||||
expect(payment.can_credit?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "#credit" do
|
||||
context "when amount <= credit_allowed" do
|
||||
it "makes the state processing" do
|
||||
payment.payment_method.name = 'Gateway'
|
||||
payment.payment_method.distributors << create(:distributor_enterprise)
|
||||
payment.payment_method.save!
|
||||
|
||||
payment.order = create(:order)
|
||||
|
||||
payment.state = 'completed'
|
||||
payment.stub(:credit_allowed).and_return(10)
|
||||
payment.partial_credit(10)
|
||||
expect(payment).to be_processing
|
||||
end
|
||||
it "calls credit on the source with the payment and amount" do
|
||||
payment.state = 'completed'
|
||||
payment.stub(:credit_allowed).and_return(10)
|
||||
payment.should_receive(:credit!).with(10)
|
||||
payment.partial_credit(10)
|
||||
end
|
||||
end
|
||||
context "when amount > credit_allowed" do
|
||||
it "should not call credit on the source" do
|
||||
payment.state = 'completed'
|
||||
payment.stub(:credit_allowed).and_return(10)
|
||||
payment.partial_credit(20)
|
||||
expect(payment).to be_completed
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#save" do
|
||||
it "should call order#update!" do
|
||||
gateway.name = 'Gateway'
|
||||
gateway.distributors << create(:distributor_enterprise)
|
||||
gateway.save!
|
||||
|
||||
order = create(:order)
|
||||
payment = Spree::Payment.create(:amount => 100, :order => order, :payment_method => gateway)
|
||||
order.should_receive(:update!)
|
||||
payment.save
|
||||
end
|
||||
|
||||
context "when profiles are supported" do
|
||||
before do
|
||||
gateway.stub :payment_profiles_supported? => true
|
||||
payment.source.stub :has_payment_profile? => false
|
||||
end
|
||||
|
||||
|
||||
context "when there is an error connecting to the gateway" do
|
||||
it "should call gateway_error " do
|
||||
pending '[Spree build] Failing spec'
|
||||
message = double("gateway_error")
|
||||
connection_error = ActiveMerchant::ConnectionError.new(message, nil)
|
||||
expect(gateway).to receive(:create_profile).and_raise(connection_error)
|
||||
expect do
|
||||
Spree::Payment.create(
|
||||
:amount => 100,
|
||||
:order => order,
|
||||
:source => card,
|
||||
:payment_method => gateway
|
||||
)
|
||||
end.should raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
|
||||
context "when successfully connecting to the gateway" do
|
||||
it "should create a payment profile" do
|
||||
gateway.name = 'Gateway'
|
||||
gateway.distributors << create(:distributor_enterprise)
|
||||
gateway.save!
|
||||
|
||||
payment.payment_method = gateway
|
||||
payment.source.save_requested_by_customer = true
|
||||
|
||||
expect(gateway).to receive(:create_profile)
|
||||
|
||||
Spree::Payment.create(
|
||||
:amount => 100,
|
||||
:order => create(:order),
|
||||
:source => card,
|
||||
:payment_method => gateway
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
context "when profiles are not supported" do
|
||||
before { gateway.stub :payment_profiles_supported? => false }
|
||||
|
||||
it "should not create a payment profile" do
|
||||
gateway.name = 'Gateway'
|
||||
gateway.distributors << create(:distributor_enterprise)
|
||||
gateway.save!
|
||||
|
||||
gateway.should_not_receive :create_profile
|
||||
payment = Spree::Payment.create(
|
||||
:amount => 100,
|
||||
:order => create(:order),
|
||||
:source => card,
|
||||
:payment_method => gateway
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#build_source" do
|
||||
it "should build the payment's source" do
|
||||
params = { :amount => 100, :payment_method => gateway,
|
||||
:source_attributes => {
|
||||
:expiry =>"1 / 99",
|
||||
:number => '1234567890123',
|
||||
:verification_value => '123'
|
||||
}
|
||||
}
|
||||
|
||||
payment = Spree::Payment.new(params)
|
||||
expect(payment).to be_valid
|
||||
expect(payment.source).not_to be_nil
|
||||
end
|
||||
|
||||
it "errors when payment source not valid" do
|
||||
params = { :amount => 100, :payment_method => gateway,
|
||||
:source_attributes => {:expiry => "1 / 12" }}
|
||||
|
||||
payment = Spree::Payment.new(params)
|
||||
expect(payment).not_to be_valid
|
||||
expect(payment.source).not_to be_nil
|
||||
expect(payment.source.errors[:number]).not_to be_empty
|
||||
expect(payment.source.errors[:verification_value]).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "#currency" do
|
||||
before { order.stub(:currency) { "ABC" } }
|
||||
it "returns the order currency" do
|
||||
expect(payment.currency).to eq("ABC")
|
||||
end
|
||||
end
|
||||
|
||||
context "#display_amount" do
|
||||
it "returns a Spree::Money for this amount" do
|
||||
expect(payment.display_amount).to eq(Spree::Money.new(payment.amount))
|
||||
end
|
||||
end
|
||||
|
||||
# Regression test for #2216
|
||||
context "#gateway_options" do
|
||||
before { order.stub(:last_ip_address => "192.168.1.1") }
|
||||
|
||||
it "contains an IP" do
|
||||
expect(payment.gateway_options[:ip]).to eq(order.last_ip_address)
|
||||
end
|
||||
end
|
||||
|
||||
context "#set_unique_identifier" do
|
||||
# Regression test for #1998
|
||||
it "sets a unique identifier on create" do
|
||||
payment.run_callbacks(:save)
|
||||
expect(payment.identifier).not_to be_blank
|
||||
expect(payment.identifier.size).to eq(8)
|
||||
expect(payment.identifier).to be_a(String)
|
||||
end
|
||||
|
||||
context "other payment exists" do
|
||||
let(:other_payment) {
|
||||
gateway.name = 'Gateway'
|
||||
gateway.distributors << create(:distributor_enterprise)
|
||||
gateway.save!
|
||||
|
||||
payment = Spree::Payment.new
|
||||
payment.source = card
|
||||
payment.order = create(:order)
|
||||
payment.payment_method = gateway
|
||||
payment
|
||||
}
|
||||
|
||||
before { other_payment.save! }
|
||||
|
||||
it "doesn't set duplicate identifier" do
|
||||
payment.should_receive(:generate_identifier).and_return(other_payment.identifier)
|
||||
payment.should_receive(:generate_identifier).and_call_original
|
||||
|
||||
payment.run_callbacks(:save)
|
||||
|
||||
expect(payment.identifier).not_to be_blank
|
||||
expect(payment.identifier).not_to eq(other_payment.identifier)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "available actions" do
|
||||
context "for most gateways" do
|
||||
let(:payment) { create(:payment, source: create(:credit_card)) }
|
||||
@@ -24,7 +704,7 @@ module Spree
|
||||
|
||||
context "for Pin Payments" do
|
||||
let(:d) { create(:distributor_enterprise) }
|
||||
let(:pin) { Gateway::Pin.create! name: 'pin', distributor_ids: [d.id] }
|
||||
let(:pin) { Spree::Gateway::Pin.create! name: 'pin', distributor_ids: [d.id] }
|
||||
let(:payment) { create(:payment, source: create(:credit_card), payment_method: pin) }
|
||||
|
||||
it "does not void" do
|
||||
@@ -45,7 +725,7 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
describe "refunding" do
|
||||
describe "refund!" do
|
||||
let(:payment) { create(:payment) }
|
||||
let(:success) { double(success?: true, authorization: 'abc123') }
|
||||
let(:failure) { double(success?: false) }
|
||||
@@ -105,9 +785,9 @@ module Spree
|
||||
|
||||
expect do
|
||||
payment.refund!
|
||||
end.to change(Payment, :count).by(1)
|
||||
end.to change(Spree::Payment, :count).by(1)
|
||||
|
||||
p = Payment.last
|
||||
p = Spree::Payment.last
|
||||
expect(p.order).to eq(payment.order)
|
||||
expect(p.source).to eq(payment)
|
||||
expect(p.payment_method).to eq(payment.payment_method)
|
||||
@@ -147,12 +827,23 @@ module Spree
|
||||
|
||||
it "creates adjustment" do
|
||||
payment = create(:payment, order: order, payment_method: payment_method,
|
||||
amount: order.total)
|
||||
amount: order.total)
|
||||
expect(payment.adjustment).to be_present
|
||||
expect(payment.adjustment.amount).not_to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'OFN specs from previously decorated model' do
|
||||
describe "applying transaction fees" do
|
||||
let!(:order) { create(:order) }
|
||||
let!(:line_item) { create(:line_item, order: order, quantity: 3, price: 5.00) }
|
||||
|
||||
before do
|
||||
order.reload.update!
|
||||
end
|
||||
|
||||
context "to Stripe payments" do
|
||||
let(:shop) { create(:enterprise) }
|
||||
@@ -229,9 +920,5 @@ module Spree
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "extends LocalizedNumber" do
|
||||
it_behaves_like "a model using the LocalizedNumber module", [:amount]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user