diff --git a/app/Casts/OrderReferenceCast.php b/app/Casts/OrderReferenceCast.php index 3a74130..563ed2a 100644 --- a/app/Casts/OrderReferenceCast.php +++ b/app/Casts/OrderReferenceCast.php @@ -6,7 +6,6 @@ namespace App\Casts; use Domain\Models\ValueObjects\OrderReference; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; -use Ramsey\Uuid\Uuid; /** * @implements CastsAttributes @@ -20,7 +19,7 @@ class OrderReferenceCast implements CastsAttributes throw new \UnexpectedValueException('OrderReference cannot be null.'); } - return new OrderReference(Uuid::fromString($value)); + return OrderReference::fromString($value); } /** @param array $attributes */ @@ -30,6 +29,6 @@ class OrderReferenceCast implements CastsAttributes throw new \UnexpectedValueException('The given value is not an instance of OrderReference.'); } - return $value->value->toString(); + return (string) $value; } } diff --git a/domain/Models/ValueObjects/OrderReference.php b/domain/Models/ValueObjects/OrderReference.php index 00a4e68..8890324 100644 --- a/domain/Models/ValueObjects/OrderReference.php +++ b/domain/Models/ValueObjects/OrderReference.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects; use Ramsey\Uuid\UuidInterface; +use Shared\Exceptions\InvalidUuidException; use Shared\Helpers\UuidHelper; final readonly class OrderReference implements \Stringable @@ -17,9 +18,29 @@ final readonly class OrderReference implements \Stringable } } + public static function fromString(string $value): self + { + if (UuidHelper::isValid($value)) { + return new self(\Ramsey\Uuid\Uuid::fromString($value)); + } + + return self::fromBase58($value); + } + + public static function fromBase58(string $base58): self + { + try { + $uuid = UuidHelper::fromBase58($base58); + } catch (InvalidUuidException) { + throw new \DomainException('Order reference must be a valid base-58 string'); + } + + return new self($uuid); + } + #[\Override] public function __toString(): string { - return $this->value->toString(); + return UuidHelper::toBase58($this->value); } } diff --git a/resources/views/emails/order-confirm-admin.blade.php b/resources/views/emails/order-confirm-admin.blade.php index 67f50b0..619cffa 100644 --- a/resources/views/emails/order-confirm-admin.blade.php +++ b/resources/views/emails/order-confirm-admin.blade.php @@ -4,7 +4,7 @@ {{ __('Hello') }}, -{{ __('The order') }} **{{ $order->reference->value }}** {{ __('from') }} {{ $order->employee->firstname }} {{ Str::upper($order->employee->lastname) }} ({{ $order->employee->matricule }}) {{ __('is confirmed.') }} +{{ __('The order') }} **{{ $order->reference }}** {{ __('from') }} {{ $order->employee->firstname }} {{ Str::upper($order->employee->lastname) }} ({{ $order->employee->matricule }}) {{ __('is confirmed.') }} | {{ __('Product') }} | {{ __('Quantity') }} | {{ __('Total TTC') }} | diff --git a/resources/views/emails/order-confirm.blade.php b/resources/views/emails/order-confirm.blade.php index d1fcf69..50ad05e 100644 --- a/resources/views/emails/order-confirm.blade.php +++ b/resources/views/emails/order-confirm.blade.php @@ -4,7 +4,7 @@ {{ __('Hello') }} {{ $order->employee->firstname }} {{ Str::upper($order->employee->lastname) }} ({{ $order->employee->matricule }}), -{{ __('Your order') }} **{{ $order->reference->value }}** {{ __('is confirmed.') }} +{{ __('Your order') }} **{{ $order->reference }}** {{ __('is confirmed.') }} | {{ __('Product') }} | {{ __('Quantity') }} | {{ __('Price per kilo TTC') }} | {{ __('Total TTC') }} | diff --git a/shared/Helpers/UuidHelper.php b/shared/Helpers/UuidHelper.php index c2cf94f..1af7a15 100644 --- a/shared/Helpers/UuidHelper.php +++ b/shared/Helpers/UuidHelper.php @@ -8,6 +8,7 @@ use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Shared\Exceptions\InvalidUuidException; +use Symfony\Component\Uid\Uuid as SymfonyUuid; final readonly class UuidHelper { @@ -16,6 +17,31 @@ final readonly class UuidHelper // Static class } + public static function isValid(string $uuid): bool + { + return Uuid::isValid($uuid); + } + + public static function toBase58(UuidInterface $uuid): string + { + return SymfonyUuid::fromString($uuid->toString())->toBase58(); + } + + public static function fromBase58(string $base58): UuidInterface + { + $base58 = trim($base58); + + if ($base58 === '' || ! preg_match('/^[1-9A-HJ-NP-Za-km-z]{21,22}$/', $base58)) { + throw InvalidUuidException::forValue($base58); + } + + try { + return Uuid::fromString(SymfonyUuid::fromBase58($base58)->toString()); + } catch (\Throwable) { + throw InvalidUuidException::forValue($base58); + } + } + public static function isV4(UuidInterface $uuid): bool { return self::version($uuid) === 4; diff --git a/tests/Feature/App/Http/Controllers/Bo/OrdersControllerTest.php b/tests/Feature/App/Http/Controllers/Bo/OrdersControllerTest.php index d28053d..aaf1e2a 100644 --- a/tests/Feature/App/Http/Controllers/Bo/OrdersControllerTest.php +++ b/tests/Feature/App/Http/Controllers/Bo/OrdersControllerTest.php @@ -93,13 +93,14 @@ describe('Bo\OrdersController', function (): void { it('can filter index by search term', function (): void { /** @var \Tests\TestCase $this */ // GIVEN - $refUuid = Uuid::uuid7(); - $ref = $refUuid->toString(); - $shop = Shop::factory()->create(['name' => 'UniqueShopSearch']); + $refUuid = Uuid::uuid7(); + $reference = new OrderReference($refUuid); + $ref = (string) $reference; + $shop = Shop::factory()->create(['name' => 'UniqueShopSearch']); Order::forceCreate([ 'id' => Uuid::uuid7()->toString(), 'shop_id' => $shop->id, - 'reference' => new OrderReference($refUuid), + 'reference' => $reference, 'status' => OrderStatus::VALIDATED->value, 'total' => new Money(1500, new Currency('EUR')), 'employee' => new OrderEmployee(new Matricule('1000102'), 'Bob', 'Jones', new ShopperEstablishmentCode('FAC01'), new ShopperSiteCode('SITE'), new ShopperServiceCode('101')), diff --git a/tests/Feature/App/Http/Controllers/Fo/Customers/OrderControllerTest.php b/tests/Feature/App/Http/Controllers/Fo/Customers/OrderControllerTest.php index d5a425a..96e3d88 100644 --- a/tests/Feature/App/Http/Controllers/Fo/Customers/OrderControllerTest.php +++ b/tests/Feature/App/Http/Controllers/Fo/Customers/OrderControllerTest.php @@ -115,13 +115,14 @@ describe('OrderController', function () { 'updated_at' => CarbonImmutable::now(), ]); - $shop = Shop::factory()->create(); - $refUuid = Uuid::uuid7(); - $ref = $refUuid->toString(); + $shop = Shop::factory()->create(); + $refUuid = Uuid::uuid7(); + $reference = new OrderReference($refUuid); + $ref = (string) $reference; Order::forceCreate([ 'id' => Uuid::uuid7()->toString(), 'shop_id' => $shop->id, - 'reference' => new OrderReference($refUuid), + 'reference' => $reference, 'status' => OrderStatus::VALIDATED->value, 'total' => new Money(5000, new Currency('EUR')), 'employee' => new OrderEmployee(new Matricule('8888001'), 'Jean', 'Dupont', new ShopperEstablishmentCode('FAC01'), new ShopperSiteCode('SITE'), new ShopperServiceCode('101')), @@ -197,13 +198,14 @@ describe('OrderController', function () { 'updated_at' => CarbonImmutable::now(), ]); - $shop = Shop::factory()->create(); - $refUuid = Uuid::uuid7(); - $ref = $refUuid->toString(); - $order = Order::forceCreate([ + $shop = Shop::factory()->create(); + $refUuid = Uuid::uuid7(); + $reference = new OrderReference($refUuid); + $ref = (string) $reference; + $order = Order::forceCreate([ 'id' => Uuid::uuid7()->toString(), 'shop_id' => $shop->id, - 'reference' => new OrderReference($refUuid), + 'reference' => $reference, 'status' => OrderStatus::VALIDATED->value, 'total' => new Money(5000, new Currency('EUR')), 'employee' => new OrderEmployee(new Matricule('8888002'), 'Paul', 'Martin', new ShopperEstablishmentCode('FAC01'), new ShopperSiteCode('SITE'), new ShopperServiceCode('101')), diff --git a/tests/Feature/App/Http/Controllers/Fo/TunnelControllerTest.php b/tests/Feature/App/Http/Controllers/Fo/TunnelControllerTest.php index 80187ce..10d5a56 100644 --- a/tests/Feature/App/Http/Controllers/Fo/TunnelControllerTest.php +++ b/tests/Feature/App/Http/Controllers/Fo/TunnelControllerTest.php @@ -501,7 +501,7 @@ describe('TunnelController', function (): void { ]); // THEN - $response->assertRedirectToRoute('fo.sales.confirmation.show-confirmed', $orderReference->value); + $response->assertRedirectToRoute('fo.sales.confirmation.show-confirmed', (string) $orderReference); }); it('redirects back with message when limit exception occurs on confirmation submit', function (): void { @@ -596,7 +596,7 @@ describe('TunnelController', function (): void { $order->save(); // WHEN - $response = $this->get('/commande/confirmation/' . $orderReference->value); + $response = $this->get('/commande/confirmation/' . (string) $orderReference); // THEN $response->assertOk() diff --git a/tests/Unit/App/Casts/OrderReferenceCastTest.php b/tests/Unit/App/Casts/OrderReferenceCastTest.php index a32b348..2b1b48d 100644 --- a/tests/Unit/App/Casts/OrderReferenceCastTest.php +++ b/tests/Unit/App/Casts/OrderReferenceCastTest.php @@ -6,8 +6,25 @@ use App\Casts\OrderReferenceCast; use Domain\Models\ValueObjects\OrderReference; use Illuminate\Database\Eloquent\Model; use Ramsey\Uuid\Uuid; +use Shared\Helpers\UuidHelper; it('gets a reference', function () { + // GIVEN + $cast = new OrderReferenceCast(); + $model = \Mockery::mock(Model::class); + $uuid = Uuid::uuid7(); + $value = UuidHelper::toBase58($uuid); + + // WHEN + $reference = $cast->get($model, 'reference', $value, []); + + // THEN + expect($reference)->toBeInstanceOf(OrderReference::class) + ->and((string) $reference)->toBe($value) + ->and($reference->value->equals($uuid))->toBeTrue(); +}); + +it('gets a reference from raw uuid string', function () { // GIVEN $cast = new OrderReferenceCast(); $model = \Mockery::mock(Model::class); @@ -19,7 +36,7 @@ it('gets a reference', function () { // THEN expect($reference)->toBeInstanceOf(OrderReference::class) - ->and((string) $reference)->toBe($value) + ->and((string) $reference)->toBe(UuidHelper::toBase58($uuid)) ->and($reference->value->equals($uuid))->toBeTrue(); }); @@ -49,7 +66,7 @@ it('throws exception when setting invalid reference type', function () { // GIVEN $cast = new OrderReferenceCast(); $model = \Mockery::mock(Model::class); - $value = Uuid::uuid7()->toString(); + $value = UuidHelper::toBase58(Uuid::uuid7()); // WHEN $cast->set($model, 'reference', $value, []); @@ -71,5 +88,5 @@ it('sets a reference', function () { $value = $cast->set($model, 'reference', $reference, []); // THEN - expect($value)->toBe($uuid->toString()); + expect($value)->toBe(UuidHelper::toBase58($uuid)); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/OrderReferenceTest.php b/tests/Unit/Domain/Models/ValueObjects/OrderReferenceTest.php index 3659d3e..9f98ac5 100644 --- a/tests/Unit/Domain/Models/ValueObjects/OrderReferenceTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/OrderReferenceTest.php @@ -7,6 +7,7 @@ namespace Tests\Unit\Domain\Models\ValueObjects; use Domain\Models\ValueObjects\OrderReference; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; +use Shared\Helpers\UuidHelper; describe('OrderReference', function () { function getNewOrderReference(UuidInterface $value): OrderReference @@ -22,9 +23,51 @@ describe('OrderReference', function () { $vo = getNewOrderReference($uuidV7); // THEN - expect($vo)->toBeInstanceOf(OrderReference::class); - expect((string) $vo)->toBe($uuidV7->toString()); - expect($vo->value)->toBe($uuidV7); + expect($vo)->toBeInstanceOf(OrderReference::class) + ->and((string) $vo)->toBe(UuidHelper::toBase58($uuidV7)) + ->and($vo->value)->toBe($uuidV7); + }); + + it('can instantiate from a valid base-58 UUID v7 string', function () { + // GIVEN + $uuidV7 = Uuid::uuid7(); + $base58 = UuidHelper::toBase58($uuidV7); + + // WHEN + $vo = OrderReference::fromBase58($base58); + + // THEN + expect($vo)->toBeInstanceOf(OrderReference::class) + ->and((string) $vo)->toBe($base58) + ->and($vo->value->equals($uuidV7))->toBeTrue(); + }); + + it('can instantiate from a valid UUID v7 string via fromString', function () { + // GIVEN + $uuidV7 = Uuid::uuid7(); + $uuidString = $uuidV7->toString(); + + // WHEN + $vo = OrderReference::fromString($uuidString); + + // THEN + expect($vo)->toBeInstanceOf(OrderReference::class) + ->and((string) $vo)->toBe(UuidHelper::toBase58($uuidV7)) + ->and($vo->value->equals($uuidV7))->toBeTrue(); + }); + + it('can instantiate from a valid base-58 string via fromString', function () { + // GIVEN + $uuidV7 = Uuid::uuid7(); + $base58 = UuidHelper::toBase58($uuidV7); + + // WHEN + $vo = OrderReference::fromString($base58); + + // THEN + expect($vo)->toBeInstanceOf(OrderReference::class) + ->and((string) $vo)->toBe($base58) + ->and($vo->value->equals($uuidV7))->toBeTrue(); }); it('throws exception if provided UUID is not v7', function () { @@ -37,4 +80,27 @@ describe('OrderReference', function () { // THEN expect($action)->toThrow(\DomainException::class, 'value must be a valid UUID v7'); }); + + it('throws exception if base-58 string is invalid', function () { + // GIVEN + $invalidBase58 = 'invalid-base58-string!'; + + // WHEN + $action = fn () => OrderReference::fromBase58($invalidBase58); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Order reference must be a valid base-58 string'); + }); + + it('throws exception if base-58 string is not from a UUID v7', function () { + // GIVEN + $uuidV4 = Uuid::uuid4(); + $base58 = UuidHelper::toBase58($uuidV4); + + // WHEN + $action = fn () => OrderReference::fromBase58($base58); + + // THEN + expect($action)->toThrow(\DomainException::class, 'value must be a valid UUID v7'); + }); });