diff --git a/README.md b/README.md index d90a102..12fc09b 100644 --- a/README.md +++ b/README.md @@ -76,27 +76,6 @@ CardNumber::isValid('2730 1684 6416 1841', 'visa'); // false CardNumber::isValid('2201 6868 4646 8444', 'visa'); // false ``` -List of available card types: - -| Bank Name | Card Type | Enum Type | -|--------------------|----------------------|------------------------------------------------------------| -| AmericanExpress | `amex` | `DragonCode\CardNumber\Enums\CardType::americanExpress` | -| Dankort | `dankort` | `DragonCode\CardNumber\Enums\CardType::dankort` | -| DinersClub | `dinersclub` | `DragonCode\CardNumber\Enums\CardType::dinersClub` | -| Discovery | `discovery` | `DragonCode\CardNumber\Enums\CardType::discovery` | -| Forbrugsforeningen | `forbrugsforeningen` | `DragonCode\CardNumber\Enums\CardType::forbrugsforeningen` | -| HiperCard | `hipercard` | `DragonCode\CardNumber\Enums\CardType::hiperCard` | -| Jcb | `jcb` | `DragonCode\CardNumber\Enums\CardType::jcb` | -| Maestro | `maestro` | `DragonCode\CardNumber\Enums\CardType::maestro` | -| MasterCard | `mastercard` | `DragonCode\CardNumber\Enums\CardType::masterCard` | -| Mir | `mir` | `DragonCode\CardNumber\Enums\CardType::mir` | -| Ralf Ringer | `ralfringer` | `DragonCode\CardNumber\Enums\CardType::ralfRinger` | -| Troy | `troy` | `DragonCode\CardNumber\Enums\CardType::troy` | -| Unionpay | `unionpay` | `DragonCode\CardNumber\Enums\CardType::unionPay` | -| Visa | `visa` | `DragonCode\CardNumber\Enums\CardType::visa` | -| VisaElectron | `visaelectron` | `DragonCode\CardNumber\Enums\CardType::visaElectron` | -| Yves Rocher | `yvesrocher` | `DragonCode\CardNumber\Enums\CardType::yvesRocher` | - You can also check for invalid numbers: ```php @@ -121,6 +100,44 @@ CardNumber::isInvalid('5580-4733x7202_47 33'); // false CardNumber::isInvalid('5580-4733x7202_47 32'); // true ``` +In addition to numerical values, you can also validate number-letter combinations. For example: + +```php +use DragonCode\CardNumber\CardNumber; +use DragonCode\CardNumber\Enums\CardType; + +CardNumber::isValid('EKN-OSX', CardType::chars); // true +CardNumber::isValid('EKN-56X', CardType::chars); // true + +CardNumber::isValid('ekn-osx', 'chars'); // true +CardNumber::isValid('ekn-56x', 'chars'); // true + +CardNumber::isValid('EKN-OSX'); // false +CardNumber::isValid('EKN-56X'); // false +``` + +List of available card types: + +| Type | Card Type | Enum Type | +|--------------------|----------------------|------------------------------------------------------------| +| AmericanExpress | `amex` | `DragonCode\CardNumber\Enums\CardType::americanExpress` | +| Chars Number | `chars` | `DragonCode\CardNumber\Enums\CardType::chars` | +| Dankort | `dankort` | `DragonCode\CardNumber\Enums\CardType::dankort` | +| DinersClub | `dinersclub` | `DragonCode\CardNumber\Enums\CardType::dinersClub` | +| Discovery | `discovery` | `DragonCode\CardNumber\Enums\CardType::discovery` | +| Forbrugsforeningen | `forbrugsforeningen` | `DragonCode\CardNumber\Enums\CardType::forbrugsforeningen` | +| HiperCard | `hipercard` | `DragonCode\CardNumber\Enums\CardType::hiperCard` | +| Jcb | `jcb` | `DragonCode\CardNumber\Enums\CardType::jcb` | +| Maestro | `maestro` | `DragonCode\CardNumber\Enums\CardType::maestro` | +| MasterCard | `mastercard` | `DragonCode\CardNumber\Enums\CardType::masterCard` | +| Mir | `mir` | `DragonCode\CardNumber\Enums\CardType::mir` | +| Ralf Ringer | `ralfringer` | `DragonCode\CardNumber\Enums\CardType::ralfRinger` | +| Troy | `troy` | `DragonCode\CardNumber\Enums\CardType::troy` | +| Unionpay | `unionpay` | `DragonCode\CardNumber\Enums\CardType::unionPay` | +| Visa | `visa` | `DragonCode\CardNumber\Enums\CardType::visa` | +| VisaElectron | `visaelectron` | `DragonCode\CardNumber\Enums\CardType::visaElectron` | +| Yves Rocher | `yvesrocher` | `DragonCode\CardNumber\Enums\CardType::yvesRocher` | + ### Generation You can also easily generate any numbers using the Luhn algorithm: @@ -192,6 +209,20 @@ CardNumber::generate(558047337202473, $formatter); // 5580/473372/024733 > * `DragonCode\CardNumber\Formatters\DefaultFormatter` > * `DragonCode\CardNumber\Formatters\BankFormatter` > * `DragonCode\CardNumber\Formatters\LoyaltyFormatter` +> * `DragonCode\CardNumber\Formatters\LoyaltyCharFormatter` + +In addition to numeric formatters, you can also use number-letter combinations. +For example, using the "LoyaltyCharsFormatter" formatter, you can generate a letter code instead of a numeric number, +which will be valid when verified by the Luhn's algorithm: + +```php +use DragonCode\CardNumber\CardNumber; +use DragonCode\CardNumber\Formatters\LoyaltyCharFormatter; + +$formatter = LoyaltyCharFormatter::create(); + +CardNumber::generate(345678123, $formatter); // KN-OSXY-AEKF +``` ### Factories diff --git a/src/CardNumber.php b/src/CardNumber.php index 566a170..d116999 100644 --- a/src/CardNumber.php +++ b/src/CardNumber.php @@ -5,6 +5,7 @@ namespace DragonCode\CardNumber; use DragonCode\CardNumber\Cards\AmericanExpress; +use DragonCode\CardNumber\Cards\Chars; use DragonCode\CardNumber\Cards\Dankort; use DragonCode\CardNumber\Cards\DefaultCard; use DragonCode\CardNumber\Cards\DinersClub; @@ -33,6 +34,7 @@ public static function isValid(int|string $number, CardType|string|null $cardTyp { return match (static::detectCardType($cardType)) { CardType::americanExpress => AmericanExpress::isValid($number), + CardType::chars => Chars::isValid($number), CardType::dankort => Dankort::isValid($number), CardType::dinersClub => DinersClub::isValid($number), CardType::discovery => Discovery::isValid($number), diff --git a/src/Cards/Card.php b/src/Cards/Card.php index e9817b6..cea0ca2 100644 --- a/src/Cards/Card.php +++ b/src/Cards/Card.php @@ -4,15 +4,17 @@ namespace DragonCode\CardNumber\Cards; +use DragonCode\CardNumber\Concerns\Stringable; use DragonCode\CardNumber\Services\Validator; use function in_array; -use function mb_strlen; use function preg_match; use function preg_replace; abstract class Card { + use Stringable; + protected static ?string $pattern = null; protected static array $numberLength = [16]; @@ -28,7 +30,11 @@ public static function isValid(int|string $cardNumber): bool protected static function isValidLength(string $number): bool { - return in_array(static::length($number), static::$numberLength, true); + if ($length = static::$numberLength) { + return in_array(static::length($number), $length, true); + } + + return true; } protected static function isValidPattern(string $number): bool @@ -45,11 +51,6 @@ protected static function isValidNumber(string $number): bool return (new Validator())->isValid($number); } - protected static function length(string $number): int - { - return mb_strlen($number, 'utf8'); - } - protected static function clear(int|string $number): string { return preg_replace('/\D/', '', (string) $number); diff --git a/src/Cards/Chars.php b/src/Cards/Chars.php new file mode 100644 index 0000000..2efee2c --- /dev/null +++ b/src/Cards/Chars.php @@ -0,0 +1,55 @@ + 'f', + 1 => 'a', + 2 => 'e', + 3 => 'k', + 4 => 'n', + 5 => 'o', + 6 => 's', + 7 => 'x', + 8 => 'y', + 9 => 'z', + ]; +} diff --git a/src/Concerns/Stringable.php b/src/Concerns/Stringable.php index ba9317e..177f438 100644 --- a/src/Concerns/Stringable.php +++ b/src/Concerns/Stringable.php @@ -5,19 +5,31 @@ namespace DragonCode\CardNumber\Concerns; use function mb_strlen; +use function mb_strtolower; +use function mb_strtoupper; use function str_pad; use const STR_PAD_LEFT; trait Stringable { - protected function length(string $number, int $offset = 0): int + protected static function length(string $value, int $offset = 0): int { - return mb_strlen($number, 'utf8') + $offset; + return mb_strlen($value, 'utf8') + $offset; } - protected function strPad(int|string $string, int $length): string + protected static function strPad(int|string $value, int $length): string { - return str_pad((string) $string, $length, '0', STR_PAD_LEFT); + return str_pad((string) $value, $length, '0', STR_PAD_LEFT); + } + + protected static function lower(int|string $value): string + { + return mb_strtolower((string) $value, 'utf8'); + } + + protected static function upper(int|string $value): string + { + return mb_strtoupper((string) $value, 'utf8'); } } diff --git a/src/Enums/CardType.php b/src/Enums/CardType.php index a7b3199..44b0d3c 100644 --- a/src/Enums/CardType.php +++ b/src/Enums/CardType.php @@ -11,6 +11,7 @@ enum CardType: string use Values; case americanExpress = 'amex'; + case chars = 'chars'; case dankort = 'dankort'; case dinersClub = 'dinersclub'; case discovery = 'discovery'; diff --git a/src/Factories/BankFactory.php b/src/Factories/BankFactory.php index 012f724..98eae45 100644 --- a/src/Factories/BankFactory.php +++ b/src/Factories/BankFactory.php @@ -25,16 +25,16 @@ public function paymentType(int|string $type): static public function bank(int|string $id, int|string $info, int|string $program): static { - $this->bankNumber = $this->strPad($id, 3); - $this->bankInfo = $this->strPad($info, 2); - $this->bankProgram = $this->strPad($program, 2); + $this->bankNumber = static::strPad($id, 3); + $this->bankInfo = static::strPad($info, 2); + $this->bankProgram = static::strPad($program, 2); return $this; } public function client(int|string $number): static { - $this->clientId = $this->strPad($number, 7); + $this->clientId = static::strPad($number, 7); return $this; } diff --git a/src/Factories/CustomerFactory.php b/src/Factories/CustomerFactory.php index d264b54..7a2b45b 100644 --- a/src/Factories/CustomerFactory.php +++ b/src/Factories/CustomerFactory.php @@ -21,14 +21,14 @@ public function __construct() public function level(int $level): static { - $this->level = $this->strPad($level, 2); + $this->level = static::strPad($level, 2); return $this; } public function customer(int $customerId): static { - $this->customerId = $this->strPad($customerId, 6); + $this->customerId = static::strPad($customerId, 6); return $this; } diff --git a/src/Formatters/Formatter.php b/src/Formatters/Formatter.php index 7d812ce..b8c2fd0 100644 --- a/src/Formatters/Formatter.php +++ b/src/Formatters/Formatter.php @@ -26,9 +26,14 @@ abstract class Formatter public function format(string $number): string { - $number = $this->strPad($number, $this->minCardLength); - $length = $this->length($number); + $number = static::strPad($number, $this->minCardLength); + $length = static::length($number); + return $this->pretty($number, $length); + } + + protected function pretty(string $number, int $length): string + { return match (true) { $length <= 4 => $number, $length <= 6 => $this->split($number, 3), diff --git a/src/Formatters/LoyaltyCharFormatter.php b/src/Formatters/LoyaltyCharFormatter.php new file mode 100644 index 0000000..93254d7 --- /dev/null +++ b/src/Formatters/LoyaltyCharFormatter.php @@ -0,0 +1,26 @@ +encode($number)), $length); + } + + protected function encode(string $value): string + { + return str_replace(array_keys(static::$chars), array_values(static::$chars), $value); + } +} diff --git a/src/Services/Parser.php b/src/Services/Parser.php index e030ce4..c5ec3b8 100644 --- a/src/Services/Parser.php +++ b/src/Services/Parser.php @@ -14,7 +14,7 @@ public function parse(string $number): array { $checksum = 0; - $length = $this->length($number, -1); + $length = static::length($number, -1); for ($i = $length; $i >= 0; --$i) { $digit = (int) $number[$i]; diff --git a/src/Services/Validator.php b/src/Services/Validator.php index 30cbdc8..a8763ab 100644 --- a/src/Services/Validator.php +++ b/src/Services/Validator.php @@ -16,9 +16,9 @@ public function __construct( public function isValid(string $number): bool { - $length = $this->length($number); + $length = static::length($number); - if ($number == 0 || $length === 1) { + if ($number == 0 || $length <= 1) { return false; } diff --git a/tests/Pest.php b/tests/Pest.php index 20c988b..b2735aa 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -57,13 +57,17 @@ function isValidGenerated(int|string $id): void isValid(generate($id)); } -function generatedEquals(Factory|int|string $id, string $expected, Formatter $formatter = new DefaultFormatter()): void -{ +function generatedEquals( + Factory|int|string $id, + string $expected, + Formatter $formatter = new DefaultFormatter(), + CardType|string|null $cardType = null +): void { $result = generate($id, $formatter); expect($result)->toBeString()->toBe($expected); - isValid($result); + isValid($result, $cardType); } function generate(Factory|int|string $id, Formatter $formatter = new DefaultFormatter()): string diff --git a/tests/Unit/Generator/Formatters/LoyaltyCharFormatterTest.php b/tests/Unit/Generator/Formatters/LoyaltyCharFormatterTest.php new file mode 100644 index 0000000..e4f00f8 --- /dev/null +++ b/tests/Unit/Generator/Formatters/LoyaltyCharFormatterTest.php @@ -0,0 +1,99 @@ +