feat(infra): attach HasUuidV7CreatedAt trait to Brand, ApiClient, and ClientAuditLog models

This commit is contained in:
Jérémy Munsch 2026-08-21 16:12:01 +02:00
parent 6d63e68908
commit 5c49b2cc4f
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
9 changed files with 125 additions and 10 deletions

View file

@ -14,6 +14,7 @@ use Infra\Casts\ClientNameCast;
use Infra\Casts\ClientSecretCast;
use Infra\Casts\UtcDateTimeCast;
use Infra\Casts\UuidCast;
use Infra\Database\Concerns\HasUuidV7CreatedAt;
/**
* @property \Ramsey\Uuid\UuidInterface $id
@ -22,6 +23,7 @@ use Infra\Casts\UuidCast;
* @property \Application\Security\Models\ValueObjects\ClientIdentifier $identifier ENCRYPTED, Unique identifier that should not change
* @property \Application\Security\Models\ValueObjects\ClientSecret $secret ENCRYPTED, api secret that can be regenerated
* @property bool $is_active Defines if the client can be used in order to gain access to the system
* @property \DateTimeImmutable $created_at
* @property \DateTimeImmutable $updated_at
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
*
@ -35,6 +37,7 @@ final class ApiClient extends Authenticatable
use HasFactory;
use HasUuids;
use HasUuidV7CreatedAt;
use Notifiable;
protected static function newFactory(): ApiClientFactory
@ -72,6 +75,7 @@ final class ApiClient extends Authenticatable
'identifier' => ClientIdentifierCast::class,
'secret' => ClientSecretCast::class,
'is_active' => 'boolean',
'created_at' => UtcDateTimeCast::class,
'updated_at' => UtcDateTimeCast::class,
];
}

View file

@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Infra\Casts\ArcadiaIdCast;
use Infra\Casts\BrandCodeCast;
use Infra\Casts\UtcDateTimeCast;
use Infra\Database\Concerns\HasUuidV7CreatedAt;
/**
* @phpcs:disable Generic.Files.LineLength.TooLong
@ -18,6 +20,7 @@ use Infra\Casts\BrandCodeCast;
* @property string $name The human readable brand name
* @property \Domain\Models\ValueObjects\Brand\BrandCode $uwil_public_id A unique public id that is generated spefically for the UWIL eco system
* @property \Domain\Models\ValueObjects\Brand\ArcadiaId $arcadia_id An unique external autoincrement id for the brand, it may be exposed but should not be used
* @property \DateTimeImmutable $created_at
*
* @method static \Database\Factories\BrandFactory factory($count = null, $state = [])
*
@ -29,6 +32,7 @@ final class Brand extends Model
use HasFactory;
use HasUuids;
use HasUuidV7CreatedAt;
protected static function newFactory(): BrandFactory
{
@ -54,6 +58,7 @@ final class Brand extends Model
return [
'arcadia_id' => ArcadiaIdCast::class,
'uwil_public_id' => BrandCodeCast::class,
'created_at' => UtcDateTimeCast::class,
];
}
}

View file

@ -10,6 +10,7 @@ use Infra\Casts\ClientIdentifierCast;
use Infra\Casts\SecurityAuditLogDetailsCast;
use Infra\Casts\UtcDateTimeCast;
use Infra\Casts\UuidCast;
use Infra\Database\Concerns\HasUuidV7CreatedAt;
/**
* @property \Ramsey\Uuid\UuidInterface $id
@ -24,6 +25,7 @@ use Infra\Casts\UuidCast;
final class ClientAuditLog extends Model
{
use HasUuids;
use HasUuidV7CreatedAt;
protected $table = 'client_audit_logs';

View file

@ -34,12 +34,11 @@ final readonly class RecordClientSecurityAudit
};
ClientAuditLog::query()->create([
'id' => Uuid::uuid7()->toString(),
'id' => Uuid::uuid7($occurredAt)->toString(),
'client_id' => $event->clientId->value,
'client_identifier' => $event->clientIdentifier->value,
'event_type' => $eventType,
'details' => $details,
'created_at' => $occurredAt,
]);
}
}

View file

@ -29,7 +29,6 @@ describe('ExportSecurityAuditLogsCommand', function (): void {
'client_identifier' => $clientIdentifier,
'event_type' => 'client_secret_generated',
'details' => null,
'created_at' => '2026-08-20 10:00:00',
]);
// WHEN
@ -80,7 +79,6 @@ describe('ExportSecurityAuditLogsCommand', function (): void {
'client_identifier' => $clientIdentifier,
'event_type' => 'client_status_toggled',
'details' => ['is_active' => 'false'],
'created_at' => '2026-08-18 15:30:00',
]);
// WHEN

View file

@ -27,30 +27,27 @@ describe('EloquentClientAuditLogDirectory Integration Tests', function (): void
$clientIdentifier2 = 'arcadia_id_' . str_repeat('2', 32);
ClientAuditLog::query()->create([
'id' => Uuid::uuid7()->toString(),
'id' => Uuid::uuid7(new \DateTimeImmutable('2026-08-01 10:00:00 UTC'))->toString(),
'client_id' => $clientId1,
'client_identifier' => $clientIdentifier1,
'event_type' => 'client_secret_generated',
'details' => null,
'created_at' => '2026-08-01 10:00:00',
]);
ClientAuditLog::query()->create([
'id' => Uuid::uuid7()->toString(),
'id' => Uuid::uuid7(new \DateTimeImmutable('2026-08-10 14:00:00 UTC'))->toString(),
'client_id' => $clientId1,
'client_identifier' => $clientIdentifier1,
'event_type' => 'client_status_toggled',
'details' => ['is_active' => 'false'],
'created_at' => '2026-08-10 14:00:00',
]);
ClientAuditLog::query()->create([
'id' => Uuid::uuid7()->toString(),
'id' => Uuid::uuid7(new \DateTimeImmutable('2026-08-15 12:00:00 UTC'))->toString(),
'client_id' => $clientId2,
'client_identifier' => $clientIdentifier2,
'event_type' => 'client_secret_generated',
'details' => null,
'created_at' => '2026-08-15 12:00:00',
]);
$directory = new EloquentClientAuditLogDirectory();

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Tests\Integration\Infra\Database\Models;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Infra\Database\Models\ApiClient;
use Ramsey\Uuid\Uuid;
use Tests\TestCase;
covers(ApiClient::class);
uses(TestCase::class, RefreshDatabase::class);
describe('ApiClient Eloquent Model & Factory', function (): void {
it('creates and persists an ApiClient record with created_at automatically derived from UUIDv7', function (): void {
// GIVEN
$expectedDate = new \DateTimeImmutable('2026-08-21 08:30:00 UTC');
$uuid7 = Uuid::uuid7($expectedDate)->toString();
// WHEN
$client = ApiClient::factory()->create([
'id' => $uuid7,
]);
$retrieved = ApiClient::query()->find($client->id);
// THEN
expect($retrieved)->not->toBeNull()
->and($retrieved?->id->toString())->toBe($uuid7)
->and($retrieved?->created_at)->toBeInstanceOf(\DateTimeImmutable::class)
->and($retrieved?->created_at->format('Y-m-d H:i:s'))->toBe('2026-08-21 08:30:00');
});
});

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Tests\Integration\Infra\Database\Models;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Infra\Database\Models\Brand;
use Ramsey\Uuid\Uuid;
use Tests\TestCase;
covers(Brand::class);
uses(TestCase::class, RefreshDatabase::class);
describe('Brand Eloquent Model & Factory', function (): void {
it('creates and persists a Brand record with computed created_at from UUIDv7', function (): void {
// GIVEN
$expectedDate = new \DateTimeImmutable('2026-08-21 10:30:00 UTC');
$uuid7 = Uuid::uuid7($expectedDate)->toString();
// WHEN
$brand = Brand::factory()->create([
'id' => $uuid7,
]);
$retrieved = Brand::query()->find($brand->id);
// THEN
expect($retrieved)->not->toBeNull()
->and($retrieved?->id)->toBe($uuid7)
->and($retrieved?->created_at)->toBeInstanceOf(\DateTimeImmutable::class)
->and($retrieved?->created_at->format('Y-m-d H:i:s'))->toBe('2026-08-21 10:30:00');
});
});

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Tests\Integration\Infra\Database\Models;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Infra\Database\Models\ClientAuditLog;
use Ramsey\Uuid\Uuid;
use Tests\TestCase;
covers(ClientAuditLog::class);
uses(TestCase::class, RefreshDatabase::class);
describe('ClientAuditLog Eloquent Model', function (): void {
it('creates and persists a ClientAuditLog record with computed created_at from UUIDv7', function (): void {
// GIVEN
$expectedDate = new \DateTimeImmutable('2026-08-20 14:15:00 UTC');
$uuid7 = Uuid::uuid7($expectedDate)->toString();
$clientId = Uuid::uuid7()->toString();
// WHEN
$log = ClientAuditLog::query()->create([
'id' => $uuid7,
'client_id' => $clientId,
'client_identifier' => 'arcadia_id_' . str_repeat('a', 32),
'event_type' => 'client_secret_generated',
'details' => null,
]);
$retrieved = ClientAuditLog::query()->find($log->id);
// THEN
expect($retrieved)->not->toBeNull()
->and($retrieved?->id->toString())->toBe($uuid7)
->and($retrieved?->created_at)->toBeInstanceOf(\DateTimeImmutable::class)
->and($retrieved?->created_at?->format('Y-m-d H:i:s'))->toBe('2026-08-20 14:15:00');
});
});