diff --git a/README.md b/README.md index 190753e..19c85ea 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ If you are using Laravel, add an alias in `config/app.php` ### Validate a card number knowing the type: ```php -$card = CreditCard::validCreditCard('5500005555555559', 'mastercard'); +$card = CreditCard::validCreditCard('5500005555555559', CreditCard::TYPE_MASTERCARD); print_r($card); ``` @@ -43,12 +43,47 @@ Output: ``` Array ( - [valid] => 1 + [valid] => true [number] => 5500005555555559 [type] => mastercard ) ``` +### Validate a card number using a range of allowed types: + +```php +$card = CreditCard::validCreditCard('5500005555555559', array(CreditCard::TYPE_VISA, CreditCard::TYPE_MASTERCARD)); +print_r($card); +``` + +Output: + +``` +Array +( + [valid] => true + [number] => 5500005555555559 + [type] => mastercard +) +``` + + +```php +$card = CreditCard::validCreditCard('371449635398431', array(CreditCard::TYPE_VISA, CreditCard::TYPE_MASTERCARD)); +print_r($card); +``` + +Output: + +``` +Array +( + [valid] => false + [number] => + [type] => +) +``` + ### Validate a card number and return the type: ```php @@ -61,7 +96,7 @@ Output: ``` Array ( - [valid] => 1 + [valid] => true [number] => 371449635398431 [type] => amex ) @@ -70,7 +105,7 @@ Array ### Validate the CVC ```php -$validCvc = CreditCard::validCvc('234', 'visa'); +$validCvc = CreditCard::validCvc('234', CreditCard::TYPE_VISA); var_dump($validCvc); ``` diff --git a/composer.json b/composer.json index dec0e6b..483d47a 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "lib-pcre": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "4.7.*" + "phpunit/phpunit": "^4.8" }, "autoload": { "psr-4": { diff --git a/src/CreditCard.php b/src/CreditCard.php index ce8f5d6..1f9d993 100644 --- a/src/CreditCard.php +++ b/src/CreditCard.php @@ -10,85 +10,127 @@ namespace Inacho; +use Inacho\Exception\CreditCardException; +use Inacho\Exception\CreditCardLengthException; +use Inacho\Exception\CreditCardLuhnException; +use Inacho\Exception\CreditCardPatternException; +use Inacho\Exception\CreditCardTypeException; + class CreditCard { + const TYPE_AMEX = 'amex'; + const TYPE_DANKORT = 'dankort'; + const TYPE_DINERS_CLUB = 'dinersclub'; + const TYPE_DISCOVER = 'discover'; + const TYPE_FORBRUGSFORENINGEN = 'forbrugsforeningen'; + const TYPE_JCB = 'jcb'; + const TYPE_MAESTRO = 'maestro'; + const TYPE_MASTERCARD = 'mastercard'; + const TYPE_MIR = 'mir'; + const TYPE_UNION_PAY = 'unionpay'; + const TYPE_UZCARD = 'uzcard'; + const TYPE_HUMO = 'humo'; + const TYPE_VISA = 'visa'; + const TYPE_VISA_ELECTRON = 'visaelectron'; + protected static $cards = array( // Debit cards must come first, since they have more specific patterns than their credit-card equivalents. - 'visaelectron' => array( - 'type' => 'visaelectron', + self::TYPE_VISA_ELECTRON => array( + 'type' => self::TYPE_VISA_ELECTRON, 'pattern' => '/^4(026|17500|405|508|844|91[37])/', 'length' => array(16), 'cvcLength' => array(3), 'luhn' => true, ), - 'maestro' => array( - 'type' => 'maestro', - 'pattern' => '/^(5(018|0[23]|[68])|6(39|7))/', + self::TYPE_MAESTRO => array( + 'type' => self::TYPE_MAESTRO, + 'pattern' => '/^(5(018|0[23]|[68])|6(05|39|7))/', 'length' => array(12, 13, 14, 15, 16, 17, 18, 19), 'cvcLength' => array(3), 'luhn' => true, ), - 'forbrugsforeningen' => array( - 'type' => 'forbrugsforeningen', + self::TYPE_FORBRUGSFORENINGEN => array( + 'type' => self::TYPE_FORBRUGSFORENINGEN, 'pattern' => '/^600/', 'length' => array(16), 'cvcLength' => array(3), 'luhn' => true, ), - 'dankort' => array( - 'type' => 'dankort', + self::TYPE_DANKORT => array( + 'type' => self::TYPE_DANKORT, 'pattern' => '/^5019/', 'length' => array(16), 'cvcLength' => array(3), 'luhn' => true, ), // Credit cards - 'visa' => array( - 'type' => 'visa', + self::TYPE_VISA => array( + 'type' => self::TYPE_VISA, 'pattern' => '/^4/', 'length' => array(13, 16), 'cvcLength' => array(3), 'luhn' => true, ), - 'mastercard' => array( - 'type' => 'mastercard', + self::TYPE_MIR => array( + 'type' => self::TYPE_MIR, + 'pattern' => '/^220[0-4]/', + 'length' => array(16), + 'cvcLength' => array(3), + 'luhn' => true, + ), + self::TYPE_MASTERCARD => array( + 'type' => self::TYPE_MASTERCARD, 'pattern' => '/^(5[0-5]|2[2-7])/', 'length' => array(16), 'cvcLength' => array(3), 'luhn' => true, ), - 'amex' => array( - 'type' => 'amex', + self::TYPE_AMEX => array( + 'type' => self::TYPE_AMEX, 'pattern' => '/^3[47]/', 'format' => '/(\d{1,4})(\d{1,6})?(\d{1,5})?/', 'length' => array(15), 'cvcLength' => array(3, 4), 'luhn' => true, ), - 'dinersclub' => array( - 'type' => 'dinersclub', + self::TYPE_DINERS_CLUB => array( + 'type' => self::TYPE_DINERS_CLUB, 'pattern' => '/^3[0689]/', 'length' => array(14), 'cvcLength' => array(3), 'luhn' => true, ), - 'discover' => array( - 'type' => 'discover', + self::TYPE_DISCOVER => array( + 'type' => self::TYPE_DISCOVER, 'pattern' => '/^6([045]|22)/', 'length' => array(16), 'cvcLength' => array(3), 'luhn' => true, ), - 'unionpay' => array( - 'type' => 'unionpay', + self::TYPE_UNION_PAY => array( + 'type' => self::TYPE_UNION_PAY, 'pattern' => '/^(62|88)/', 'length' => array(16, 17, 18, 19), 'cvcLength' => array(3), 'luhn' => false, ), - 'jcb' => array( - 'type' => 'jcb', + self::TYPE_UZCARD => array( + 'type' => self::TYPE_UZCARD, + 'pattern' => '/^8600/', + 'length' => array(16), + 'cvcLength' => array(3), + 'luhn' => true, + ), + self::TYPE_HUMO => array( + 'type' => self::TYPE_HUMO, + 'pattern' => '/^9860/', + 'length' => array(16), + 'cvcLength' => array(3), + 'luhn' => true, + ), + self::TYPE_JCB => array( + 'type' => self::TYPE_JCB, 'pattern' => '/^35/', 'length' => array(16), 'cvcLength' => array(3), @@ -96,7 +138,12 @@ class CreditCard ), ); - public static function validCreditCard($number, $type = null) + /** + * @param string $number + * @param string|string[]|null $allowTypes By default, all card types are allowed + * @return array + */ + public static function validCreditCard($number, $allowTypes = null) { $ret = array( 'valid' => false, @@ -105,12 +152,18 @@ public static function validCreditCard($number, $type = null) ); // Strip non-numeric characters - $number = preg_replace('/[^0-9]/', '', $number); + $number = preg_replace('/\D/', '', $number); - if (empty($type)) { + if (is_string($allowTypes)) { + $type = $allowTypes; + } else { $type = self::creditCardType($number); } + if (empty($type) || is_array($allowTypes) && !in_array($type, $allowTypes)) { + return $ret; + } + if (array_key_exists($type, self::$cards) && self::validCard($number, $type)) { return array( 'valid' => true, @@ -122,6 +175,39 @@ public static function validCreditCard($number, $type = null) return $ret; } + /** + * @param string $number + * @param string|string[]|null $allowTypes By default, all card types are allowed + * @throws CreditCardException + */ + public static function checkCreditCard($number, $allowTypes = null) + { + // Strip non-numeric characters + $number = preg_replace('/\D/', '', $number); + + if (is_string($allowTypes)) { + $type = $allowTypes; + } else { + $type = self::creditCardType($number); + } + + if (empty($type) || (is_array($allowTypes) && !in_array($type, $allowTypes))) { + throw new CreditCardTypeException(sprintf('Type "%s" card is not allowed', $type)); + } + + if (!self::validPattern($number, $type)) { + throw new CreditCardPatternException(sprintf('Wrong "%s" card pattern', $number)); + } + + if (!self::validLength($number, $type)) { + throw new CreditCardLengthException(sprintf('Incorrect "%s" card length', $number)); + } + + if (!self::validLuhn($number, $type)) { + throw new CreditCardLuhnException(sprintf('Invalid card number: "%s". Checksum is wrong', $number)); + } + } + public static function validCvc($cvc, $type) { return (ctype_digit($cvc) && array_key_exists($type, self::$cards) && self::validCvcLength($cvc, $type)); diff --git a/src/Exception/CreditCardException.php b/src/Exception/CreditCardException.php new file mode 100644 index 0000000..1cea882 --- /dev/null +++ b/src/Exception/CreditCardException.php @@ -0,0 +1,6 @@ + array( + CreditCard::TYPE_VISA_ELECTRON => array( '4917300800000000', ), - 'maestro' => array( + CreditCard::TYPE_MAESTRO => array( '6759649826438453', '6799990100000000019', ), - 'forbrugsforeningen' => array( + CreditCard::TYPE_FORBRUGSFORENINGEN => array( '6007220000000004', ), - 'dankort' => array( + CreditCard::TYPE_DANKORT => array( '5019717010103742', ), // Credit cards - 'visa' => array( + CreditCard::TYPE_VISA => array( '4111111111111111', '4012888888881881', '4222222222222', '4462030000000000', '4484070000000000', ), - 'mastercard' => array( + CreditCard::TYPE_MIR => array( + '2200654321000000', + ), + CreditCard::TYPE_MASTERCARD => array( '5555555555554444', '5454545454545454', '2221000002222221', ), - 'amex' => array( + CreditCard::TYPE_AMEX => array( '378282246310005', '371449635398431', '378734493671000', // American Express Corporate ), - 'dinersclub' => array( + CreditCard::TYPE_DINERS_CLUB => array( '30569309025904', '38520000023237', '36700102000000', '36148900647913', ), - 'discover' => array( + CreditCard::TYPE_DISCOVER => array( '6011111111111117', '6011000990139424', ), - 'unionpay' => array( + CreditCard::TYPE_UNION_PAY => array( '6271136264806203568', '6236265930072952775', '6204679475679144515', '6216657720782466507', ), - 'jcb' => array( + CreditCard::TYPE_UZCARD => array( + ), + CreditCard::TYPE_HUMO => array( + ), + CreditCard::TYPE_JCB => array( '3530111333300000', '3566002020360505', ), @@ -79,6 +86,33 @@ public function testCardsTypes() } } + public function testCardAllowedTypes() + { + // Card type specified + $result = CreditCard::validCreditCard($this->validCards[CreditCard::TYPE_VISA][0], CreditCard::TYPE_VISA); + $this->assertEquals(true, $result['valid'], 'Invalid card, expected valid.'); + $this->assertEquals(CreditCard::TYPE_VISA, $result['type'], 'Invalid type.'); + + // Wrong card type + $result = CreditCard::validCreditCard($this->validCards[CreditCard::TYPE_VISA][0], CreditCard::TYPE_MASTERCARD); + $this->assertEquals(false, $result['valid'], 'Valid card, expected invalid.'); + + // Allowed types specified as an array + $result = CreditCard::validCreditCard( + $this->validCards[CreditCard::TYPE_VISA][0], + array(CreditCard::TYPE_VISA, CreditCard::TYPE_MASTERCARD) + ); + $this->assertEquals(true, $result['valid'], 'Invalid card, expected valid.'); + $this->assertEquals(CreditCard::TYPE_VISA, $result['type'], 'Invalid type.'); + + // Card type is not in the allowed types array + $result = CreditCard::validCreditCard( + $this->validCards[CreditCard::TYPE_AMEX][0], + array(CreditCard::TYPE_VISA, CreditCard::TYPE_MASTERCARD) + ); + $this->assertEquals(false, $result['valid'], 'Valid card, expected invalid.'); + } + public function testNumbers() { // Empty number @@ -110,6 +144,30 @@ public function testNumbers() $this->assertEquals(false, $result['valid']); } + public function testLengthException() + { + $this->setExpectedException('Inacho\Exception\CreditCardLengthException'); + CreditCard::checkCreditCard('42424242424242424'); + } + + public function testTypeException() + { + $this->setExpectedException('Inacho\Exception\CreditCardTypeException'); + CreditCard::checkCreditCard('4242-4242-4242-4242', array(CreditCard::TYPE_MASTERCARD, CreditCard::TYPE_AMEX)); + } + + public function testPatternException() + { + $this->setExpectedException('Inacho\Exception\CreditCardPatternException'); + CreditCard::checkCreditCard('6266-4242-42942-4242', CreditCard::TYPE_VISA); + } + + public function testLuhnException() + { + $this->setExpectedException('Inacho\Exception\CreditCardLuhnException'); + CreditCard::checkCreditCard('4233-3333-3333-4242'); + } + public function testLuhn() { $result = CreditCard::validCreditCard('4242424242424241'); @@ -155,18 +213,18 @@ public function testCvc() $this->assertEquals(false, CreditCard::validCvc('123', '')); // Empty number - $this->assertEquals(false, CreditCard::validCvc('', 'visa')); + $this->assertEquals(false, CreditCard::validCvc('', CreditCard::TYPE_VISA)); // Valid - $this->assertEquals(true, CreditCard::validCvc('123', 'visa')); + $this->assertEquals(true, CreditCard::validCvc('123', CreditCard::TYPE_VISA)); // Non digits - $this->assertEquals(false, CreditCard::validCvc('12e', 'visa')); + $this->assertEquals(false, CreditCard::validCvc('12e', CreditCard::TYPE_VISA)); // Less than 3 digits - $this->assertEquals(false, CreditCard::validCvc('12', 'visa')); + $this->assertEquals(false, CreditCard::validCvc('12', CreditCard::TYPE_VISA)); // More than 3 digits - $this->assertEquals(false, CreditCard::validCvc('1234', 'visa')); + $this->assertEquals(false, CreditCard::validCvc('1234', CreditCard::TYPE_VISA)); } }