Skip to content

Commit

Permalink
Merge pull request #11 from TheDragonCode/1.x
Browse files Browse the repository at this point in the history
Added ability to generate and validate alphanumeric numbers
  • Loading branch information
andrey-helldar authored Jul 2, 2023
2 parents 0033579 + aba69f2 commit aa05a24
Show file tree
Hide file tree
Showing 17 changed files with 423 additions and 46 deletions.
73 changes: 52 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/CardNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
15 changes: 8 additions & 7 deletions src/Cards/Card.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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
Expand All @@ -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);
Expand Down
55 changes: 55 additions & 0 deletions src/Cards/Chars.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace DragonCode\CardNumber\Cards;

use DragonCode\CardNumber\Concerns\Charsable;

use function array_keys;
use function array_values;
use function implode;
use function preg_match;
use function preg_replace;
use function str_replace;

class Chars extends Card
{
use Charsable;

protected static array $numberLength = [];

public static function isValid(int|string $cardNumber): bool
{
$number = static::clear($cardNumber);

return static::isValidLength($number)
&& static::isValidChars($number)
&& static::isValidPattern($number)
&& static::isValidNumber($number);
}

protected static function isValidNumber(string $number): bool
{
return parent::isValidNumber(
str_replace(array_values(static::$chars), array_keys(static::$chars), $number)
);
}

protected static function clear(int|string $number): string
{
return preg_replace('/[^\w]/', '', static::lower($number));
}

protected static function isValidChars(string $number): bool
{
$chars = static::charsPattern();

return ! preg_match("/[^$chars\\d]/", $number);
}

protected static function charsPattern(): string
{
return implode('', array_values(static::$chars));
}
}
21 changes: 21 additions & 0 deletions src/Concerns/Charsable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace DragonCode\CardNumber\Concerns;

trait Charsable
{
protected static array $chars = [
0 => 'f',
1 => 'a',
2 => 'e',
3 => 'k',
4 => 'n',
5 => 'o',
6 => 's',
7 => 'x',
8 => 'y',
9 => 'z',
];
}
20 changes: 16 additions & 4 deletions src/Concerns/Stringable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
1 change: 1 addition & 0 deletions src/Enums/CardType.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum CardType: string
use Values;

case americanExpress = 'amex';
case chars = 'chars';
case dankort = 'dankort';
case dinersClub = 'dinersclub';
case discovery = 'discovery';
Expand Down
8 changes: 4 additions & 4 deletions src/Factories/BankFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Factories/CustomerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
9 changes: 7 additions & 2 deletions src/Formatters/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
26 changes: 26 additions & 0 deletions src/Formatters/LoyaltyCharFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace DragonCode\CardNumber\Formatters;

use DragonCode\CardNumber\Concerns\Charsable;

use function array_keys;
use function array_values;
use function str_replace;

class LoyaltyCharFormatter extends Formatter
{
use Charsable;

protected function pretty(string $number, int $length): string
{
return parent::pretty(static::upper($this->encode($number)), $length);
}

protected function encode(string $value): string
{
return str_replace(array_keys(static::$chars), array_values(static::$chars), $value);
}
}
2 changes: 1 addition & 1 deletion src/Services/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
4 changes: 2 additions & 2 deletions src/Services/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
10 changes: 7 additions & 3 deletions tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit aa05a24

Please sign in to comment.