lib/active_merchant/billing/credit_card.rb in activemerchant-1.43.3 vs lib/active_merchant/billing/credit_card.rb in activemerchant-1.44.0

- old
+ new

@@ -1,8 +1,8 @@ require 'time' require 'date' -require 'active_merchant/billing/expiry_date' +require "active_merchant/billing/model" module ActiveMerchant #:nodoc: module Billing #:nodoc: # A +CreditCard+ object represents a physical credit card, and is capable of validating the various # data associated with these. @@ -41,34 +41,37 @@ # :year => '2010', # :brand => 'visa', # :number => '4242424242424242' # ) # - # cc.valid? # => true + # cc.validate # => {} # cc.display_number # => XXXX-XXXX-XXXX-4242 # - class CreditCard + class CreditCard < Model include CreditCardMethods - include Validateable cattr_accessor :require_verification_value self.require_verification_value = true # Returns or sets the credit card number. # # @return [String] - attr_accessor :number + attr_reader :number + def number=(value) + @number = (empty?(value) ? value : value.to_s.gsub(/[^\d]/, "")) + end + # Returns or sets the expiry month for the card. # # @return [Integer] - attr_accessor :month + attr_reader :month # Returns or sets the expiry year for the card. # # @return [Integer] - attr_accessor :year + attr_reader :year # Returns or sets the credit card brand. # # Valid card types are # @@ -86,12 +89,23 @@ # * +'laser'+ # # Or, if you wish to test your implementation, +'bogus'+. # # @return (String) the credit card brand - attr_accessor :brand + def brand + if !defined?(@brand) || empty?(@brand) + self.class.brand?(number) + else + @brand + end + end + def brand=(value) + value = value && value.to_s.dup + @brand = (value.respond_to?(:downcase) ? value.downcase : value) + end + # Returns or sets the first name of the card holder. # # @return [String] attr_accessor :first_name @@ -99,11 +113,12 @@ # # @return [String] attr_accessor :last_name # Required for Switch / Solo cards - attr_accessor :start_month, :start_year, :issue_number + attr_reader :start_month, :start_year + attr_accessor :issue_number # Returns or sets the card verification value. # # This attribute is optional but recommended. The verification value is # a {card security code}[http://en.wikipedia.org/wiki/Card_security_code]. If provided, @@ -116,16 +131,16 @@ # # @return [String] attr_accessor :track_data def type - self.class.deprecated "CreditCard#type is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#brand instead." + ActiveMerchant.deprecated "CreditCard#type is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#brand instead." brand end def type=(value) - self.class.deprecated "CreditCard#type is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#brand instead." + ActiveMerchant.deprecated "CreditCard#type is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#brand instead." self.brand = value end # Provides proxy access to an expiry date object # @@ -146,33 +161,46 @@ first_name? || last_name? end # Returns whether the +first_name+ attribute has been set. def first_name? - @first_name.present? + first_name.present? end # Returns whether the +last_name+ attribute has been set. def last_name? - @last_name.present? + last_name.present? end # Returns the full name of the card holder. # # @return [String] the full name of the card holder def name - [@first_name, @last_name].compact.join(' ') + [first_name, last_name].compact.join(' ') end def name=(full_name) names = full_name.split self.last_name = names.pop self.first_name = names.join(" ") end + %w(month year start_month start_year).each do |m| + class_eval %( + def #{m}=(v) + @#{m} = case v + when "", nil, 0 + nil + else + v.to_i + end + end + ) + end + def verification_value? - !@verification_value.blank? + !verification_value.blank? end # Returns a display-friendly version of the card number. # # All but the last 4 numbers are replaced with an "X", and hyphens are @@ -197,86 +225,127 @@ # Validates the credit card details. # # Any validation errors are added to the {#errors} attribute. def validate - validate_essential_attributes + errors = validate_essential_attributes + validate_verification_value # Bogus card is pretty much for testing purposes. Lets just skip these extra tests if its used - return if brand == 'bogus' + return errors_hash(errors) if brand == 'bogus' - validate_card_brand - validate_card_number - validate_verification_value - validate_switch_or_solo_attributes + errors_hash( + errors + + validate_card_brand_and_number + + validate_switch_or_solo_attributes + ) end def self.requires_verification_value? require_verification_value end private - def before_validate #:nodoc: - self.month = month.to_i - self.year = year.to_i - self.start_month = start_month.to_i unless start_month.nil? - self.start_year = start_year.to_i unless start_year.nil? - self.number = number.to_s.gsub(/[^\d]/, "") - self.brand.downcase! if brand.respond_to?(:downcase) - self.brand = self.class.brand?(number) if brand.blank? + def validate_essential_attributes #:nodoc: + errors = [] + + errors << [:first_name, "cannot be empty"] if first_name.blank? + errors << [:last_name, "cannot be empty"] if last_name.blank? + + if(empty?(month) || empty?(year)) + errors << [:month, "is required"] if empty?(month) + errors << [:year, "is required"] if empty?(year) + else + errors << [:month, "is not a valid month"] if !valid_month?(month) + + if expired? + errors << [:year, "expired"] + else + errors << [:year, "is not a valid year"] if !valid_expiry_year?(year) + end + end + + errors end - def validate_card_number #:nodoc: - if number.blank? - errors.add :number, "is required" + def validate_card_brand_and_number #:nodoc: + errors = [] + + if !empty?(brand) + errors << [:brand, "is invalid"] if !CreditCard.card_companies.keys.include?(brand) + end + + if empty?(number) + errors << [:number, "is required"] elsif !CreditCard.valid_number?(number) - errors.add :number, "is not a valid credit card number" + errors << [:number, "is not a valid credit card number"] end - unless errors.on(:number) || errors.on(:brand) - errors.add :brand, "does not match the card number" unless CreditCard.matching_brand?(number, brand) + if errors.empty? + errors << [:brand, "does not match the card number"] if !CreditCard.matching_brand?(number, brand) end - end - def validate_card_brand #:nodoc: - errors.add :brand, "is required" if brand.blank? && number.present? - errors.add :brand, "is invalid" unless brand.blank? || CreditCard.card_companies.keys.include?(brand) + errors end - alias_method :validate_card_type, :validate_card_brand + def validate_verification_value #:nodoc: + errors = [] - def validate_essential_attributes #:nodoc: - errors.add :first_name, "cannot be empty" if @first_name.blank? - errors.add :last_name, "cannot be empty" if @last_name.blank? - - if @month.to_i.zero? || @year.to_i.zero? - errors.add :month, "is required" if @month.to_i.zero? - errors.add :year, "is required" if @year.to_i.zero? - else - errors.add :month, "is not a valid month" unless valid_month?(@month) - errors.add :year, "expired" if expired? - errors.add :year, "is not a valid year" unless expired? || valid_expiry_year?(@year) + if verification_value? + unless valid_card_verification_value?(verification_value, brand) + errors << [:verification_value, "should be #{card_verification_value_length(brand)} digits"] + end + elsif CreditCard.requires_verification_value? + errors << [:verification_value, "is required"] end + errors end def validate_switch_or_solo_attributes #:nodoc: + errors = [] + if %w[switch solo].include?(brand) - unless valid_month?(@start_month) && valid_start_year?(@start_year) || valid_issue_number?(@issue_number) - if @issue_number.blank? - errors.add :start_month, "is invalid" unless valid_month?(@start_month) - errors.add :start_year, "is invalid" unless valid_start_year?(@start_year) - errors.add :issue_number, "cannot be empty" + valid_start_month = valid_month?(start_month) + valid_start_year = valid_start_year?(start_year) + + if((!valid_start_month || !valid_start_year) && !valid_issue_number?(issue_number)) + if empty?(issue_number) + errors << [:issue_number, "cannot be empty"] + errors << [:start_month, "is invalid"] if !valid_start_month + errors << [:start_year, "is invalid"] if !valid_start_year else - errors.add :issue_number, "is invalid" unless valid_issue_number?(@issue_number) + errors << [:issue_number, "is invalid"] if !valid_issue_number?(issue_number) end end end + + errors end - def validate_verification_value #:nodoc: - if CreditCard.requires_verification_value? - errors.add :verification_value, "is required" unless verification_value? + class ExpiryDate #:nodoc: + attr_reader :month, :year + def initialize(month, year) + @month = month.to_i + @year = year.to_i + end + + def expired? #:nodoc: + Time.now.utc > expiration + end + + def expiration #:nodoc: + begin + Time.utc(year, month, month_days, 23, 59, 59) + rescue ArgumentError + Time.at(0).utc + end + end + + private + def month_days + mdays = [nil,31,28,31,30,31,30,31,31,30,31,30,31] + mdays[2] = 29 if Date.leap?(year) + mdays[month] end end end end end