feat(brands): add pagination caching, fast local resolution and resilient database fallback
This commit is contained in:
parent
df04ab87ac
commit
106d4467c4
8 changed files with 429 additions and 68 deletions
|
|
@ -28,7 +28,7 @@ final readonly class FindAllBrands implements Usecase
|
|||
$brandItems = [];
|
||||
|
||||
foreach ($paginated->getItems() as $arcadiaBrand) {
|
||||
$brand = $this->brandResolver->resolve($arcadiaBrand->id);
|
||||
$brand = $this->brandResolver->resolveFromArcadia($arcadiaBrand);
|
||||
$brandItems[] = BrandItem::fromDomain($brand);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use Domain\Events\BrandCreated;
|
|||
use Domain\Events\BrandRenamed;
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Ports\Directories\ArcadiaBrandDirectory;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
|
|
@ -31,10 +32,30 @@ final readonly class BrandResolver
|
|||
public function resolve(ArcadiaId $arcadiaBrandCode): Brand
|
||||
{
|
||||
$existingBrand = $this->brandRepository->findByArcadiaId($arcadiaBrandCode);
|
||||
$arcadiaBrand = $this->arcadiaBrandDirectory->find($arcadiaBrandCode);
|
||||
|
||||
if ($existingBrand !== null) {
|
||||
if ($arcadiaBrand !== null && ! $existingBrand->name->equals($arcadiaBrand->name)) {
|
||||
return $existingBrand;
|
||||
}
|
||||
|
||||
$arcadiaBrand = $this->arcadiaBrandDirectory->find($arcadiaBrandCode);
|
||||
|
||||
if ($arcadiaBrand === null) {
|
||||
throw BrandNotFoundException::forArcadiaId($arcadiaBrandCode);
|
||||
}
|
||||
|
||||
return $this->resolveFromArcadia($arcadiaBrand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an existing or new Brand entity from an ArcadiaBrand VO.
|
||||
* Handles name updates (BrandRenamed) and creation (BrandCreated).
|
||||
*/
|
||||
public function resolveFromArcadia(ArcadiaBrand $arcadiaBrand): Brand
|
||||
{
|
||||
$existingBrand = $this->brandRepository->findByArcadiaId($arcadiaBrand->id);
|
||||
|
||||
if ($existingBrand !== null) {
|
||||
if (! $existingBrand->name->equals($arcadiaBrand->name)) {
|
||||
$oldName = $existingBrand->name;
|
||||
$existingBrand = $existingBrand->rename($arcadiaBrand->name);
|
||||
|
||||
|
|
@ -53,10 +74,6 @@ final readonly class BrandResolver
|
|||
return $existingBrand;
|
||||
}
|
||||
|
||||
if ($arcadiaBrand === null) {
|
||||
throw BrandNotFoundException::forArcadiaId($arcadiaBrandCode);
|
||||
}
|
||||
|
||||
do {
|
||||
$code = $this->brandCodeGenerator->generate($arcadiaBrand->name);
|
||||
} while ($this->brandRepository->findByCode($code) !== null);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,13 @@ namespace Infra\Adapters\Directories;
|
|||
use Database\ArcadiaReplicate\Tables\Refp\MarqueTable;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Domain\Ports\Directories\ArcadiaBrandDirectory as ArcadiaBrandDirectoryPort;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Infra\Adapters\Mappers\ArcadiaBrandMapper;
|
||||
use Infra\Cache\BrandCache;
|
||||
use Infra\Database\Models\Brand as BrandModel;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
|
|
@ -21,40 +24,57 @@ final class ArcadiaMarqueBrandDirectory implements ArcadiaBrandDirectoryPort
|
|||
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly ?BrandCache $brandCache = null,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function findAll(Pagination $pagination): Paginated
|
||||
{
|
||||
$query = $this->connection
|
||||
->table(MarqueTable::table($this->connection))
|
||||
->where('COD_MARQ', '>', 0)
|
||||
->where('LIB_MARQ', 'NOT LIKE', 'Z -%')
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNull('A_EPURER')->orWhere('A_EPURER', '!=', 1);
|
||||
})
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNull('TYPE_MARQUE')->orWhere('TYPE_MARQUE', '!=', 99);
|
||||
});
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
|
||||
$rows = (clone $query)
|
||||
->orderBy('COD_MARQ', 'asc')
|
||||
->offset($pagination->from())
|
||||
->limit($pagination->pageSize())
|
||||
->get();
|
||||
|
||||
$items = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$domain = ArcadiaBrandMapper::toDomain($row);
|
||||
$this->cache[$domain->id->value] = $domain;
|
||||
$items[] = $domain;
|
||||
if ($this->brandCache !== null) {
|
||||
$cached = $this->brandCache->getArcadiaPaginated($pagination);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
return new Paginated($pagination, $items, $total);
|
||||
try {
|
||||
$query = $this->connection
|
||||
->table(MarqueTable::table($this->connection))
|
||||
->where('COD_MARQ', '>', 0)
|
||||
->where('LIB_MARQ', 'NOT LIKE', 'Z -%')
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNull('A_EPURER')->orWhere('A_EPURER', '!=', 1);
|
||||
})
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNull('TYPE_MARQUE')->orWhere('TYPE_MARQUE', '!=', 99);
|
||||
});
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
|
||||
$rows = (clone $query)
|
||||
->orderBy('COD_MARQ', 'asc')
|
||||
->offset($pagination->from())
|
||||
->limit($pagination->pageSize())
|
||||
->get();
|
||||
|
||||
$items = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$domain = ArcadiaBrandMapper::toDomain($row);
|
||||
$this->cache[$domain->id->value] = $domain;
|
||||
$items[] = $domain;
|
||||
}
|
||||
|
||||
/** @var list<ArcadiaBrand> $items */
|
||||
$paginated = new Paginated($pagination, $items, $total);
|
||||
|
||||
$this->brandCache?->putArcadiaPaginated($pagination, $paginated);
|
||||
|
||||
return $paginated;
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fallbackFindAll($pagination, $e);
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
|
|
@ -64,26 +84,83 @@ final class ArcadiaMarqueBrandDirectory implements ArcadiaBrandDirectoryPort
|
|||
return $this->cache[$id->value];
|
||||
}
|
||||
|
||||
$row = $this->connection
|
||||
->table(MarqueTable::table($this->connection))
|
||||
->where('COD_MARQ', $id->value)
|
||||
->where('COD_MARQ', '>', 0)
|
||||
->where('LIB_MARQ', 'NOT LIKE', 'Z -%')
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNull('A_EPURER')->orWhere('A_EPURER', '!=', 1);
|
||||
})
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNull('TYPE_MARQUE')->orWhere('TYPE_MARQUE', '!=', 99);
|
||||
})
|
||||
try {
|
||||
$row = $this->connection
|
||||
->table(MarqueTable::table($this->connection))
|
||||
->where('COD_MARQ', $id->value)
|
||||
->where('COD_MARQ', '>', 0)
|
||||
->where('LIB_MARQ', 'NOT LIKE', 'Z -%')
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNull('A_EPURER')->orWhere('A_EPURER', '!=', 1);
|
||||
})
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNull('TYPE_MARQUE')->orWhere('TYPE_MARQUE', '!=', 99);
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($row === null) {
|
||||
$this->cache[$id->value] = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$domain = ArcadiaBrandMapper::toDomain($row);
|
||||
$this->cache[$id->value] = $domain;
|
||||
|
||||
return $domain;
|
||||
} catch (\Throwable) {
|
||||
return $this->fallbackFind($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Paginated<ArcadiaBrand>
|
||||
*/
|
||||
private function fallbackFindAll(Pagination $pagination, \Throwable $originalException): Paginated
|
||||
{
|
||||
$query = BrandModel::query()->orderBy('name', 'asc');
|
||||
$total = $query->count();
|
||||
|
||||
if ($total === 0) {
|
||||
throw $originalException;
|
||||
}
|
||||
|
||||
$models = $query
|
||||
->offset($pagination->from())
|
||||
->limit($pagination->pageSize())
|
||||
->get();
|
||||
|
||||
$items = [];
|
||||
foreach ($models as $model) {
|
||||
$arcadiaId = $model->arcadia_id instanceof ArcadiaId
|
||||
? $model->arcadia_id
|
||||
: new ArcadiaId((string) $model->arcadia_id);
|
||||
|
||||
$items[] = new ArcadiaBrand(
|
||||
$arcadiaId,
|
||||
new BrandName($model->name)
|
||||
);
|
||||
}
|
||||
|
||||
/** @var list<ArcadiaBrand> $items */
|
||||
return new Paginated($pagination, $items, $total);
|
||||
}
|
||||
|
||||
private function fallbackFind(ArcadiaId $id): ?ArcadiaBrand
|
||||
{
|
||||
$model = BrandModel::query()
|
||||
->where('arcadia_id', $id->value)
|
||||
->first();
|
||||
|
||||
if ($row === null) {
|
||||
$this->cache[$id->value] = null;
|
||||
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$domain = ArcadiaBrandMapper::toDomain($row);
|
||||
$arcadiaId = $model->arcadia_id instanceof ArcadiaId
|
||||
? $model->arcadia_id
|
||||
: new ArcadiaId((string) $model->arcadia_id);
|
||||
|
||||
$domain = new ArcadiaBrand($arcadiaId, new BrandName($model->name));
|
||||
$this->cache[$id->value] = $domain;
|
||||
|
||||
return $domain;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Infra\Cache;
|
||||
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
|
|
@ -12,6 +13,8 @@ use Domain\Models\ValueObjects\Brand\BrandName;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
final readonly class BrandCache
|
||||
{
|
||||
|
|
@ -90,6 +93,60 @@ final readonly class BrandCache
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Paginated<ArcadiaBrand>|null
|
||||
*/
|
||||
public function getArcadiaPaginated(Pagination $pagination): ?Paginated
|
||||
{
|
||||
try {
|
||||
$data = $this->cacheStore->get($this->paginationKey($pagination));
|
||||
|
||||
if (! is_array($data) || ! isset($data['total'], $data['items']) || ! is_int($data['total']) || ! is_array($data['items'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($data['items'] as $item) {
|
||||
if (! is_array($item) || ! isset($item['id'], $item['name']) || ! is_string($item['id']) || ! is_string($item['name'])) {
|
||||
return null;
|
||||
}
|
||||
$items[] = new ArcadiaBrand(
|
||||
new ArcadiaId($item['id']),
|
||||
new BrandName($item['name'])
|
||||
);
|
||||
}
|
||||
|
||||
/** @var list<ArcadiaBrand> $items */
|
||||
return new Paginated($pagination, $items, $data['total']);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::getArcadiaPaginated failed', ['exception' => $e->getMessage()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Paginated<ArcadiaBrand> $paginated
|
||||
*/
|
||||
public function putArcadiaPaginated(Pagination $pagination, Paginated $paginated): void
|
||||
{
|
||||
try {
|
||||
$items = array_map(static fn (ArcadiaBrand $brand): array => [
|
||||
'id' => $brand->id->value,
|
||||
'name' => $brand->name->value,
|
||||
], $paginated->getItems());
|
||||
|
||||
$data = [
|
||||
'total' => $paginated->getTotal(),
|
||||
'items' => $items,
|
||||
];
|
||||
|
||||
$this->cacheStore->set($this->paginationKey($pagination), $data, self::CACHE_TTL_SECONDS);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::putArcadiaPaginated failed', ['exception' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function put(Brand $brand): void
|
||||
{
|
||||
try {
|
||||
|
|
@ -163,6 +220,11 @@ final readonly class BrandCache
|
|||
return "brand:arcadia:{$arcadiaId->value}";
|
||||
}
|
||||
|
||||
private function paginationKey(Pagination $pagination): string
|
||||
{
|
||||
return "brand:arcadia:page:{$pagination->pageNumber()}:size:{$pagination->pageSize()}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, name: string, code: string, arcadiaId: string}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -85,7 +85,10 @@ class DomainServiceProvider extends ServiceProvider
|
|||
);
|
||||
$this->app->bind(
|
||||
ArcadiaBrandDirectory::class,
|
||||
fn (Application $app) => new ArcadiaMarqueBrandDirectory($this->getArcadiaConnection($app))
|
||||
fn (Application $app) => new ArcadiaMarqueBrandDirectory(
|
||||
$this->getArcadiaConnection($app),
|
||||
$app->make(BrandCache::class)
|
||||
)
|
||||
);
|
||||
$this->app->bind(BrandRepository::class, EloquentBrandRepository::class);
|
||||
$this->app->bind(ClientAuditLogDirectory::class, EloquentClientAuditLogDirectory::class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Integration\Infra\Adapters\Directories;
|
||||
|
||||
use Database\ArcadiaReplicate\Tables\Refp\MarqueTable;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Infra\Adapters\Directories\ArcadiaMarqueBrandDirectory;
|
||||
use Infra\Cache\BrandCache;
|
||||
use Infra\Database\Models\Brand as BrandModel;
|
||||
use Mockery;
|
||||
use Shared\Pagination;
|
||||
use Tests\TestCase;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
covers(ArcadiaMarqueBrandDirectory::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
Cache::flush();
|
||||
BrandModel::query()->delete();
|
||||
});
|
||||
|
||||
describe('ArcadiaMarqueBrandDirectory Integration Tests', function (): void {
|
||||
it('finds brands and caches the paginated result', function (): void {
|
||||
// GIVEN
|
||||
$conn = DB::connection();
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$directory = new ArcadiaMarqueBrandDirectory($conn, $brandCache);
|
||||
|
||||
MarqueTable::up($conn->getSchemaBuilder());
|
||||
|
||||
$conn->table(MarqueTable::table($conn))->insert([
|
||||
'COD_MARQ' => 1,
|
||||
'LIB_MARQ' => 'Le Gaulois',
|
||||
'A_EPURER' => 0,
|
||||
'TYPE_MARQUE' => 1,
|
||||
]);
|
||||
$conn->table(MarqueTable::table($conn))->insert([
|
||||
'COD_MARQ' => 2,
|
||||
'LIB_MARQ' => 'Maître Coq',
|
||||
'A_EPURER' => 0,
|
||||
'TYPE_MARQUE' => 1,
|
||||
]);
|
||||
|
||||
$pagination = Pagination::ofPage(1, 25);
|
||||
|
||||
// WHEN
|
||||
$result = $directory->findAll($pagination);
|
||||
|
||||
// THEN
|
||||
expect($result->getItems())->toHaveCount(2)
|
||||
->and($result->getTotal())->toBe(2)
|
||||
->and($result->getItems()[0]->name->value)->toBe('Le Gaulois')
|
||||
->and($result->getItems()[1]->name->value)->toBe('Maître Coq');
|
||||
|
||||
// Check it was cached
|
||||
$cached = $brandCache->getArcadiaPaginated($pagination);
|
||||
expect($cached)->not->toBeNull()
|
||||
->and($cached->getItems())->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('falls back to local BrandModel when database connection fails during findAll', function (): void {
|
||||
// GIVEN
|
||||
BrandModel::query()->create([
|
||||
'id' => (string) Uuid::uuid7(),
|
||||
'name' => 'Local Gaulois',
|
||||
'arcadia_id' => new ArcadiaId('1'),
|
||||
'uwil_public_id' => new BrandCode('LGL01'),
|
||||
]);
|
||||
|
||||
/** @var Connection&Mockery\MockInterface $failingConnection */
|
||||
$failingConnection = Mockery::mock(Connection::class);
|
||||
$failingConnection->shouldReceive('table')
|
||||
->andThrow(new \RuntimeException('Oracle is down'));
|
||||
|
||||
$directory = new ArcadiaMarqueBrandDirectory($failingConnection, null);
|
||||
$pagination = Pagination::ofPage(1, 25);
|
||||
|
||||
// WHEN
|
||||
$result = $directory->findAll($pagination);
|
||||
|
||||
// THEN
|
||||
expect($result->getItems())->toHaveCount(1)
|
||||
->and($result->getItems()[0]->id->value)->toBe('1')
|
||||
->and($result->getItems()[0]->name->value)->toBe('Local Gaulois');
|
||||
});
|
||||
|
||||
it('falls back to local BrandModel when database connection fails during find', function (): void {
|
||||
// GIVEN
|
||||
BrandModel::query()->create([
|
||||
'id' => (string) Uuid::uuid7(),
|
||||
'name' => 'Fallback Brand',
|
||||
'arcadia_id' => new ArcadiaId('42'),
|
||||
'uwil_public_id' => new BrandCode('FLB01'),
|
||||
]);
|
||||
|
||||
/** @var Connection&Mockery\MockInterface $failingConnection */
|
||||
$failingConnection = Mockery::mock(Connection::class);
|
||||
$failingConnection->shouldReceive('table')
|
||||
->andThrow(new \RuntimeException('Oracle is down'));
|
||||
|
||||
$directory = new ArcadiaMarqueBrandDirectory($failingConnection, null);
|
||||
|
||||
// WHEN
|
||||
$found = $directory->find(new ArcadiaId('42'));
|
||||
|
||||
// THEN
|
||||
expect($found)->not->toBeNull()
|
||||
->and($found->id->value)->toBe('42')
|
||||
->and($found->name->value)->toBe('Fallback Brand');
|
||||
});
|
||||
});
|
||||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace Tests\Unit\Domain\Services;
|
||||
|
||||
use Domain\Events\BrandCreated;
|
||||
use Domain\Events\BrandRenamed;
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
||||
|
|
@ -20,6 +19,7 @@ use Infra\Adapters\Repositories\InMemoryBrandRepository;
|
|||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Event\DomainEventDispatcher;
|
||||
use Shared\DDD\Event\Event;
|
||||
use Domain\Events\BrandRenamed;
|
||||
|
||||
covers(BrandResolver::class);
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ describe('BrandResolver Unit Tests', function (): void {
|
|||
->toThrow(BrandNotFoundException::class, 'Arcadia brand with ID 999 not found.');
|
||||
});
|
||||
|
||||
it('updates brand name, persists and dispatches BrandRenamed when Arcadia brand name changes', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
|
||||
it('returns existing brand directly without querying Arcadia directory when already in repository', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
|
||||
// GIVEN
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
$oldBrand = Brand::hydrate(
|
||||
|
|
@ -129,7 +129,7 @@ describe('BrandResolver Unit Tests', function (): void {
|
|||
);
|
||||
$brandRepository->add($oldBrand);
|
||||
|
||||
// Arcadia has renamed the brand
|
||||
// Arcadia directory has a different name, but resolve() prioritizes local repository without extra Oracle calls
|
||||
$arcadiaDirectory->add(new ArcadiaBrand(new ArcadiaId('1'), new BrandName('Le Gaulois France')));
|
||||
|
||||
$dispatcher = new class () implements DomainEventDispatcher
|
||||
|
|
@ -149,22 +149,9 @@ describe('BrandResolver Unit Tests', function (): void {
|
|||
$resolved = $resolver->resolve(new ArcadiaId('1'));
|
||||
|
||||
// THEN
|
||||
expect($resolved->name->value)->toBe('Le Gaulois France')
|
||||
->and($resolved->id()->value)->toBe($brandId->value)
|
||||
->and($resolved->code->value)->toBe('LGL01');
|
||||
|
||||
$persisted = $brandRepository->findByArcadiaId(new ArcadiaId('1'));
|
||||
expect($persisted)->not->toBeNull()
|
||||
->and($persisted->name->value)->toBe('Le Gaulois France');
|
||||
|
||||
expect($dispatcher->dispatched)->toHaveCount(1)
|
||||
->and($dispatcher->dispatched[0])->toBeInstanceOf(BrandRenamed::class);
|
||||
|
||||
/** @var \Domain\Events\BrandRenamed $event */
|
||||
$event = $dispatcher->dispatched[0];
|
||||
expect($event->oldName->value)->toBe('Le Gaulois')
|
||||
->and($event->newName->value)->toBe('Le Gaulois France')
|
||||
->and($event->brandId->value)->toBe($brandId->value);
|
||||
expect($resolved)->toBe($oldBrand)
|
||||
->and($resolved->name->value)->toBe('Le Gaulois')
|
||||
->and($dispatcher->dispatched)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('preserves and returns existing brand when brand was removed from Arcadia directory', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
|
||||
|
|
@ -202,4 +189,52 @@ describe('BrandResolver Unit Tests', function (): void {
|
|||
->and($resolved->name->value)->toBe('Historical Brand')
|
||||
->and($dispatcher->dispatched)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('updates brand name, persists and dispatches BrandRenamed when resolving from ArcadiaBrand with changed name', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
|
||||
// GIVEN
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
$oldBrand = Brand::hydrate(
|
||||
id: $brandId,
|
||||
name: new BrandName('Le Gaulois'),
|
||||
code: new BrandCode('LGL01'),
|
||||
arcadiaId: new ArcadiaId('1'),
|
||||
);
|
||||
$brandRepository->add($oldBrand);
|
||||
|
||||
$renamedArcadia = new ArcadiaBrand(new ArcadiaId('1'), new BrandName('Le Gaulois France'));
|
||||
|
||||
$dispatcher = new class () implements DomainEventDispatcher
|
||||
{
|
||||
/** @var list<Event> */
|
||||
public array $dispatched = [];
|
||||
|
||||
public function dispatch(Event $event): void
|
||||
{
|
||||
$this->dispatched[] = $event;
|
||||
}
|
||||
};
|
||||
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
|
||||
|
||||
// WHEN
|
||||
$resolved = $resolver->resolveFromArcadia($renamedArcadia);
|
||||
|
||||
// THEN
|
||||
expect($resolved->name->value)->toBe('Le Gaulois France')
|
||||
->and($resolved->id()->value)->toBe($brandId->value)
|
||||
->and($resolved->code->value)->toBe('LGL01');
|
||||
|
||||
$persisted = $brandRepository->findByArcadiaId(new ArcadiaId('1'));
|
||||
expect($persisted)->not->toBeNull()
|
||||
->and($persisted->name->value)->toBe('Le Gaulois France');
|
||||
|
||||
expect($dispatcher->dispatched)->toHaveCount(1)
|
||||
->and($dispatcher->dispatched[0])->toBeInstanceOf(BrandRenamed::class);
|
||||
|
||||
/** @var \Domain\Events\BrandRenamed $event */
|
||||
$event = $dispatcher->dispatched[0];
|
||||
expect($event->oldName->value)->toBe('Le Gaulois')
|
||||
->and($event->newName->value)->toBe('Le Gaulois France')
|
||||
->and($event->brandId->value)->toBe($brandId->value);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ use Mockery;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
covers(BrandCache::class);
|
||||
|
||||
|
|
@ -311,4 +314,49 @@ describe('BrandCache Unit Tests', function (): void {
|
|||
// No exception is thrown
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('gets and puts arcadia paginated results to cache', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::ofPage(1, 25);
|
||||
$brand1 = new ArcadiaBrand(new ArcadiaId('1'), new BrandName('Brand One'));
|
||||
$brand2 = new ArcadiaBrand(new ArcadiaId('2'), new BrandName('Brand Two'));
|
||||
$paginated = new Paginated($pagination, [$brand1, $brand2], 2);
|
||||
|
||||
$expectedData = [
|
||||
'total' => 2,
|
||||
'items' => [
|
||||
['id' => '1', 'name' => 'Brand One'],
|
||||
['id' => '2', 'name' => 'Brand Two'],
|
||||
],
|
||||
];
|
||||
|
||||
/** @var CacheInterface&Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('set')
|
||||
->once()
|
||||
->with('brand:arcadia:page:1:size:25', $expectedData, 86400);
|
||||
|
||||
$cacheMock->shouldReceive('get')
|
||||
->once()
|
||||
->with('brand:arcadia:page:1:size:25')
|
||||
->andReturn($expectedData);
|
||||
|
||||
/** @var \Psr\Log\LoggerInterface&Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->zeroOrMoreTimes();
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$brandCache->putArcadiaPaginated($pagination, $paginated);
|
||||
$result = $brandCache->getArcadiaPaginated($pagination);
|
||||
|
||||
// THEN
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result->getTotal())->toBe(2)
|
||||
->and($result->getItems())->toHaveCount(2)
|
||||
->and($result->getItems()[0]->id->value)->toBe('1')
|
||||
->and($result->getItems()[0]->name->value)->toBe('Brand One')
|
||||
->and($result->getItems()[1]->id->value)->toBe('2')
|
||||
->and($result->getItems()[1]->name->value)->toBe('Brand Two');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue