CreditCard   A
last analyzed

Complexity

Total Complexity 19

Size/Duplication

Total Lines 259
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 154
c 3
b 0
f 0
dl 0
loc 259
ccs 32
cts 32
cp 1
rs 10
wmc 19

4 Methods

Rating   Name   Duplication   Size   Complexity  
A fillParameters() 0 5 1
A validCC() 0 31 6
B check() 0 44 8
A isValidLuhn() 0 18 4
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
215 2
            goto label;
216
        }
217
218 2
        return true;
219
    }
220
221
    /**
222
     * Valid credit card according to the card type
223
     */
224
    protected function validCC(array $info, mixed $value): bool
225
    {
226
        // Make sure it's a valid length for this card
227 2
        $lengths = explode(',', $info['length']);
228
229
        if (! in_array((string) strlen($value), $lengths, true)) {
230 2
            return false;
231
        }
232
233
        // Make sure it has a valid prefix
234 2
        $prefixes = explode(',', $info['prefixes']);
235
236 2
        $validPrefix = false;
237
238
        foreach ($prefixes as $prefix) {
239
            if (str_starts_with($value, $prefix)) {
240 2
                $validPrefix = true;
241 2
                break;
242
            }
243
        }
244
245
        if ($validPrefix === false) {
246 2
            return false;
247
        }
248
249
        // Still here? Then check the number against the Luhn algorithm, if required
250
        if ($info['checkdigit'] === true) {
251 2
            return $this->isValidLuhn($value);
252
        }
253
254 2
        return true;
255
    }
256
257
    /**
258
     * Checks the given number to see if the number passing a Luhn algorithm check.
259
     *
260
     * @see https://en.wikipedia.org/wiki/Luhn_algorithm
261
     * @credit <a href="https://github.com/milwad-dev/laravel-validate">milwad/laravel-validate - Milwad\LaravelValidate\Rules\ValidCreditCard</a>
262
     */
263
    protected function isValidLuhn(string $number): bool
264
    {
265 2
        $numLength = strlen($number);
266 2
        $sum       = 0;
267 2
        $reverse   = strrev($number);
268
269 2
        for ($i = 0; $i < $numLength; $i++) {
270 2
            $currentNum = (int) ($reverse[$i]);
271
            if ($i % 2 === 1) {
272 2
                $currentNum *= 2;
273
                if ($currentNum > 9) {
274 2
                    $currentNum -= 9;
275
                }
276
            }
277 2
            $sum += $currentNum;
278
        }
279
280 2
        return $sum % 10 === 0;
281
    }
282
}
283