From 5bb806399934ace279a35a775c19e4e7565ef40e Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 13 Oct 2026 17:30:01 +0200 Subject: [PATCH 1/3] feat: :sparkles: update legal and validity information of a coupon --- .../UseCases/Coupon/Update/Input.php | 8 +++- .../UseCases/Coupon/Update/UpdateCoupon.php | 12 ++++++ src/Domain/Models/Entities/Coupon.php | 22 ++++++++++ .../Coupon/CouponCountryCoverage.php | 8 ++++ .../Coupon/Update/UpdateCouponTest.php | 40 +++++++++++++++++++ .../Coupon/CouponCountryCoverageTest.php | 34 ++++++++++++++++ 6 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/Application/UseCases/Coupon/Update/Input.php b/src/Application/UseCases/Coupon/Update/Input.php index fc8f41b..421b17a 100644 --- a/src/Application/UseCases/Coupon/Update/Input.php +++ b/src/Application/UseCases/Coupon/Update/Input.php @@ -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.', ); } diff --git a/src/Application/UseCases/Coupon/Update/UpdateCoupon.php b/src/Application/UseCases/Coupon/Update/UpdateCoupon.php index a3b50ed..6d44e0b 100644 --- a/src/Application/UseCases/Coupon/Update/UpdateCoupon.php +++ b/src/Application/UseCases/Coupon/Update/UpdateCoupon.php @@ -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( diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index f7d8fc9..6648fb4 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -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(), + ); + } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php index b00aca7..f336808 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php @@ -29,6 +29,14 @@ final readonly class CouponCountryCoverage throw new \DomainException(sprintf('A coupon requires legal information for %s.', $country->name)); } } + + if (count($validityInformations->value) !== count($processingCodes->value)) { + throw new \DomainException('A coupon requires exactly one validity information per diffusion country.'); + } + + if (count($legalInformations->value) !== count($processingCodes->value)) { + throw new \DomainException('A coupon requires exactly one legal information per diffusion country.'); + } } private static function hasInformationFor( diff --git a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php index 0ef1d32..e37cdca 100644 --- a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php @@ -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'); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php index 1a7bcf0..d6dbe67 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php @@ -90,6 +90,40 @@ describe('coupon country coverage', function (): void { expect($action)->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.'); }); + it('rejects validity 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 validity information per diffusion country.'); + }); + + it('rejects legal information for a country where the coupon is not diffused', function () use ( + $franceCode, + $franceValidity, + $franceLegal, + $beneluxLegal, + ): void { + // GIVEN / WHEN + $action = fn () => CouponCountryCoverage::assert( + new ProcessingCodeArray([$franceCode]), + new CouponValidityInformationArray([$franceValidity]), + new CouponLegalInformationArray([$franceLegal, $beneluxLegal]), + ); + + // THEN + expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one legal information per diffusion country.'); + }); + it('cannot be instantiated from the outside', function (): void { // GIVEN $reflection = new \ReflectionClass(CouponCountryCoverage::class); From 44e3a646a337c5a2515e84797e1fac6f31c9b646 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 13 Oct 2026 17:33:16 +0200 Subject: [PATCH 2/3] fix: :bug: correct imports and use ean13 processing codes --- .../CampaignRequest/Create/CreateCampaignRequestTest.php | 1 + .../UseCases/CampaignRequest/Find/FindCampaignRequestTest.php | 1 + .../UseCases/CampaignRequest/List/ListCampaignsRequestTest.php | 2 ++ .../CampaignRequest/Reject/RejectCampaignRequestTest.php | 1 + tests/Unit/Domain/Models/Entities/CampaignRequestTest.php | 1 + 5 files changed, 6 insertions(+) diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php index 60e8429..4bb30ed 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php @@ -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), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index ef0ec38..dd00697 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -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), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index c4d8b81..e849099 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -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), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 3c54d9c..b4de50e 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -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), diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index edf6a44..a47b3f2 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -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), From 5b2da5859b94bd6fc3021c1e7fb4979448263922 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 14 Oct 2026 10:15:35 +0200 Subject: [PATCH 3/3] refactor: :recycle: extract diffusion country count check --- .../Coupon/CouponCountryCoverage.php | 18 +++++++++------- .../Coupon/CouponCountryCoverageTest.php | 21 ++----------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php index f336808..b64b92a 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php @@ -30,13 +30,8 @@ final readonly class CouponCountryCoverage } } - if (count($validityInformations->value) !== count($processingCodes->value)) { - throw new \DomainException('A coupon requires exactly one validity information per diffusion country.'); - } - - if (count($legalInformations->value) !== count($processingCodes->value)) { - throw new \DomainException('A coupon requires exactly one legal information per diffusion country.'); - } + self::countDiffusionCountries($validityInformations, $processingCodes); + self::countDiffusionCountries($legalInformations, $processingCodes); } private static function hasInformationFor( @@ -48,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.'); + } + } } diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php index d6dbe67..47c19d2 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php @@ -90,7 +90,7 @@ describe('coupon country coverage', function (): void { expect($action)->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.'); }); - it('rejects validity information for a country where the coupon is not diffused', function () use ( + it('rejects information for a country where the coupon is not diffused', function () use ( $franceCode, $franceValidity, $beneluxValidity, @@ -104,24 +104,7 @@ describe('coupon country coverage', function (): void { ); // THEN - expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one validity information per diffusion country.'); - }); - - it('rejects legal information for a country where the coupon is not diffused', function () use ( - $franceCode, - $franceValidity, - $franceLegal, - $beneluxLegal, - ): void { - // GIVEN / WHEN - $action = fn () => CouponCountryCoverage::assert( - new ProcessingCodeArray([$franceCode]), - new CouponValidityInformationArray([$franceValidity]), - new CouponLegalInformationArray([$franceLegal, $beneluxLegal]), - ); - - // THEN - expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one legal information per diffusion country.'); + expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one information per diffusion country.'); }); it('cannot be instantiated from the outside', function (): void {