diff --git a/app/models/spree/payment.rb b/app/models/spree/payment.rb new file mode 100644 index 0000000000..ccda57105c --- /dev/null +++ b/app/models/spree/payment.rb @@ -0,0 +1,158 @@ +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 :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 + def build_source + return if source_attributes.nil? + if payment_method and payment_method.payment_source_class + self.source = payment_method.payment_source_class.new(source_attributes) + end + 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 + + 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 diff --git a/app/models/spree/payment/processing.rb b/app/models/spree/payment/processing.rb new file mode 100644 index 0000000000..98e2cf4c1e --- /dev/null +++ b/app/models/spree/payment/processing.rb @@ -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 + 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 + + 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 diff --git a/spec/factories.rb b/spec/factories.rb index ad3c804c92..327565ea1b 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -15,6 +15,22 @@ require 'spree/testing_support/factories' # * order_with_inventory_unit_shipped # * completed_order_with_totals # + +# allows credit card info to be saved to the database which is needed for factories to work properly +class TestCard < Spree::CreditCard + def remove_readonly_attributes(attributes) attributes; end +end + +FactoryBot.define do + factory :credit_card, class: TestCard do + verification_value 123 + month 12 + year { Time.now.year + 1 } + number '4111111111111111' + cc_type 'visa' + end +end + FactoryBot.define do factory :classification, class: Spree::Classification do end diff --git a/spec/models/spree/payment_original_spec.rb b/spec/models/spree/payment_original_spec.rb new file mode 100644 index 0000000000..8f28ac14dd --- /dev/null +++ b/spec/models/spree/payment_original_spec.rb @@ -0,0 +1,639 @@ +require 'spec_helper' + +describe Spree::Payment do + let(:order) do + order = Spree::Order.new(:bill_address => Spree::Address.new, + :ship_address => Spree::Address.new) + end + + let(:gateway) do + gateway = Spree::Gateway::Bogus.new(:environment => 'test', :active => true) + gateway.stub :source_required => true + gateway + end + + let(:card) do + mock_model(Spree::CreditCard, :number => "4111111111111111", + :has_payment_profile? => true) + end + + let(:payment) do + payment = Spree::Payment.new + 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 + payment.log_entries.stub(:create) + end + + context 'validations' do + it "returns useful error messages when source is invalid" do + payment.source = Spree::CreditCard.new + payment.should_not be_valid + cc_errors = payment.errors['Credit Card'] + cc_errors.should include("Number can't be blank") + cc_errors.should include("Month is not a number") + cc_errors.should include("Year is not a number") + cc_errors.should 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 + payment.state.should eql('failed') + end + + it 'should transition to failed from processing state' do + payment.state = 'processing' + payment.failure + payment.state.should eql('failed') + end + + end + + context 'invalidate' do + it 'should transition from checkout to invalid' do + payment.state = 'checkout' + payment.invalidate + payment.state.should 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) + payment.state.should 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.log_entries.should_receive(:create).with(:details => anything) + payment.authorize! + 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! + payment.response_code.should == '123' + payment.avs_response.should == 'avs-code' + payment.cvv_response_code.should == 'cvv-code' + payment.cvv_response_message.should == '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) + lambda { + payment.authorize! + }.should 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.log_entries.should_receive(:create).with(:details => anything) + payment.purchase! + 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! + payment.response_code.should == '123' + payment.avs_response.should == '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! + payment.response_code.should == '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.log_entries.should_receive(:create).with(:details => anything) + payment.void_transaction! + 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! + payment.response_code.should == '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 = 'complete' + 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.log_entries.should_receive(:create).with(:details => anything) + payment.credit! + end + + context "when gateway does not match the environment" do + it "should raise an exception" do + gateway.stub :environment => "foo" + lambda { payment.credit! }.should raise_error(Spree::Core::GatewayError) + end + end + + context "when response is successful" do + it "should create an offsetting payment" do + Spree::Payment.should_receive(:create) + payment.credit! + end + + it "resulting payment should have correct values" do + payment.order.stub :outstanding_balance => 100 + payment.stub :credit_allowed => 10 + + offsetting_payment = payment.credit! + offsetting_payment.amount.to_f.should == -10 + offsetting_payment.should be_completed + offsetting_payment.response_code.should == '12345' + offsetting_payment.source.should == payment + 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!) + payment.process!.should 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) + payment.credit_allowed.should == 100 + payment.stub(:offsets_total).and_return(80) + payment.credit_allowed.should == 20 + end + end + + context "#can_credit?" do + it "is true if credit_allowed > 0" do + payment.stub(:credit_allowed).and_return(100) + payment.can_credit?.should be_true + end + it "is false if credit_allowed is 0" do + payment.stub(:credit_allowed).and_return(0) + payment.can_credit?.should be_false + end + end + + context "#credit" do + context "when amount <= credit_allowed" do + it "makes the state processing" do + payment.state = 'completed' + payment.stub(:credit_allowed).and_return(10) + payment.partial_credit(10) + payment.should 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) + payment.should be_completed + end + end + end + + context "#save" do + it "should call order#update!" do + payment = Spree::Payment.create(:amount => 100, :order => order) + 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) + lambda 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 + payment.payment_method.should_receive :create_profile + payment = Spree::Payment.create( + :amount => 100, + :order => 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.should_not_receive :create_profile + payment = Spree::Payment.create( + :amount => 100, + :order => 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) + payment.should be_valid + payment.source.should_not 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) + payment.should_not be_valid + payment.source.should_not be_nil + payment.source.should have(1).error_on(:number) + payment.source.should have(1).error_on(:verification_value) + end + end + + context "#currency" do + before { order.stub(:currency) { "ABC" } } + it "returns the order currency" do + payment.currency.should == "ABC" + end + end + + context "#display_amount" do + it "returns a Spree::Money for this amount" do + payment.display_amount.should == 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 + payment.gateway_options[:ip].should == 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) + payment.identifier.should_not be_blank + payment.identifier.size.should == 8 + payment.identifier.should be_a(String) + end + + context "other payment exists" do + let(:other_payment) { + payment = Spree::Payment.new + payment.source = card + payment.order = 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) + + payment.identifier.should_not be_blank + payment.identifier.should_not == other_payment.identifier + end + end + end +end