CreditCardValidations
Gem adds a validator to check whether a given number actually falls within the ranges of possible numbers prior to performing verification — CreditCardValidations verifies that the credit card number provided is well-formed.
More info about card BIN numbers: http://en.wikipedia.org/wiki/Bank_card_number
Installation
Add this line to your application's Gemfile:
$ gem 'credit_card_validations'And then execute:
$ bundleOr install it yourself as:
$ gem install credit_card_validationsDefault brands
These brands are detected out of the box. They are the international majors that most acquirers, gateways, and payment forms care about:
| Name | Key |
|---|---|
| American Express | :amex |
| China UnionPay | :unionpay |
| Diners Club | :diners |
| Discover | :discover |
| JCB | :jcb |
| Maestro | :maestro |
| MasterCard | :mastercard |
| Visa | :visa |
Opt-in plugins
Everything else is detected only when its plugin is explicitly required. Plugins add no startup cost, no API surface, and no chance of misdetection to apps that don't accept the brand.
Active regional and specialty networks
| Name | Key |
|---|---|
| Cabal | :cabal |
| Carnet | :carnet |
| Cartes Bancaires | :cartes_bancaires |
| Dankort | :dankort |
| DinaCard | :dinacard |
| Elo | :elo |
| Girocard | :girocard |
| Hiper | :hiper |
| Hipercard | :hipercard |
| Humo | :humocard |
| Mada | :mada |
| MIR | :mir |
| Naranja | :naranja |
| RuPay | :rupay |
| Troy | :troy |
| UATP | :uatp |
| Uzcard | :uzcard |
| V Pay | :vpay |
| Verve | :verve |
| Voyager | :voyager |
Legacy / withdrawn networks
| Name | Key | Status |
|---|---|---|
| Diners Club US | :diners_us |
Issued on MasterCard BINs since 2004 |
| EnRoute | :en_route |
Withdrawn 1989 |
| Laser | :laser |
Withdrawn 2014 |
| Solo | :solo |
Withdrawn 2011 |
| Switch | :switch |
Withdrawn 2002 |
Loading plugins
# in an initializer or before first use
require 'credit_card_validations/plugins/mir'
require 'credit_card_validations/plugins/elo'
require 'credit_card_validations/plugins/hipercard'
# ... whichever brands the app actually acceptsMigrating from v9.x → v10.0 (unreleased)
On master, ahead of the v10.0 release, the auto-require shim is gone. In v9.0 the seven brands below moved out of the
default set, and referencing one of them still worked — the plugin was loaded
for you and a deprecation warning was printed once. v10.0 removes that.
If your code names any of these brands, add the matching require:
# config/initializers/credit_card_validations.rb
require 'credit_card_validations/plugins/mir'
require 'credit_card_validations/plugins/elo'
# ... whichever brands the app actually acceptsWithout it the brand is simply unknown. Nothing warns — the v9 deprecation
warning is gone too — and the read-only paths stay quiet, so a missing
require will not announce itself where you are most likely to notice it:
Detector.new('2202 1234 1234 1234').brand #=> nil
Detector.new('2202 1234 1234 1234').valid?(:mir) #=> false
Detector.new('2202 1234 1234 1234').brand_name #=> nil
Detector.new('2202 1234 1234 1234').possible_brands #=> []
Detector.brand_name(:mir) #=> nil
Detector.valid_cvv?('123', :mir) #=> false
Detector.has_luhn_check_rule?(:mir) #=> true (nothing opted out)
'2202 1234 1234 1234'.credit_card_brand #=> nil
'2202 1234 1234 1234'.credit_card_brand_name #=> nil
'2202 1234 1234 1234'.valid_credit_card_brand?(:mir) #=> falseFour paths do raise, because there is nothing sensible to return:
Detector.new(pan).mir? # NoMethodError — the predicate is never defined
CreditCardValidations::Factory.random(:mir) # CreditCardValidations::Error: Unsupported brand
Detector.add_rule(:mir, 16, ['2202']) # CreditCardValidations::Error: brand mir is undefined
Detector::LEGACY_PLUGIN_BRANDS # NameError — see belowActiveModel validators never raise on a missing plugin; the record is just invalid. Which means a PAN field restricted to a plugin brand rejects every number you give it, including a valid card of that brand:
validates :number, credit_card_number: { brands: [:mir] } # nothing passes
validates :cvv, credit_card_cvv: { brand: :mir } # nothing passesDetector::LEGACY_PLUGIN_BRANDS was a public constant in v9 and is gone. An
app that read it to build its own require list now gets NameError; replace it
with a literal list of the brands you accept.
So check the brand list against your initializer rather than waiting for an error — in the paths an app exercises most, there will not be one.
| Brand | Status |
|---|---|
:dankort |
Active (Denmark) |
:elo |
Active (Brazil) |
:hipercard |
Active (Brazil) |
:mir |
Active (Russia) |
:rupay |
Active (India) |
:solo |
Withdrawn 2011 |
:switch |
Withdrawn 2002 |
Migrating from v8.x → v9.0
Seven brands moved from the default brand set to opt-in plugins in v9.0. Up to v9.x an auto-require shim kept existing code working with a one-time deprecation warning per brand; it is removed on master for v10.0, see above.
Other breaking changes in v9.0
-
Hipercardcleaned up. Length changed from 19 to 16 (which is the issued length); legacy637*prefixes that actually belong to Hiper were dropped. If you used Hipercard before, your detection now matches the brand's real spec. -
Discovercleaned up. Diners-only prefixes (300-305, 3095, 36, 38, 39) were dropped from Discover. Diners cards now correctly detect as:dinersinstead of:discover. Apps that branched on:discoverfor routing should branch on[:diners, :discover]. -
Luhn.valid?is now strict. It accepts a digit-only string and returnsfalsefornil, empty input, or any non-digit character. User-facing input handling moved intoDetector#initialize, which strips whitespace and dashes before delegating. -
Brand YAML is loaded via
YAML.safe_load_file. Custom brand sources may need to declarepermitted_classes: [Symbol]if they relied on extra Ruby objects.
Usage
String monkey patch
require 'credit_card_validations/string'
'5274 5763 9425 9961'.credit_card_brand #=> :mastercard
'5274 5763 9425 9961'.credit_card_brand_name #=> "MasterCard"
'5274 5763 9425 9961'.valid_credit_card_brand?(:mastercard, :visa) #=> true
'5274 5763 9425 9961'.valid_credit_card_brand?(:amex) #=> false
'5274 5763 9425 9961'.valid_credit_card_brand?('MasterCard') #=> trueActiveModel validators
Restrict to a brand list:
class CreditCardModel
attr_accessor :number
include ActiveModel::Validations
validates :number, credit_card_number: { brands: [:amex, :maestro] }
endAccept any known brand:
validates :number, presence: true, credit_card_number: trueCVV validator
CVV against a brand pulled from another attribute (the PAN):
class Payment
include ActiveModel::Validations
attr_accessor :card_number, :cvv
validates :card_number, credit_card_number: true
validates :cvv, credit_card_cvv: { brand_from: :card_number }
endCVV against a literal brand:
validates :cvv, credit_card_cvv: { brand: :amex }Expiration validator
A single string attribute (MM/YY, MM/YYYY, MMYY, ...):
validates :expiration, credit_card_expiration: trueTwo separate fields (typical month + year dropdowns) — use the Expiration class in a validate block:
class Payment
attr_accessor :exp_month, :exp_year
validate do
exp = CreditCardValidations::Expiration.new(exp_month, exp_year)
errors.add(:exp_month, :invalid) unless exp.valid?
end
endCreditCardValidations::Card
A composite model wrapping Detector and Expiration behind a single ActiveModel-aware object:
card = CreditCardValidations::Card.new(
number: '4111 1111 1111 1111',
month: 12, year: 2027,
verification_value: '123',
name: 'John Smith'
)
card.valid? # => true
card.brand # => :visa
card.display_number # => "************1111"
card.last_digits # => "1111"
card.expired? # => false
card.formatted_number # => "4111 1111 1111 1111"Using Detector directly
number = '4111111111111111'
detector = CreditCardValidations::Detector.new(number)
detector.brand # => :visa
detector.visa? # => true
detector.valid?(:mastercard, :maestro) # => false
detector.valid?(:visa, :mastercard) # => true
detector.issuer_category # => "Banking and financial"
detector.last4 # => "1111"
detector.masked # => "************1111"
detector.formatted # => "4111 1111 1111 1111"
detector.possible_brands # => [:visa] (during live input)
detector.valid_cvv?('123') # => trueDetecting against a fixed brand list
#valid? and #brand take a brand list per call. with_brands holds that
list instead, and applies it to the methods that take no brand list of their
own — possible_brands, brand_name, formatted, valid_cvv?:
set = CreditCardValidations.with_brands(:visa, :mastercard)
set.brands # => [:visa, :mastercard]
set.detect('4111111111111111').brand # => :visa
set.detect('348051773827666').brand # => nil
set.detect('348051773827666').valid? # => false
set.detect('34').possible_brands # => []detect returns a Detector, so the whole instance API works on it.
Keys are brand keys, as symbols or strings, in any case. An unknown key raises
CreditCardValidations::Error rather than being dropped, since a dropped brand
would mean a valid card is quietly rejected; an empty list raises for the same
reason.
A set narrows which brands are considered, not what they are — definitions
are read from Detector.brands at every lookup, so add_brand, add_rule,
delete_brand and a replaced brand source all apply to an existing set.
Which predicate methods exist stays global: add_brand defines them on
Detector, so a detector from a visa-only set still responds to
mastercard? (with false), and delete_brand(:mastercard) undefines it
everywhere. Use valid?(:mastercard) where that matters.
Adding a custom brand at runtime
CreditCardValidations.add_brand(:voyager, { length: 15, prefixes: '86' })
CreditCardValidations::Detector.new('869926275400212').voyager? # => trueA prefix is the literal digits it spells — list a range out (%w[51 52 53]),
rather than writing 5[1-3], which would only ever match that text.
add_brand adds; it does not redefine. A brand that is already registered
raises CreditCardValidations::Error, because the call replaces the whole
entry — the rules, and with them the brand's name, CVV size, segments and
skip_luhn opt-out — so a call that only meant to change the rules used to
leave a brand that no longer detected its own cards.
To widen a registered brand, add a rule:
CreditCardValidations::Detector.add_rule(:visa, 16, ['9'])Keys are converted to symbols, so add_brand, add_rule and delete_brand
all take :visa or "visa" alike.
To replace one, remove it first:
CreditCardValidations::Detector.delete_brand(:visa)
CreditCardValidations.add_brand(:visa, { length: 16, prefixes: '9' })Removing a brand at runtime
CreditCardValidations::Detector.delete_brand(:maestro)Luhn check
CreditCardValidations::Detector.new(number).valid_luhn?
# or, on a clean digit string:
CreditCardValidations::Luhn.valid?(number)Generating test numbers and test cards
CreditCardValidations::Factory.random_number(:visa) #=> "4012888888881881"
card = CreditCardValidations::Factory.random_card(:amex)
card.valid? #=> true
card.brand #=> :amex
card.verification_value #=> "8812" (4 for Amex, 3 for the other bundled brands)random_number returns a Luhn-valid PAN the detector accepts for the brand,
including for brands that declare skip_luhn — that flag means detection
tolerates a missing check digit, not that real cards lack one, and a computed
digit passes either way. Factory.random is kept as an alias of
random_number.
Breaking: the low-level Factory.number(prefix, length, skip_luhn) lost
its third argument and is now number(prefix, length). The argument only
selected the behaviour described above, so there is nothing for it to mean.
random_card returns a Card with a number, an
expiration 1-60 months out and a verification value. The number comes from the
requested brand's rules; the CVV is sized from the brand the PAN detects
as, since that is the one Card validates against. Where ranges overlap those
are not the same brand, and card.brand reports the detected one.
Called with no brand, random_card draws only among brands that declare a
:code:
CreditCardValidations::Factory.random_card.valid? #=> truerandom_card needs the brand to declare a :code size, and raises
CreditCardValidations::Error otherwise.
Configuration
To override the default brand source, copy the bundled brands.yaml, edit it, and point the gem at it in a Rails initializer:
CreditCardValidations.configure do |config|
config.source = '/path/to/my_brands.yml'
endContributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Open a Pull Request