Created
November 14, 2014 11:35
-
-
Save pamio/db2703f6ff6368cde94a to your computer and use it in GitHub Desktop.
Credit Card Verification
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CreditCard | |
require 'date' | |
SUPPORTED_CARD_BRANDS = { | |
:MASTERCARD => "mastercard", | |
:AMEX => "american_express", | |
:DINNERS => "diners_club", | |
:DISCOVER => "discover", | |
:MAESTRO => "maestro", | |
:VISA => "visa", | |
:JCB => "jcb" | |
} | |
def initialize(card_num, month, year, cvv) | |
@card = card_num | |
@cvv = cvv | |
@month = month | |
@year = year | |
end | |
def validate | |
errors = validate_essential_attributes + validate_verification_value | |
errors_hash( | |
errors + | |
validate_card_brand_and_number | |
) | |
end | |
def errors_hash(array) | |
array.inject({}) do |hash, (attribute, error)| | |
(hash[attribute] ||= []) << error | |
hash | |
end | |
end | |
def expiry_date | |
ExpiryDate.new(@month, @year) | |
end | |
def expired? | |
expiry_date.expired? | |
end | |
def brand | |
type | |
end | |
def valid_card_verification_value?(cvv, type) | |
cvv.to_s =~ /^\d{#{card_verification_value_length(type)}}$/ | |
end | |
def card_verification_value_length(type) | |
type == 'american_express' ? 4 : 3 | |
end | |
def valid_month?(month) | |
(1..12).include?(month.to_i) | |
end | |
def valid_expiry_year?(year) | |
(Time.now.year..Time.now.year + 20).include?(year.to_i) | |
end | |
def valid_start_year?(year) | |
((year.to_s =~ /^\d{4}$/) && (year.to_i > 1987)) | |
end | |
# check sum of credit card number using luhn's alogorithm | |
def self.check_luhn(card_num) | |
card_num.gsub!(/[^0-9]/, "") | |
ss = card_num.reverse.split(//) | |
alternate = false | |
total = 0 | |
ss.each do |c| | |
if alternate | |
total += double_it(c.to_i) | |
else | |
total += c.to_i | |
end | |
alternate = !alternate | |
end | |
(total % 10) == 0 | |
end | |
# return credit card type if its one of the supported cards | |
def type | |
if @card =~ /^5[1-5][0-9]{14}$/ | |
return SUPPORTED_CARD_BRANDS[:MASTERCARD] | |
elsif @card.match(/^4[0-9]{12}([0-9]{3})?$/) | |
return SUPPORTED_CARD_BRANDS[:VISA] | |
elsif @card.match(/^3[47][0-9]{13}$/) | |
return SUPPORTED_CARD_BRANDS[:AMEX] | |
elsif @card =~ /^3(0[0-5]|[68][0-9])[0-9]{11}$/ | |
return SUPPORTED_CARD_BRANDS[:DINNERS] | |
elsif @card =~ /^6011[0-9]{12}$/ | |
return SUPPORTED_CARD_BRANDS[:DISCOVER] | |
elsif @card =~ /^(3[0-9]{4}|2131|1800)[0-9]{11}$/ | |
return SUPPORTED_CARD_BRANDS[:JCB] | |
elsif @card =~ /^(5[06-8]|6)[0-9]{10,17}$/ | |
return SUPPORTED_CARD_BRANDS[:MAESTRO] | |
else | |
return nil | |
end | |
end | |
def valid_number? | |
valid = CreditCard::check_luhn(@card) | |
end | |
# check if valid | |
def valid? | |
errors = validate() | |
return false unless empty?(errors) | |
return true | |
end | |
def empty?(value) | |
case value | |
when nil | |
true | |
when Array, Hash | |
value.empty? | |
when String | |
value.strip.empty? | |
when Numeric | |
(value == 0) | |
else | |
false | |
end | |
end | |
private | |
def validate_essential_attributes | |
errors = [] | |
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_brand_and_number | |
errors = [] | |
if empty?(@card) | |
errors << [:number, "is required"] | |
elsif !valid_number? | |
errors << [:number, "is not a valid credit card number"] | |
end | |
if errors.empty? | |
errors << [:brand, "card not supported"] if type.nil? | |
end | |
errors | |
end | |
def validate_verification_value | |
errors = [] | |
if @cvv | |
unless valid_card_verification_value?(@cvv, self.type) | |
errors << [:cvv, "should be #{card_verification_value_length(self.type)} digits"] | |
end | |
else | |
errors << [:cvv, "is required"] | |
end | |
errors | |
end | |
def self.double_it(i) | |
i = i * 2 | |
if i > 9 | |
i = i % 10 + 1 | |
end | |
i | |
end | |
class ExpiryDate | |
attr_reader :month, :year | |
def initialize(month, year) | |
@month = month.to_i | |
@year = year.to_i | |
end | |
def expired? | |
Time.now.utc > expiration | |
end | |
def expiration | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment