Class: CreditCardValidations::Detector
- Inherits:
-
Object
- Object
- CreditCardValidations::Detector
- Includes:
- Mmi
- Defined in:
- lib/credit_card_validations/detector.rb
Constant Summary collapse
- LEGACY_PLUGIN_BRANDS =
Brands that were part of the default set up to v8.x and moved to opt-in plugins in v9.0. The shim below auto-loads the plugin on first reference and emits a one-time deprecation warning. To be removed in v10.0 — users should add explicit ‘require` statements by then.
%i[mir rupay elo dankort hipercard solo switch].freeze
- @@legacy_autoloaded =
{}
Constants included from Mmi
Instance Attribute Summary collapse
-
#number ⇒ Object
readonly
Returns the value of attribute number.
Class Method Summary collapse
-
.add_brand(key, rules, options = {}) ⇒ Object
add brand.
-
.add_rule(key, length, prefixes) ⇒ Object
create rule for detecting brand.
- .brand_key(brand_name) ⇒ Object
- .brand_name(brand_key) ⇒ Object
-
.delete_brand(key) ⇒ Object
CreditCardValidations.delete_brand(:en_route).
- .has_luhn_check_rule?(key) ⇒ Boolean
-
.valid_cvv?(code, brand) ⇒ Boolean
Class-level CVV check: validates a code against an explicit brand, without needing a Detector instance.
Instance Method Summary collapse
-
#brand(*keys) ⇒ Object
brand name.
- #brand_name ⇒ Object
-
#formatted(separator = ' ') ⇒ Object
Human-readable PAN grouped per network convention.
-
#initialize(number) ⇒ Detector
constructor
A new instance of Detector.
-
#last4 ⇒ Object
Last four digits of the PAN, or nil if the PAN has fewer than 4 digits.
-
#masked(mask_char = '*') ⇒ Object
PAN with every digit but the last 4 replaced by mask_char.
-
#possible_brands ⇒ Object
All brands whose prefixes can still match the (possibly partial) PAN.
-
#valid?(*brands) ⇒ Boolean
credit card number validation.
-
#valid_cvv?(code) ⇒ Boolean
Validates the card verification value against the detected brand’s declared :code size.
-
#valid_luhn? ⇒ Boolean
check if luhn valid.
- #valid_number?(*keys) ⇒ Boolean
Methods included from Mmi
Constructor Details
#initialize(number) ⇒ Detector
Returns a new instance of Detector.
21 22 23 |
# File 'lib/credit_card_validations/detector.rb', line 21 def initialize(number) @number = number.to_s.gsub(/[\s\-]/, '') end |
Instance Attribute Details
#number ⇒ Object (readonly)
Returns the value of attribute number.
19 20 21 |
# File 'lib/credit_card_validations/detector.rb', line 19 def number @number end |
Class Method Details
.add_brand(key, rules, options = {}) ⇒ Object
add brand
CreditCardValidations.add_brand(:en_route, {length: 15, prefixes: ['2014', '2149']}, {skip_luhn: true}) #skip luhn
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/credit_card_validations/detector.rb', line 178 def add_brand(key, rules, = {}) # Mark legacy plugin brands as handled so the v9 auto-require shim # never re-loads them after the user takes any explicit action # (require, add_brand, or a later delete_brand). @@legacy_autoloaded[key] = true if LEGACY_PLUGIN_BRANDS.include?(key) brands[key] = {rules: [], options: || {}} Array.wrap(rules).each do |rule| add_rule(key, rule[:length], rule[:prefixes]) end define_brand_method(key) end |
.add_rule(key, length, prefixes) ⇒ Object
create rule for detecting brand
217 218 219 220 221 222 223 |
# File 'lib/credit_card_validations/detector.rb', line 217 def add_rule(key, length, prefixes) unless brands.has_key?(key) raise Error.new("brand #{key} is undefined, please use #add_brand method") end length, prefixes = Array(length), Array(prefixes) brands[key][:rules] << {length: length, regexp: compile_regexp(prefixes), prefixes: prefixes} end |
.brand_key(brand_name) ⇒ Object
203 204 205 206 207 |
# File 'lib/credit_card_validations/detector.rb', line 203 def brand_key(brand_name) brands.detect do |_, brand| brand[:options][:brand_name] == brand_name end.try(:first) end |
.brand_name(brand_key) ⇒ Object
194 195 196 197 198 199 200 201 |
# File 'lib/credit_card_validations/detector.rb', line 194 def brand_name(brand_key) brand = brands[brand_key] if brand brand.fetch(:options, {})[:brand_name] || brand_key.to_s.titleize else nil end end |
.delete_brand(key) ⇒ Object
CreditCardValidations.delete_brand(:en_route)
210 211 212 213 214 |
# File 'lib/credit_card_validations/detector.rb', line 210 def delete_brand(key) key = key.to_sym undef_brand_method(key) brands.reject! { |k, _| k == key } end |
.has_luhn_check_rule?(key) ⇒ Boolean
159 160 161 |
# File 'lib/credit_card_validations/detector.rb', line 159 def has_luhn_check_rule?(key) !brands[key].fetch(:options, {}).fetch(:skip_luhn, false) end |
.valid_cvv?(code, brand) ⇒ Boolean
Class-level CVV check: validates a code against an explicit brand, without needing a Detector instance. Useful when only the brand is known (form input bound to a brand select, separate CVV field, etc.).
166 167 168 169 170 171 |
# File 'lib/credit_card_validations/detector.rb', line 166 def valid_cvv?(code, brand) return false if code.nil? || brand.nil? || !code.to_s.match?(/\A\d+\z/) spec = brands.dig(brand, :options, :code) raise Error, "brand #{brand.inspect} has no :code option" if spec.nil? code.to_s.length == spec[:size] end |
Instance Method Details
#brand(*keys) ⇒ Object
brand name
31 32 33 |
# File 'lib/credit_card_validations/detector.rb', line 31 def brand(*keys) valid_number?(*keys) end |
#brand_name ⇒ Object
56 57 58 |
# File 'lib/credit_card_validations/detector.rb', line 56 def brand_name self.class.brand_name(brand) end |
#formatted(separator = ' ') ⇒ Object
Human-readable PAN grouped per network convention. Falls back to the first possible brand while the user is still typing.
90 91 92 93 94 95 |
# File 'lib/credit_card_validations/detector.rb', line 90 def formatted(separator = ' ') groups_for(brand || possible_brands.first).each_with_object([]) do |size, acc| slice = number[acc.join.length, size] acc << slice if slice && !slice.empty? end.join(separator) end |
#last4 ⇒ Object
Last four digits of the PAN, or nil if the PAN has fewer than 4 digits.
61 62 63 |
# File 'lib/credit_card_validations/detector.rb', line 61 def last4 number.length >= 4 ? number[-4, 4] : nil end |
#masked(mask_char = '*') ⇒ Object
PAN with every digit but the last 4 replaced by mask_char. Returns the original number when shorter than 4 digits — never raises.
67 68 69 70 |
# File 'lib/credit_card_validations/detector.rb', line 67 def masked(mask_char = '*') return number if number.length < 4 mask_char.to_s[0] * (number.length - 4) + last4.to_s end |
#possible_brands ⇒ Object
All brands whose prefixes can still match the (possibly partial) PAN. Length and Luhn are not checked — useful for live UX before the user finishes typing.
75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/credit_card_validations/detector.rb', line 75 def possible_brands return [] if number.empty? self.class.brands.each_with_object([]) do |(key, brand), acc| next unless brand.fetch(:rules).any? do |rule| rule[:prefixes].any? do |prefix| n = [number.length, prefix.length].min number[0, n] == prefix[0, n] end end acc << key end end |
#valid?(*brands) ⇒ Boolean
credit card number validation
26 27 28 |
# File 'lib/credit_card_validations/detector.rb', line 26 def valid?(*brands) !!valid_number?(*brands) end |
#valid_cvv?(code) ⇒ Boolean
Validates the card verification value against the detected brand’s declared :code size. Returns false when the brand cannot be determined from the PAN or the input has the wrong shape. Raises when a detected brand is missing :code in the registry.
101 102 103 |
# File 'lib/credit_card_validations/detector.rb', line 101 def valid_cvv?(code) self.class.valid_cvv?(code, brand) end |
#valid_luhn? ⇒ Boolean
check if luhn valid
52 53 54 |
# File 'lib/credit_card_validations/detector.rb', line 52 def valid_luhn? @valid_luhn ||= Luhn.valid?(number) end |
#valid_number?(*keys) ⇒ Boolean
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/credit_card_validations/detector.rb', line 35 def valid_number?(*keys) selected_brands = keys.blank? ? self.brands : resolve_keys(*keys) if selected_brands.any? matched_brands = [] selected_brands.each do |key, brand| match_data = matches_brand?(brand) matched_brands << {brand: key, matched_prefix_length: match_data.to_s.length} if match_data end if matched_brands.present? return matched_brands.sort{|a, b| a[:matched_prefix_length] <=> b[:matched_prefix_length]}.last[:brand] end end nil end |