Validating Credit Card Numbers With Ruby (Part 2)

Posted by admin Sat, 24 Mar 2007 03:45:00 GMT

As promised here is the second article about validating credit cards with Ruby.

This edition wraps a module and class around the code in preparation for future enhancement.

Feel free to change whatever you want. If you do please send me an update and at least give me credit for the original.

To use it just do something like this in your controller:

if params[:cc]
  begin
    cc = Payment::CreditCard.new(params[:cc])
    cc.valid?
    @user.update_attribute(:verified_at, Time.now)
  rescue Exception => e
    logger.debug e.inspect
    flash[:notice] = "Your profile has been updated. However, #{e}."
    return
  end
end

This assumes you have a form like this somewhere in your view:

<p><label>Name On Card</label><%= text_field :cc, :name, :class => "text-n" %></p>
<p><label>Card Type</label><%= select :cc, :card_type, FundingSource.get_card_types %></p>
<p><label>Card Number</label><%= text_field :cc, :number, {:size => 16, :maxlength => 16, :class => "text-n"} %><br />
  <label></label><small>(15-16 digits)</small></p>
<p><label>Expiration Date</label><%= date_select :cc, :expiration, 
  :start_year => Time.now.year,
  :end_year => Time.now.year+10, 
  :use_month_numbers => true,
  :discard_day => true, 
  :include_blank => true,
  :order => [:month, :year] %></p>
<p><label>Security Code</label><%= text_field :cc, :security_code, :size => 4, :class => "text-s" %><br />
  <label></label><small>(3 Digit Code on back of credit card)</small></p>

Obviously, this code comes with no warranty of any kind and could hurt your application, your data, your home, your feelings, etc. Don’t sue me. Other than that, use it as you see fit. Just please give me some credit.

module Payment
    class CreditCard

        CARD_TYPE = {
            :master_card => 0,
            :visa => 1,
            :american_express => 2,
            :diners_club => 3,
            :discover => 4
        }

        ###################################################################
        # Construct the object and do minimal validation
        # Params:
        #   :name                 - cardholder's name (optional, for future use)
        #   :card_type            - type of card (required)
        #   :number               - 15-16 digit card number (required)
        #   :security_code    - 3-4 digit security code (required, only
        #                                           needs to exist and be the right length)
        #   'expiration(1i)'  - expiration year (required).
        #   'expiration(2i)'  - expiration month (required).
        ###################################################################
        def initialize(attributes = {})
            # Cardholders Name
            if (@name = attributes[:name]).nil? or @name.empty?
                raise "Cardholder Name is required"
                return
            end
            # Card Type
            @card_type = attributes[:card_type]
            @card_type = convert_cc_type(@card_type)
            if @card_type.nil? or !CARD_TYPE.has_value?(@card_type)
                raise "A valid Card Type is required"
                return
            end
            # Card Number
            if (@number = attributes[:number]).nil? or @number.to_i.nil? or
                    @number.length < 15
                raise "A valid Card Number is required"
                return
            end
            # Security Code
            if ((@security_code = attributes[:security_code]).nil? or
                    @security_code.to_i.nil? or
                    (@security_code.length != 3 and
                        @card_type != CARD_TYPE[:american_express]) or
                    (@security_code.length != 4 and
                        @card_type == CARD_TYPE[:american_express]))
                raise "A valid Security Code Number is required"
                return
            end
            # Card Expiration
            if ((@card_expiration_month = attributes['expiration(2i)']).nil? or
                    @card_expiration_month.to_i.nil? or
                    @card_expiration_month.to_i < 1 or
                    @card_expiration_month.to_i > 12)
                raise "A valid Card expiration month is required"
                return
            end
            if ((@card_expiration_year = attributes['expiration(1i)']).nil? or
                    @card_expiration_year.to_i.nil?)
                raise "A valid Card expiration year is required"
                return
            end
            @card_expiration_date = Time.gm("#{@card_expiration_year}".to_i,
                @card_expiration_month).next_month
            if (@card_expiration_date <= Time.now)
                raise "Card is expired."
                return
            end
        end

        ###################################################################
        # Check number format for given card type and check whole number
        # against the Mod 10 algorithm
        ###################################################################
        def valid?
            valid_format = false
            pass_check = false
            # check format
            case @card_type
                when CARD_TYPE[:master_card]
                    valid_format = @number[/^5[1-5][0-9]{14}$/] == @number
                when CARD_TYPE[:visa]
                    valid_format = @number[/^4[0-9]{12}$|^4[0-9]{15}$/] == @number
                when CARD_TYPE[:american_express]
                    valid_format = @number[/^3[4|7][0-9]{13}$/] == @number
                when CARD_TYPE[:diners_club]
                    valid_format = @number[/^30[0-5][0-9]{11}$|^3[6|8][0-9]{12}$/] == @number
                when CARD_TYPE[:discover]
                    valid_format = @number[/^6011[0-9]{12}$/] == @number
            end
            raise "credit card number is invalid." if valid_format == false

            # check Mod 10
            reverse_card_num = @number.reverse
            sum = 0
            reverse_card_num.scan(/./).each_with_index do |digit, index|
                digit = digit.to_i
                digit *= 2 if index % 2 != 0
                if digit.to_s.length == 2
                    first_num = digit.to_s[0..0]
                    second_num = digit.to_s[1..1]
                    digit = first_num.to_i + second_num.to_i
                end
                sum += digit
            end
            pass_check = sum % 10 == 0 ? true : false
            raise "credit card is invalid." if pass_check == false
            true
        end

        ###################################################################
        # Return a safe (masked) credit card number
        # char is the mask character, count is the number of last x digits
        # to display unmasked
        ###################################################################
        def masked_number(char = 'X', count = 4)
            len = @number.to_s.length
            card_number = char * (len - count)
            card_number << @number[-count..-1]
        end

    ###################################################################
    private
    ###################################################################

        ###################################################################
        # This allows the user to pass raw_type = 'visa',
        # 'american express', etc.
        ###################################################################
        def convert_cc_type(raw_type)
            card_type = nil
            if raw_type.is_a?(String)
                card_type = CARD_TYPE[raw_type.downcase.gsub(' ', '_').to_sym]
            else
                card_type = raw_type
            end
        end

    end

end

Tags , , , , , ,  | no comments | 1 trackback

Validating Credit Card Numbers With Ruby (Part 1)

Posted by admin Wed, 21 Mar 2007 03:50:00 GMT

Recently, while working on one of our client’s projects, I found myself needing to validate credit card numbers. Of course the most secure way to do it is to use your merchant services (i.e., Verisign PayFlowPro, etc.). However most often those services cost anywhere from $15/month and 3 cents per transaction and up.

For most purposes the business wants to simply prevent its customers from fat-fingering their credit card numbers when typing it in. But there are several pieces of information that can be validated for any given credit card like: expiration date, billing address, security code, cardholder’s name, etc.

For our purposes we simply wanted to protect customers from their own fat fingers. The Luhn algorithm does nicely for that purpose, and for the most part, keeps honest people honest.

Here it is using Ruby:

def validate_credit_card(number)
  reverse_card_num = @number.reverse
  sum = 0
  reverse_card_num.scan(/./).each_with_index do |digit, index|
    digit = digit.to_i
    digit *= 2 if index % 2 != 0
    if digit.to_s.length == 2
      first_num = digit.to_s[0..0]
      second_num = digit.to_s[1..1]
      digit = first_num.to_i + second_num.to_i
    end
    sum += digit
  end
  pass = sum % 10 == 0 ? true : false
end

To use it just pass in your 15-16 digit credit card number and it will return a boolean for pass or fail.

When I have some more time I’ll post some additional validation code that CBCI currently uses for credit cards.

Tags , , , , ,  | 5 comments | no trackbacks

RoR Sorting Collection of Hashes

Posted by admin Sat, 07 Oct 2006 04:42:00 GMT

I have found myself needing to sort a hash collection often, by a value within the hashes. The last instance where I needed this was to sort an aggregated result set of multiple union queries. So sorting with an order by clause would not work across the unions.

The solution is below. I pass a sortclause into the method that would look like this companyname asc or company_name desc.

def stats(sort_clause)

    # queries, etc.
    ...

    find_by_sql(sql).each do |acct|
      aid = acct.id.to_i
      results[aid] = {} unless results[aid]
      month = acct.monthname
      case (month)
        when 'last'
          results[aid][:spend_last] = acct.spend.to_f
        when 'this'
          results[aid][:spend_this] = acct.spend.to_f
        when 'total'
          results[aid][:company_name] = acct.company_name
          results[aid][:balance]      = acct.bal.to_f
          results[aid][:campaigns]    = acct.campaign_count.to_i
          results[aid][:keywords]     = acct.keyword_count.to_i
      end
    end

    sort_col, order = sort_clause.split(' ')
    logger.debug("sort_clause: #{sort_clause}")
    results = results.sort_by{ |item|
      item[1][sort_col.intern]
    }
    if (order =~ /desc/i)
      results.reverse!
    end
    results
end

Tags , , , , ,  | no comments | no trackbacks