From 20ec692160ddc2bc8300546a6555cf7c31f688aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Thu, 20 Aug 2026 17:35:42 +0200 Subject: [PATCH] feat(infra): add Brand Eloquent models, migrations, casts, adapters and seeders --- database/factories/ArcadiaBrandFactory.php | 28 +++++ database/factories/BrandFactory.php | 36 ++++++ .../2026_08_19_085607_create_brands_table.php | 33 ++++++ ..._20_114010_create_arcadia_brands_table.php | 36 ++++++ database/seeders/BrandSeeder.php | 44 +++++++ database/seeders/DatabaseSeeder.php | 1 + .../EloquentArcadiaBrandDirectory.php | 45 +++++++ .../InMemoryArcadiaBrandDirectory.php | 41 +++++++ .../Repositories/EloquentBrandRepository.php | 110 ++++++++++++++++++ .../Repositories/InMemoryBrandRepository.php | 84 +++++++++++++ src/Infra/Casts/ArcadiaIdCast.php | 47 ++++++++ src/Infra/Casts/BrandCodeCast.php | 43 +++++++ src/Infra/Database/Models/ArcadiaBrand.php | 61 ++++++++++ src/Infra/Database/Models/Brand.php | 76 ++++++++++++ .../Relations/ArcadiaBrandBelongsTo.php | 27 +++++ src/Infra/Providers/DomainServiceProvider.php | 6 + .../Database/Seeders/BrandSeederTest.php | 35 ++++++ .../Database/Seeders/DatabaseSeederTest.php | 8 +- .../InMemoryArcadiaBrandDirectoryTest.php | 52 +++++++++ .../InMemoryBrandRepositoryTest.php | 88 ++++++++++++++ tests/Unit/Infra/Casts/ArcadiaIdCastTest.php | 108 +++++++++++++++++ tests/Unit/Infra/Casts/BrandCodeCastTest.php | 88 ++++++++++++++ 22 files changed, 1095 insertions(+), 2 deletions(-) create mode 100644 database/factories/ArcadiaBrandFactory.php create mode 100644 database/factories/BrandFactory.php create mode 100644 database/migrations/2026_08_19_085607_create_brands_table.php create mode 100644 database/migrations/2026_08_20_114010_create_arcadia_brands_table.php create mode 100644 database/seeders/BrandSeeder.php create mode 100644 src/Infra/Adapters/Directories/EloquentArcadiaBrandDirectory.php create mode 100644 src/Infra/Adapters/Directories/InMemoryArcadiaBrandDirectory.php create mode 100644 src/Infra/Adapters/Repositories/EloquentBrandRepository.php create mode 100644 src/Infra/Adapters/Repositories/InMemoryBrandRepository.php create mode 100644 src/Infra/Casts/ArcadiaIdCast.php create mode 100644 src/Infra/Casts/BrandCodeCast.php create mode 100644 src/Infra/Database/Models/ArcadiaBrand.php create mode 100644 src/Infra/Database/Models/Brand.php create mode 100644 src/Infra/Database/Relations/ArcadiaBrandBelongsTo.php create mode 100644 tests/Integration/Database/Seeders/BrandSeederTest.php create mode 100644 tests/Unit/Infra/Adapters/Directories/InMemoryArcadiaBrandDirectoryTest.php create mode 100644 tests/Unit/Infra/Adapters/Repositories/InMemoryBrandRepositoryTest.php create mode 100644 tests/Unit/Infra/Casts/ArcadiaIdCastTest.php create mode 100644 tests/Unit/Infra/Casts/BrandCodeCastTest.php diff --git a/database/factories/ArcadiaBrandFactory.php b/database/factories/ArcadiaBrandFactory.php new file mode 100644 index 0000000..912fba6 --- /dev/null +++ b/database/factories/ArcadiaBrandFactory.php @@ -0,0 +1,28 @@ + + */ +final class ArcadiaBrandFactory extends Factory +{ + protected $model = ArcadiaBrand::class; + + /** + * @return array + */ + #[\Override] + public function definition(): array + { + return [ + 'code' => fake()->unique()->numberBetween(1, 999), + 'libelle' => fake()->company(), + ]; + } +} diff --git a/database/factories/BrandFactory.php b/database/factories/BrandFactory.php new file mode 100644 index 0000000..3aaff85 --- /dev/null +++ b/database/factories/BrandFactory.php @@ -0,0 +1,36 @@ + + */ +final class BrandFactory extends Factory +{ + protected $model = Brand::class; + + /** + * @return array + */ + #[\Override] + public function definition(): array + { + $generator = new BrandCodeGenerator(); + $code = $generator->generate(new BrandName(fake()->company())); + + return [ + 'id' => Uuid::uuid7()->toString(), + 'arcadia_id' => ArcadiaBrand::factory(), + 'uwil_public_id' => $code->value, + ]; + } +} diff --git a/database/migrations/2026_08_19_085607_create_brands_table.php b/database/migrations/2026_08_19_085607_create_brands_table.php new file mode 100644 index 0000000..3b86f0c --- /dev/null +++ b/database/migrations/2026_08_19_085607_create_brands_table.php @@ -0,0 +1,33 @@ +uuid('id')->primary(); + + $table->string('uwil_public_id')->unique() + ->comment('A unique public id that is generated spefically for the UWIL eco system'); + $table->bigInteger('arcadia_id')->unique() + ->comment('An unique external autoincrement id for the brand, it may be exposed but should not be used'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('brands'); + } +}; diff --git a/database/migrations/2026_08_20_114010_create_arcadia_brands_table.php b/database/migrations/2026_08_20_114010_create_arcadia_brands_table.php new file mode 100644 index 0000000..307c29c --- /dev/null +++ b/database/migrations/2026_08_20_114010_create_arcadia_brands_table.php @@ -0,0 +1,36 @@ +dropIfExists('arcadia_brands'); + + $schema->create('arcadia_brands', function (Blueprint $table) { + $table->integer('code')->unsigned()->primary(); + $table->string('libelle'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $schema = Schema::connection(Config::string('database.arcadia_meta_model')); + + $schema->dropIfExists('arcadia_brands'); + } +}; diff --git a/database/seeders/BrandSeeder.php b/database/seeders/BrandSeeder.php new file mode 100644 index 0000000..17a70c8 --- /dev/null +++ b/database/seeders/BrandSeeder.php @@ -0,0 +1,44 @@ + $brands */ + $brands = [ + ['code' => 1, 'libelle' => 'Le Gaulois', 'uwil_public_id' => 'LGL01'], + ['code' => 2, 'libelle' => 'Maître CoQ', 'uwil_public_id' => 'MCQ02'], + ['code' => 3, 'libelle' => 'Loué', 'uwil_public_id' => 'LOU03'], + ['code' => 4, 'libelle' => 'Marie', 'uwil_public_id' => 'MAR04'], + ['code' => 5, 'libelle' => "Tradition d'Asie", 'uwil_public_id' => 'TDA05'], + ['code' => 6, 'libelle' => 'Réghalal', 'uwil_public_id' => 'REG06'], + ]; + + foreach ($brands as $brandData) { + ArcadiaBrand::query()->updateOrCreate( + ['code' => $brandData['code']], + ['libelle' => $brandData['libelle']], + ); + + Brand::query()->firstOrCreate( + ['arcadia_id' => $brandData['code']], + [ + 'id' => Uuid::uuid7()->toString(), + 'uwil_public_id' => $brandData['uwil_public_id'], + ], + ); + } + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index f00efe6..a3305b6 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -17,6 +17,7 @@ final class DatabaseSeeder extends Seeder EmployeeSeeder::class, IncoSheetSeeder::class, ApiClientSeeder::class, + BrandSeeder::class, ]); } } diff --git a/src/Infra/Adapters/Directories/EloquentArcadiaBrandDirectory.php b/src/Infra/Adapters/Directories/EloquentArcadiaBrandDirectory.php new file mode 100644 index 0000000..99364cf --- /dev/null +++ b/src/Infra/Adapters/Directories/EloquentArcadiaBrandDirectory.php @@ -0,0 +1,45 @@ +get() + ->map(fn (ArcadiaBrandModel $model): ArcadiaBrand => $this->toDomain($model)) + ->all() + ); + } + + #[\Override] + public function find(ArcadiaId $id): ?ArcadiaBrand + { + $model = ArcadiaBrandModel::query()->find($id->value); + + if ($model === null) { + return null; + } + + return $this->toDomain($model); + } + + private function toDomain(ArcadiaBrandModel $model): ArcadiaBrand + { + return new ArcadiaBrand( + id: new ArcadiaId((int) $model->code), + name: new BrandName((string) $model->libelle), + ); + } +} diff --git a/src/Infra/Adapters/Directories/InMemoryArcadiaBrandDirectory.php b/src/Infra/Adapters/Directories/InMemoryArcadiaBrandDirectory.php new file mode 100644 index 0000000..f102ada --- /dev/null +++ b/src/Infra/Adapters/Directories/InMemoryArcadiaBrandDirectory.php @@ -0,0 +1,41 @@ + */ + private array $brands = []; + + public function add(ArcadiaBrand ...$brands): self + { + foreach ($brands as $brand) { + $this->brands[$brand->id->value] = $brand; + } + + return $this; + } + + public function clear(): void + { + $this->brands = []; + } + + #[\Override] + public function findAll(): array + { + return array_values($this->brands); + } + + #[\Override] + public function find(ArcadiaId $id): ?ArcadiaBrand + { + return $this->brands[$id->value] ?? null; + } +} diff --git a/src/Infra/Adapters/Repositories/EloquentBrandRepository.php b/src/Infra/Adapters/Repositories/EloquentBrandRepository.php new file mode 100644 index 0000000..37d708c --- /dev/null +++ b/src/Infra/Adapters/Repositories/EloquentBrandRepository.php @@ -0,0 +1,110 @@ +with('arcadiaBrand') + ->find($id->value); + + if ($model === null) { + return null; + } + + return $this->toDomain($model); + } + + #[\Override] + public function findByCode(BrandCode $code): ?Brand + { + $model = BrandModel::query() + ->with('arcadiaBrand') + ->where('uwil_public_id', $code->value) + ->first(); + + if ($model === null) { + return null; + } + + return $this->toDomain($model); + } + + #[\Override] + public function findByArcadiaId(ArcadiaId $arcadiaId): ?Brand + { + $model = BrandModel::query() + ->with('arcadiaBrand') + ->where('arcadia_id', $arcadiaId->value) + ->first(); + + if ($model === null) { + return null; + } + + return $this->toDomain($model); + } + + #[\Override] + public function findAll(): array + { + return array_values( + BrandModel::query() + ->with('arcadiaBrand') + ->get() + ->map(fn (BrandModel $model): Brand => $this->toDomain($model)) + ->all() + ); + } + + #[\Override] + public function persist(Brand $entity): void + { + BrandModel::query()->updateOrCreate( + ['id' => $entity->id()->value], + [ + 'arcadia_id' => $entity->arcadiaId, + 'uwil_public_id' => $entity->code, + ] + ); + } + + #[\Override] + public function delete(Brand $entity): void + { + $deleted = BrandModel::query() + ->where('id', $entity->id()->value) + ->delete(); + + if ($deleted === 0) { + throw BrandNotFoundException::forBrand($entity->id()); + } + } + + private function toDomain(BrandModel $model): Brand + { + $name = $model->arcadiaBrand !== null ? $model->arcadiaBrand->libelle : ''; + + return Brand::hydrate( + id: new BrandId(Uuid::fromString($model->id)), + name: new BrandName($name), + code: $model->uwil_public_id, + arcadiaId: $model->arcadia_id, + ); + } +} diff --git a/src/Infra/Adapters/Repositories/InMemoryBrandRepository.php b/src/Infra/Adapters/Repositories/InMemoryBrandRepository.php new file mode 100644 index 0000000..3956b65 --- /dev/null +++ b/src/Infra/Adapters/Repositories/InMemoryBrandRepository.php @@ -0,0 +1,84 @@ + */ + private array $brands = []; + + public function add(Brand ...$brands): self + { + foreach ($brands as $brand) { + $this->brands[$brand->id()->value] = $brand; + } + + return $this; + } + + public function clear(): void + { + $this->brands = []; + } + + #[\Override] + public function find(BrandId $id): ?Brand + { + return $this->brands[$id->value] ?? null; + } + + #[\Override] + public function findByCode(BrandCode $code): ?Brand + { + foreach ($this->brands as $brand) { + if ($brand->code->value === $code->value) { + return $brand; + } + } + + return null; + } + + #[\Override] + public function findByArcadiaId(ArcadiaId $arcadiaId): ?Brand + { + foreach ($this->brands as $brand) { + if ($brand->arcadiaId->value === $arcadiaId->value) { + return $brand; + } + } + + return null; + } + + #[\Override] + public function findAll(): array + { + return array_values($this->brands); + } + + #[\Override] + public function persist(Brand $entity): void + { + $this->brands[$entity->id()->value] = $entity; + } + + #[\Override] + public function delete(Brand $entity): void + { + if (! isset($this->brands[$entity->id()->value])) { + throw BrandNotFoundException::forBrand($entity->id()); + } + + unset($this->brands[$entity->id()->value]); + } +} diff --git a/src/Infra/Casts/ArcadiaIdCast.php b/src/Infra/Casts/ArcadiaIdCast.php new file mode 100644 index 0000000..6acc3f6 --- /dev/null +++ b/src/Infra/Casts/ArcadiaIdCast.php @@ -0,0 +1,47 @@ + */ +final class ArcadiaIdCast implements CastsAttributes +{ + public function get(Model $model, string $key, mixed $value, array $attributes): ArcadiaId + { + if ($value === null) { + throw new \InvalidArgumentException('ArcadiaId value cannot be null'); + } + + if (! is_int($value) && ! (is_string($value) && ctype_digit($value))) { + throw new \InvalidArgumentException('Value must be an integer or numeric string'); + } + + return new ArcadiaId((int) $value); + } + + public function set(Model $model, string $key, mixed $value, array $attributes): int + { + if ($value === null) { + throw new \InvalidArgumentException('ArcadiaId value cannot be null'); + } + + if ($value instanceof ArcadiaId) { + return $value->value; + } + + if (is_int($value)) { + return (new ArcadiaId($value))->value; + } + + if (is_string($value) && ctype_digit($value)) { + return (new ArcadiaId((int) $value))->value; + } + + throw new \InvalidArgumentException('Value must be an integer, numeric string, or instance of ArcadiaId'); + } +} diff --git a/src/Infra/Casts/BrandCodeCast.php b/src/Infra/Casts/BrandCodeCast.php new file mode 100644 index 0000000..74096ea --- /dev/null +++ b/src/Infra/Casts/BrandCodeCast.php @@ -0,0 +1,43 @@ + */ +final class BrandCodeCast implements CastsAttributes +{ + public function get(Model $model, string $key, mixed $value, array $attributes): BrandCode + { + if ($value === null) { + throw new \InvalidArgumentException('BrandCode value cannot be null'); + } + + if (! is_string($value)) { + throw new \InvalidArgumentException('Value must be a string'); + } + + return new BrandCode($value); + } + + public function set(Model $model, string $key, mixed $value, array $attributes): string + { + if ($value === null) { + throw new \InvalidArgumentException('BrandCode value cannot be null'); + } + + if ($value instanceof BrandCode) { + return $value->value; + } + + if (is_string($value)) { + return (new BrandCode($value))->value; + } + + throw new \InvalidArgumentException('Value must be a string or instance of BrandCode'); + } +} diff --git a/src/Infra/Database/Models/ArcadiaBrand.php b/src/Infra/Database/Models/ArcadiaBrand.php new file mode 100644 index 0000000..52ca2df --- /dev/null +++ b/src/Infra/Database/Models/ArcadiaBrand.php @@ -0,0 +1,61 @@ + */ + use HasFactory; + + protected static function newFactory(): ArcadiaBrandFactory + { + return ArcadiaBrandFactory::new(); + } + + #[\Override] + public function getConnectionName(): string + { + return Config::string('database.arcadia_meta_model'); + } + + protected $table = 'arcadia_brands'; + + protected $primaryKey = 'code'; + + public $incrementing = false; + + /** @var string */ + protected $keyType = 'int'; + + public $timestamps = false; + + /** @var list */ + protected $fillable = [ + 'code', + 'libelle', + ]; + + /** @return array */ + #[\Override] + protected function casts(): array + { + return [ + 'code' => 'integer', + ]; + } +} diff --git a/src/Infra/Database/Models/Brand.php b/src/Infra/Database/Models/Brand.php new file mode 100644 index 0000000..6e2c688 --- /dev/null +++ b/src/Infra/Database/Models/Brand.php @@ -0,0 +1,76 @@ + */ + use HasFactory; + + use HasUuids; + + protected static function newFactory(): BrandFactory + { + return BrandFactory::new(); + } + + protected $table = 'brands'; + + public $timestamps = false; + + /** @var list */ + protected $fillable = [ + 'id', + 'arcadia_id', + 'uwil_public_id', + ]; + + /** @return array */ + #[\Override] + protected function casts(): array + { + return [ + 'arcadia_id' => ArcadiaIdCast::class, + 'uwil_public_id' => BrandCodeCast::class, + ]; + } + + /** + * @return ArcadiaBrandBelongsTo + */ + public function arcadiaBrand(): BelongsTo + { + $instance = $this->newRelatedInstance(ArcadiaBrand::class); + + return new ArcadiaBrandBelongsTo( + $instance->newQuery(), + $this, + 'arcadia_id', + 'code', + 'arcadiaBrand' + ); + } +} diff --git a/src/Infra/Database/Relations/ArcadiaBrandBelongsTo.php b/src/Infra/Database/Relations/ArcadiaBrandBelongsTo.php new file mode 100644 index 0000000..313c4c5 --- /dev/null +++ b/src/Infra/Database/Relations/ArcadiaBrandBelongsTo.php @@ -0,0 +1,27 @@ + + */ +final class ArcadiaBrandBelongsTo extends BelongsTo +{ + #[\Override] + protected function getForeignKeyFrom(Model $model): mixed + { + $foreignKey = parent::getForeignKeyFrom($model); + + if ($foreignKey instanceof ArcadiaId) { + return $foreignKey->value; + } + + return $foreignKey; + } +} diff --git a/src/Infra/Providers/DomainServiceProvider.php b/src/Infra/Providers/DomainServiceProvider.php index 6bd4504..2a6c549 100644 --- a/src/Infra/Providers/DomainServiceProvider.php +++ b/src/Infra/Providers/DomainServiceProvider.php @@ -4,16 +4,20 @@ declare(strict_types=1); namespace Infra\Providers; +use Domain\Ports\Directories\ArcadiaBrandDirectory; use Domain\Ports\Directories\EmployeeDirectory; use Domain\Ports\Directories\IncoCompositionDirectory; use Domain\Ports\Directories\IncoSheetDirectory; use Domain\Ports\Directories\IncoShortSheetDirectory; +use Domain\Ports\Repositories\BrandRepository; use Illuminate\Support\ServiceProvider; +use Infra\Adapters\Directories\EloquentArcadiaBrandDirectory; use Infra\Adapters\Directories\EloquentEmployeeDirectory; use Infra\Adapters\Directories\EloquentIncoCompositionDirectory; use Infra\Adapters\Directories\EloquentIncoSheetDirectory; use Infra\Adapters\Directories\EloquentIncoShortSheetDirectory; use Infra\Adapters\Events\LaravelDomainEventDispatcher; +use Infra\Adapters\Repositories\EloquentBrandRepository; use Infra\Usecase\LaravelExecutor; use Shared\DDD\Event\DomainEventDispatcher; use Shared\DDD\Usecase\Builder\UsecaseExecutor; @@ -27,6 +31,8 @@ class DomainServiceProvider extends ServiceProvider $this->app->bind(IncoSheetDirectory::class, EloquentIncoSheetDirectory::class); $this->app->bind(IncoShortSheetDirectory::class, EloquentIncoShortSheetDirectory::class); $this->app->bind(IncoCompositionDirectory::class, EloquentIncoCompositionDirectory::class); + $this->app->bind(ArcadiaBrandDirectory::class, EloquentArcadiaBrandDirectory::class); + $this->app->bind(BrandRepository::class, EloquentBrandRepository::class); $this->app->bind(DomainEventDispatcher::class, LaravelDomainEventDispatcher::class); $this->app->singleton(UsecaseExecutor::class, LaravelExecutor::class); } diff --git a/tests/Integration/Database/Seeders/BrandSeederTest.php b/tests/Integration/Database/Seeders/BrandSeederTest.php new file mode 100644 index 0000000..c6b3c56 --- /dev/null +++ b/tests/Integration/Database/Seeders/BrandSeederTest.php @@ -0,0 +1,35 @@ +run(); + + // THEN + expect(ArcadiaBrand::count())->toBe(6) + ->and(Brand::count())->toBe(6); + + $brand = Brand::query()->where('uwil_public_id', 'LGL01')->first(); + expect($brand)->not->toBeNull() + ->and($brand?->arcadia_id->value)->toBe(1) + ->and($brand?->arcadiaBrand)->not->toBeNull() + ->and($brand?->arcadiaBrand?->libelle)->toBe('Le Gaulois'); + }); +}); diff --git a/tests/Integration/Database/Seeders/DatabaseSeederTest.php b/tests/Integration/Database/Seeders/DatabaseSeederTest.php index 896d44c..2d56b43 100644 --- a/tests/Integration/Database/Seeders/DatabaseSeederTest.php +++ b/tests/Integration/Database/Seeders/DatabaseSeederTest.php @@ -6,11 +6,13 @@ namespace Tests\Integration\Database\Seeders; use Database\Seeders\DatabaseSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; +use Infra\Database\Models\ArcadiaBrand; +use Infra\Database\Models\Brand; use Infra\Database\Models\Employee; use Infra\Database\Models\IncoSheet; use Tests\TestCase; -covers(Employee::class, IncoSheet::class); +covers(Employee::class, IncoSheet::class, Brand::class, ArcadiaBrand::class); uses(TestCase::class, RefreshDatabase::class); @@ -24,6 +26,8 @@ describe('DatabaseSeeder Integration Test', function (): void { // THEN expect(Employee::count())->toBeGreaterThan(0) - ->and(IncoSheet::count())->toBeGreaterThan(0); + ->and(IncoSheet::count())->toBeGreaterThan(0) + ->and(Brand::count())->toBeGreaterThan(0) + ->and(ArcadiaBrand::count())->toBeGreaterThan(0); }); }); diff --git a/tests/Unit/Infra/Adapters/Directories/InMemoryArcadiaBrandDirectoryTest.php b/tests/Unit/Infra/Adapters/Directories/InMemoryArcadiaBrandDirectoryTest.php new file mode 100644 index 0000000..9ff8f10 --- /dev/null +++ b/tests/Unit/Infra/Adapters/Directories/InMemoryArcadiaBrandDirectoryTest.php @@ -0,0 +1,52 @@ +add($brand); + + // WHEN + $found = $directory->find(new ArcadiaId(1)); + $missing = $directory->find(new ArcadiaId(999)); + + // THEN + expect($found)->toBe($brand) + ->and($missing)->toBeNull(); + }); + + it('returns all arcadia brands and clears them', function (): void { + // GIVEN + $directory = new InMemoryArcadiaBrandDirectory(); + $b1 = new ArcadiaBrand(new ArcadiaId(1), new BrandName('Le Gaulois')); + $b2 = new ArcadiaBrand(new ArcadiaId(2), new BrandName('Maître CoQ')); + $directory->add($b1, $b2); + + // WHEN + $all = $directory->findAll(); + + // THEN + expect($all)->toHaveCount(2); + + // WHEN + $directory->clear(); + + // THEN + expect($directory->findAll())->toBeEmpty(); + }); +}); diff --git a/tests/Unit/Infra/Adapters/Repositories/InMemoryBrandRepositoryTest.php b/tests/Unit/Infra/Adapters/Repositories/InMemoryBrandRepositoryTest.php new file mode 100644 index 0000000..b3315ca --- /dev/null +++ b/tests/Unit/Infra/Adapters/Repositories/InMemoryBrandRepositoryTest.php @@ -0,0 +1,88 @@ +add($brand); + + // WHEN + $foundById = $repository->find($brand->id()); + $foundByCode = $repository->findByCode(new BrandCode('LGL01')); + $missingById = $repository->find(new BrandId(Uuid::uuid7())); + $missingByCode = $repository->findByCode(new BrandCode('MCQ98')); + + // THEN + expect($foundById)->toBe($brand) + ->and($foundByCode)->toBe($brand) + ->and($missingById)->toBeNull() + ->and($missingByCode)->toBeNull(); + }); + + it('persists and deletes a brand', function (): void { + // GIVEN + $repository = new InMemoryBrandRepository(); + $brand = createBrand('MCQ02', 'Maître CoQ', 2); + + // WHEN + $repository->persist($brand); + + // THEN + expect($repository->find($brand->id()))->toBe($brand); + + // WHEN + $repository->delete($brand); + + // THEN + expect($repository->find($brand->id()))->toBeNull(); + }); + + it('throws BrandNotFoundException when deleting non-existent brand', function (): void { + // GIVEN + $repository = new InMemoryBrandRepository(); + $brand = createBrand('MCQ02', 'Maître CoQ', 2); + + // WHEN & THEN + expect(fn () => $repository->delete($brand)) + ->toThrow(BrandNotFoundException::class); + }); + + it('clears all brands', function (): void { + // GIVEN + $repository = new InMemoryBrandRepository(); + $brand = createBrand('MCQ02', 'Maître CoQ', 2); + $repository->add($brand); + + // WHEN + $repository->clear(); + + // THEN + expect($repository->find($brand->id()))->toBeNull(); + }); +}); diff --git a/tests/Unit/Infra/Casts/ArcadiaIdCastTest.php b/tests/Unit/Infra/Casts/ArcadiaIdCastTest.php new file mode 100644 index 0000000..23e46fc --- /dev/null +++ b/tests/Unit/Infra/Casts/ArcadiaIdCastTest.php @@ -0,0 +1,108 @@ +get($model, 'arcadia_id', $dbValue, []); + + // THEN + expect($result)->toBeInstanceOf(ArcadiaId::class) + ->and($result->value)->toBe(123); + }); + + it('transforms numeric string from database to ArcadiaId instance on get', function () use ($cast, $model): void { + // GIVEN + $dbValue = '456'; + + // WHEN + $result = $cast->get($model, 'arcadia_id', $dbValue, []); + + // THEN + expect($result)->toBeInstanceOf(ArcadiaId::class) + ->and($result->value)->toBe(456); + }); + + it('throws InvalidArgumentException on get when value is null', function () use ($cast, $model): void { + // GIVEN & WHEN + expect(fn () => $cast->get($model, 'arcadia_id', null, [])) + + // THEN + ->toThrow(\InvalidArgumentException::class, 'ArcadiaId value cannot be null'); + }); + + it('throws InvalidArgumentException on get when value is invalid type', function () use ($cast, $model): void { + // GIVEN & WHEN + expect(fn () => $cast->get($model, 'arcadia_id', 'abc', [])) + + // THEN + ->toThrow(\InvalidArgumentException::class, 'Value must be an integer or numeric string'); + }); + + it('transforms ArcadiaId instance to integer on set', function () use ($cast, $model): void { + // GIVEN + $arcadiaId = new ArcadiaId(789); + + // WHEN + $result = $cast->set($model, 'arcadia_id', $arcadiaId, []); + + // THEN + expect($result)->toBe(789); + }); + + it('transforms raw integer to integer on set', function () use ($cast, $model): void { + // GIVEN + $value = 42; + + // WHEN + $result = $cast->set($model, 'arcadia_id', $value, []); + + // THEN + expect($result)->toBe(42); + }); + + it('transforms numeric string to integer on set', function () use ($cast, $model): void { + // GIVEN + $value = '99'; + + // WHEN + $result = $cast->set($model, 'arcadia_id', $value, []); + + // THEN + expect($result)->toBe(99); + }); + + it('throws InvalidArgumentException on set when value is null', function () use ($cast, $model): void { + // GIVEN & WHEN + expect(fn () => $cast->set($model, 'arcadia_id', null, [])) + + // THEN + ->toThrow(\InvalidArgumentException::class, 'ArcadiaId value cannot be null'); + }); + + it('throws InvalidArgumentException on set when value is invalid type', function () use ($cast, $model): void { + // GIVEN + $invalid = ['array-not-supported']; + + // WHEN + expect(fn () => $cast->set($model, 'arcadia_id', $invalid, [])) + + // THEN + ->toThrow(\InvalidArgumentException::class, 'Value must be an integer, numeric string, or instance of ArcadiaId'); + }); +}); diff --git a/tests/Unit/Infra/Casts/BrandCodeCastTest.php b/tests/Unit/Infra/Casts/BrandCodeCastTest.php new file mode 100644 index 0000000..8cbe07b --- /dev/null +++ b/tests/Unit/Infra/Casts/BrandCodeCastTest.php @@ -0,0 +1,88 @@ +get($model, 'uwil_public_id', $dbValue, []); + + // THEN + expect($result)->toBeInstanceOf(BrandCode::class) + ->and($result->value)->toBe('LGL89'); + }); + + it('throws InvalidArgumentException on get when value is null', function () use ($cast, $model): void { + // GIVEN & WHEN + expect(fn () => $cast->get($model, 'uwil_public_id', null, [])) + + // THEN + ->toThrow(\InvalidArgumentException::class, 'BrandCode value cannot be null'); + }); + + it('throws InvalidArgumentException on get when value is not a string', function () use ($cast, $model): void { + // GIVEN + /** @var mixed $invalid */ + $invalid = 12345; + + // WHEN + expect(fn () => $cast->get($model, 'uwil_public_id', $invalid, [])) + + // THEN + ->toThrow(\InvalidArgumentException::class, 'Value must be a string'); + }); + + it('transforms BrandCode instance to string on set', function () use ($cast, $model): void { + // GIVEN + $brandCode = new BrandCode('ABC12'); + + // WHEN + $result = $cast->set($model, 'uwil_public_id', $brandCode, []); + + // THEN + expect($result)->toBe('ABC12'); + }); + + it('transforms raw string to validated string on set', function () use ($cast, $model): void { + // GIVEN + $value = 'XYZ98'; + + // WHEN + $result = $cast->set($model, 'uwil_public_id', $value, []); + + // THEN + expect($result)->toBe('XYZ98'); + }); + + it('throws InvalidArgumentException on set when value is null', function () use ($cast, $model): void { + // GIVEN & WHEN + expect(fn () => $cast->set($model, 'uwil_public_id', null, [])) + // THEN + ->toThrow(\InvalidArgumentException::class, 'BrandCode value cannot be null'); + }); + + it('throws InvalidArgumentException on set when value is not a string or BrandCode', function () use ($cast, $model): void { + // GIVEN + $invalid = 12345; + + // WHEN + expect(fn () => $cast->set($model, 'uwil_public_id', $invalid, [])) + + // THEN + ->toThrow(\InvalidArgumentException::class, 'Value must be a string or instance of BrandCode'); + }); +});