From 81520a5c7ca772059d69f2b6fa7d0c340cff132e Mon Sep 17 00:00:00 2001 From: Kefa7y Date: Wed, 20 Mar 2024 18:52:48 +0400 Subject: [PATCH] Update ActiveMerchant::Billing::CreditCardMethods to v1.135.0 from upstream --- .../billing/credit_card_methods.rb | 393 +++++++++++++++--- test/unit/credit_card_methods_test.rb | 344 +++++++++++++-- 2 files changed, 639 insertions(+), 98 deletions(-) diff --git a/lib/active_merchant/billing/credit_card_methods.rb b/lib/active_merchant/billing/credit_card_methods.rb index 8dc47186c1b..d9fcc362232 100644 --- a/lib/active_merchant/billing/credit_card_methods.rb +++ b/lib/active_merchant/billing/credit_card_methods.rb @@ -1,24 +1,58 @@ +require 'set' + +# Updated to v1.135.0 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})?(\d{3})?$/, - 'master' => /^(5[1-5]\d{4}|677189|222[1-9]\d{2}|22[3-9]\d{3}|2[3-6]\d{4}|27[01]\d{3}|2720\d{2})\d{10}$/, - 'discover' => /^(6011|65\d{2}|64[4-9]\d)\d{12}|(62\d{14})$/, - 'american_express' => /^3[47]\d{13}$/, - 'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/, - 'jcb' => /^35(28|29|[3-8]\d)\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|6706|6709|6771(?!89))\d{8}(\d{4}|\d{6,7})?$/, - 'sodexo' => /^(606071|603389|606070|606069|606068|600818)\d{8}$/, - 'vr' => /^(627416|637036)\d{8}$/ + CARD_COMPANY_DETECTORS = { + 'visa' => ->(num) { num =~ /^4\d{12}(\d{3})?(\d{3})?$/ }, + 'master' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), MASTERCARD_RANGES) }, + 'elo' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), ELO_RANGES) }, + 'cabal' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 8), CABAL_RANGES) }, + 'alelo' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), ALELO_RANGES) }, + 'discover' => ->(num) { num =~ /^(6011|65\d{2}|64[4-9]\d)\d{12,15}$/ }, + 'american_express' => ->(num) { num =~ /^3[47]\d{13}$/ }, + 'naranja' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), NARANJA_RANGES) }, + 'diners_club' => ->(num) { num =~ /^3(0[0-5]|[68]\d)\d{11,16}$/ }, + 'jcb' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 4), JCB_RANGES) }, + 'dankort' => ->(num) { num =~ /^5019\d{12}$/ }, + 'maestro' => lambda { |num| + (12..19).cover?(num&.size) && ( + in_bin_range?(num.slice(0, 6), MAESTRO_RANGES) || + MAESTRO_BINS.any? { |bin| num.slice(0, bin.size) == bin } + ) + }, + 'maestro_no_luhn' => ->(num) { num =~ /^(501080|501081|501082)\d{6,13}$/ }, + 'forbrugsforeningen' => ->(num) { num =~ /^600722\d{10}$/ }, + 'sodexo' => ->(num) { num =~ /^(606071|603389|606070|606069|606068|600818|505864|505865)\d{10}$/ }, + 'alia' => ->(num) { num =~ /^(504997|505878|601030|601073|505874)\d{10}$/ }, + 'vr' => ->(num) { num =~ /^(627416|637036)\d{10}$/ }, + 'unionpay' => ->(num) { (16..19).cover?(num&.size) && in_bin_range?(num.slice(0, 8), UNIONPAY_RANGES) }, + 'carnet' => lambda { |num| + num&.size == 16 && ( + in_bin_range?(num.slice(0, 6), CARNET_RANGES) || + CARNET_BINS.any? { |bin| num.slice(0, bin.size) == bin } + ) + }, + 'cartes_bancaires' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), CARTES_BANCAIRES_RANGES) }, + 'olimpica' => ->(num) { num =~ /^636853\d{10}$/ }, + 'creditel' => ->(num) { num =~ /^601933\d{10}$/ }, + 'confiable' => ->(num) { num =~ /^560718\d{10}$/ }, + 'synchrony' => ->(num) { num =~ /^700600\d{10}$/ }, + 'routex' => ->(num) { num =~ /^(700676|700678)\d{13}$/ }, + 'mada' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), MADA_RANGES) }, + 'bp_plus' => ->(num) { num =~ /^(7050\d\s\d{9}\s\d{3}$|705\d\s\d{8}\s\d{5}$)/ }, + 'passcard' => ->(num) { num =~ /^628026\d{10}$/ }, + 'edenred' => ->(num) { num =~ /^637483\d{10}$/ }, + 'anda' => ->(num) { num =~ /^603199\d{10}$/ }, + 'tarjeta-d' => ->(num) { num =~ /^601828\d{10}$/ }, + 'hipercard' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), HIPERCARD_RANGES) }, + 'panal' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), PANAL_RANGES) } } + SODEXO_NO_LUHN = ->(num) { num =~ /^(505864|505865)\d{10}$/ } + # http://www.barclaycard.co.uk/business/files/bin_rules.pdf ELECTRON_RANGES = [ [400115], @@ -39,12 +73,198 @@ module CreditCardMethods (491730..491759), ] + CARNET_RANGES = [ + (506199..506499), + ] + + CARNET_BINS = Set.new( + %w[ + 286900 502275 606333 627535 636318 636379 639388 + 639484 639559 50633601 50633606 58877274 62753500 + 60462203 60462204 588772 + ] + ) + + CARTES_BANCAIRES_RANGES = [ + (507589..507590), + (507593..507595), + [507597], + [560408], + [581752], + (585402..585405), + (585501..585505), + (585577..585582) + ] + + # https://www.mastercard.us/content/dam/mccom/global/documents/mastercard-rules.pdf, page 73 + MASTERCARD_RANGES = [ + (222100..272099), + (510000..559999), + [605272], + [606282], + [637095], + [637568], + (637599..637600), + [637609], + ] + + MAESTRO_BINS = Set.new( + %w[ 500057 + 501018 501043 501045 501047 501049 501051 501072 501075 501083 501087 501089 501095 + 501500 501623 + 501879 502113 502120 502121 502301 + 503175 503337 503645 503670 + 504310 504338 504363 504533 504587 504620 504639 504656 504738 504781 504910 + 507001 507002 507004 507082 507090 + 560014 560565 561033 + 572402 572610 572626 + 576904 + 578614 + 581149 + 585274 585697 + 586509 + 588729 588792 + 589244 589407 589471 589605 589633 589647 589671 589916 + 590043 590206 590263 590265 590278 590361 590362 590379 590393 590590 + 591235 591420 591481 591620 591770 591948 591994 + 592024 592161 592184 592186 592201 592384 592393 592528 592566 592704 592735 592879 592884 + 593074 593264 593272 593355 593496 593556 593589 593666 593709 593825 593963 593994 + 594184 594409 594468 594475 594581 594665 594691 594710 594874 594968 + 595355 595364 595532 595547 595561 595568 595743 595929 + 596245 596289 596399 596405 596590 596608 596645 596646 596791 596808 596815 596846 + 597077 597094 597143 597370 597410 597765 597855 597862 + 598053 598054 598395 598585 598793 598794 598815 598835 598838 598880 598889 + 599000 599069 599089 599148 599191 599310 599741 599742 599867 + 601070 601452 601628 601638 + 602648 + 603326 603450 603689 + 604983 + 606126 + 608710 + 627339 627453 627454 627973 + 636117 636380 636422 636502 636639 + 637046 637529 637568 637600 637756 + 639130 639229 639350 + 690032] + ) + + # https://www.mastercard.us/content/dam/mccom/global/documents/mastercard-rules.pdf, page 79 + MAESTRO_RANGES = [ + (500032..500033), + (501015..501016), + (501020..501021), + (501023..501029), + (501038..501041), + (501053..501058), + (501060..501063), + (501066..501067), + (501091..501092), + (501104..501105), + (501107..501108), + (501104..501105), + (501107..501108), + (501800..501899), + (502000..502099), + (503800..503899), + (561200..561269), + (561271..561299), + (561320..561356), + (581700..581751), + (581753..581800), + (589300..589399), + (589998..591259), + (591261..596770), + (596772..598744), + (598746..599999), + (600297..600314), + (600316..600335), + (600337..600362), + (600364..600382), + (601232..601254), + (601256..601276), + (601640..601652), + (601689..601700), + (602011..602048), + [602050], + (630400..630499), + (639000..639099), + (670000..679999), + ] + + # https://dev.elo.com.br/apis/tabela-de-bins, download csv from left sidebar + ELO_RANGES = [ + 506707..506708, 506715..506715, 506717..506722, 506724..506736, 506739..506743, + 506745..506747, 506753..506753, 506774..506778, 509000..509007, 509009..509014, + 509020..509030, 509035..509042, 509044..509089, 509091..509101, 509104..509807, + 509831..509877, 509897..509900, 509918..509964, 509971..509986, 509995..509999, + 627780..627780, 636297..636298, 636368..636368, 650031..650033, 650035..650051, + 650057..650081, 650406..650439, 650485..650504, 650506..650538, 650552..650598, + 650720..650727, 650901..650922, 650928..650928, 650938..650939, 650946..650978, + 651652..651704, 655000..655019, 655021..655057 + ] + + # Alelo provides BIN ranges by e-mailing them out periodically. + # The BINs beginning with the digit 4 overlap with Visa's range of valid card numbers. + # By placing the 'alelo' entry in CARD_COMPANY_DETECTORS below the 'visa' entry, we + # identify these cards as Visa. This works because transactions with such cards will + # run on Visa rails. + ALELO_RANGES = [ + 402588..402588, 404347..404347, 405876..405876, 405882..405882, 405884..405884, + 405886..405886, 430471..430471, 438061..438061, 438064..438064, 470063..470066, + 496067..496067, 506699..506704, 506706..506706, 506713..506714, 506716..506716, + 506749..506750, 506752..506752, 506754..506756, 506758..506767, 506770..506771, + 506773..506773, 509015..509019, 509880..509882, 509884..509885, 509887..509887, + 509987..509992 + ] + + CABAL_RANGES = [ + 60420100..60440099, + 58965700..58965799, + 60352200..60352299, + 65027200..65027299, + 65008700..65008700 + ] + + MADA_RANGES = [ + 504300..504300, 506968..506968, 508160..508160, 585265..585265, 588848..588848, + 588850..588850, 588982..588983, 589005..589005, 589206..589206, 604906..604906, + 605141..605141, 636120..636120, 968201..968209, 968211..968211 + ] + + NARANJA_RANGES = [ + 589562..589562 + ] + + # https://www.discoverglobalnetwork.com/content/dam/discover/en_us/dgn/pdfs/IPP-VAR-Enabler-Compliance.pdf + UNIONPAY_RANGES = [ + 62000000..62000000, 62212600..62379699, 62400000..62699999, 62820000..62889999, + 81000000..81099999, 81100000..81319999, 81320000..81519999, 81520000..81639999, 81640000..81719999 + ] + + JCB_RANGES = [ + 3528..3589, 3088..3094, 3096..3102, 3112..3120, 3158..3159, 3337..3349 + ] + + HIPERCARD_RANGES = [ + 384100..384100, 384140..384140, 384160..384160, 606282..606282, 637095..637095, + 637568..637568, 637599..637599, 637609..637609, 637612..637612 + ] + + PANAL_RANGES = [[602049]] + def self.included(base) base.extend(ClassMethods) end + def self.in_bin_range?(number, ranges) + bin = number.to_i + ranges.any? do |range| + range.include?(bin) + end + end + def valid_month?(month) - (1..12).include?(month.to_i) + (1..12).cover?(month.to_i) end def credit_card? @@ -52,7 +272,7 @@ def credit_card? end def valid_expiry_year?(year) - (Time.now.year..Time.now.year + 20).include?(year.to_i) + (Time.now.year..Time.now.year + 20).cover?(year.to_i) end def valid_start_year?(year) @@ -77,7 +297,14 @@ def valid_card_verification_value?(cvv, brand) end def card_verification_value_length(brand) - brand == 'american_express' ? 4 : 3 + case brand + when 'american_express' + 4 + when 'maestro' + 0 + else + 3 + end end def valid_issue_number?(number) @@ -99,46 +326,27 @@ module ClassMethods def valid_number?(number) valid_test_mode_card_number?(number) || valid_card_number_length?(number) && - valid_card_number_characters?(number) && - valid_checksum?(number) + valid_card_number_characters?(brand?(number), number) && + valid_by_algorithm?(brand?(number), 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 + CARD_COMPANY_DETECTORS.keys end # Returns a string containing the brand 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 brand?(number) - # return 'visa' if valid_test_mode_card_number?(number) - # card_companies.find([nil]) { |brand, regexp| number =~ regexp }.first.dup - # end - # def brand?(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 + CARD_COMPANY_DETECTORS.each do |company, func| + return company.dup if func.call(number) end - return 'maestro' if number =~ card_companies['maestro'] - return nil end def electron?(number) - return false unless [16, 19].include?(number.length) + return false unless [16, 19].include?(number&.length) # don't recalculate for each range bank_identification_number = first_digits(number).to_i @@ -149,16 +357,18 @@ def electron?(number) end def type?(number) - ActiveMerchant.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?(number) end def first_digits(number) - number.to_s.slice(0,6) + number&.slice(0, 6) || '' end def last_digits(number) - number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1) + return '' if number.nil? + + number.length <= 4 ? number : number.slice(-4..-1) end def mask(number) @@ -171,26 +381,52 @@ def matching_brand?(number, brand) end def matching_type?(number, brand) - ActiveMerchant.deprecated "CreditCard#matching_type? is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#matching_brand? instead." + ActiveMerchant.deprecated 'CreditCard#matching_type? is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#matching_brand? instead.' matching_brand?(number, brand) end private def valid_card_number_length?(number) #:nodoc: - number.to_s.length >= 12 + return false if number.nil? + + number.length >= 12 end - def valid_card_number_characters?(number) #:nodoc: - !number.to_s.match(/\D/) + def valid_card_number_characters?(brand, number) #:nodoc: + return false if number.nil? + return number =~ /\A[0-9 ]+\Z/ if brand == 'bp_plus' + + !number.match(/\D/) end def valid_test_mode_card_number?(number) #:nodoc: ActiveMerchant::Billing::Base.test? && - %w[1 2 3 success failure error].include?(number.to_s) + %w[1 2 3 success failure error].include?(number) + end + + def sodexo_no_luhn?(numbers) + SODEXO_NO_LUHN.call(numbers) + end + + def valid_by_algorithm?(brand, numbers) #:nodoc: + case brand + when 'naranja' + valid_naranja_algo?(numbers) + when 'creditel' + valid_creditel_algo?(numbers) + when 'alia', 'confiable', 'maestro_no_luhn', 'anda', 'tarjeta-d', 'hipercard' + true + when 'sodexo' + sodexo_no_luhn?(numbers) ? true : valid_luhn?(numbers) + when 'bp_plus', 'passcard', 'edenred' + valid_luhn_non_zero_check_digit?(numbers) + else + valid_luhn?(numbers) + end end - ODD_LUHN_VALUE = { + BYTES_TO_DIGITS = { 48 => 0, 49 => 1, 50 => 2, @@ -204,7 +440,7 @@ def valid_test_mode_card_number?(number) #:nodoc: nil => 0 }.freeze - EVEN_LUHN_VALUE = { + BYTES_TO_DIGITS_DOUBLED = { 48 => 0, # 0 * 2 49 => 2, # 1 * 2 50 => 4, # 2 * 2 @@ -214,29 +450,68 @@ def valid_test_mode_card_number?(number) #:nodoc: 54 => 3, # 6 * 2 - 9 55 => 5, # etc ... 56 => 7, - 57 => 9, + 57 => 9 }.freeze # Checks the validity of a card number by use of the Luhn Algorithm. # Please see http://en.wikipedia.org/wiki/Luhn_algorithm for details. # This implementation is from the luhn_checksum gem, https://github.com/zendesk/luhn_checksum. - def valid_checksum?(numbers) #:nodoc: + def valid_luhn?(numbers) #:nodoc: sum = 0 odd = true - numbers.reverse.bytes.each do |number| + numbers.reverse.bytes.each do |bytes| if odd odd = false - sum += ODD_LUHN_VALUE[number] + sum += BYTES_TO_DIGITS[bytes] else odd = true - sum += EVEN_LUHN_VALUE[number] + sum += BYTES_TO_DIGITS_DOUBLED[bytes] end end sum % 10 == 0 end + + def valid_luhn_with_check_digit?(numbers, check_digit) + sum = 0 + + doubler = true + + numbers.reverse.bytes.each do |bytes| + doubler ? sum += BYTES_TO_DIGITS_DOUBLED[bytes] : sum += BYTES_TO_DIGITS[bytes] + doubler = !doubler + end + + (10 - (sum % 10)) % 10 == check_digit.to_i + end + + def valid_luhn_non_zero_check_digit?(numbers) + return valid_luhn?(numbers.delete(' ')) if numbers[5] == ' ' + + check_digit = numbers[-1] + luhn_payload = numbers.delete(' ').chop + valid_luhn_with_check_digit?(luhn_payload, check_digit) + end + + # Checks the validity of a card number by use of specific algorithms + def valid_naranja_algo?(numbers) #:nodoc: + num_array = numbers.to_s.chars.map(&:to_i) + multipliers = [4, 3, 2, 7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2] + num_sum = num_array[0..14].zip(multipliers).map { |a, b| a * b }.reduce(:+) + intermediate = 11 - (num_sum % 11) + final_num = intermediate > 9 ? 0 : intermediate + final_num == num_array[15] + end + + def valid_creditel_algo?(numbers) #:nodoc: + num_array = numbers.to_s.chars.map(&:to_i) + multipliers = [5, 4, 3, 2, 1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 9] + num_sum = num_array[0..14].zip(multipliers).map { |a, b| a * b }.reduce(:+) + final_num = num_sum % 10 + final_num == num_array[15] + end end end end -end +end \ No newline at end of file diff --git a/test/unit/credit_card_methods_test.rb b/test/unit/credit_card_methods_test.rb index 2e99504a258..a4e06d52ac1 100644 --- a/test/unit/credit_card_methods_test.rb +++ b/test/unit/credit_card_methods_test.rb @@ -1,5 +1,6 @@ require 'test_helper' +# Updated to v1.135.0 class CreditCardMethodsTest < Test::Unit::TestCase include ActiveMerchant::Billing::CreditCardMethods @@ -9,19 +10,43 @@ class CreditCard def maestro_card_numbers %w[ - 5000000000000000 5099999999999999 5600000000000000 - 5899999999999999 6000000000000000 6999999999999999 - 6761999999999999 6763000000000000 5038999999999999 + 5612590000000000 5817500000000000 5818000000000000 + 6390000000000000 6390700000000000 6390990000000000 + 6761999999999999 6763000000000000 6799999999999999 + 5000330000000000 5811499999999999 5010410000000000 + 5010630000000000 5892440000000000 5016230000000000 ] end def non_maestro_card_numbers %w[ 4999999999999999 5100000000000000 5599999999999999 - 5900000000000000 5999999999999999 7000000000000000 + 5612709999999999 5817520000000000 5818019999999999 + 5912600000000000 6000009999999999 7000000000000000 ] end + def maestro_bins + %w[500032 500057 501015 501016 501018 501020 501021 501023 501024 501025 501026 501027 501028 501029 + 501038 501039 501040 501041 501043 501045 501047 501049 501051 501053 501054 501055 501056 501057 + 501058 501060 501061 501062 501063 501066 501067 501072 501075 501083 501087 501623 + 501800 501089 501091 501092 501095 501104 501105 501107 501108 501500 501879 + 502000 502113 502301 503175 503645 503800 + 503670 504310 504338 504363 504533 504587 504620 504639 504656 504738 504781 504910 + 507001 507002 507004 507082 507090 560014 560565 561033 572402 572610 572626 576904 578614 + 585274 585697 586509 588729 588792 589244 589300 589407 589471 589605 589633 589647 589671 + 590043 590206 590263 590265 + 590278 590361 590362 590379 590393 590590 591235 591420 591481 591620 591770 591948 591994 592024 + 592161 592184 592186 592201 592384 592393 592528 592566 592704 592735 592879 592884 593074 593264 + 593272 593355 593496 593556 593589 593666 593709 593825 593963 593994 594184 594409 594468 594475 + 594581 594665 594691 594710 594874 594968 595355 595364 595532 595547 595561 595568 595743 595929 + 596245 596289 596399 596405 596590 596608 596645 596646 596791 596808 596815 596846 597077 597094 + 597143 597370 597410 597765 597855 597862 598053 598054 598395 598585 598793 598794 598815 598835 + 598838 598880 598889 599000 599069 599089 599148 599191 599310 599741 599742 599867 + 601070 604983 601638 606126 + 630400 636380 636422 636502 636639 637046 637756 639130 639229 690032] + end + def test_should_be_able_to_identify_valid_expiry_months assert_false valid_month?(-1) assert_false valid_month?(13) @@ -47,11 +72,11 @@ def test_should_be_able_to_identify_valid_start_years end def test_valid_start_year_can_handle_strings - assert valid_start_year?("2009") + assert valid_start_year?('2009') end def test_valid_month_can_handle_strings - assert valid_month?("1") + assert valid_month?('1') end def test_valid_expiry_year_can_handle_strings @@ -99,67 +124,268 @@ def test_should_detect_electron_dk_as_visa def test_should_detect_diners_club assert_equal 'diners_club', CreditCard.brand?('36148010000000') + assert_equal 'diners_club', CreditCard.brand?('3000000000000004') end def test_should_detect_diners_club_dk assert_equal 'diners_club', CreditCard.brand?('30401000000000') end + def test_should_detect_jcb_cards + assert_equal 'jcb', CreditCard.brand?('3528000000000000') + assert_equal 'jcb', CreditCard.brand?('3580000000000000') + assert_equal 'jcb', CreditCard.brand?('3088000000000017') + assert_equal 'jcb', CreditCard.brand?('3094000000000017') + assert_equal 'jcb', CreditCard.brand?('3096000000000000') + assert_equal 'jcb', CreditCard.brand?('3102000000000017') + assert_equal 'jcb', CreditCard.brand?('3112000000000000') + assert_equal 'jcb', CreditCard.brand?('3120000000000017') + assert_equal 'jcb', CreditCard.brand?('3158000000000000') + assert_equal 'jcb', CreditCard.brand?('3159000000000017') + assert_equal 'jcb', CreditCard.brand?('3337000000000000') + assert_equal 'jcb', CreditCard.brand?('3349000000000017') + end + def test_should_detect_maestro_dk_as_maestro assert_equal 'maestro', CreditCard.brand?('6769271000000000') end def test_should_detect_maestro_cards - assert_equal 'maestro', CreditCard.brand?('5020100000000000') + assert_equal 'maestro', CreditCard.brand?('675675000000000') maestro_card_numbers.each { |number| assert_equal 'maestro', CreditCard.brand?(number) } + maestro_bins.each { |bin| assert_equal 'maestro', CreditCard.brand?("#{bin}0000000000") } non_maestro_card_numbers.each { |number| assert_not_equal 'maestro', CreditCard.brand?(number) } end def test_should_detect_mastercard - assert_equal 'master', CreditCard.brand?('6771890000000000') + assert_equal 'master', CreditCard.brand?('2720890000000000') assert_equal 'master', CreditCard.brand?('5413031000000000') + assert_equal 'master', CreditCard.brand?('6052721000000000') + assert_equal 'master', CreditCard.brand?('6062821000000000') + assert_equal 'master', CreditCard.brand?('6370951000000000') + assert_equal 'master', CreditCard.brand?('6375681000000000') + assert_equal 'master', CreditCard.brand?('6375991000000000') + assert_equal 'master', CreditCard.brand?('6376091000000000') end def test_should_detect_forbrugsforeningen assert_equal 'forbrugsforeningen', CreditCard.brand?('6007221000000000') end - def test_should_detect_laser_card - # 16 digits - assert_equal 'laser', CreditCard.brand?('6304985028090561') + def test_should_detect_sodexo_card + assert_equal 'sodexo', CreditCard.brand?('6060694495764400') + end - # 18 digits - assert_equal 'laser', CreditCard.brand?('630498502809056151') + def test_should_detect_alia_card + assert_equal 'alia', CreditCard.brand?('5049970000000000') + assert_equal 'alia', CreditCard.brand?('5058780000000000') + assert_equal 'alia', CreditCard.brand?('6010300000000000') + assert_equal 'alia', CreditCard.brand?('6010730000000000') + assert_equal 'alia', CreditCard.brand?('5058740000000000') + end - # 19 digits - assert_equal 'laser', CreditCard.brand?('6304985028090561515') + def test_should_detect_mada_card + assert_equal 'mada', CreditCard.brand?('5043000000000000') + assert_equal 'mada', CreditCard.brand?('5852650000000000') + assert_equal 'mada', CreditCard.brand?('5888500000000000') + assert_equal 'mada', CreditCard.brand?('6361200000000000') + assert_equal 'mada', CreditCard.brand?('9682040000000000') + end - # 17 digits - assert_not_equal 'laser', CreditCard.brand?('63049850280905615') + def test_alia_number_not_validated + 10.times do + number = rand(5058740000000001..5058749999999999).to_s + assert_equal 'alia', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + end + end - # 15 digits - assert_not_equal 'laser', CreditCard.brand?('630498502809056') + def test_should_detect_confiable_card + assert_equal 'confiable', CreditCard.brand?('5607180000000000') + end - # Alternate format - assert_equal 'laser', CreditCard.brand?('6706950000000000000') + def test_should_detect_bp_plus_card + assert_equal 'bp_plus', CreditCard.brand?('70501 501021600 378') + assert_equal 'bp_plus', CreditCard.brand?('70502 111111111 111') + assert_equal 'bp_plus', CreditCard.brand?('7050 15605297 00114') + assert_equal 'bp_plus', CreditCard.brand?('7050 15546992 00062') + end - # Alternate format (16 digits) - assert_equal 'laser', CreditCard.brand?('6706123456789012') + def test_should_validate_bp_plus_card + assert_true CreditCard.valid_number?('70501 501021600 378') + assert_true CreditCard.valid_number?('7050 15605297 00114') + assert_true CreditCard.valid_number?('7050 15546992 00062') + assert_true CreditCard.valid_number?('7050 16150146 00110') + assert_true CreditCard.valid_number?('7050 16364764 00070') - # New format (16 digits) - assert_equal 'laser', CreditCard.brand?('6709123456789012') + # numbers with invalid formats + assert_false CreditCard.valid_number?('7050_15546992_00062') + assert_false CreditCard.valid_number?('70501 55469920 0062') + assert_false CreditCard.valid_number?('70 501554699 200062') - # Ulster bank (Ireland) with 12 digits - assert_equal 'laser', CreditCard.brand?('677117111234') + # numbers that are luhn-invalid + assert_false CreditCard.valid_number?('70502 111111111 111') + assert_false CreditCard.valid_number?('7050 16364764 00071') + assert_false CreditCard.valid_number?('7050 16364764 00072') end - def test_should_detect_sodexo_card - assert_equal 'sodexo', CreditCard.brand?('60606944957644') + def test_confiable_number_not_validated + 10.times do + number = rand(5607180000000001..5607189999999999).to_s + assert_equal 'confiable', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + end + end + + def test_should_detect_maestro_no_luhn_card + assert_equal 'maestro_no_luhn', CreditCard.brand?('5010800000000000') + assert_equal 'maestro_no_luhn', CreditCard.brand?('5010810000000000') + assert_equal 'maestro_no_luhn', CreditCard.brand?('5010820000000000') + assert_equal 'maestro_no_luhn', CreditCard.brand?('501082000000') + assert_equal 'maestro_no_luhn', CreditCard.brand?('5010820000000000000') + end + + def test_maestro_no_luhn_number_not_validated + 10.times do + number = rand(5010800000000001..5010829999999999).to_s + assert_equal 'maestro_no_luhn', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + end + end + + def test_should_detect_olimpica_card + assert_equal 'olimpica', CreditCard.brand?('6368530000000000') + end + + def test_should_detect_sodexo_no_luhn_card + number1 = '5058645584812145' + number2 = '5058655584812145' + assert_equal 'sodexo', CreditCard.brand?(number1) + assert CreditCard.valid_number?(number1) + assert_equal 'sodexo', CreditCard.brand?(number2) + assert CreditCard.valid_number?(number2) + end + + def test_should_validate_sodexo_no_luhn_card + assert_true CreditCard.valid_number?('5058645584812145') + assert_false CreditCard.valid_number?('5058665584812110') + end + + def test_should_detect_passcard_card + assert_equal 'passcard', CreditCard.brand?('6280260025383009') + assert_equal 'passcard', CreditCard.brand?('6280260025383280') + assert_equal 'passcard', CreditCard.brand?('6280260025383298') + assert_equal 'passcard', CreditCard.brand?('6280260025383306') + assert_equal 'passcard', CreditCard.brand?('6280260025383314') + end + + def test_should_validate_passcard_card + assert_true CreditCard.valid_number?('6280260025383009') + # numbers with invalid formats + assert_false CreditCard.valid_number?('6280_26002538_0005') + # numbers that are luhn-invalid + assert_false CreditCard.valid_number?('6280260025380991') + end + + def test_should_detect_edenred_card + assert_equal 'edenred', CreditCard.brand?('6374830000000823') + assert_equal 'edenred', CreditCard.brand?('6374830000000799') + assert_equal 'edenred', CreditCard.brand?('6374830000000807') + assert_equal 'edenred', CreditCard.brand?('6374830000000815') + assert_equal 'edenred', CreditCard.brand?('6374830000000823') + end + + def test_should_validate_edenred_card + assert_true CreditCard.valid_number?('6374830000000369') + # numbers with invalid formats + assert_false CreditCard.valid_number?('6374 8300000 00369') + # numbers that are luhn-invalid + assert_false CreditCard.valid_number?('6374830000000111') + end + + def test_should_detect_anda_card + assert_equal 'anda', CreditCard.brand?('6031998427187914') + end + + # Creditos directos a.k.a tarjeta d + def test_should_detect_tarjetad_card + assert_equal 'tarjeta-d', CreditCard.brand?('6018282227431033') + end + + def test_should_detect_creditel_card + assert_equal 'creditel', CreditCard.brand?('6019330047539016') end def test_should_detect_vr_card - assert_equal 'vr', CreditCard.brand?('63703644957644') + assert_equal 'vr', CreditCard.brand?('6370364495764400') + assert_equal 'vr', CreditCard.brand?('6274160000000001') + end + + def test_should_detect_elo_card + assert_equal 'elo', CreditCard.brand?('5090510000000000') + assert_equal 'elo', CreditCard.brand?('5067530000000000') + assert_equal 'elo', CreditCard.brand?('6277800000000000') + assert_equal 'elo', CreditCard.brand?('6509550000000000') + assert_equal 'elo', CreditCard.brand?('5090890000000000') + assert_equal 'elo', CreditCard.brand?('5092570000000000') + assert_equal 'elo', CreditCard.brand?('5094100000000000') + end + + def test_should_detect_alelo_card + assert_equal 'alelo', CreditCard.brand?('5067490000000010') + assert_equal 'alelo', CreditCard.brand?('5067700000000028') + assert_equal 'alelo', CreditCard.brand?('5067600000000036') + assert_equal 'alelo', CreditCard.brand?('5067600000000044') + assert_equal 'alelo', CreditCard.brand?('5099920000000000') + assert_equal 'alelo', CreditCard.brand?('5067630000000000') + assert_equal 'alelo', CreditCard.brand?('5098870000000000') + end + + def test_should_detect_naranja_card + assert_equal 'naranja', CreditCard.brand?('5895627823453005') + assert_equal 'naranja', CreditCard.brand?('5895620000000002') + assert_equal 'naranja', CreditCard.brand?('5895626746595650') + end + + # Alelo BINs beginning with the digit 4 overlap with Visa's range of valid card numbers. + # We intentionally misidentify these cards as Visa, which works because transactions with + # such cards will run on Visa rails. + def test_should_detect_alelo_number_beginning_with_4_as_visa + assert_equal 'visa', CreditCard.brand?('4025880000000010') + assert_equal 'visa', CreditCard.brand?('4025880000000028') + assert_equal 'visa', CreditCard.brand?('4025880000000036') + assert_equal 'visa', CreditCard.brand?('4025880000000044') + end + + def test_should_detect_cabal_card + assert_equal 'cabal', CreditCard.brand?('6044009000000000') + assert_equal 'cabal', CreditCard.brand?('5896575500000000') + assert_equal 'cabal', CreditCard.brand?('6035224400000000') + assert_equal 'cabal', CreditCard.brand?('6502723300000000') + assert_equal 'cabal', CreditCard.brand?('6500870000000000') + end + + def test_should_detect_unionpay_card + assert_equal 'unionpay', CreditCard.brand?('6221260000000000') + assert_equal 'unionpay', CreditCard.brand?('6250941006528599') + assert_equal 'unionpay', CreditCard.brand?('6282000000000000') + assert_equal 'unionpay', CreditCard.brand?('8100000000000000') + assert_equal 'unionpay', CreditCard.brand?('814400000000000000') + assert_equal 'unionpay', CreditCard.brand?('8171999927660000') + assert_equal 'unionpay', CreditCard.brand?('8171999900000000021') + assert_equal 'unionpay', CreditCard.brand?('6200000000000005') + end + + def test_should_detect_synchrony_card + assert_equal 'synchrony', CreditCard.brand?('7006000000000000') + end + + def test_should_detect_routex_card + number = '7006760000000000000' + assert_equal 'routex', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + assert_equal 'routex', CreditCard.brand?('7006789224703725591') end def test_should_detect_when_an_argument_brand_does_not_match_calculated_brand @@ -168,14 +394,14 @@ def test_should_detect_when_an_argument_brand_does_not_match_calculated_brand end def test_detecting_full_range_of_maestro_card_numbers - maestro = '50000000000' + maestro = '63900000000' assert_equal 11, maestro.length assert_not_equal 'maestro', CreditCard.brand?(maestro) while maestro.length < 19 maestro << '0' - assert_equal 'maestro', CreditCard.brand?(maestro) + assert_equal 'maestro', CreditCard.brand?(maestro), "Failed for bin #{maestro}" end assert_equal 19, maestro.length @@ -187,7 +413,6 @@ def test_detecting_full_range_of_maestro_card_numbers def test_matching_discover_card assert_equal 'discover', CreditCard.brand?('6011000000000000') assert_equal 'discover', CreditCard.brand?('6500000000000000') - assert_equal 'discover', CreditCard.brand?('6221260000000000') assert_equal 'discover', CreditCard.brand?('6450000000000000') assert_not_equal 'discover', CreditCard.brand?('6010000000000000') @@ -195,26 +420,60 @@ def test_matching_discover_card end def test_matching_invalid_card - assert_nil CreditCard.brand?("XXXXXXXXXXXX0000") - assert_false CreditCard.valid_number?("XXXXXXXXXXXX0000") + assert_nil CreditCard.brand?('XXXXXXXXXXXX0000') + assert_false CreditCard.valid_number?('XXXXXXXXXXXX0000') + assert_false CreditCard.valid_number?(nil) + end + + def test_matching_valid_naranja + number = '5895627823453005' + assert_equal 'naranja', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + end + + def test_matching_valid_creditel + number = '6019330047539016' + assert_equal 'creditel', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) end def test_16_digit_maestro_uk number = '6759000000000000' assert_equal 16, number.length - assert_equal 'switch', CreditCard.brand?(number) + assert_equal 'maestro', CreditCard.brand?(number) end def test_18_digit_maestro_uk number = '675900000000000000' assert_equal 18, number.length - assert_equal 'switch', CreditCard.brand?(number) + assert_equal 'maestro', CreditCard.brand?(number) end def test_19_digit_maestro_uk number = '6759000000000000000' assert_equal 19, number.length - assert_equal 'switch', CreditCard.brand?(number) + assert_equal 'maestro', CreditCard.brand?(number) + end + + def test_carnet_cards + numbers = %w[ + 5062280000000000 + 6046220312312312 + 6393889871239871 + 5022751231231231 + 6275350000000001 + ] + numbers.each do |num| + assert_equal 16, num.length + assert_equal 'carnet', CreditCard.brand?(num) + end + end + + def test_should_detect_cartes_bancaires_cards + assert_equal 'cartes_bancaires', CreditCard.brand?('5855010000000000') + assert_equal 'cartes_bancaires', CreditCard.brand?('5075935000000000') + assert_equal 'cartes_bancaires', CreditCard.brand?('5075901100000000') + assert_equal 'cartes_bancaires', CreditCard.brand?('5075890130000000') end def test_electron_cards @@ -230,6 +489,9 @@ def test_electron_cards end end + # nil check + assert_false electron_test.call(nil) + # Visa range assert_false electron_test.call('4245180000000000') assert_false electron_test.call('4918810000000000') @@ -241,7 +503,11 @@ def test_electron_cards assert_false electron_test.call('42496200000000000') end + def test_should_detect_panal_card + assert_equal 'panal', CreditCard.brand?('6020490000000000') + end + def test_credit_card? assert credit_card.credit_card? end -end +end \ No newline at end of file