diff --git a/app/models/spree/credit_card.rb b/app/models/spree/credit_card.rb new file mode 100644 index 0000000000..948dcf829d --- /dev/null +++ b/app/models/spree/credit_card.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +module Spree + class CreditCard < ActiveRecord::Base + belongs_to :payment_method + belongs_to :user + + has_many :payments, as: :source + + before_save :set_last_digits + + attr_accessor :verification_value + attr_reader :number + attr_writer :save_requested_by_customer # For holding customer preference in memory + + validates :month, :year, numericality: { only_integer: true } + validates :number, presence: true, unless: :has_payment_profile?, on: :create + validates :verification_value, presence: true, unless: :has_payment_profile?, on: :create + validate :expiry_not_in_the_past + + after_create :ensure_single_default_card + after_save :ensure_single_default_card, if: :default_card_needs_updating? + + scope :with_payment_profile, -> { where('gateway_customer_profile_id IS NOT NULL') } + + # needed for some of the ActiveMerchant gateways (eg. SagePay) + alias_attribute :brand, :cc_type + + def expiry=(expiry) + self[:month], self[:year] = expiry.split(" / ") + self[:year] = "20" + self[:year] + end + + def number=(num) + @number = begin + num.gsub(/[^0-9]/, '') + rescue StandardError + nil + end + end + + # cc_type is set by jquery.payment, which helpfully provides different + # types from Active Merchant. Converting them is necessary. + def cc_type=(type) + real_type = case type + when 'mastercard', 'maestro' + 'master' + when 'amex' + 'american_express' + when 'dinersclub' + 'diners_club' + else + type + end + self[:cc_type] = real_type + end + + def set_last_digits + number.to_s.gsub!(/\s/, '') + verification_value.to_s.gsub!(/\s/, '') + self.last_digits ||= number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1) + end + + def name? + first_name? && last_name? + end + + def name + "#{first_name} #{last_name}" + end + + def verification_value? + verification_value.present? + end + + # Show the card number, with all but last 4 numbers replace with "X". (XXXX-XXXX-XXXX-4338) + def display_number + "XXXX-XXXX-XXXX-#{last_digits}" + end + + def actions + %w{capture void credit} + end + + # Indicates whether its possible to capture the payment + def can_capture?(payment) + payment.pending? || payment.checkout? + end + + # Indicates whether its possible to void the payment. + def can_void?(payment) + !payment.void? + end + + # Indicates whether its possible to credit the payment. Note that most gateways require that the + # payment be settled first which generally happens within 12-24 hours of the transaction. + def can_credit?(payment) + return false unless payment.completed? + return false unless payment.order.payment_state == 'credit_owed' + + payment.credit_allowed.positive? + end + + # Allows us to use a gateway_payment_profile_id to store Stripe Tokens + def has_payment_profile? + gateway_customer_profile_id.present? || gateway_payment_profile_id.present? + end + + def to_active_merchant + ActiveMerchant::Billing::CreditCard.new( + number: number, + month: month, + year: year, + verification_value: verification_value, + first_name: first_name, + last_name: last_name + ) + end + + def save_requested_by_customer? + !!@save_requested_by_customer + end + + private + + def expiry_not_in_the_past + return unless year.present? && month.present? + + time = "#{year}-#{month}-1".to_time + return unless time < Time.zone.now.to_time.beginning_of_month + + errors.add(:base, :card_expired) + end + + def reusable? + gateway_customer_profile_id.present? + end + + def default_missing? + !user.credit_cards.exists?(is_default: true) + end + + def default_card_needs_updating? + is_default_changed? || gateway_customer_profile_id_changed? + end + + def ensure_single_default_card + return unless user + return unless is_default? || (reusable? && default_missing?) + + user.credit_cards.update_all(['is_default=(id=?)', id]) + self.is_default = true + end + end +end diff --git a/app/models/spree/credit_card_decorator.rb b/app/models/spree/credit_card_decorator.rb deleted file mode 100644 index b8c78f01b7..0000000000 --- a/app/models/spree/credit_card_decorator.rb +++ /dev/null @@ -1,47 +0,0 @@ -Spree::CreditCard.class_eval do - # For holding customer preference in memory - attr_writer :save_requested_by_customer - - # Should be able to remove once we reach Spree v2.2.0 - # https://github.com/spree/spree/commit/411010f3975c919ab298cb63962ee492455b415c - belongs_to :payment_method - - belongs_to :user - - after_create :ensure_single_default_card - after_save :ensure_single_default_card, if: :default_card_needs_updating? - - # Allows us to use a gateway_payment_profile_id to store Stripe Tokens - # Should be able to remove once we reach Spree v2.2.0 - # Commit: https://github.com/spree/spree/commit/5a4d690ebc64b264bf12904a70187e7a8735ef3f - # See also: https://github.com/spree/spree_gateway/issues/111 - def has_payment_profile? # rubocop:disable Naming/PredicateName - gateway_customer_profile_id.present? || gateway_payment_profile_id.present? - end - - def save_requested_by_customer? - !!@save_requested_by_customer - end - - private - - def reusable? - gateway_customer_profile_id.present? - end - - def default_missing? - !user.credit_cards.exists?(is_default: true) - end - - def default_card_needs_updating? - is_default_changed? || gateway_customer_profile_id_changed? - end - - def ensure_single_default_card - return unless user - return unless is_default? || (reusable? && default_missing?) - - user.credit_cards.update_all(['is_default=(id=?)', id]) - self.is_default = true - end -end diff --git a/app/models/spree/gateway.rb b/app/models/spree/gateway.rb new file mode 100644 index 0000000000..216f0cfe6b --- /dev/null +++ b/app/models/spree/gateway.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spree/concerns/payment_method_distributors' + +module Spree + class Gateway < PaymentMethod + include Spree::PaymentMethodDistributors + + delegate_belongs_to :provider, :authorize, :purchase, :capture, :void, :credit + + validates :name, :type, presence: true + + # Default to live + preference :server, :string, default: 'live' + preference :test_mode, :boolean, default: false + + def payment_source_class + CreditCard + end + + # instantiates the selected gateway and configures with the options stored in the database + def self.current + super + end + + def provider + gateway_options = options + gateway_options.delete :login if gateway_options.key?(:login) && gateway_options[:login].nil? + if gateway_options[:server] + ActiveMerchant::Billing::Base.gateway_mode = gateway_options[:server].to_sym + end + @provider ||= provider_class.new(gateway_options) + end + + def options + preferences.each_with_object({}){ |(key, value), memo| memo[key.to_sym] = value; } + end + + def method_missing(method, *args) + if @provider.nil? || !@provider.respond_to?(method) + super + else + provider.__send__(method, *args) + end + end + + def payment_profiles_supported? + false + end + + def method_type + 'gateway' + end + + def supports?(source) + return true unless provider_class.respond_to? :supports? + return false unless source.brand + + provider_class.supports?(source.brand) + end + end +end diff --git a/app/models/spree/gateway/bogus.rb b/app/models/spree/gateway/bogus.rb new file mode 100644 index 0000000000..76dde5f211 --- /dev/null +++ b/app/models/spree/gateway/bogus.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Spree + class Gateway + class Bogus < Spree::Gateway + TEST_VISA = ['4111111111111111', '4012888888881881', '4222222222222'].freeze + TEST_MC = ['5500000000000004', '5555555555554444', '5105105105105100'].freeze + TEST_AMEX = ['378282246310005', '371449635398431', + '378734493671000', '340000000000009'].freeze + TEST_DISC = ['6011000000000004', '6011111111111117', '6011000990139424'].freeze + + VALID_CCS = ['1', TEST_VISA, TEST_MC, TEST_AMEX, TEST_DISC].flatten + + attr_accessor :test + + def provider_class + self.class + end + + def preferences + {} + end + + def create_profile(payment) + # simulate the storage of credit card profile using remote service + success = VALID_CCS.include? payment.source.number + payment.source.update(gateway_customer_profile_id: generate_profile_id(success)) + end + + def authorize(_money, credit_card, _options = {}) + profile_id = credit_card.gateway_customer_profile_id + if VALID_CCS.include?(credit_card.number) || profile_id&.starts_with?('BGS-') + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, + test: true, authorization: '12345', + avs_result: { code: 'A' }) + else + ActiveMerchant::Billing::Response.new(false, 'Bogus Gateway: Forced failure', + { message: 'Bogus Gateway: Forced failure' }, + test: true) + end + end + + def purchase(_money, credit_card, _options = {}) + profile_id = credit_card.gateway_customer_profile_id + if VALID_CCS.include?(credit_card.number) || profile_id&.starts_with?('BGS-') + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, + test: true, authorization: '12345', + avs_result: { code: 'A' }) + else + ActiveMerchant::Billing::Response.new(false, 'Bogus Gateway: Forced failure', + message: 'Bogus Gateway: Forced failure', + test: true) + end + end + + def credit(_money, _credit_card, _response_code, _options = {}) + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, + test: true, authorization: '12345') + end + + def capture(authorization, _credit_card, _gateway_options) + if authorization.response_code == '12345' + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, + test: true, authorization: '67890') + else + ActiveMerchant::Billing::Response.new(false, 'Bogus Gateway: Forced failure', + error: 'Bogus Gateway: Forced failure', test: true) + end + end + + def void(_response_code, _credit_card, _options = {}) + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, + test: true, authorization: '12345') + end + + def test? + # Test mode is not really relevant with bogus gateway (no such thing as live server) + true + end + + def payment_profiles_supported? + true + end + + def actions + %w(capture void credit) + end + + private + + def generate_profile_id(success) + record = true + prefix = success ? 'BGS' : 'FAIL' + while record + random = "#{prefix}-#{Array.new(6){ rand(6) }.join}" + record = CreditCard.find_by(gateway_customer_profile_id: random) + end + random + end + end + end +end diff --git a/app/models/spree/gateway/bogus_simple.rb b/app/models/spree/gateway/bogus_simple.rb new file mode 100644 index 0000000000..2a7c9e7778 --- /dev/null +++ b/app/models/spree/gateway/bogus_simple.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Bogus Gateway that doesn't support payment profiles +module Spree + class Gateway + class BogusSimple < Spree::Gateway::Bogus + def payment_profiles_supported? + false + end + + def authorize(_money, credit_card, _options = {}) + if VALID_CCS.include? credit_card.number + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, + test: true, authorization: '12345', + avs_result: { code: 'A' }) + else + ActiveMerchant::Billing::Response.new(false, 'Bogus Gateway: Forced failure', + { message: 'Bogus Gateway: Forced failure' }, + test: true) + end + end + + def purchase(_money, credit_card, _options = {}) + if VALID_CCS.include? credit_card.number + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, + test: true, authorization: '12345', + avs_result: { code: 'A' }) + else + ActiveMerchant::Billing::Response.new(false, 'Bogus Gateway: Forced failure', + message: 'Bogus Gateway: Forced failure', + test: true) + end + end + end + end +end diff --git a/app/models/spree/gateway_decorator.rb b/app/models/spree/gateway_decorator.rb deleted file mode 100644 index c62702a321..0000000000 --- a/app/models/spree/gateway_decorator.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'spree/concerns/payment_method_distributors' - -Spree::Gateway.class_eval do - include Spree::PaymentMethodDistributors - - # Default to live - preference :server, :string, default: 'live' - preference :test_mode, :boolean, default: false -end diff --git a/app/models/spree/payment_method.rb b/app/models/spree/payment_method.rb new file mode 100644 index 0000000000..39df36486d --- /dev/null +++ b/app/models/spree/payment_method.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'spree/concerns/payment_method_distributors' + +module Spree + class PaymentMethod < ActiveRecord::Base + include Spree::Core::CalculatedAdjustments + include Spree::PaymentMethodDistributors + + acts_as_taggable + acts_as_paranoid + + DISPLAY = [:both, :front_end, :back_end].freeze + default_scope -> { where(deleted_at: nil) } + + has_many :credit_cards, class_name: "Spree::CreditCard" + + validates :name, presence: true + validate :distributor_validation + + after_initialize :init + + scope :production, -> { where(environment: 'production') } + + scope :managed_by, lambda { |user| + if user.has_spree_role?('admin') + where(nil) + else + joins(:distributors). + where('distributors_payment_methods.distributor_id IN (?)', + user.enterprises.select(&:id)). + select('DISTINCT spree_payment_methods.*') + end + } + + scope :for_distributors, ->(distributors) { + non_unique_matches = unscoped.joins(:distributors).where(enterprises: { id: distributors }) + where(id: non_unique_matches.map(&:id)) + } + + scope :for_distributor, lambda { |distributor| + joins(:distributors). + where('enterprises.id = ?', distributor) + } + + scope :for_subscriptions, -> { where(type: Subscription::ALLOWED_PAYMENT_METHOD_TYPES) } + + scope :by_name, -> { order('spree_payment_methods.name ASC') } + + scope :available, lambda { |display_on = 'both'| + where(active: true). + where('spree_payment_methods.display_on=? OR spree_payment_methods.display_on=? OR spree_payment_methods.display_on IS NULL', display_on, ''). + where('spree_payment_methods.environment=? OR spree_payment_methods.environment=? OR spree_payment_methods.environment IS NULL', Rails.env, '') + } + + def self.providers + Rails.application.config.spree.payment_methods + end + + def provider_class + raise 'You must implement provider_class method for this gateway.' + end + + # The class that will process payments for this payment type, used for @payment.source + # e.g. CreditCard in the case of a the Gateway payment type + # nil means the payment method doesn't require a source e.g. check + def payment_source_class + raise 'You must implement payment_source_class method for this gateway.' + end + + def self.active? + where(type: to_s, environment: Rails.env, active: true).count.positive? + end + + def method_type + type.demodulize.downcase + end + + def self.find_with_destroyed(*args) + unscoped { find(*args) } + end + + def payment_profiles_supported? + false + end + + def source_required? + true + end + + def auto_capture? + Spree::Config[:auto_capture] + end + + def supports?(_source) + true + end + + def init + unless reflections.key?(:calculator) + self.class.include Spree::Core::CalculatedAdjustments + end + + self.calculator ||= Calculator::FlatRate.new(preferred_amount: 0) + end + + def has_distributor?(distributor) + distributors.include?(distributor) + end + + def self.clean_name + case name + when "Spree::PaymentMethod::Check" + "Cash/EFT/etc. (payments for which automatic validation is not required)" + when "Spree::Gateway::Migs" + "MasterCard Internet Gateway Service (MIGS)" + when "Spree::Gateway::Pin" + "Pin Payments" + when "Spree::Gateway::StripeConnect" + "Stripe" + when "Spree::Gateway::StripeSCA" + "Stripe SCA" + when "Spree::Gateway::PayPalExpress" + "PayPal Express" + else + i = name.rindex('::') + 2 + name[i..-1] + end + end + + private + + def distributor_validation + validates_with DistributorsValidator + end + end +end diff --git a/app/models/spree/payment_method/check.rb b/app/models/spree/payment_method/check.rb new file mode 100644 index 0000000000..9819a52c6c --- /dev/null +++ b/app/models/spree/payment_method/check.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Spree + class PaymentMethod + class Check < Spree::PaymentMethod + def actions + %w{capture void} + end + + # Indicates whether its possible to capture the payment + def can_capture?(payment) + ['checkout', 'pending'].include?(payment.state) + end + + # Indicates whether its possible to void the payment. + def can_void?(payment) + payment.state != 'void' + end + + def capture(*_args) + ActiveMerchant::Billing::Response.new(true, "", {}, {}) + end + + def void(*_args) + ActiveMerchant::Billing::Response.new(true, "", {}, {}) + end + + def source_required? + false + end + end + end +end diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb deleted file mode 100644 index 4c2770daea..0000000000 --- a/app/models/spree/payment_method_decorator.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'spree/concerns/payment_method_distributors' - -Spree::PaymentMethod.class_eval do - include Spree::Core::CalculatedAdjustments - include Spree::PaymentMethodDistributors - - acts_as_taggable - - has_many :credit_cards, class_name: "Spree::CreditCard" # from Spree v.2.3.0 d470b31798f37 - - after_initialize :init - - validate :distributor_validation - - # -- Scopes - scope :managed_by, lambda { |user| - if user.has_spree_role?('admin') - where(nil) - else - joins(:distributors). - where('distributors_payment_methods.distributor_id IN (?)', user.enterprises.select(&:id)). - select('DISTINCT spree_payment_methods.*') - end - } - - scope :for_distributors, ->(distributors) { - non_unique_matches = unscoped.joins(:distributors).where(enterprises: { id: distributors }) - where(id: non_unique_matches.map(&:id)) - } - - scope :for_distributor, lambda { |distributor| - joins(:distributors). - where('enterprises.id = ?', distributor) - } - - scope :for_subscriptions, -> { where(type: Subscription::ALLOWED_PAYMENT_METHOD_TYPES) } - - scope :by_name, -> { order('spree_payment_methods.name ASC') } - - # Rewrite Spree's ruby-land class method as a scope - scope :available, lambda { |display_on = 'both'| - where(active: true). - where('spree_payment_methods.display_on=? OR spree_payment_methods.display_on=? OR spree_payment_methods.display_on IS NULL', display_on, ''). - where('spree_payment_methods.environment=? OR spree_payment_methods.environment=? OR spree_payment_methods.environment IS NULL', Rails.env, '') - } - - def init - unless reflections.key?(:calculator) - self.class.include Spree::Core::CalculatedAdjustments - end - - self.calculator ||= Calculator::FlatRate.new(preferred_amount: 0) - end - - def has_distributor?(distributor) - distributors.include?(distributor) - end - - def self.clean_name - case name - when "Spree::PaymentMethod::Check" - "Cash/EFT/etc. (payments for which automatic validation is not required)" - when "Spree::Gateway::Migs" - "MasterCard Internet Gateway Service (MIGS)" - when "Spree::Gateway::Pin" - "Pin Payments" - when "Spree::Gateway::StripeConnect" - "Stripe" - when "Spree::Gateway::StripeSCA" - "Stripe SCA" - when "Spree::Gateway::PayPalExpress" - "PayPal Express" - else - i = name.rindex('::') + 2 - name[i..-1] - end - end - - private - - def distributor_validation - validates_with DistributorsValidator - end -end diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index bc8005806e..9280befea3 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -18,9 +18,6 @@ Spree::Gateway.class_eval do acts_as_taggable end -require "#{Rails.root}/app/models/spree/payment_method_decorator" -require "#{Rails.root}/app/models/spree/gateway_decorator" - Spree.config do |config| config.shipping_instructions = true config.address_requires_state = true diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index 2cdc226bdf..851c1be1a0 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -81,7 +81,8 @@ feature ' end scenario "updating a payment method", js: true do - payment_method = create(:payment_method, distributors: [@distributors[0]]) + payment_method = create(:payment_method, distributors: [@distributors[0]], + calculator: build(:calculator_flat_rate)) login_as_admin_and_visit spree.edit_admin_payment_method_path payment_method fill_in 'payment_method_name', with: 'New PM Name' diff --git a/spec/models/spree/credit_card_spec.rb b/spec/models/spree/credit_card_spec.rb index 845b1dd84a..c1d8b8ae82 100644 --- a/spec/models/spree/credit_card_spec.rb +++ b/spec/models/spree/credit_card_spec.rb @@ -2,6 +2,220 @@ require 'spec_helper' module Spree describe CreditCard do + describe "original specs from Spree" do + let(:valid_credit_card_attributes) { + { + number: '4111111111111111', + verification_value: '123', + month: 12, + year: Time.zone.now.year + 1 + } + } + + def self.payment_states + Spree::Payment.state_machine.states.keys + end + + def stub_rails_env(environment) + allow(Rails).to receive_messages(env: ActiveSupport::StringInquirer.new(environment)) + end + + let(:credit_card) { Spree::CreditCard.new } + + before(:each) do + @order = create(:order) + @payment = create(:payment, amount: 100, order: @order) + + @success_response = double('gateway_response', success?: true, + authorization: '123', + avs_result: { 'code' => 'avs-code' }) + @fail_response = double('gateway_response', success?: false) + + @payment_gateway = create(:payment_method, + environment: 'test') + allow(@payment_gateway).to receive_messages :payment_profiles_supported? => true, + :authorize => @success_response, + :purchase => @success_response, + :capture => @success_response, + :void => @success_response, + :credit => @success_response + allow(@payment).to receive_messages payment_method: @payment_gateway + end + + context "#can_capture?" do + it "should be true if payment is pending" do + payment = create(:payment, created_at: Time.zone.now) + allow(payment).to receive(:pending?) { true } + expect(credit_card.can_capture?(payment)).to be_truthy + end + + it "should be true if payment is checkout" do + payment = create(:payment, created_at: Time.zone.now) + allow(payment).to receive_messages :pending? => false, + :checkout? => true + expect(credit_card.can_capture?(payment)).to be_truthy + end + end + + context "#can_void?" do + it "should be true if payment is not void" do + payment = create(:payment) + allow(payment).to receive(:void?) { false } + expect(credit_card.can_void?(payment)).to be_truthy + end + end + + context "#can_credit?" do + it "should be false if payment is not completed" do + payment = create(:payment) + allow(payment).to receive(:completed?) { false } + expect(credit_card.can_credit?(payment)).to be_falsy + end + + it "should be false when order payment_state is not 'credit_owed'" do + payment = create(:payment, + order: create(:order, payment_state: 'paid')) + allow(payment).to receive(:completed?) { true } + expect(credit_card.can_credit?(payment)).to be_falsy + end + + it "should be false when credit_allowed is zero" do + payment = create(:payment, + order: create(:order, payment_state: 'credit_owed')) + allow(payment).to receive_messages :completed? => true, + :credit_allowed => 0 + + expect(credit_card.can_credit?(payment)).to be_falsy + end + end + + context "#valid?" do + it "should validate presence of number" do + credit_card.attributes = valid_credit_card_attributes.except(:number) + expect(credit_card).to_not be_valid + expect(credit_card.errors[:number]).to eq ["can't be blank"] + end + + it "should validate presence of security code" do + credit_card.attributes = valid_credit_card_attributes.except(:verification_value) + expect(credit_card).to_not be_valid + expect(credit_card.errors[:verification_value]).to eq ["can't be blank"] + end + + it "should validate expiration is not in the past" do + credit_card.month = 1.month.ago.month + credit_card.year = 1.month.ago.year + expect(credit_card).to_not be_valid + expect(credit_card.errors[:base]).to eq ["has expired"] + end + + it "does not run expiration in the past validation if month is not set" do + credit_card.month = nil + credit_card.year = Time.zone.now.year + expect(credit_card).to_not be_valid + expect(credit_card.errors[:base]).to be_blank + end + + it "does not run expiration in the past validation if year is not set" do + credit_card.month = Time.zone.now.month + credit_card.year = nil + expect(credit_card).to_not be_valid + expect(credit_card.errors[:base]).to be_blank + end + + it "does not run expiration in the past validation if year and month are empty" do + credit_card.year = "" + credit_card.month = "" + expect(credit_card).to_not be_valid + expect(credit_card.errors[:card]).to be_blank + end + + it "should only validate on create" do + credit_card.attributes = valid_credit_card_attributes + credit_card.save + expect(credit_card).to be_valid + end + end + + context "#save" do + before do + credit_card.attributes = valid_credit_card_attributes + credit_card.save! + end + + let!(:persisted_card) { Spree::CreditCard.find(credit_card.id) } + + it "should not actually store the number" do + expect(persisted_card.number).to be_blank + end + + it "should not actually store the security code" do + expect(persisted_card.verification_value).to be_blank + end + end + + context "#number=" do + it "should strip non-numeric characters from card input" do + credit_card.number = "6011000990139424" + expect(credit_card.number).to eq "6011000990139424" + + credit_card.number = " 6011-0009-9013-9424 " + expect(credit_card.number).to eq "6011000990139424" + end + + it "should not raise an exception on non-string input" do + credit_card.number = ({}) + expect(credit_card.number).to be_nil + end + end + + context "#cc_type=" do + it "converts between the different types" do + credit_card.cc_type = 'mastercard' + expect(credit_card.cc_type).to eq 'master' + + credit_card.cc_type = 'maestro' + expect(credit_card.cc_type).to eq 'master' + + credit_card.cc_type = 'amex' + expect(credit_card.cc_type).to eq 'american_express' + + credit_card.cc_type = 'dinersclub' + expect(credit_card.cc_type).to eq 'diners_club' + + credit_card.cc_type = 'some_outlandish_cc_type' + expect(credit_card.cc_type).to eq 'some_outlandish_cc_type' + end + end + + context "#associations" do + it "should be able to access its payments" do + expect { credit_card.payments.to_a }.not_to raise_error + end + end + + context "#to_active_merchant" do + before do + credit_card.number = "4111111111111111" + credit_card.year = Time.zone.now.year + credit_card.month = Time.zone.now.month + credit_card.first_name = "Bob" + credit_card.last_name = "Boblaw" + credit_card.verification_value = 123 + end + + it "converts to an ActiveMerchant::Billing::CreditCard object" do + am_card = credit_card.to_active_merchant + expect(am_card.number).to eq "4111111111111111" + expect(am_card.year).to eq Time.zone.now.year + expect(am_card.month).to eq Time.zone.now.month + expect(am_card.first_name).to eq "Bob" + am_card.last_name = "Boblaw" + expect(am_card.verification_value).to eq 123 + end + end + end + describe "setting default credit card for a user" do let(:user) { create(:user) } let(:onetime_card_attrs) do diff --git a/spec/models/spree/gateway_spec.rb b/spec/models/spree/gateway_spec.rb new file mode 100644 index 0000000000..294073a559 --- /dev/null +++ b/spec/models/spree/gateway_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Spree::Gateway do + class Provider + def initialize(options); end + + def imaginary_method; end + end + + class TestGateway < Spree::Gateway + def provider_class + Provider + end + end + + it "passes through all arguments on a method_missing call" do + gateway = TestGateway.new + expect(gateway.provider).to receive(:imaginary_method).with('foo') + gateway.imaginary_method('foo') + end +end diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index 396b9f1ab0..3e0d99c5fa 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -1,7 +1,46 @@ require 'spec_helper' +class Spree::Gateway::Test < Spree::Gateway +end + module Spree describe PaymentMethod do + describe "#available" do + let(:enterprise) { create(:enterprise) } + + before do + Spree::PaymentMethod.delete_all + + [nil, 'both', 'front_end', 'back_end'].each do |display_on| + Spree::Gateway::Test.create( + name: 'Display Both', + display_on: display_on, + active: true, + environment: 'test', + description: 'foofah', + distributors: [enterprise] + ) + end + expect(Spree::PaymentMethod.all.size).to eq 4 + end + + it "should return all methods available to front-end/back-end when no parameter is passed" do + expect(Spree::PaymentMethod.available.size).to eq 2 + end + + it "should return all methods available to front-end/back-end when display_on = :both" do + expect(Spree::PaymentMethod.available(:both).size).to eq 2 + end + + it "should return all methods available to front-end when display_on = :front_end" do + expect(Spree::PaymentMethod.available(:front_end).size).to eq 2 + end + + it "should return all methods available to back-end when display_on = :back_end" do + expect(Spree::PaymentMethod.available(:back_end).size).to eq 2 + end + end + it "orders payment methods by name" do pm1 = create(:payment_method, name: 'ZZ') pm2 = create(:payment_method, name: 'AA')