require 'time' require 'delegate' require 'date' module ActiveMerchant #:nodoc: module Billing #:nodoc: # This credit card object can be used as a stand alone object. It acts just like a active record object # but doesn't support the .save method as its not backed by a database. class CreditCard cattr_accessor :require_verification_value self.require_verification_value = false def self.requires_verification_value? require_verification_value end include Validateable class ExpiryMonth < DelegateClass(Fixnum)#:nodoc: def to_s(format = :default) #:nodoc: case format when :default __getobj__.to_s when :two_digit sprintf("%.2i", self)[-2..-1] else super end end def valid? #:nodoc: (1..12).include?(self) end end class ExpiryYear < DelegateClass(Fixnum)#:nodoc: def to_s(format = :default) #:nodoc: case format when :default __getobj__.to_s when :two_digit sprintf("%.2i", self)[-2..-1] when :four_digit sprintf("%.4i", self) else super end end def valid? #:nodoc: (Time.now.year..Time.now.year + 20).include?(self) end end class ExpiryDate #:nodoc: attr_reader :month, :year def initialize(month, year) @month = ExpiryMonth.new(month) @year = ExpiryYear.new(year) end def expired? #:nodoc: Time.now > expiration rescue true end def expiration #:nodoc: Time.parse("#{month}/#{month_days}/#{year} 23:59:59") rescue Time.at(0) 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 # required attr_accessor :number, :month, :year, :type, :first_name, :last_name # Optional verification_value (CVV, CVV2 etc) # # Gateways will try their best to run validation on the passed in value if it is supplied # attr_accessor :verification_value def before_validate self.type.downcase! if type.respond_to?(:downcase) self.month = month.to_i self.year = year.to_i self.number.to_s.gsub!(/[^\d]/, "") end def validate @errors.add "year", "expired" if expired? @errors.add "first_name", "cannot be empty" if @first_name.blank? @errors.add "last_name", "cannot be empty" if @last_name.blank? @errors.add "month", "cannot be empty" unless month.valid? @errors.add "year", "cannot be empty" unless year.valid? # Bogus card is pretty much for testing purposes. Lets just skip these extra tests if its used return if type == 'bogus' @errors.add "number", "is not a valid credit card number" unless CreditCard.valid_number?(number) @errors.add "type", "is invalid" unless CreditCard.card_companies.keys.include?(type) @errors.add "type", "is not the correct card type" unless CreditCard.type?(number) == type if CreditCard.requires_verification_value? @errors.add "verification_value", "is required" unless verification_value? end end def expired? expiry_date.expired? end def name? @first_name != nil and @last_name != nil end def first_name? @first_name != nil end def last_name? @last_name != nil end def name "#{@first_name} #{@last_name}" end def verification_value? !@verification_value.blank? end # Regular expressions for the known card companies # == Known card types # Card Type Prefix Length # -------------------------------------------------------------------------- # master 51-55 16 # visa 4 13, 16 # american_express 34, 37 15 # diners_club 300-305, 36, 38 14 # discover 6011 16 # jcb 3 16 # jcb 2131, 1800 15 # switch various 16,18,19 # solo 63, 6767 16,18,19 def self.card_companies { 'visa' => /^4\d{12}(\d{3})?$/, 'master' => /^5[1-5]\d{14}$/, 'discover' => /^6011\d{12}$/, 'american_express' => /^3[47]\d{13}$/, 'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/, 'jcb' => /^(3\d{4}|2131|1800)\d{11}$/, 'switch' => [/^49(03(0[2-9]|3[5-9])|11(0[1-2]|7[4-9]|8[1-2])|36[0-9]{2})\d{10}(\d{2,3})?$/, /^564182\d{10}(\d{2,3})?$/, /^6(3(33[0-4][0-9])|759[0-9]{2})\d{10}(\d{2,3})?$/], 'solo' => /^6(3(34[5-9][0-9])|767[0-9]{2})\d{10}(\d{2,3})?$/ } end # Returns a string containing the type of card from the list of known information below. def self.type?(number) return 'visa' if Base.gateway_mode == :test and ['1','2','3','success','failure','error'].include?(number.to_s) card_companies.each do |company, patterns| return company if [patterns].flatten.any? { |pattern| number =~ pattern } end return nil end # 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 self.valid_number?(number) return true if Base.gateway_mode == :test and ['1','2','3','success','failure','error'].include?(number.to_s) return false unless number.to_s.length >= 13 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 # 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 last_digits number.nil? ? "" : number.last(4) end def month expiry_date.month end def year expiry_date.year end def expiry_date ExpiryDate.new(@month, @year) end end end end