module ActiveMerchant #:nodoc: module Billing #:nodoc: # Convenience methods that can be included into a custom Credit Card object, such as an ActiveRecord based Credit Card object. module CreditCardMethods CARD_COMPANIES = { 'visa' => /^4\d{12}(\d{3})?$/, 'master' => /^(5[1-5]\d{4}|677189)\d{10}$/, 'discover' => /^(6011|65\d{2})\d{12}$/, 'american_express' => /^3[47]\d{13}$/, 'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/, 'jcb' => /^3528\d{12}$/, 'switch' => /^6759\d{12}(\d{2,3})?$/, 'solo' => /^6767\d{12}(\d{2,3})?$/, 'dankort' => /^5019\d{12}$/, 'maestro' => /^(5[06-8]|6\d)\d{10,17}$/, 'forbrugsforeningen' => /^600722\d{10}$/, 'laser' => /^(6304[89]\d{11}(\d{2,3})?|670695\d{13})$/ } def self.included(base) base.extend(ClassMethods) end def valid_month?(month) (1..12).include?(month) end def valid_expiry_year?(year) (Time.now.year..Time.now.year + 20).include?(year) end def valid_start_year?(year) year.to_s =~ /^\d{4}$/ && year.to_i > 1987 end def valid_issue_number?(number) number.to_s =~ /^\d{1,2}$/ end module ClassMethods # Returns true if it validates. Optionally, you can pass a card type as an argument and # make sure it is of the correct type. # # References: # - http://perl.about.com/compute/perl/library/nosearch/P073000.htm # - http://www.beachnet.com/~hstiles/cardtype.html def valid_number?(number) valid_test_mode_card_number?(number) || valid_card_number_length?(number) && valid_checksum?(number) end # Regular expressions for the known card companies. # # References: # - http://en.wikipedia.org/wiki/Credit_card_number # - http://www.barclaycardbusiness.co.uk/information_zone/processing/bin_rules.html def card_companies CARD_COMPANIES end # Returns a string containing the type of card from the list of known information below. # Need to check the cards in a particular order, as there is some overlap of the allowable ranges #-- # TODO Refactor this method. We basically need to tighten up the Maestro Regexp. # # Right now the Maestro regexp overlaps with the MasterCard regexp (IIRC). If we can tighten # things up, we can boil this whole thing down to something like... # # def type?(number) # return 'visa' if valid_test_mode_card_number?(number) # card_companies.find([nil]) { |type, regexp| number =~ regexp }.first.dup # end # def type?(number) return 'bogus' if valid_test_mode_card_number?(number) card_companies.reject { |c,p| c == 'maestro' }.each do |company, pattern| return company.dup if number =~ pattern end return 'maestro' if number =~ card_companies['maestro'] return nil end def last_digits(number) number.to_s.slice(-4..-1) if number.to_s.length >= 4 end def mask(number) "XXXX-XXXX-XXXX-#{last_digits(number)}" if number.to_s.length >= 4 end # Checks to see if the calculated type matches the specified type def matching_type?(number, type) type?(number) == type end private def valid_card_number_length?(number) #:nodoc: number.to_s.length >= 12 end def valid_test_mode_card_number?(number) #:nodoc: ActiveMerchant::Billing::Base.test? && %w[1 2 3 success failure error].include?(number.to_s) end # Checks the validity of a card number by use of the the Luhn Algorithm. # Please see http://en.wikipedia.org/wiki/Luhn_algorithm for details. def valid_checksum?(number) #:nodoc: sum = 0 for i in 0..number.length weight = number[-1 * (i + 2), 1].to_i * (2 - (i % 2)) sum += (weight < 10) ? weight : weight - 9 end (number[-1,1].to_i == (10 - sum % 10) % 10) end end end end end