credit_card_validations 8.1.0 → 9.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 +186 -97
- 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/card.rb +61 -0
- data/lib/credit_card_validations/detector.rb +92 -1
- data/lib/credit_card_validations/expiration.rb +63 -0
- 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 +5 -1
- data/lib/data/brands.yaml +34 -248
- metadata +18 -4
- 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: d26fc84c0dd18fbfcacaeead238fe16ecefe51486e4122e63c3dcf4836fd4ce6
|
|
4
|
+
data.tar.gz: a1bad2adea7168439991cf98bb68a25a2d62c4372602cbe54743f23eda12c11e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14d43e6661c30034f8f52f4bf0558eb4704843ad048dfb79943a0822f4f1d6b221d31a98bd1b73dd23cd3a6d76904fdc88b6758abfb5ea1a701da001e938b73a
|
|
7
|
+
data.tar.gz: c41da3b3de8f45d93413a41c4fe88c8e543eea2c8e149ea8500d594ee1c40802c2554cb7f72f87e732d600b7aded5d69478f9ff4a9acebea9bbed85edc35873f
|
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,250 @@ 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` | Merged into Discover for US routing in 2008 |
|
|
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
|
+
```
|
|
95
|
+
|
|
96
|
+
## Migrating from v8.x → v9.0
|
|
75
97
|
|
|
98
|
+
Seven brands moved from the default brand set to opt-in plugins in v9.0. The auto-require shim keeps existing code working for one major version with a one-time deprecation warning per brand.
|
|
76
99
|
|
|
100
|
+
| Brand | Status | Auto-loaded until |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `:dankort` | Active (Denmark) | v10.0 |
|
|
103
|
+
| `:elo` | Active (Brazil) | v10.0 |
|
|
104
|
+
| `:hipercard` | Active (Brazil) | v10.0 |
|
|
105
|
+
| `:mir` | Active (Russia) | v10.0 |
|
|
106
|
+
| `:rupay` | Active (India) | v10.0 |
|
|
107
|
+
| `:solo` | Withdrawn 2011 | v10.0 |
|
|
108
|
+
| `:switch` | Withdrawn 2002 | v10.0 |
|
|
77
109
|
|
|
78
|
-
|
|
110
|
+
If your code references any of these brands by symbol, add the matching `require` to your initializer to silence the warning and survive v10:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# config/initializers/credit_card_validations.rb
|
|
114
|
+
require 'credit_card_validations/plugins/mir'
|
|
115
|
+
require 'credit_card_validations/plugins/elo'
|
|
116
|
+
# ...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
When v10 lands, the auto-load disappears. Code that names these brands without a matching `require` will see them as unknown — `Detector#brand` returns `nil`, predicate methods (`mir?`, `elo?`, …) are not defined, and `valid?(:mir)` returns false.
|
|
120
|
+
|
|
121
|
+
### Other breaking changes in v9.0
|
|
122
|
+
|
|
123
|
+
- **`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.
|
|
124
|
+
- **`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]`.
|
|
125
|
+
- **`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.
|
|
126
|
+
- **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.
|
|
127
|
+
|
|
128
|
+
## Usage
|
|
129
|
+
|
|
130
|
+
### String monkey patch
|
|
79
131
|
|
|
80
132
|
```ruby
|
|
81
133
|
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')
|
|
134
|
+
'5274 5763 9425 9961'.credit_card_brand #=> :mastercard
|
|
135
|
+
'5274 5763 9425 9961'.credit_card_brand_name #=> "MasterCard"
|
|
136
|
+
'5274 5763 9425 9961'.valid_credit_card_brand?(:mastercard, :visa) #=> true
|
|
137
|
+
'5274 5763 9425 9961'.valid_credit_card_brand?(:amex) #=> false
|
|
138
|
+
'5274 5763 9425 9961'.valid_credit_card_brand?('MasterCard') #=> true
|
|
87
139
|
```
|
|
88
140
|
|
|
89
|
-
### ActiveModel
|
|
141
|
+
### ActiveModel validators
|
|
90
142
|
|
|
91
|
-
|
|
143
|
+
Restrict to a brand list:
|
|
92
144
|
|
|
93
145
|
```ruby
|
|
94
146
|
class CreditCardModel
|
|
95
147
|
attr_accessor :number
|
|
96
148
|
include ActiveModel::Validations
|
|
97
|
-
validates :number, credit_card_number: {brands: [:amex, :maestro]}
|
|
149
|
+
validates :number, credit_card_number: { brands: [:amex, :maestro] }
|
|
98
150
|
end
|
|
99
151
|
```
|
|
100
152
|
|
|
101
|
-
|
|
153
|
+
Accept any known brand:
|
|
102
154
|
|
|
103
155
|
```ruby
|
|
104
156
|
validates :number, presence: true, credit_card_number: true
|
|
105
157
|
```
|
|
106
158
|
|
|
107
|
-
###
|
|
159
|
+
### CVV validator
|
|
160
|
+
|
|
161
|
+
CVV against a brand pulled from another attribute (the PAN):
|
|
108
162
|
|
|
109
163
|
```ruby
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
164
|
+
class Payment
|
|
165
|
+
include ActiveModel::Validations
|
|
166
|
+
attr_accessor :card_number, :cvv
|
|
167
|
+
|
|
168
|
+
validates :card_number, credit_card_number: true
|
|
169
|
+
validates :cvv, credit_card_cvv: { brand_from: :card_number }
|
|
170
|
+
end
|
|
117
171
|
```
|
|
118
172
|
|
|
119
|
-
|
|
120
|
-
passing name,length(integer/array of integers) and prefix(string/array of strings)
|
|
121
|
-
Example
|
|
173
|
+
CVV against a literal brand:
|
|
122
174
|
|
|
123
175
|
```ruby
|
|
124
|
-
|
|
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
|
|
176
|
+
validates :cvv, credit_card_cvv: { brand: :amex }
|
|
128
177
|
```
|
|
129
178
|
|
|
130
|
-
###
|
|
179
|
+
### Expiration validator
|
|
180
|
+
|
|
181
|
+
A single string attribute (`MM/YY`, `MM/YYYY`, `MMYY`, ...):
|
|
131
182
|
|
|
132
183
|
```ruby
|
|
133
|
-
|
|
184
|
+
validates :expiration, credit_card_expiration: true
|
|
134
185
|
```
|
|
135
186
|
|
|
136
|
-
|
|
187
|
+
Two separate fields (typical month + year dropdowns) — use the `Expiration` class in a `validate` block:
|
|
137
188
|
|
|
138
189
|
```ruby
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
190
|
+
class Payment
|
|
191
|
+
attr_accessor :exp_month, :exp_year
|
|
192
|
+
|
|
193
|
+
validate do
|
|
194
|
+
exp = CreditCardValidations::Expiration.new(exp_month, exp_year)
|
|
195
|
+
errors.add(:exp_month, :invalid) unless exp.valid?
|
|
196
|
+
end
|
|
197
|
+
end
|
|
142
198
|
```
|
|
143
199
|
|
|
144
|
-
###
|
|
200
|
+
### `CreditCardValidations::Card`
|
|
201
|
+
|
|
202
|
+
A composite model wrapping `Detector` and `Expiration` behind a single ActiveModel-aware object:
|
|
145
203
|
|
|
146
204
|
```ruby
|
|
147
|
-
CreditCardValidations::
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
205
|
+
card = CreditCardValidations::Card.new(
|
|
206
|
+
number: '4111 1111 1111 1111',
|
|
207
|
+
month: 12, year: 2027,
|
|
208
|
+
verification_value: '123',
|
|
209
|
+
name: 'John Smith'
|
|
210
|
+
)
|
|
211
|
+
card.valid? # => true
|
|
212
|
+
card.brand # => :visa
|
|
213
|
+
card.display_number # => "************1111"
|
|
214
|
+
card.last_digits # => "1111"
|
|
215
|
+
card.expired? # => false
|
|
216
|
+
card.formatted_number # => "4111 1111 1111 1111"
|
|
151
217
|
```
|
|
152
218
|
|
|
153
|
-
###
|
|
219
|
+
### Using `Detector` directly
|
|
154
220
|
|
|
155
221
|
```ruby
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
222
|
+
number = '4111111111111111'
|
|
223
|
+
detector = CreditCardValidations::Detector.new(number)
|
|
224
|
+
|
|
225
|
+
detector.brand # => :visa
|
|
226
|
+
detector.visa? # => true
|
|
227
|
+
detector.valid?(:mastercard, :maestro) # => false
|
|
228
|
+
detector.valid?(:visa, :mastercard) # => true
|
|
229
|
+
detector.issuer_category # => "Banking and financial"
|
|
230
|
+
detector.last4 # => "1111"
|
|
231
|
+
detector.masked # => "************1111"
|
|
232
|
+
detector.formatted # => "4111 1111 1111 1111"
|
|
233
|
+
detector.possible_brands # => [:visa] (during live input)
|
|
234
|
+
detector.valid_cvv?('123') # => true
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Adding a custom brand at runtime
|
|
159
238
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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'
|
|
239
|
+
```ruby
|
|
240
|
+
CreditCardValidations.add_brand(:voyager, { length: 15, prefixes: '86' })
|
|
241
|
+
CreditCardValidations::Detector.new('869926275400212').voyager? # => true
|
|
171
242
|
```
|
|
172
243
|
|
|
244
|
+
### Removing a brand at runtime
|
|
173
245
|
|
|
174
|
-
|
|
246
|
+
```ruby
|
|
247
|
+
CreditCardValidations::Detector.delete_brand(:maestro)
|
|
248
|
+
```
|
|
175
249
|
|
|
176
|
-
|
|
250
|
+
### Luhn check
|
|
177
251
|
|
|
178
252
|
```ruby
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
253
|
+
CreditCardValidations::Detector.new(number).valid_luhn?
|
|
254
|
+
# or, on a clean digit string:
|
|
255
|
+
CreditCardValidations::Luhn.valid?(number)
|
|
182
256
|
```
|
|
183
257
|
|
|
258
|
+
### Generating Luhn-valid test numbers
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
CreditCardValidations::Factory.random(:amex)
|
|
262
|
+
# => "348051773827666"
|
|
263
|
+
CreditCardValidations::Factory.random(:maestro)
|
|
264
|
+
# => "6010430241237266856"
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Configuration
|
|
268
|
+
|
|
269
|
+
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:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
CreditCardValidations.configure do |config|
|
|
273
|
+
config.source = '/path/to/my_brands.yml'
|
|
274
|
+
end
|
|
275
|
+
```
|
|
184
276
|
|
|
185
277
|
## Contributing
|
|
186
278
|
|
|
@@ -188,7 +280,4 @@ In order to override default data source you can copy [original one](https://git
|
|
|
188
280
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
189
281
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
190
282
|
4. Push to the branch (`git push origin my-new-feature`)
|
|
191
|
-
5.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
283
|
+
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,61 @@
|
|
|
1
|
+
require 'active_model'
|
|
2
|
+
|
|
3
|
+
# == CreditCardValidations::Card
|
|
4
|
+
#
|
|
5
|
+
# Slim alternative to ActiveMerchant::Billing::CreditCard for the validation
|
|
6
|
+
# use case. Wraps PAN, expiration, verification value and cardholder name
|
|
7
|
+
# behind a single ActiveModel-aware object.
|
|
8
|
+
#
|
|
9
|
+
# card = CreditCardValidations::Card.new(
|
|
10
|
+
# number: '4111 1111 1111 1111',
|
|
11
|
+
# month: 12, year: 2027,
|
|
12
|
+
# verification_value: '123',
|
|
13
|
+
# name: 'John Smith'
|
|
14
|
+
# )
|
|
15
|
+
# card.valid? # => true
|
|
16
|
+
# card.brand # => :visa
|
|
17
|
+
# card.display_number # => "************1111"
|
|
18
|
+
# card.last_digits # => "1111"
|
|
19
|
+
# card.expired? # => false
|
|
20
|
+
# card.formatted_number # => "4111 1111 1111 1111"
|
|
21
|
+
#
|
|
22
|
+
module CreditCardValidations
|
|
23
|
+
class Card
|
|
24
|
+
include ActiveModel::Model
|
|
25
|
+
|
|
26
|
+
attr_accessor :number, :month, :year, :verification_value, :name
|
|
27
|
+
|
|
28
|
+
validates :number, credit_card_number: true
|
|
29
|
+
validates :verification_value, credit_card_cvv: { brand_from: :number }
|
|
30
|
+
validate :expiration_must_be_valid
|
|
31
|
+
|
|
32
|
+
def brand = detector.brand
|
|
33
|
+
def brand_name = detector.brand_name
|
|
34
|
+
def last_digits = detector.last4
|
|
35
|
+
def display_number = detector.masked
|
|
36
|
+
def formatted_number = detector.formatted
|
|
37
|
+
|
|
38
|
+
def expiration
|
|
39
|
+
return nil unless month && year
|
|
40
|
+
Expiration.new(month, year)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def expired?
|
|
44
|
+
exp = expiration
|
|
45
|
+
exp.nil? || exp.expired?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def detector
|
|
49
|
+
@detector ||= Detector.new(number)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def expiration_must_be_valid
|
|
55
|
+
return if month.blank? && year.blank?
|
|
56
|
+
exp = expiration
|
|
57
|
+
errors.add(:base, :expired) if exp && exp.expired?
|
|
58
|
+
errors.add(:month, :invalid) if exp && !(1..12).cover?(exp.month)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -9,10 +9,17 @@ module CreditCardValidations
|
|
|
9
9
|
class_attribute :brands
|
|
10
10
|
self.brands = {}
|
|
11
11
|
|
|
12
|
+
# Brands that were part of the default set up to v8.x and moved to
|
|
13
|
+
# opt-in plugins in v9.0. The shim below auto-loads the plugin on first
|
|
14
|
+
# reference and emits a one-time deprecation warning. To be removed in
|
|
15
|
+
# v10.0 — users should add explicit `require` statements by then.
|
|
16
|
+
LEGACY_PLUGIN_BRANDS = %i[mir rupay elo dankort hipercard solo switch].freeze
|
|
17
|
+
@@legacy_autoloaded = {}
|
|
18
|
+
|
|
12
19
|
attr_reader :number
|
|
13
20
|
|
|
14
21
|
def initialize(number)
|
|
15
|
-
@number = number.to_s.
|
|
22
|
+
@number = number.to_s.gsub(/[\s\-]/, '')
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
# credit card number validation
|
|
@@ -50,8 +57,62 @@ module CreditCardValidations
|
|
|
50
57
|
self.class.brand_name(brand)
|
|
51
58
|
end
|
|
52
59
|
|
|
60
|
+
# Last four digits of the PAN, or nil if the PAN has fewer than 4 digits.
|
|
61
|
+
def last4
|
|
62
|
+
number.length >= 4 ? number[-4, 4] : nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# PAN with every digit but the last 4 replaced by mask_char.
|
|
66
|
+
# Returns the original number when shorter than 4 digits — never raises.
|
|
67
|
+
def masked(mask_char = '*')
|
|
68
|
+
return number if number.length < 4
|
|
69
|
+
mask_char.to_s[0] * (number.length - 4) + last4.to_s
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# All brands whose prefixes can still match the (possibly partial) PAN.
|
|
73
|
+
# Length and Luhn are not checked — useful for live UX before the user
|
|
74
|
+
# finishes typing.
|
|
75
|
+
def possible_brands
|
|
76
|
+
return [] if number.empty?
|
|
77
|
+
self.class.brands.each_with_object([]) do |(key, brand), acc|
|
|
78
|
+
next unless brand.fetch(:rules).any? do |rule|
|
|
79
|
+
rule[:prefixes].any? do |prefix|
|
|
80
|
+
n = [number.length, prefix.length].min
|
|
81
|
+
number[0, n] == prefix[0, n]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
acc << key
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Human-readable PAN grouped per network convention. Falls back to the
|
|
89
|
+
# first possible brand while the user is still typing.
|
|
90
|
+
def formatted(separator = ' ')
|
|
91
|
+
groups_for(brand || possible_brands.first).each_with_object([]) do |size, acc|
|
|
92
|
+
slice = number[acc.join.length, size]
|
|
93
|
+
acc << slice if slice && !slice.empty?
|
|
94
|
+
end.join(separator)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Validates the card verification value against the detected brand's
|
|
98
|
+
# declared :code size. Returns false when the brand cannot be determined
|
|
99
|
+
# from the PAN or the input has the wrong shape. Raises when a detected
|
|
100
|
+
# brand is missing :code in the registry.
|
|
101
|
+
def valid_cvv?(code)
|
|
102
|
+
self.class.valid_cvv?(code, brand)
|
|
103
|
+
end
|
|
104
|
+
|
|
53
105
|
protected
|
|
54
106
|
|
|
107
|
+
def groups_for(detected_brand)
|
|
108
|
+
segments = self.class.brands.dig(detected_brand, :options, :segments)
|
|
109
|
+
return segments if segments
|
|
110
|
+
groups = Array.new(number.length / 4, 4)
|
|
111
|
+
remainder = number.length % 4
|
|
112
|
+
groups << remainder if remainder.positive?
|
|
113
|
+
groups
|
|
114
|
+
end
|
|
115
|
+
|
|
55
116
|
def resolve_keys(*keys)
|
|
56
117
|
brand_keys = keys.map do |el|
|
|
57
118
|
if el.is_a? String
|
|
@@ -60,9 +121,25 @@ module CreditCardValidations
|
|
|
60
121
|
end
|
|
61
122
|
el.downcase
|
|
62
123
|
end
|
|
124
|
+
brand_keys.each { |k| autoload_legacy_plugin(k) }
|
|
63
125
|
self.brands.slice(*brand_keys)
|
|
64
126
|
end
|
|
65
127
|
|
|
128
|
+
def autoload_legacy_plugin(key)
|
|
129
|
+
return unless LEGACY_PLUGIN_BRANDS.include?(key)
|
|
130
|
+
return if self.class.brands.key?(key)
|
|
131
|
+
return if @@legacy_autoloaded[key]
|
|
132
|
+
@@legacy_autoloaded[key] = true
|
|
133
|
+
|
|
134
|
+
Warning.warn(
|
|
135
|
+
"[credit_card_validations] :#{key} was moved to a plugin in v9.0. " \
|
|
136
|
+
"Auto-loading 'credit_card_validations/plugins/#{key}' for backward " \
|
|
137
|
+
"compatibility. Add `require 'credit_card_validations/plugins/#{key}'` " \
|
|
138
|
+
"to your initializer to silence; auto-load is removed in v10.\n"
|
|
139
|
+
)
|
|
140
|
+
load "credit_card_validations/plugins/#{key}.rb"
|
|
141
|
+
end
|
|
142
|
+
|
|
66
143
|
def matches_brand?(brand)
|
|
67
144
|
rules = brand.fetch(:rules)
|
|
68
145
|
options = brand.fetch(:options, {})
|
|
@@ -83,12 +160,26 @@ module CreditCardValidations
|
|
|
83
160
|
!brands[key].fetch(:options, {}).fetch(:skip_luhn, false)
|
|
84
161
|
end
|
|
85
162
|
|
|
163
|
+
# Class-level CVV check: validates a code against an explicit brand,
|
|
164
|
+
# without needing a Detector instance. Useful when only the brand is
|
|
165
|
+
# known (form input bound to a brand select, separate CVV field, etc.).
|
|
166
|
+
def valid_cvv?(code, brand)
|
|
167
|
+
return false if code.nil? || brand.nil? || !code.to_s.match?(/\A\d+\z/)
|
|
168
|
+
spec = brands.dig(brand, :options, :code)
|
|
169
|
+
raise Error, "brand #{brand.inspect} has no :code option" if spec.nil?
|
|
170
|
+
code.to_s.length == spec[:size]
|
|
171
|
+
end
|
|
172
|
+
|
|
86
173
|
#
|
|
87
174
|
# add brand
|
|
88
175
|
#
|
|
89
176
|
# CreditCardValidations.add_brand(:en_route, {length: 15, prefixes: ['2014', '2149']}, {skip_luhn: true}) #skip luhn
|
|
90
177
|
#
|
|
91
178
|
def add_brand(key, rules, options = {})
|
|
179
|
+
# Mark legacy plugin brands as handled so the v9 auto-require shim
|
|
180
|
+
# never re-loads them after the user takes any explicit action
|
|
181
|
+
# (require, add_brand, or a later delete_brand).
|
|
182
|
+
@@legacy_autoloaded[key] = true if LEGACY_PLUGIN_BRANDS.include?(key)
|
|
92
183
|
|
|
93
184
|
brands[key] = {rules: [], options: options || {}}
|
|
94
185
|
|