Class: CreditCardValidations::Detector

Inherits:
Object
  • Object
show all
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

Mmi::ISSUER_CATEGORIES

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Mmi

#issuer_category

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

#numberObject (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, options = {})
  # 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: 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

Returns:

  • (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.).

Returns:

  • (Boolean)

Raises:



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_nameObject



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

#last4Object

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_brandsObject

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

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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

Returns:

  • (Boolean)


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

Returns:

  • (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