credit_card_validations 8.1.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +318 -96
- data/lib/active_model/credit_card_cvv_validator.rb +35 -0
- data/lib/active_model/credit_card_expiration_validator.rb +26 -0
- data/lib/credit_card_validations/brand_set.rb +63 -0
- data/lib/credit_card_validations/card.rb +68 -0
- data/lib/credit_card_validations/detector.rb +161 -27
- data/lib/credit_card_validations/expiration.rb +63 -0
- data/lib/credit_card_validations/factory.rb +63 -17
- data/lib/credit_card_validations/luhn.rb +12 -7
- data/lib/credit_card_validations/plugins/carnet.rb +30 -0
- data/lib/credit_card_validations/plugins/cartes_bancaires.rb +21 -0
- data/lib/credit_card_validations/plugins/dankort.rb +7 -0
- data/lib/credit_card_validations/plugins/elo.rb +9 -0
- data/lib/credit_card_validations/plugins/hipercard.rb +7 -0
- data/lib/credit_card_validations/plugins/mada.rb +17 -0
- data/lib/credit_card_validations/plugins/mir.rb +7 -0
- data/lib/credit_card_validations/plugins/naranja.rb +6 -0
- data/lib/credit_card_validations/plugins/rupay.rb +8 -0
- data/lib/credit_card_validations/plugins/solo.rb +9 -0
- data/lib/credit_card_validations/plugins/switch.rb +9 -0
- data/lib/credit_card_validations/version.rb +1 -1
- data/lib/credit_card_validations.rb +15 -1
- data/lib/data/brands.yaml +34 -248
- metadata +24 -9
- data/Changelog.md +0 -156
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b16ea3c34b40baacacb68877a244e7c30710323e7949b6b626e5b327156198aa
|
|
4
|
+
data.tar.gz: 596b070c9b31cefa5aacde43cff367ac609237ccea747cd20e9e2e8c9f6a95ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bd5c35081bc1085c11232c7c13d14befed6edb45b7b4faca8ac27b8b0b39b5bacd61e3ed8e4ecda977c34e50b0546f2904883602c12ea35a7d82adc942d1260
|
|
7
|
+
data.tar.gz: 9b8df4c6df565cf26f7084683a0eb68bc40e2dfbf06b86131103f92360b681772aa938d72e7389821463789bdf90bfec95713b2aef82c5cec8a7cf4540dc7670
|
data/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|

|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
Gem adds validator
|
|
8
|
+
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.
|
|
9
9
|
|
|
10
|
-
More info about card BIN numbers http://en.wikipedia.org/wiki/Bank_card_number
|
|
10
|
+
More info about card BIN numbers: http://en.wikipedia.org/wiki/Bank_card_number
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
@@ -29,158 +29,383 @@ Or install it yourself as:
|
|
|
29
29
|
$ gem install credit_card_validations
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Default brands
|
|
33
33
|
|
|
34
|
+
These brands are detected out of the box. They are the international majors that most acquirers, gateways, and payment forms care about:
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
|
38
|
-
|
|
39
|
-
[
|
|
40
|
-
[
|
|
41
|
-
[
|
|
42
|
-
[
|
|
43
|
-
[
|
|
44
|
-
[
|
|
45
|
-
[Hipercard](http://pt.wikipedia.org/wiki/Hipercard) | :hipercard
|
|
46
|
-
[JCB](http://en.wikipedia.org/wiki/Japan_Credit_Bureau) | :jcb
|
|
47
|
-
[Maestro](http://en.wikipedia.org/wiki/Maestro_%28debit_card%29) | :maestro
|
|
48
|
-
[MasterCard](http://en.wikipedia.org/wiki/MasterCard) | :mastercard
|
|
49
|
-
[MIR](http://www.nspk.ru/en/cards-mir/) | :mir
|
|
50
|
-
[Rupay](http://en.wikipedia.org/wiki/RuPay) | :rupay
|
|
51
|
-
[Solo](http://en.wikipedia.org/wiki/Solo_(debit_card)) | :solo
|
|
52
|
-
[Switch](http://en.wikipedia.org/wiki/Switch_(debit_card)) | :switch
|
|
53
|
-
[Visa](http://en.wikipedia.org/wiki/Visa_Inc.) | :visa
|
|
36
|
+
| Name | Key |
|
|
37
|
+
--------------------- | ------------|
|
|
38
|
+
[American Express](http://en.wikipedia.org/wiki/American_Express) | `:amex`
|
|
39
|
+
[China UnionPay](http://en.wikipedia.org/wiki/China_UnionPay) | `:unionpay`
|
|
40
|
+
[Diners Club](http://en.wikipedia.org/wiki/Diners_Club_International) | `:diners`
|
|
41
|
+
[Discover](http://en.wikipedia.org/wiki/Discover_Card) | `:discover`
|
|
42
|
+
[JCB](http://en.wikipedia.org/wiki/Japan_Credit_Bureau) | `:jcb`
|
|
43
|
+
[Maestro](http://en.wikipedia.org/wiki/Maestro_%28debit_card%29) | `:maestro`
|
|
44
|
+
[MasterCard](http://en.wikipedia.org/wiki/MasterCard) | `:mastercard`
|
|
45
|
+
[Visa](http://en.wikipedia.org/wiki/Visa_Inc.) | `:visa`
|
|
54
46
|
|
|
47
|
+
## Opt-in plugins
|
|
55
48
|
|
|
49
|
+
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.
|
|
56
50
|
|
|
57
|
-
|
|
51
|
+
### Active regional and specialty networks
|
|
58
52
|
|
|
59
53
|
| Name | Key |
|
|
60
54
|
--------------------- | ------------|
|
|
61
|
-
[Cabal](https://en.wikipedia.org/wiki/Cabal_(debit_card)) |
|
|
62
|
-
[
|
|
63
|
-
[
|
|
64
|
-
[
|
|
65
|
-
[
|
|
66
|
-
[
|
|
67
|
-
[
|
|
68
|
-
[
|
|
69
|
-
[
|
|
70
|
-
[
|
|
71
|
-
[
|
|
72
|
-
[
|
|
73
|
-
[
|
|
74
|
-
[
|
|
55
|
+
[Cabal](https://en.wikipedia.org/wiki/Cabal_(debit_card)) | `:cabal`
|
|
56
|
+
[Carnet](https://en.wikipedia.org/wiki/Carnet_(card)) | `:carnet`
|
|
57
|
+
[Cartes Bancaires](https://en.wikipedia.org/wiki/Cartes_Bancaires) | `:cartes_bancaires`
|
|
58
|
+
[Dankort](http://en.wikipedia.org/wiki/Dankort) | `:dankort`
|
|
59
|
+
[DinaCard](https://en.wikipedia.org/wiki/DinaCard) | `:dinacard`
|
|
60
|
+
[Elo](https://pt.wikipedia.org/wiki/Elo_Participa%C3%A7%C3%B5es_S/A) | `:elo`
|
|
61
|
+
[Girocard](https://en.wikipedia.org/wiki/Girocard) | `:girocard`
|
|
62
|
+
[Hiper](https://en.wikipedia.org/wiki/Itau_Unibanco) | `:hiper`
|
|
63
|
+
[Hipercard](http://pt.wikipedia.org/wiki/Hipercard) | `:hipercard`
|
|
64
|
+
[Humo](https://en.wikipedia.org/wiki/Humo_(payment_system)) | `:humocard`
|
|
65
|
+
[Mada](https://en.wikipedia.org/wiki/Mada_(payment_system)) | `:mada`
|
|
66
|
+
[MIR](http://www.nspk.ru/en/cards-mir/) | `:mir`
|
|
67
|
+
[Naranja](https://en.wikipedia.org/wiki/Tarjeta_Naranja) | `:naranja`
|
|
68
|
+
[RuPay](http://en.wikipedia.org/wiki/RuPay) | `:rupay`
|
|
69
|
+
[Troy](https://en.wikipedia.org/wiki/Troy_(payment_system)) | `:troy`
|
|
70
|
+
[UATP](https://en.wikipedia.org/wiki/Universal_Air_Travel_Plan) | `:uatp`
|
|
71
|
+
[Uzcard](https://en.wikipedia.org/wiki/Uzcard) | `:uzcard`
|
|
72
|
+
[V Pay](https://en.wikipedia.org/wiki/V_Pay) | `:vpay`
|
|
73
|
+
[Verve](https://en.wikipedia.org/wiki/Verve_(payment_card)) | `:verve`
|
|
74
|
+
[Voyager](https://en.wikipedia.org/wiki/Voyager_card) | `:voyager`
|
|
75
|
+
|
|
76
|
+
### Legacy / withdrawn networks
|
|
77
|
+
|
|
78
|
+
| Name | Key | Status |
|
|
79
|
+
--------------------- | ------------| ------|
|
|
80
|
+
[Diners Club US](http://en.wikipedia.org/wiki/Diners_Club_International#MasterCard_alliance) | `:diners_us` | Issued on MasterCard BINs since 2004 |
|
|
81
|
+
[EnRoute](https://en.wikipedia.org/wiki/EnRoute_(credit_card)) | `:en_route` | Withdrawn 1989 |
|
|
82
|
+
[Laser](https://en.wikipedia.org/wiki/Laser_%28debit_card%29) | `:laser` | Withdrawn 2014 |
|
|
83
|
+
[Solo](https://en.wikipedia.org/wiki/Solo_(debit_card)) | `:solo` | Withdrawn 2011 |
|
|
84
|
+
[Switch](https://en.wikipedia.org/wiki/Switch_(debit_card)) | `:switch` | Withdrawn 2002 |
|
|
85
|
+
|
|
86
|
+
### Loading plugins
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# in an initializer or before first use
|
|
90
|
+
require 'credit_card_validations/plugins/mir'
|
|
91
|
+
require 'credit_card_validations/plugins/elo'
|
|
92
|
+
require 'credit_card_validations/plugins/hipercard'
|
|
93
|
+
# ... whichever brands the app actually accepts
|
|
94
|
+
```
|
|
75
95
|
|
|
96
|
+
## Migrating from v9.x → v10.0 (unreleased)
|
|
76
97
|
|
|
98
|
+
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
|
|
99
|
+
default set, and referencing one of them still worked — the plugin was loaded
|
|
100
|
+
for you and a deprecation warning was printed once. v10.0 removes that.
|
|
77
101
|
|
|
78
|
-
|
|
102
|
+
If your code names any of these brands, add the matching `require`:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# config/initializers/credit_card_validations.rb
|
|
106
|
+
require 'credit_card_validations/plugins/mir'
|
|
107
|
+
require 'credit_card_validations/plugins/elo'
|
|
108
|
+
# ... whichever brands the app actually accepts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Without it the brand is simply unknown. **Nothing warns** — the v9 deprecation
|
|
112
|
+
warning is gone too — and the read-only paths stay quiet, so a missing
|
|
113
|
+
`require` will not announce itself where you are most likely to notice it:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
Detector.new('2202 1234 1234 1234').brand #=> nil
|
|
117
|
+
Detector.new('2202 1234 1234 1234').valid?(:mir) #=> false
|
|
118
|
+
Detector.new('2202 1234 1234 1234').brand_name #=> nil
|
|
119
|
+
Detector.new('2202 1234 1234 1234').possible_brands #=> []
|
|
120
|
+
Detector.brand_name(:mir) #=> nil
|
|
121
|
+
Detector.valid_cvv?('123', :mir) #=> false
|
|
122
|
+
Detector.has_luhn_check_rule?(:mir) #=> true (nothing opted out)
|
|
123
|
+
'2202 1234 1234 1234'.credit_card_brand #=> nil
|
|
124
|
+
'2202 1234 1234 1234'.credit_card_brand_name #=> nil
|
|
125
|
+
'2202 1234 1234 1234'.valid_credit_card_brand?(:mir) #=> false
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Four paths do raise, because there is nothing sensible to return:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
Detector.new(pan).mir? # NoMethodError — the predicate is never defined
|
|
132
|
+
CreditCardValidations::Factory.random(:mir) # CreditCardValidations::Error: Unsupported brand
|
|
133
|
+
Detector.add_rule(:mir, 16, ['2202']) # CreditCardValidations::Error: brand mir is undefined
|
|
134
|
+
Detector::LEGACY_PLUGIN_BRANDS # NameError — see below
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
ActiveModel validators never raise on a missing plugin; the record is just
|
|
138
|
+
invalid. Which means a PAN field restricted to a plugin brand **rejects every
|
|
139
|
+
number you give it**, including a valid card of that brand:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
validates :number, credit_card_number: { brands: [:mir] } # nothing passes
|
|
143
|
+
validates :cvv, credit_card_cvv: { brand: :mir } # nothing passes
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`Detector::LEGACY_PLUGIN_BRANDS` was a public constant in v9 and is gone. An
|
|
147
|
+
app that read it to build its own require list now gets `NameError`; replace it
|
|
148
|
+
with a literal list of the brands you accept.
|
|
149
|
+
|
|
150
|
+
So check the brand list against your initializer rather than waiting for an
|
|
151
|
+
error — in the paths an app exercises most, there will not be one.
|
|
152
|
+
|
|
153
|
+
| Brand | Status |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `:dankort` | Active (Denmark) |
|
|
156
|
+
| `:elo` | Active (Brazil) |
|
|
157
|
+
| `:hipercard` | Active (Brazil) |
|
|
158
|
+
| `:mir` | Active (Russia) |
|
|
159
|
+
| `:rupay` | Active (India) |
|
|
160
|
+
| `:solo` | Withdrawn 2011 |
|
|
161
|
+
| `:switch` | Withdrawn 2002 |
|
|
162
|
+
|
|
163
|
+
## Migrating from v8.x → v9.0
|
|
164
|
+
|
|
165
|
+
Seven brands moved from the default brand set to opt-in plugins in v9.0. Up to
|
|
166
|
+
v9.x an auto-require shim kept existing code working with a one-time
|
|
167
|
+
deprecation warning per brand; it is removed on master for v10.0, see above.
|
|
168
|
+
|
|
169
|
+
### Other breaking changes in v9.0
|
|
170
|
+
|
|
171
|
+
- **`Hipercard` cleaned up.** Length changed from 19 to 16 (which is the issued length); legacy `637*` prefixes that actually belong to Hiper were dropped. If you used Hipercard before, your detection now matches the brand's real spec.
|
|
172
|
+
- **`Discover` cleaned up.** Diners-only prefixes (`300-305, 3095, 36, 38, 39`) were dropped from Discover. Diners cards now correctly detect as `:diners` instead of `:discover`. Apps that branched on `:discover` for routing should branch on `[:diners, :discover]`.
|
|
173
|
+
- **`Luhn.valid?` is now strict.** It accepts a digit-only string and returns `false` for `nil`, empty input, or any non-digit character. User-facing input handling moved into `Detector#initialize`, which strips whitespace and dashes before delegating.
|
|
174
|
+
- **Brand YAML is loaded via `YAML.safe_load_file`.** Custom brand sources may need to declare `permitted_classes: [Symbol]` if they relied on extra Ruby objects.
|
|
175
|
+
|
|
176
|
+
## Usage
|
|
177
|
+
|
|
178
|
+
### String monkey patch
|
|
79
179
|
|
|
80
180
|
```ruby
|
|
81
181
|
require 'credit_card_validations/string'
|
|
82
|
-
'5274 5763 9425 9961'.credit_card_brand
|
|
83
|
-
'5274 5763 9425 9961'.credit_card_brand_name
|
|
84
|
-
'5274 5763 9425 9961'.valid_credit_card_brand?(:mastercard, :visa)
|
|
85
|
-
'5274 5763 9425 9961'.valid_credit_card_brand?(:amex)
|
|
86
|
-
'5274 5763 9425 9961'.valid_credit_card_brand?('MasterCard')
|
|
182
|
+
'5274 5763 9425 9961'.credit_card_brand #=> :mastercard
|
|
183
|
+
'5274 5763 9425 9961'.credit_card_brand_name #=> "MasterCard"
|
|
184
|
+
'5274 5763 9425 9961'.valid_credit_card_brand?(:mastercard, :visa) #=> true
|
|
185
|
+
'5274 5763 9425 9961'.valid_credit_card_brand?(:amex) #=> false
|
|
186
|
+
'5274 5763 9425 9961'.valid_credit_card_brand?('MasterCard') #=> true
|
|
87
187
|
```
|
|
88
188
|
|
|
89
|
-
### ActiveModel
|
|
189
|
+
### ActiveModel validators
|
|
90
190
|
|
|
91
|
-
|
|
191
|
+
Restrict to a brand list:
|
|
92
192
|
|
|
93
193
|
```ruby
|
|
94
194
|
class CreditCardModel
|
|
95
195
|
attr_accessor :number
|
|
96
196
|
include ActiveModel::Validations
|
|
97
|
-
validates :number, credit_card_number: {brands: [:amex, :maestro]}
|
|
197
|
+
validates :number, credit_card_number: { brands: [:amex, :maestro] }
|
|
98
198
|
end
|
|
99
199
|
```
|
|
100
200
|
|
|
101
|
-
|
|
201
|
+
Accept any known brand:
|
|
102
202
|
|
|
103
203
|
```ruby
|
|
104
204
|
validates :number, presence: true, credit_card_number: true
|
|
105
205
|
```
|
|
106
206
|
|
|
107
|
-
###
|
|
207
|
+
### CVV validator
|
|
208
|
+
|
|
209
|
+
CVV against a brand pulled from another attribute (the PAN):
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
class Payment
|
|
213
|
+
include ActiveModel::Validations
|
|
214
|
+
attr_accessor :card_number, :cvv
|
|
215
|
+
|
|
216
|
+
validates :card_number, credit_card_number: true
|
|
217
|
+
validates :cvv, credit_card_cvv: { brand_from: :card_number }
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
CVV against a literal brand:
|
|
108
222
|
|
|
109
223
|
```ruby
|
|
110
|
-
|
|
224
|
+
validates :cvv, credit_card_cvv: { brand: :amex }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Expiration validator
|
|
228
|
+
|
|
229
|
+
A single string attribute (`MM/YY`, `MM/YYYY`, `MMYY`, ...):
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
validates :expiration, credit_card_expiration: true
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Two separate fields (typical month + year dropdowns) — use the `Expiration` class in a `validate` block:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class Payment
|
|
239
|
+
attr_accessor :exp_month, :exp_year
|
|
240
|
+
|
|
241
|
+
validate do
|
|
242
|
+
exp = CreditCardValidations::Expiration.new(exp_month, exp_year)
|
|
243
|
+
errors.add(:exp_month, :invalid) unless exp.valid?
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### `CreditCardValidations::Card`
|
|
249
|
+
|
|
250
|
+
A composite model wrapping `Detector` and `Expiration` behind a single ActiveModel-aware object:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
card = CreditCardValidations::Card.new(
|
|
254
|
+
number: '4111 1111 1111 1111',
|
|
255
|
+
month: 12, year: 2027,
|
|
256
|
+
verification_value: '123',
|
|
257
|
+
name: 'John Smith'
|
|
258
|
+
)
|
|
259
|
+
card.valid? # => true
|
|
260
|
+
card.brand # => :visa
|
|
261
|
+
card.display_number # => "************1111"
|
|
262
|
+
card.last_digits # => "1111"
|
|
263
|
+
card.expired? # => false
|
|
264
|
+
card.formatted_number # => "4111 1111 1111 1111"
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Using `Detector` directly
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
number = '4111111111111111'
|
|
111
271
|
detector = CreditCardValidations::Detector.new(number)
|
|
112
|
-
|
|
113
|
-
detector.visa
|
|
114
|
-
detector.
|
|
115
|
-
detector.valid?(:
|
|
116
|
-
detector.
|
|
272
|
+
|
|
273
|
+
detector.brand # => :visa
|
|
274
|
+
detector.visa? # => true
|
|
275
|
+
detector.valid?(:mastercard, :maestro) # => false
|
|
276
|
+
detector.valid?(:visa, :mastercard) # => true
|
|
277
|
+
detector.issuer_category # => "Banking and financial"
|
|
278
|
+
detector.last4 # => "1111"
|
|
279
|
+
detector.masked # => "************1111"
|
|
280
|
+
detector.formatted # => "4111 1111 1111 1111"
|
|
281
|
+
detector.possible_brands # => [:visa] (during live input)
|
|
282
|
+
detector.valid_cvv?('123') # => true
|
|
117
283
|
```
|
|
118
284
|
|
|
119
|
-
###
|
|
120
|
-
|
|
121
|
-
|
|
285
|
+
### Detecting against a fixed brand list
|
|
286
|
+
|
|
287
|
+
`#valid?` and `#brand` take a brand list per call. `with_brands` holds that
|
|
288
|
+
list instead, and applies it to the methods that take no brand list of their
|
|
289
|
+
own — `possible_brands`, `brand_name`, `formatted`, `valid_cvv?`:
|
|
122
290
|
|
|
123
291
|
```ruby
|
|
124
|
-
CreditCardValidations.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
292
|
+
set = CreditCardValidations.with_brands(:visa, :mastercard)
|
|
293
|
+
|
|
294
|
+
set.brands # => [:visa, :mastercard]
|
|
295
|
+
set.detect('4111111111111111').brand # => :visa
|
|
296
|
+
set.detect('348051773827666').brand # => nil
|
|
297
|
+
set.detect('348051773827666').valid? # => false
|
|
298
|
+
set.detect('34').possible_brands # => []
|
|
128
299
|
```
|
|
129
300
|
|
|
130
|
-
|
|
301
|
+
`detect` returns a `Detector`, so the whole instance API works on it.
|
|
302
|
+
|
|
303
|
+
Keys are brand keys, as symbols or strings, in any case. An unknown key raises
|
|
304
|
+
`CreditCardValidations::Error` rather than being dropped, since a dropped brand
|
|
305
|
+
would mean a valid card is quietly rejected; an empty list raises for the same
|
|
306
|
+
reason.
|
|
307
|
+
|
|
308
|
+
A set narrows which brands are considered, not what they are — definitions
|
|
309
|
+
are read from `Detector.brands` at every lookup, so `add_brand`, `add_rule`,
|
|
310
|
+
`delete_brand` and a replaced brand source all apply to an existing set.
|
|
311
|
+
|
|
312
|
+
Which predicate methods exist stays global: `add_brand` defines them on
|
|
313
|
+
`Detector`, so a detector from a visa-only set still responds to
|
|
314
|
+
`mastercard?` (with `false`), and `delete_brand(:mastercard)` undefines it
|
|
315
|
+
everywhere. Use `valid?(:mastercard)` where that matters.
|
|
316
|
+
|
|
317
|
+
### Adding a custom brand at runtime
|
|
131
318
|
|
|
132
319
|
```ruby
|
|
133
|
-
CreditCardValidations
|
|
320
|
+
CreditCardValidations.add_brand(:voyager, { length: 15, prefixes: '86' })
|
|
321
|
+
CreditCardValidations::Detector.new('869926275400212').voyager? # => true
|
|
134
322
|
```
|
|
135
323
|
|
|
136
|
-
|
|
324
|
+
A prefix is the literal digits it spells — list a range out (`%w[51 52 53]`),
|
|
325
|
+
rather than writing `5[1-3]`, which would only ever match that text.
|
|
326
|
+
|
|
327
|
+
`add_brand` adds; it does not redefine. A brand that is already registered
|
|
328
|
+
raises `CreditCardValidations::Error`, because the call replaces the whole
|
|
329
|
+
entry — the rules, and with them the brand's name, CVV size, segments and
|
|
330
|
+
`skip_luhn` opt-out — so a call that only meant to change the rules used to
|
|
331
|
+
leave a brand that no longer detected its own cards.
|
|
332
|
+
|
|
333
|
+
To widen a registered brand, add a rule:
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
CreditCardValidations::Detector.add_rule(:visa, 16, ['9'])
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Keys are converted to symbols, so `add_brand`, `add_rule` and `delete_brand`
|
|
340
|
+
all take `:visa` or `"visa"` alike.
|
|
341
|
+
|
|
342
|
+
To replace one, remove it first:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
CreditCardValidations::Detector.delete_brand(:visa)
|
|
346
|
+
CreditCardValidations.add_brand(:visa, { length: 16, prefixes: '9' })
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Removing a brand at runtime
|
|
137
350
|
|
|
138
351
|
```ruby
|
|
139
|
-
CreditCardValidations::Detector.
|
|
140
|
-
#or
|
|
141
|
-
CreditCardValidations::Luhn.valid?(@credit_card_number)
|
|
352
|
+
CreditCardValidations::Detector.delete_brand(:maestro)
|
|
142
353
|
```
|
|
143
354
|
|
|
144
|
-
###
|
|
355
|
+
### Luhn check
|
|
145
356
|
|
|
146
357
|
```ruby
|
|
147
|
-
CreditCardValidations::
|
|
148
|
-
#
|
|
149
|
-
CreditCardValidations::
|
|
150
|
-
# => "6010430241237266856"
|
|
358
|
+
CreditCardValidations::Detector.new(number).valid_luhn?
|
|
359
|
+
# or, on a clean digit string:
|
|
360
|
+
CreditCardValidations::Luhn.valid?(number)
|
|
151
361
|
```
|
|
152
362
|
|
|
153
|
-
###
|
|
363
|
+
### Generating test numbers and test cards
|
|
154
364
|
|
|
155
365
|
```ruby
|
|
156
|
-
|
|
157
|
-
require 'credit_card_validations/plugins/laser'
|
|
158
|
-
require 'credit_card_validations/plugins/diners_us'
|
|
366
|
+
CreditCardValidations::Factory.random_number(:visa) #=> "4012888888881881"
|
|
159
367
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
require 'credit_card_validations/plugins/humocard'
|
|
165
|
-
require 'credit_card_validations/plugins/troy'
|
|
166
|
-
require 'credit_card_validations/plugins/uatp'
|
|
167
|
-
require 'credit_card_validations/plugins/uzcard'
|
|
168
|
-
require 'credit_card_validations/plugins/verve'
|
|
169
|
-
require 'credit_card_validations/plugins/voyager'
|
|
170
|
-
require 'credit_card_validations/plugins/vpay'
|
|
368
|
+
card = CreditCardValidations::Factory.random_card(:amex)
|
|
369
|
+
card.valid? #=> true
|
|
370
|
+
card.brand #=> :amex
|
|
371
|
+
card.verification_value #=> "8812" (4 for Amex, 3 for the other bundled brands)
|
|
171
372
|
```
|
|
172
373
|
|
|
374
|
+
`random_number` returns a Luhn-valid PAN the detector accepts for the brand,
|
|
375
|
+
including for brands that declare `skip_luhn` — that flag means detection
|
|
376
|
+
tolerates a missing check digit, not that real cards lack one, and a computed
|
|
377
|
+
digit passes either way. `Factory.random` is kept as an alias of
|
|
378
|
+
`random_number`.
|
|
379
|
+
|
|
380
|
+
**Breaking:** the low-level `Factory.number(prefix, length, skip_luhn)` lost
|
|
381
|
+
its third argument and is now `number(prefix, length)`. The argument only
|
|
382
|
+
selected the behaviour described above, so there is nothing for it to mean.
|
|
173
383
|
|
|
174
|
-
|
|
384
|
+
`random_card` returns a [`Card`](#creditcardvalidationscard) with a number, an
|
|
385
|
+
expiration 1-60 months out and a verification value. The number comes from the
|
|
386
|
+
requested brand's rules; the CVV is sized from the brand the PAN **detects**
|
|
387
|
+
as, since that is the one `Card` validates against. Where ranges overlap those
|
|
388
|
+
are not the same brand, and `card.brand` reports the detected one.
|
|
175
389
|
|
|
176
|
-
|
|
390
|
+
Called with no brand, `random_card` draws only among brands that declare a
|
|
391
|
+
`:code`:
|
|
177
392
|
|
|
178
393
|
```ruby
|
|
179
|
-
|
|
180
|
-
config.source = '/path/to/my_brands.yml'
|
|
181
|
-
end
|
|
394
|
+
CreditCardValidations::Factory.random_card.valid? #=> true
|
|
182
395
|
```
|
|
183
396
|
|
|
397
|
+
`random_card` needs the brand to declare a `:code` size, and raises
|
|
398
|
+
`CreditCardValidations::Error` otherwise.
|
|
399
|
+
|
|
400
|
+
## Configuration
|
|
401
|
+
|
|
402
|
+
To override the default brand source, copy [the bundled `brands.yaml`](https://github.com/didww/credit_card_validations/blob/master/lib/data/brands.yaml), edit it, and point the gem at it in a Rails initializer:
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
CreditCardValidations.configure do |config|
|
|
406
|
+
config.source = '/path/to/my_brands.yml'
|
|
407
|
+
end
|
|
408
|
+
```
|
|
184
409
|
|
|
185
410
|
## Contributing
|
|
186
411
|
|
|
@@ -188,7 +413,4 @@ In order to override default data source you can copy [original one](https://git
|
|
|
188
413
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
189
414
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
190
415
|
4. Push to the branch (`git push origin my-new-feature`)
|
|
191
|
-
5.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
416
|
+
5. Open a Pull Request
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# == ActiveModel CreditCardCvvValidator
|
|
2
|
+
#
|
|
3
|
+
# Validates a card verification value (CVV / CVC / CID) against either a
|
|
4
|
+
# brand passed explicitly, or a brand derived from another attribute.
|
|
5
|
+
#
|
|
6
|
+
# class Payment
|
|
7
|
+
# include ActiveModel::Validations
|
|
8
|
+
# attr_accessor :card_number, :cvv
|
|
9
|
+
#
|
|
10
|
+
# validates :cvv, credit_card_cvv: { brand_from: :card_number }
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# class Payment
|
|
14
|
+
# validates :cvv, credit_card_cvv: { brand: :amex }
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
module ActiveModel
|
|
18
|
+
module Validations
|
|
19
|
+
class CreditCardCvvValidator < EachValidator
|
|
20
|
+
def validate_each(record, attribute, value)
|
|
21
|
+
brand = resolve_brand(record)
|
|
22
|
+
return if brand && CreditCardValidations::Detector.valid_cvv?(value, brand)
|
|
23
|
+
record.errors.add(attribute, options[:message] || :invalid)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def resolve_brand(record)
|
|
29
|
+
return options[:brand] if options[:brand]
|
|
30
|
+
pan = record.public_send(options[:brand_from]).to_s if options[:brand_from]
|
|
31
|
+
CreditCardValidations::Detector.new(pan).brand if pan
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# == ActiveModel CreditCardExpirationValidator
|
|
2
|
+
#
|
|
3
|
+
# Validates a card expiration date held in a single attribute as a parseable
|
|
4
|
+
# string (MM/YY, MM/YYYY, MMYY, ...).
|
|
5
|
+
#
|
|
6
|
+
# class Payment
|
|
7
|
+
# include ActiveModel::Validations
|
|
8
|
+
# attr_accessor :expiration
|
|
9
|
+
#
|
|
10
|
+
# validates :expiration, credit_card_expiration: true
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# For forms with separate month + year fields, use the Expiration class
|
|
14
|
+
# directly in a plain validate block (see README).
|
|
15
|
+
#
|
|
16
|
+
module ActiveModel
|
|
17
|
+
module Validations
|
|
18
|
+
class CreditCardExpirationValidator < EachValidator
|
|
19
|
+
def validate_each(record, attribute, value)
|
|
20
|
+
expiration = CreditCardValidations::Expiration.parse(value)
|
|
21
|
+
return if expiration && expiration.valid?
|
|
22
|
+
record.errors.add(attribute, options[:message] || :invalid)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module CreditCardValidations
|
|
2
|
+
# A fixed list of brands to detect against, so the list is named once
|
|
3
|
+
# instead of being repeated at every call site.
|
|
4
|
+
#
|
|
5
|
+
# set = CreditCardValidations.with_brands(:visa, :mastercard)
|
|
6
|
+
# set.detect(pan).brand # same as Detector.new(pan).brand(:visa, :mastercard)
|
|
7
|
+
# set.detect(pan).possible_brands # ...but this one has no per-call form
|
|
8
|
+
#
|
|
9
|
+
# Build one with CreditCardValidations.with_brands.
|
|
10
|
+
#
|
|
11
|
+
# A set narrows *which* brands are considered. It does not copy *what* they
|
|
12
|
+
# are: the definitions are read from Detector.brands on every lookup, so a
|
|
13
|
+
# set never disagrees with the rest of the process about what a Visa number
|
|
14
|
+
# looks like. Two objects giving different answers for one card would be a
|
|
15
|
+
# worse failure than the stale global registry a snapshot would protect
|
|
16
|
+
# against.
|
|
17
|
+
class BrandSet
|
|
18
|
+
|
|
19
|
+
def initialize(keys)
|
|
20
|
+
raise Error, 'with_brands needs at least one brand' if keys.empty?
|
|
21
|
+
|
|
22
|
+
# Brand keys only, in any case: a list of brands usually arrives as
|
|
23
|
+
# strings from a config file or an environment variable.
|
|
24
|
+
@brands = keys.map { |key| key.to_s.downcase.to_sym }.freeze
|
|
25
|
+
missing = @brands - Detector.brands.keys
|
|
26
|
+
unless missing.empty?
|
|
27
|
+
raise Error, "unknown brand(s) #{missing.map(&:inspect).join(', ')}; " \
|
|
28
|
+
"registered: #{Detector.brands.keys.map(&:inspect).join(', ')}" \
|
|
29
|
+
' (a plugin brand needs its plugin required first)'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A Detector subclass whose registry is the global one, narrowed to this
|
|
33
|
+
# set. class_attribute gives every subclass its own `brands`, and every
|
|
34
|
+
# lookup in Detector -- the class methods and the instance ones alike --
|
|
35
|
+
# goes through it, so the whole API scopes with nothing added to
|
|
36
|
+
# Detector itself. The reader is a method rather than a stored hash so
|
|
37
|
+
# that it stays live.
|
|
38
|
+
scoped_keys = @brands
|
|
39
|
+
@detector_class = Class.new(Detector)
|
|
40
|
+
@detector_class.define_singleton_method(:brands) do
|
|
41
|
+
# Registry order, not the order the set was built with: Hash#slice
|
|
42
|
+
# answers in argument order, Array#& in the receiver's.
|
|
43
|
+
registry = Detector.brands
|
|
44
|
+
registry.slice(*(registry.keys & scoped_keys))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Keys of the brands in this set, in the order they were requested.
|
|
49
|
+
# Frozen: this is the set's own list, not a copy to edit.
|
|
50
|
+
attr_reader :brands
|
|
51
|
+
|
|
52
|
+
# A Detector that sees only this set's brands. It is a Detector, so the
|
|
53
|
+
# whole instance API works on it.
|
|
54
|
+
def detect(number)
|
|
55
|
+
detector_class.new(number)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
attr_reader :detector_class
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
end
|