Skip to content

Commit

Permalink
Added the GTIN validator (standalone) and Laravel Validation rule cla…
Browse files Browse the repository at this point in the history
…sses
  • Loading branch information
fulopattila122 committed Mar 5, 2025
1 parent e8d0721 commit 55de04e
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 0 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Changed the minimum Laravel 10 version to v10.48
- Added Laravel 12 Support
- PHP 8.4 deprecation fixes
- Added the GTIN validator (standalone) and Laravel Validation rule classes

## 4.x Series

Expand Down
89 changes: 89 additions & 0 deletions Tests/GtinValidationRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator;
use Illuminate\Validation\Validator;
use PHPUnit\Framework\TestCase;
use Vanilo\Support\Validation\Rules\MustBeAValidGtin;

class GtinValidationRuleTest extends TestCase
{
private $translator;

protected function setUp(): void
{
parent::setUp();

$this->translator = new Translator(new ArrayLoader(), 'en_US');
app()->instance('translator', $this->translator);
}

/** @test */
public function it_accepts_a_valid_gtin()
{
$validator = new Validator(
$this->translator,
['gtin' => '1300000000000'],
['gtin' => [new MustBeAValidGtin()]]
);

$this->assertTrue($validator->passes());
}

/** @test */
public function it_rejects_an_invalid_gtin()
{
$validator = new Validator(
$this->translator,
['gtin' => '1300000000001'],
['gtin' => [new MustBeAValidGtin()]]
);

$this->assertFalse($validator->passes());
}

/** @test */
public function it_rejects_an_array_as_field_value()
{
$validator = new Validator(
$this->translator,
['gtin' => []],
['gtin' => [new MustBeAValidGtin()]]
);

$this->assertFalse($validator->passes());
}

/** @test */
public function it_returns_the_correct_error_message()
{
$validator = new Validator(
$this->translator,
['gtin' => 'invalid-gtin'],
['gtin' => [new MustBeAValidGtin()]]
);

$this->assertEquals(
'The gtin must be a valid Global Trade Item Number (GTIN) [8, 12, 13 or 14 digits with a valid check digit]',
$validator->errors()->first('gtin')
);
}

/** @test */
public function the_validation_message_can_be_overridden()
{
$validator = new Validator(
$this->translator,
['gtin' => 'invalid-gtin'],
['gtin' => [new MustBeAValidGtin()]],
['gtin' => 'Custom GTIN validation error message.']
);

$this->assertEquals(
'Custom GTIN validation error message.',
$validator->errors()->first('gtin')
);
}
}
105 changes: 105 additions & 0 deletions Tests/GtinValidatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use Vanilo\Support\Validation\GtinValidator;

class GtinValidatorTest extends TestCase
{
/** @test */
public function a_valid_gtin8_should_pass()
{
$this->assertTrue(GtinValidator::isValid('80000006'));
}

/** @test */
public function an_invalid_gtin8_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('80000000'));
}

/** @test */
public function a_valid_gtin12_should_pass()
{
$this->assertTrue(GtinValidator::isValid('120000000005'));
}

/** @test */
public function an_invalid_gtin12_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('120000000000'));
}

/** @test */
public function a_valid_gtin13_should_pass()
{
$this->assertTrue(GtinValidator::isValid('1300000000000'));
}

/** @test */
public function an_invalid_gtin13_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('1300000000001'));
}

/** @test */
public function a_valid_gtin14_should_pass()
{
$this->assertTrue(GtinValidator::isValid('14000000000003'));
}

/** @test */
public function an_invalid_gtin14_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('14000000000000'));
}

/** @test */
public function a_valid_gtin_integer_value_should_pass()
{
$this->assertTrue(GtinValidator::isValid(80000006));
}

/** @test */
public function a_too_short_value_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('7000003'));
}

/** @test */
public function a_too_long_value_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('150000000000004'));
}

/** @test */
public function a_value_with_a_length_of_nine_digits_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('900000001'));
}

/** @test */
public function a_value_with_a_length_of_ten_digits_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('1000000007'));
}

/** @test */
public function a_value_with_a_length_of_eleven_digits_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('11000000006'));
}

/** @test */
public function zeros_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('0000000000000'));
}

/** @test */
public function a_non_numeric_string_should_not_pass()
{
$this->assertFalse(GtinValidator::isValid('string'));
}
}
45 changes: 45 additions & 0 deletions Validation/GtinValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Vanilo\Support\Validation;

class GtinValidator
{
protected static array $cache = [];

public static function isValid(string|int $gtin): bool
{
$gtin = (string) $gtin;
if (! is_numeric($gtin)) {
return false;
}

if (isset(static::$cache[$gtin])) {
return static::$cache[$gtin];
}

if (! preg_match('/^\d{8}(?:\d{4,6})?$/', $gtin)) {
return false;
}

return static::$cache[$gtin] = static::isCheckSumCorrect($gtin);
}

protected static function isCheckSumCorrect(string $value): bool
{
return substr($value, 0, -1).collect(str_split($value))
->slice(0, -1)
->pipe(function ($collection) {
return $collection->sum() === 0 ? collect(1) : $collection;
})
->reverse()
->values()
->map(function ($digit, $key) {
return $key % 2 === 0 ? $digit * 3 : $digit;
})
->pipe(function ($collection) {
return ceil($collection->sum() / 10) * 10 - $collection->sum();
}) === $value;
}
}
25 changes: 25 additions & 0 deletions Validation/Rules/MustBeAValidGtin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Vanilo\Support\Validation\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Vanilo\Support\Validation\GtinValidator;

class MustBeAValidGtin implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) && !is_int($value)) {
$fail(__('The :attribute must be a numeric string'));

return;
}

if (!GtinValidator::isValid($value)) {
$fail(__('The :attribute must be a valid Global Trade Item Number (GTIN) [8, 12, 13 or 14 digits with a valid check digit]'));
}
}
}

0 comments on commit 55de04e

Please sign in to comment.