feat: ✨ add use case to find coupon details by code
This commit is contained in:
parent
a21415269c
commit
cbd5a56e48
7 changed files with 370 additions and 0 deletions
68
src/Application/UseCases/Coupon/Find/FindCoupon.php
Normal file
68
src/Application/UseCases/Coupon/Find/FindCoupon.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find;
|
||||
|
||||
use Application\UseCases\Coupon\Find\Output\Output;
|
||||
use Application\UseCases\Coupon\Find\Output\CouponFound;
|
||||
use Application\UseCases\Coupon\Find\Output\CouponNotFound;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Repositories\CampaignRepository;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\Coupon\Find\Input, \Application\UseCases\Coupon\Find\Output\Output> */
|
||||
final readonly class FindCoupon implements UseCase
|
||||
{
|
||||
public function __construct(
|
||||
private CouponRepository $couponRepository,
|
||||
private BrandRepository $brandRepository,
|
||||
private CampaignRepository $campaignRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$coupon = $this->couponRepository->findByCode($input->code);
|
||||
|
||||
if (! $coupon instanceof Coupon) {
|
||||
return new CouponNotFound();
|
||||
}
|
||||
|
||||
$brand = $this->brandRepository->findByCode($coupon->brandCode);
|
||||
|
||||
if (! $brand instanceof Brand) {
|
||||
throw new \DomainException(sprintf('Brand code "%s" was not found.', $coupon->brandCode->value));
|
||||
}
|
||||
|
||||
$campaigns = $this->campaignRepository->findByCouponCode($coupon->code);
|
||||
|
||||
return new CouponFound(
|
||||
code: $coupon->code,
|
||||
name: $coupon->name,
|
||||
amount: $coupon->amount,
|
||||
picture: $coupon->picture,
|
||||
diffusion: $coupon->diffusion,
|
||||
availabilityCalendar: $coupon->availabilityCalendar,
|
||||
validityInformations: $coupon->validityInformations,
|
||||
legalInformations: $coupon->legalInformations,
|
||||
processingCode: $coupon->processingCode,
|
||||
brand: $brand,
|
||||
createdBy: $coupon->createdBy,
|
||||
products: $coupon->products,
|
||||
movements: $coupon->movements,
|
||||
quantity: $coupon->quantity,
|
||||
campaigns: $campaigns,
|
||||
createdAt: $coupon->createdAt(),
|
||||
updatedAt: $coupon->updatedAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/Application/UseCases/Coupon/Find/Input.php
Normal file
16
src/Application/UseCases/Coupon/Find/Input.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find;
|
||||
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public CouponCode $code,
|
||||
) {
|
||||
}
|
||||
}
|
||||
49
src/Application/UseCases/Coupon/Find/Output/CouponFound.php
Normal file
49
src/Application/UseCases/Coupon/Find/Output/CouponFound.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find\Output;
|
||||
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\Entities\Campaign;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponQuantity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
|
||||
final readonly class CouponFound implements Output
|
||||
{
|
||||
/**
|
||||
* @param array<Campaign> $campaigns
|
||||
*/
|
||||
|
||||
public function __construct(
|
||||
public CouponCode $code,
|
||||
public CouponName $name,
|
||||
public MoneyAmount $amount,
|
||||
public CouponPicture $picture,
|
||||
public CouponDiffusion $diffusion,
|
||||
public ?CouponAvailabilityCalendar $availabilityCalendar,
|
||||
public CouponValidityInformationArray $validityInformations,
|
||||
public CouponLegalInformationArray $legalInformations,
|
||||
public CouponProcessingCodeArray $processingCode,
|
||||
public Brand $brand,
|
||||
public ActorId $createdBy,
|
||||
public CouponProductArray $products,
|
||||
public CouponMovementArray $movements,
|
||||
public CouponQuantity $quantity,
|
||||
public array $campaigns,
|
||||
public \DateTimeInterface $createdAt,
|
||||
public ?\DateTimeInterface $updatedAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find\Output;
|
||||
|
||||
final readonly class CouponNotFound implements Output
|
||||
{
|
||||
}
|
||||
11
src/Application/UseCases/Coupon/Find/Output/Output.php
Normal file
11
src/Application/UseCases/Coupon/Find/Output/Output.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find\Output;
|
||||
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
|
||||
interface Output extends UseCaseOutput
|
||||
{
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Ports\Repositories;
|
||||
|
||||
use Domain\Models\Entities\Campaign;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Shared\DDD\Ports\Repository;
|
||||
|
||||
/**
|
||||
|
|
@ -11,4 +13,10 @@ use Shared\DDD\Ports\Repository;
|
|||
*/
|
||||
interface CampaignRepository extends Repository
|
||||
{
|
||||
/**
|
||||
* Returns the campaigns that currently contain the given coupon
|
||||
*
|
||||
* @return array<Campaign>
|
||||
*/
|
||||
public function findByCouponCode(CouponCode $couponCode): array;
|
||||
}
|
||||
|
|
|
|||
209
tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php
Normal file
209
tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\Coupon\Find;
|
||||
|
||||
use Application\UseCases\Coupon\Find\FindCoupon;
|
||||
use Application\UseCases\Coupon\Find\Input;
|
||||
use Application\UseCases\Coupon\Find\Output\CouponFound;
|
||||
use Application\UseCases\Coupon\Find\Output\CouponNotFound;
|
||||
use DateTime;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\Entities\Campaign;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCouponArray;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovement;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponQuantity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Repositories\CampaignRepository;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Email;
|
||||
use Shared\ValueObjects\Money;
|
||||
use Shared\ValueObjects\Rcs;
|
||||
|
||||
covers(FindCoupon::class, Input::class, CouponFound::class, CouponNotFound::class);
|
||||
|
||||
describe('find coupon use case', function (): void {
|
||||
$brand = Brand::create(
|
||||
code: new BrandCode('AAA01'),
|
||||
name: new BrandName('Name'),
|
||||
logo: new PicturePath('files/picture.jpg'),
|
||||
rcs: new Rcs('RCS PARIS 111 222 333'),
|
||||
contact: new Email('contact@example.com'),
|
||||
);
|
||||
$coupon = Coupon::create(
|
||||
code: new CouponCode('COU01'),
|
||||
name: new CouponName('Coupon name'),
|
||||
amount: new MoneyAmount(new Money(Currency::EUR, 100)),
|
||||
picture: new CouponPicture(
|
||||
new PicturePath('files/product.jpg'),
|
||||
new PicturePath('files/thumbnail.jpg'),
|
||||
new PicturePath('files/mobile_thumbnail.jpg'),
|
||||
),
|
||||
diffusion: new CouponDiffusion(
|
||||
new Period(
|
||||
new DateTime('2026-01-01'),
|
||||
new DateTime('2026-02-01'),
|
||||
),
|
||||
),
|
||||
validityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE
|
||||
),
|
||||
]),
|
||||
legalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE
|
||||
),
|
||||
]),
|
||||
processingCode: new CouponProcessingCodeArray([
|
||||
new CouponProcessingCode('5425003944130', Country::FRANCE),
|
||||
]),
|
||||
brandCode: $brand->code,
|
||||
createdBy: ActorId::generate(),
|
||||
products: new CouponProductArray([
|
||||
new CouponIncoCode(35784),
|
||||
new CouponIncoCode(15522),
|
||||
]),
|
||||
movements: new CouponMovementArray([
|
||||
new CouponMovement(1000)
|
||||
]),
|
||||
quantity: new CouponQuantity(1000),
|
||||
);
|
||||
$campaign = Campaign::create(
|
||||
code: new CampaignCode('CMP01'),
|
||||
name: new CampaignName('Campaign name'),
|
||||
publication: true,
|
||||
printAllAtOnce: true,
|
||||
coupons: new CampaignCouponArray([
|
||||
new CampaignCoupon($coupon->code),
|
||||
]),
|
||||
brandCode: $brand->code,
|
||||
actorId: ActorId::generate(),
|
||||
);
|
||||
|
||||
it('finds a coupon by its code', function () use ($brand, $coupon, $campaign): void {
|
||||
// GIVEN
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('findByCode')->once()->with($coupon->brandCode)->andReturn($brand);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCouponCode')->once()->with($coupon->code)->andReturn([$campaign]);
|
||||
|
||||
$useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(code: $coupon->code));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CouponFound::class)
|
||||
->and($result->code)->toBe($coupon->code)
|
||||
->and($result->name)->toBe($coupon->name)
|
||||
->and($result->amount)->toBe($coupon->amount)
|
||||
->and($result->picture)->toBe($coupon->picture)
|
||||
->and($result->diffusion)->toBe($coupon->diffusion)
|
||||
->and($result->availabilityCalendar)->toBe($coupon->availabilityCalendar)
|
||||
->and($result->validityInformations)->toBe($coupon->validityInformations)
|
||||
->and($result->legalInformations)->toBe($coupon->legalInformations)
|
||||
->and($result->processingCode)->toBe($coupon->processingCode)
|
||||
->and($result->brand)->toBe($brand)
|
||||
->and($result->createdBy)->toBe($coupon->createdBy)
|
||||
->and($result->products)->toBe($coupon->products)
|
||||
->and($result->movements)->toBe($coupon->movements)
|
||||
->and($result->quantity)->toBe($coupon->quantity)
|
||||
->and($result->campaigns)->toBe([$campaign])
|
||||
->and($result->createdAt)->toBeInstanceOf(\DateTimeInterface::class)
|
||||
->and($result->updatedAt)->toBeNull();
|
||||
});
|
||||
|
||||
it('returns CouponNotFound when the code is not found', function (): void {
|
||||
// GIVEN
|
||||
$code = new CouponCode('COU02');
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(null);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldNotReceive('findByCode');
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldNotReceive('findByCouponCode');
|
||||
|
||||
$useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(code: $code));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CouponNotFound::class);
|
||||
});
|
||||
|
||||
it('throws when the brand of the coupon is not found', function () use ($coupon): void {
|
||||
// GIVEN
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('findByCode')->once()->with($coupon->brandCode)->andReturn(null);
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldNotReceive('findByCouponCode');
|
||||
|
||||
$useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository);
|
||||
|
||||
// WHEN / THEN
|
||||
expect(fn () => $useCase->execute(new Input(code: $coupon->code)))
|
||||
->toThrow(\DomainException::class);
|
||||
});
|
||||
|
||||
it('finds a coupon not attached to any campaign', function () use ($brand, $coupon): void {
|
||||
// GIVEN
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('findByCode')->once()->with($coupon->brandCode)->andReturn($brand);
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCouponCode')->once()->with($coupon->code)->andReturn([]);
|
||||
|
||||
$useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(code: $coupon->code));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CouponFound::class)
|
||||
->and($result->campaigns)->toBe([]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue