Merge branch 'update-coupon-info' into 'coupon'

feat: ✨ update legal and validity information of a coupon

See merge request ldc-web/projets/mesbr!64
This commit is contained in:
Corentin HUET 2026-10-14 08:27:08 +00:00
commit 7c8708496a
11 changed files with 116 additions and 1 deletions

View file

@ -5,7 +5,9 @@ declare(strict_types=1);
namespace Application\UseCases\Coupon\Update;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Shared\DDD\UseCases\Input as UseCaseInput;
use Webmozart\Assert\Assert;
@ -14,9 +16,13 @@ final readonly class Input implements UseCaseInput
public function __construct(
public CouponCode $code,
public ?CouponPicture $picture = null,
public ?CouponValidityInformationArray $validityInformations = null,
public ?CouponLegalInformationArray $legalInformations = null,
) {
Assert::true(
$this->picture instanceof CouponPicture,
$this->picture instanceof CouponPicture
|| $this->validityInformations instanceof CouponValidityInformationArray
|| $this->legalInformations instanceof CouponLegalInformationArray,
'At least one field must be provided to update the coupon.',
);
}

View file

@ -8,7 +8,9 @@ use Application\UseCases\Coupon\Update\Output\CouponNotFound;
use Application\UseCases\Coupon\Update\Output\CouponUpdated;
use Application\UseCases\Coupon\Update\Output\Output;
use Domain\Models\Entities\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Ports\Repositories\CouponRepository;
use Shared\DDD\UseCases\Input as BaseInput;
use Shared\DDD\UseCases\Transactional;
@ -38,6 +40,16 @@ final readonly class UpdateCoupon implements UseCase, Transactional
$coupon = $coupon->changePicture($input->picture);
}
if (
$input->validityInformations instanceof CouponValidityInformationArray
|| $input->legalInformations instanceof CouponLegalInformationArray
) {
$coupon = $coupon->changeInformations(
$input->validityInformations ?? $coupon->validityInformations,
$input->legalInformations ?? $coupon->legalInformations,
);
}
$this->repository->save($coupon);
return new CouponUpdated(

View file

@ -165,4 +165,26 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier
updatedAt: DatesHelper::now(),
);
}
public function changeInformations(CouponValidityInformationArray $validityInformations, CouponLegalInformationArray $legalInformations): self
{
return new self(
id: $this->id,
code: $this->code,
name: $this->name,
amount: $this->amount,
picture: $this->picture,
diffusion: $this->diffusion,
validityInformations: $validityInformations,
legalInformations: $legalInformations,
processingCode: $this->processingCode,
brandCode: $this->brandCode,
createdBy: $this->createdBy,
products: $this->products,
movements: $this->movements,
quantity: $this->quantity,
availabilityCalendar: $this->availabilityCalendar,
updatedAt: DatesHelper::now(),
);
}
}

View file

@ -29,6 +29,9 @@ final readonly class CouponCountryCoverage
throw new \DomainException(sprintf('A coupon requires legal information for %s.', $country->name));
}
}
self::countDiffusionCountries($validityInformations, $processingCodes);
self::countDiffusionCountries($legalInformations, $processingCodes);
}
private static function hasInformationFor(
@ -40,4 +43,13 @@ final readonly class CouponCountryCoverage
fn (CouponValidityInformation | CouponLegalInformation $information): bool => $information->forLocale === $country,
);
}
private static function countDiffusionCountries(
CouponValidityInformationArray | CouponLegalInformationArray $informations,
ProcessingCodeArray $processingCodes,
): void {
if (count($informations->value) !== count($processingCodes->value)) {
throw new \DomainException('A coupon requires exactly one information per diffusion country.');
}
}
}

View file

@ -57,6 +57,7 @@ describe('create campaign request use case', function (): void {
),
couponProcessingCodes: new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
new ProcessingCode('4006381333931', Country::BENELUX),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),

View file

@ -61,6 +61,7 @@ describe('find campaign request use case', function (): void {
),
couponProcessingCodes: new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
new ProcessingCode('4006381333931', Country::BENELUX),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),

View file

@ -55,6 +55,7 @@ describe('list campaigns request use case', function (): void {
),
couponProcessingCodes: new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
new ProcessingCode('4006381333931', Country::BENELUX),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
@ -80,6 +81,7 @@ describe('list campaigns request use case', function (): void {
),
couponProcessingCodes: new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
new ProcessingCode('4006381333931', Country::BENELUX),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),

View file

@ -56,6 +56,7 @@ describe('reject campaign request use case', function (): void {
),
couponProcessingCodes: new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
new ProcessingCode('4006381333931', Country::BENELUX),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),

View file

@ -142,6 +142,46 @@ describe('update coupon use case', function (): void {
expect($result)->toBeInstanceOf(CouponNotFound::class);
});
it('updates the legal and validity information and keeps the other fields', function () use ($makeCoupon): void {
// GIVEN
$coupon = $makeCoupon();
$newValidityInformations = new CouponValidityInformationArray([
new CouponValidityInformation('New validity information.', Country::FRANCE),
]);
$newLegalInformations = new CouponLegalInformationArray([
new CouponLegalInformation('New legal information.', Country::FRANCE),
]);
$input = new Input(
code: $coupon->code,
validityInformations: $newValidityInformations,
legalInformations: $newLegalInformations,
);
$repository = \Mockery::mock(CouponRepository::class);
$repository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
$repository->shouldReceive('save')->once()->with(\Mockery::on(
fn (Coupon $saved): bool => $saved->validityInformations === $newValidityInformations
&& $saved->legalInformations === $newLegalInformations
&& $saved->id === $coupon->id
&& $saved->name === $coupon->name
&& $saved->amount === $coupon->amount
&& $saved->picture === $coupon->picture
&& $saved->diffusion === $coupon->diffusion
&& $saved->movements === $coupon->movements
&& $saved->updatedAt instanceof \DateTimeInterface,
));
$useCase = new UpdateCoupon($repository);
// WHEN
$result = $useCase->execute($input);
// THEN
expect($result)->toBeInstanceOf(CouponUpdated::class)
->and($result->validityInformations)->toBe($newValidityInformations)
->and($result->legalInformations)->toBe($newLegalInformations);
});
it('rejects an input with nothing to update', function (): void {
// GIVEN
$code = new CouponCode('COU01');

View file

@ -51,6 +51,7 @@ describe('campaign request', function (): void {
),
couponProcessingCodes: new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
new ProcessingCode('4006381333931', Country::BENELUX),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),

View file

@ -90,6 +90,23 @@ describe('coupon country coverage', function (): void {
expect($action)->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.');
});
it('rejects information for a country where the coupon is not diffused', function () use (
$franceCode,
$franceValidity,
$beneluxValidity,
$franceLegal,
): void {
// GIVEN / WHEN
$action = fn () => CouponCountryCoverage::assert(
new ProcessingCodeArray([$franceCode]),
new CouponValidityInformationArray([$franceValidity, $beneluxValidity]),
new CouponLegalInformationArray([$franceLegal]),
);
// THEN
expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one information per diffusion country.');
});
it('cannot be instantiated from the outside', function (): void {
// GIVEN
$reflection = new \ReflectionClass(CouponCountryCoverage::class);