feat: ✨ create Campaign entity and associated VOs

This commit is contained in:
Corentin Huet 2026-09-23 13:13:49 +02:00
parent b5155c8ade
commit ab1923c05b
9 changed files with 331 additions and 0 deletions

View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Domain\Models\Entities;
use Domain\Enums\Country;
use Domain\Models\Entities\Tag;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Campaign\CampaignCode;
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
use Domain\Models\ValueObjects\Campaign\CampaignId;
use Domain\Models\ValueObjects\Campaign\CampaignName;
use Shared\DDD\Models\AggregateRoot;
use Shared\DDD\Models\HasTimestampIdentifier;
/**
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Campaign\CampaignId>
*/
final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier
{
private function __construct(
public CampaignId $id,
public CampaignCode $code,
public Tag $tag,
public CampaignName $name,
public bool $publication,
public bool $printAllAtOnce,
public Country $availableIn,
/** @var CampaignCoupon[] */
public array $coupons,
public BrandCode $brand,
public ?\DateTimeInterface $updatedAt = null,
) {
}
public function id(): CampaignId
{
return $this->id;
}
public function createdAt(): \DateTimeInterface
{
return $this->id->toDateTime();
}
public function updatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public static function create(
CampaignCode $code,
Tag $tag,
CampaignName $name,
bool $publication,
bool $printAllAtOnce,
Country $availableIn,
/** @var CampaignCoupon[] */
array $coupons,
BrandCode $brand,
?\DateTimeInterface $updatedAt = null,
): Campaign {
return new self(
id: CampaignId::generate(),
code: $code,
tag: $tag,
name: $name,
publication: $publication,
printAllAtOnce: $printAllAtOnce,
availableIn: $availableIn,
coupons: $coupons,
brand: $brand,
updatedAt: $updatedAt,
);
}
public static function hydrate(
CampaignId $id,
CampaignCode $code,
Tag $tag,
CampaignName $name,
bool $publication,
bool $printAllAtOnce,
Country $availableIn,
/** @var CampaignCoupon[] */
array $coupons,
BrandCode $brand,
?\DateTimeInterface $updatedAt = null,
): self {
return new self(
id: $id,
code: $code,
tag: $tag,
name: $name,
publication: $publication,
printAllAtOnce: $printAllAtOnce,
availableIn: $availableIn,
coupons: $coupons,
brand: $brand,
updatedAt: $updatedAt,
);
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects\Campaign;
final readonly class CampaignCode
{
public function __construct(
public string $code,
) {
$trimmed = trim($code);
if ($trimmed === '') {
throw new \DomainException('Campaign code cannot be empty.');
}
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects\Campaign;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
final readonly class CampaignCoupon
{
public function __construct(
public ProcessingCode $code,
public StockId $qty,
public ?PrintRules $printRules,
public int $warningThresholdQty
) {
if ($this->warningThresholdQty <= 0) {
throw new \DomainException('Campaign Coupon warningThresholdQty must be positive.');
}
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects\Campaign;
use Ramsey\Uuid\Uuid;
use Shared\DDD\Models\Uuidv7EntityId;
final readonly class CampaignId extends Uuidv7EntityId
{
public static function generate(): self
{
return new self(Uuid::uuid7());
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects\Campaign;
final readonly class CampaignName
{
public function __construct(
public string $value,
) {
$trimmed = trim($value);
if ($trimmed === '') {
throw new \DomainException('Campaign name cannot be empty.');
}
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Campaign;
use Domain\Models\ValueObjects\Campaign\CampaignCode;
covers(CampaignCode::class);
describe('campaign code', function (): void {
it('creates a valid campaign code', function (): void {
// GIVEN
$code = 'Name';
// WHEN
$name = new CampaignCode($code);
// THEN
expect($name->code)->toBe($code);
});
it('rejects invalid campaign code', function (): void {
// GIVEN
$code = '';
// GIVEN / THEN
expect(fn (): CampaignCode => new CampaignCode($code))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Campaign;
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
covers(CampaignCoupon::class);
describe('campaign coupon', function (): void {
it('creates a valid campaign coupon', function (): void {
// GIVEN
$code = new ProcessingCode('PC1');
$qty = new StockId();
$printRules = new PrintRules(maxPerCustomer: 3);
$warningThresholdQty = 100;
// WHEN
$campaignCoupon = new CampaignCoupon(code: $code, qty: $qty, printRules: $printRules, warningThresholdQty: $warningThresholdQty);
// THEN
expect($campaignCoupon->code)->toBe($code)
->and($campaignCoupon->qty)->toBe($qty)
->and($campaignCoupon->printRules)->toBe($printRules)
->and($campaignCoupon->warningThresholdQty)->toBe($warningThresholdQty);
});
it('creates a valid campaign coupon whitout print rules', function (): void {
// GIVEN
$code = new ProcessingCode('PC1');
$qty = new StockId();
$warningThresholdQty = 100;
// WHEN
$campaignCoupon = new CampaignCoupon(code: $code, qty: $qty, warningThresholdQty: $warningThresholdQty);
// THEN
expect($campaignCoupon->code)->toBe($code)
->and($campaignCoupon->qty)->toBe($qty)
->and($campaignCoupon->warningThresholdQty)->toBe($warningThresholdQty);
});
it('rejects invalid campaign coupon', function (): void {
// GIVEN
$code = new ProcessingCode('PC1');
$qty = new StockId();
$printRules = new PrintRules(maxPerCustomer: 3);
$warningThresholdQty = -100;
// GIVEN / THEN
expect(fn (): CampaignCoupon => new CampaignCoupon(code: $code, qty: $qty, printRules: $printRules, warningThresholdQty: $warningThresholdQty))->toThrow(\DomainException::class);
});
it('rejects invalid campaign coupon whitout print rules', function (): void {
// GIVEN
$code = new ProcessingCode('PC1');
$qty = new StockId();
$warningThresholdQty = -100;
// GIVEN / THEN
expect(fn (): CampaignCoupon => new CampaignCoupon(code: $code, qty: $qty, warningThresholdQty: $warningThresholdQty))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Campaign;
use Domain\Models\ValueObjects\Campaign\CampaignId;
covers(CampaignId::class);
describe('campaign', function (): void {
it('generates a unique campaign id', function (): void {
// WHEN
$id = CampaignId::generate();
// THEN
expect($id)->toBeInstanceOf(CampaignId::class);
});
it('generates different ids on each call', function (): void {
// WHEN
$first = CampaignId::generate();
$second = CampaignId::generate();
// THEN
expect((string) $first)->not->toBe((string) $second);
});
});

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Campaign;
use Domain\Models\ValueObjects\Campaign\CampaignName;
covers(CampaignName::class);
describe('campaign name', function (): void {
it('creates a valid campaign name', function (): void {
// GIVEN
$value = 'Name';
// WHEN
$name = new CampaignName($value);
// THEN
expect($name->value)->toBe($value);
});
it('rejects invalid campaign name', function (): void {
// GIVEN
$value = '';
// GIVEN / THEN
expect(fn (): CampaignName => new CampaignName($value))->toThrow(\DomainException::class);
});
});