Passed
Push — main ( 926316...d9c688 )
by Dimitri
02:57
created

CreditCard::validCC()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 14
c 1
b 0
f 0
nc 10
nop 2
dl 0
loc 31
ccs 9
cts 9
cp 1
crap 6
rs 9.2222
1
<?php
2
3
/**
4
 * This file is part of Dimtrovich/Validation.
5
 *
6
 * (c) 2023 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace Dimtrovich\Validation\Rules;
13
14
use Rakit\Validation\Rule;
15
16
/**
17
 * @credit <a href="https://github.com/milwad-dev/laravel-validate">milwad/laravel-validate - Milwad\LaravelValidate\Rules\ValidCreditCard</a>
18
 * @credit <a href="https://codeigniter.com">CodeIgniter4 - CodeIgniter\Validation\CreditCardRules</a>
19
 *
20
 * @see http://en.wikipedia.org/wiki/Credit_card_number
21
 */
22
class CreditCard extends AbstractRule
23
{
24
    /**
25
     * The cards that we support, with the defining details:
26
     *
27
     *  name        - The type of card as found in the form. Must match the user's value
28
     *  length      - List of possible lengths for the card number
29
     *  prefixes    - List of possible prefixes for the card
30
     *  checkdigit  - Boolean on whether we should do a modulus10 check on the numbers.
31
     */
32
    protected array $cards = [
33
        'American Express' => [
34
            'name'       => 'amex',
35
            'length'     => '15',
36
            'prefixes'   => '34,37',
37
            'checkdigit' => true,
38
        ],
39
        'China UnionPay' => [
40
            'name'       => 'unionpay',
41
            'length'     => '16,17,18,19',
42
            'prefixes'   => '62',
43
            'checkdigit' => true,
44
        ],
45
        'Dankort' => [
46
            'name'       => 'dankort',
47
            'length'     => '16',
48
            'prefixes'   => '5019,4175,4571,4',
49
            'checkdigit' => true,
50
        ],
51
        'DinersClub' => [
52
            'name'       => 'dinersclub',
53
            'length'     => '14,16',
54
            'prefixes'   => '300,301,302,303,304,305,309,36,38,39,54,55',
55
            'checkdigit' => true,
56
        ],
57
        'DinersClub CarteBlanche' => [
58
            'name'       => 'carteblanche',
59
            'length'     => '14',
60
            'prefixes'   => '300,301,302,303,304,305',
61
            'checkdigit' => true,
62
        ],
63
        'Discover Card' => [
64
            'name'       => 'discover',
65
            'length'     => '16,19',
66
            'prefixes'   => '6011,622,644,645,656,647,648,649,65',
67
            'checkdigit' => true,
68
        ],
69
        'InterPayment' => [
70
            'name'       => 'interpayment',
71
            'length'     => '16,17,18,19',
72
            'prefixes'   => '4',
73
            'checkdigit' => true,
74
        ],
75
        'JCB' => [
76
            'name'       => 'jcb',
77
            'length'     => '16,17,18,19',
78
            'prefixes'   => '352,353,354,355,356,357,358',
79
            'checkdigit' => true,
80
        ],
81
        'Maestro' => [
82
            'name'       => 'maestro',
83
            'length'     => '12,13,14,15,16,18,19',
84
            'prefixes'   => '50,56,57,58,59,60,61,62,63,64,65,66,67,68,69',
85
            'checkdigit' => true,
86
        ],
87
        'MasterCard' => [
88
            'name'       => 'mastercard',
89
            'length'     => '16',
90
            'prefixes'   => '51,52,53,54,55,22,23,24,25,26,27',
91
            'checkdigit' => true,
92
        ],
93
        'NSPK MIR' => [
94
            'name'       => 'mir',
95
            'length'     => '16',
96
            'prefixes'   => '2200,2201,2202,2203,2204',
97
            'checkdigit' => true,
98
        ],
99
        'Troy' => [
100
            'name'       => 'troy',
101
            'length'     => '16',
102
            'prefixes'   => '979200,979289',
103
            'checkdigit' => true,
104
        ],
105
        'UATP' => [
106
            'name'       => 'uatp',
107
            'length'     => '15',
108
            'prefixes'   => '1',
109
            'checkdigit' => true,
110
        ],
111
        'Verve' => [
112
            'name'       => 'verve',
113
            'length'     => '16,19',
114
            'prefixes'   => '506,650',
115
            'checkdigit' => true,
116
        ],
117
        'Visa' => [
118
            'name'       => 'visa',
119
            'length'     => '13,16,19',
120
            'prefixes'   => '4',
121
            'checkdigit' => true,
122
        ],
123
        // Canadian Cards
124
        'BMO ABM Card' => [
125
            'name'       => 'bmoabm',
126
            'length'     => '16',
127
            'prefixes'   => '500',
128
            'checkdigit' => false,
129
        ],
130
        'CIBC Convenience Card' => [
131
            'name'       => 'cibc',
132
            'length'     => '16',
133
            'prefixes'   => '4506',
134
            'checkdigit' => false,
135
        ],
136
        'HSBC Canada Card' => [
137
            'name'       => 'hsbc',
138
            'length'     => '16',
139
            'prefixes'   => '56',
140
            'checkdigit' => false,
141
        ],
142
        'Royal Bank of Canada Client Card' => [
143
            'name'       => 'rbc',
144
            'length'     => '16',
145
            'prefixes'   => '45',
146
            'checkdigit' => false,
147
        ],
148
        'Scotiabank Scotia Card' => [
149
            'name'       => 'scotia',
150
            'length'     => '16',
151
            'prefixes'   => '4536',
152
            'checkdigit' => false,
153
        ],
154
        'TD Canada Trust Access Card' => [
155
            'name'       => 'tdtrust',
156
            'length'     => '16',
157
            'prefixes'   => '589297',
158
            'checkdigit' => false,
159
        ],
160
    ];
161
162
    /**
163
     * {@inheritDoc}
164
     */
165
    public function fillParameters(array $params): Rule
166
    {
167 2
        $this->params['types'] = $params;
168
169 2
        return $this;
170
    }
171
    
172
    /**
173
     * @param mixed $value
174
     */
175
    public function check($value): bool
176
    {
177 2
        $value = preg_replace('/\D/', '', (string) $value);
178 2
        $value = str_replace([' ', '-'], '', $value);
179
        
180
        // Non-numeric values cannot be a number...duh
181
        if (! is_numeric($value)) {
182 2
            return false;
183
        }
184
185
        if (empty($types = $this->parameter('types'))) {
186
            // if the card type is not specified, a rough check is performed directly using Luhn's algorithm,
187
            // without taking into account the constraints of each card type (prefix, number of characters).
188 2
            return $this->isValidLuhn($value);
189
        }
190
191 2
        $info = null;
192
        
193
        label:
194
195
        if ($types === []) {
196 2
            return false;
197
        }
198
199
        // Get our card info based on provided name.
200
        foreach ($this->cards as $card) {
201
            if ($card['name'] === $types[0]) {
202 2
                $info = $card;
203 2
                break;
204
            }
205
        }
206
207
        // If empty, it's not a card type we recognize, or invalid type.
208
        if (empty($info)) {
209 2
            return false;
210
        }
211
212
        if (! $this->validCC($info, $value)) {
213 2
            array_shift($types);
214 2
            goto label;
215
        }
216
217 2
        return true;
218
    }
219
220
    /**
221
     * Valid credit card according to the card type
222
     */
223
    protected function validCC(array $info, mixed $value): bool
224
    {
225
        // Make sure it's a valid length for this card
226 2
        $lengths = explode(',', $info['length']);
227
228
        if (! in_array((string) strlen($value), $lengths, true)) {
229 2
            return false;
230
        }
231
232
        // Make sure it has a valid prefix
233 2
        $prefixes = explode(',', $info['prefixes']);
234
235 2
        $validPrefix = false;
236
237
        foreach ($prefixes as $prefix) {
238
            if (str_starts_with($value, $prefix)) {
239 2
                $validPrefix = true;
240 2
                break;
241
            }
242
        }
243
244
        if ($validPrefix === false) {
245 2
            return false;
246
        }
247
248
        // Still here? Then check the number against the Luhn algorithm, if required
249
        if ($info['checkdigit'] === true) {
250 2
            return $this->isValidLuhn($value);
251
        }
252
253 2
        return true;
254
    }
255
256
    /**
257
     * Checks the given number to see if the number passing a Luhn algorithm check.
258
     *
259
     * @see https://en.wikipedia.org/wiki/Luhn_algorithm
260
     * @credit <a href="https://github.com/milwad-dev/laravel-validate">milwad/laravel-validate - Milwad\LaravelValidate\Rules\ValidCreditCard</a>
261
     */
262
    protected function isValidLuhn(string $number): bool
263
    {
264 2
        $numLength = strlen($number);
265 2
        $sum       = 0;
266 2
        $reverse   = strrev($number);
267
268 2
        for ($i = 0; $i < $numLength; $i++) {
269 2
            $currentNum = (int) ($reverse[$i]);
270
            if ($i % 2 === 1) {
271 2
                $currentNum *= 2;
272
                if ($currentNum > 9) {
273 2
                    $currentNum -= 9;
274
                }
275
            }
276 2
            $sum += $currentNum;
277
        }
278
279 2
        return $sum % 10 === 0;
280
    }
281
}
282