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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ea48b31cccd9e050dd0e59cdd71bb784cf319d355dfa2eb0a185635308b1b81
4
- data.tar.gz: 3c0358bce72eeb6f265ffb0130dc3a5a843dbf326ef989c118d827c025b8e0c3
3
+ metadata.gz: b16ea3c34b40baacacb68877a244e7c30710323e7949b6b626e5b327156198aa
4
+ data.tar.gz: 596b070c9b31cefa5aacde43cff367ac609237ccea747cd20e9e2e8c9f6a95ac
5
5
  SHA512:
6
- metadata.gz: 62586d60302f937ec7f47a98440d89767fba557506a5568733e498e2c41d491d5d8f283923590808abc4f1ab3ebe3466dcadb8fbd4a21b721d07337ea4e34133
7
- data.tar.gz: 366c5f5239778f08035b207b9d1e88e2f3c5f747913b3d86c79b1555fd571a640d16eca7121e78b62fe93f87298a05b9bb9161c7acfe1dcdad2e6a6b100662b2
6
+ metadata.gz: 7bd5c35081bc1085c11232c7c13d14befed6edb45b7b4faca8ac27b8b0b39b5bacd61e3ed8e4ecda977c34e50b0546f2904883602c12ea35a7d82adc942d1260
7
+ data.tar.gz: 9b8df4c6df565cf26f7084683a0eb68bc40e2dfbf06b86131103f92360b681772aa938d72e7389821463789bdf90bfec95713b2aef82c5cec8a7cf4540dc7670
data/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  ![Coverage](https://didww.github.io/credit_card_validations/badge.svg)
6
6
 
7
7
 
8
- Gem adds validator to check whether or not a given number actually falls within the ranges of possible numbers prior to performing such verification, and, as such, CreditCardValidations simply verifies that the credit card number provided is well-formed.
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
- ## Usage
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
- The following issuing institutes are accepted:
36
-
37
- | Name | Key |
38
- --------------------- | ------------|
39
- [American Express](http://en.wikipedia.org/wiki/American_Express) | :amex
40
- [China UnionPay](http://en.wikipedia.org/wiki/China_UnionPay) | :unionpay
41
- [Dankort](http://en.wikipedia.org/wiki/Dankort) | :dankort
42
- [Diners Club](http://en.wikipedia.org/wiki/Diners_Club_International) | :diners
43
- [Elo](https://pt.wikipedia.org/wiki/Elo_Participa%C3%A7%C3%B5es_S/A) | :elo
44
- [Discover](http://en.wikipedia.org/wiki/Discover_Card) | :discover
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
- The following are supported with plugins
51
+ ### Active regional and specialty networks
58
52
 
59
53
  | Name | Key |
60
54
  --------------------- | ------------|
61
- [Cabal](https://en.wikipedia.org/wiki/Cabal_(debit_card)) | :cabal
62
- [DinaCard](https://en.wikipedia.org/wiki/DinaCard) | :dinacard
63
- [Diners Club US](http://en.wikipedia.org/wiki/Diners_Club_International#MasterCard_alliance) | :diners_us
64
- [EnRoute](https://en.wikipedia.org/wiki/EnRoute_(credit_card)) | :en_route
65
- [Girocard](https://en.wikipedia.org/wiki/Girocard) | :girocard
66
- [Hiper](https://en.wikipedia.org/wiki/Itau_Unibanco) | :hiper
67
- [Humo](https://en.wikipedia.org/wiki/Humo_(payment_system)) | :humocard
68
- [Laser](https://en.wikipedia.org/wiki/Laser_%28debit_card%29) | :laser
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
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
- ### Examples using string monkey patch
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 #=> :mastercard
83
- '5274 5763 9425 9961'.credit_card_brand_name #=> "MasterCard"
84
- '5274 5763 9425 9961'.valid_credit_card_brand?(:mastercard, :visa) #=> true
85
- '5274 5763 9425 9961'.valid_credit_card_brand?(:amex) #=> false
86
- '5274 5763 9425 9961'.valid_credit_card_brand?('MasterCard') #=> true
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 support
189
+ ### ActiveModel validators
90
190
 
91
- only for certain brands
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
- for all known brands
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
- ### Examples using CreditCardValidations::Detector class
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
- number = "4111111111111111"
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
- detector.brand #:visa
113
- detector.visa? #true
114
- detector.valid?(:mastercard,:maestro) #false
115
- detector.valid?(:visa, :mastercard) #true
116
- detector.issuer_category #"Banking and financial"
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
- ### Also You can add your own brand rules to detect other credit card brands/types
120
- passing name,length(integer/array of integers) and prefix(string/array of strings)
121
- Example
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.add_brand(:voyager, {length: 15, prefixes: '86'})
125
- voyager_test_card_number = '869926275400212'
126
- CreditCardValidations::Detector.new(voyager_test_card_number).brand #:voyager
127
- CreditCardValidations::Detector.new(voyager_test_card_number).voyager? #true
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
- ### Remove brands also supported
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::Detector.delete_brand(:maestro)
320
+ CreditCardValidations.add_brand(:voyager, { length: 15, prefixes: '86' })
321
+ CreditCardValidations::Detector.new('869926275400212').voyager? # => true
134
322
  ```
135
323
 
136
- ### Check luhn
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.new(@credit_card_number).valid_luhn?
140
- #or
141
- CreditCardValidations::Luhn.valid?(@credit_card_number)
352
+ CreditCardValidations::Detector.delete_brand(:maestro)
142
353
  ```
143
354
 
144
- ### Generate credit card numbers that pass validation
355
+ ### Luhn check
145
356
 
146
357
  ```ruby
147
- CreditCardValidations::Factory.random(:amex)
148
- # => "348051773827666"
149
- CreditCardValidations::Factory.random(:maestro)
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
- ### Plugins
363
+ ### Generating test numbers and test cards
154
364
 
155
365
  ```ruby
156
- require 'credit_card_validations/plugins/en_route'
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
- require 'credit_card_validations/plugins/cabal'
161
- require 'credit_card_validations/plugins/dinacard'
162
- require 'credit_card_validations/plugins/girocard'
163
- require 'credit_card_validations/plugins/hiper'
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
- ### Configuration
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
- In order to override default data source you can copy [original one](https://github.com/didww/credit_card_validations/blob/master/lib/data/brands.yaml) , change it and configure during rails initializer
390
+ Called with no brand, `random_card` draws only among brands that declare a
391
+ `:code`:
177
392
 
178
393
  ```ruby
179
- CreditCardValidations.configure do |config|
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. Create new Pull Request
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