feat: ✨ Transform CampaignRequest into Campaign + Coupon - #38 - US0.3

This commit is contained in:
Noah Lancien 2026-10-29 18:10:17 +01:00
parent 6faeb77d02
commit 491e349959
38 changed files with 787 additions and 341 deletions

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Application\UseCases\CampaignRequest\Transform;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
use Shared\DDD\UseCases\Input as UseCaseInput;
final readonly class Input implements UseCaseInput
{
public function __construct(
public CampaignRequestId $id
) {
}
}

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Application\UseCases\CampaignRequest\Transform\Outputs;
use Shared\DDD\UseCases\Output;
final readonly class CampaignRequestAlreadyTranformed implements Output
{
}

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Application\UseCases\CampaignRequest\Transform\Outputs;
use Shared\DDD\UseCases\Output;
final readonly class CampaignRequestNotFound implements Output
{
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Application\UseCases\CampaignRequest\Transform\Outputs;
use Domain\Models\Entities\Campaign;
use Domain\Models\Entities\Coupon;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
use Shared\DDD\UseCases\Output;
final readonly class CampaignRequestTransformed implements Output
{
public function __construct(
public CampaignRequestId $id,
public CampaignRequestStatus $status,
public Campaign $campaign,
public Coupon $coupon,
) {
}
}

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Application\UseCases\CampaignRequest\Transform\Outputs;
use Shared\DDD\UseCases\Output;
final readonly class CouponAlreadyExists implements Output
{
}

View file

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Application\UseCases\CampaignRequest\Transform;
use Application\UseCases\CampaignRequest\Transform\Outputs\CampaignRequestAlreadyTranformed;
use Application\UseCases\CampaignRequest\Transform\Outputs\CampaignRequestNotFound;
use Application\UseCases\CampaignRequest\Transform\Outputs\CampaignRequestTransformed;
use Application\UseCases\CampaignRequest\Transform\Outputs\CouponAlreadyExists;
use Domain\Models\Entities\Campaign;
use Domain\Models\Entities\CampaignRequest;
use Domain\Models\Entities\Coupon;
use Domain\Models\ValueObjects\Actor\ActorId;
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
use Domain\Models\ValueObjects\Campaign\CampaignCouponArray;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
use Domain\Models\ValueObjects\Coupon\CouponMovement;
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
use Domain\Ports\Repositories\CampaignRepository;
use Domain\Ports\Repositories\CampaignRequestRepository;
use Domain\Ports\Repositories\CouponRepository;
use Shared\DDD\UseCases\Input as BaseInput;
use Shared\DDD\UseCases\Output;
use Shared\DDD\UseCases\Transactional;
use Shared\DDD\UseCases\UseCase;
use Webmozart\Assert\Assert;
/** @implements UseCase<\Application\UseCases\CampaignRequest\Transform\Input, \Shared\DDD\UseCases\Output> */
final readonly class TransformCampaignRequest implements UseCase, Transactional
{
public function __construct(
private CampaignRequestRepository $repository,
private CampaignRepository $campaignRepository,
private CouponRepository $couponRepository,
) {
}
#[\Override]
public function execute(BaseInput $input): Output
{
Assert::isInstanceOf($input, Input::class);
$campaignRequest = $this->repository->findById($input->id);
if (! $campaignRequest instanceof CampaignRequest) {
return new CampaignRequestNotFound();
}
if ($campaignRequest->status !== CampaignRequestStatus::CREATED) {
return new CampaignRequestAlreadyTranformed();
}
// TODO: get the real ActorID
$actorId = ActorId::generate();
$createdCoupon = self::createCouponFromRequest($campaignRequest, $actorId);
$campaignCoupon = new CampaignCoupon($createdCoupon->code, $campaignRequest->requestedCoupon->printRules);
$campaignCreated = self::createCampaignFromRequest($campaignRequest, $campaignCoupon, $actorId);
$campaignRequest = $campaignRequest->updateStatusWhenTransformed();
$this->repository->save($campaignRequest);
return new CampaignRequestTransformed(
id: $campaignRequest->id(),
status: $campaignRequest->status,
campaign: $campaignCreated,
coupon: $createdCoupon,
);
}
private function createCouponFromRequest(CampaignRequest $campaignRequest, ActorId $actorId): Coupon | CouponAlreadyExists
{
$requestedCoupon = $campaignRequest->requestedCoupon;
foreach ($requestedCoupon->couponProcessingCodes->toArray() as $processingCode) {
if ($this->couponRepository->existsByProcessingCode($processingCode)) {
return new CouponAlreadyExists();
}
}
$coupon = Coupon::create(
name: $requestedCoupon->couponName,
amount: $requestedCoupon->moneyAmount,
picture: $requestedCoupon->couponPicture,
diffusion: new CouponDiffusion($requestedCoupon->diffusion),
validityInformations: $requestedCoupon->couponValidityInformations,
legalInformations: $requestedCoupon->couponLegalInformations,
processingCode: $requestedCoupon->couponProcessingCodes,
brandCode: $campaignRequest->brandCode,
createdBy: $actorId,
products: $requestedCoupon->couponProducts,
movements: new CouponMovementArray([
new CouponMovement($requestedCoupon->quantity->value)
]),
);
$this->couponRepository->save($coupon);
return $coupon;
}
private function createCampaignFromRequest(CampaignRequest $campaignRequest, CampaignCoupon $campaignCoupon, ActorId $actorId): Campaign
{
$isGame = $campaignRequest->campaignType === CampaignRequestType::GAME;
$campaign = Campaign::create(
name: $campaignRequest->name,
printAllAtOnce: $isGame,
coupons: new CampaignCouponArray([$campaignCoupon]),
brandCode: $campaignRequest->brandCode,
actorId: $actorId
);
$this->campaignRepository->save($campaign);
return $campaign;
}
}

View file

@ -34,12 +34,13 @@ final readonly class CreateCoupon implements UseCase, Transactional
return new BrandNotFound();
}
if ($this->couponRepository->existsByCode($input->code)) {
return new CouponCodeAlreadyExists();
foreach ($input->processingCode->toArray() as $processingCode) {
if ($this->couponRepository->existsByProcessingCode($processingCode)) {
return new CouponCodeAlreadyExists();
}
}
$coupon = Coupon::create(
code: $input->code,
name: $input->name,
amount: $input->amount,
picture: $input->picture,

View file

@ -7,7 +7,6 @@ namespace Application\UseCases\Coupon\Create;
use Domain\Models\ValueObjects\Actor\ActorId;
use Domain\Models\ValueObjects\Brand\BrandCode;
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;
@ -22,7 +21,6 @@ use Shared\DDD\UseCases\Input as UseCaseInput;
final readonly class Input implements UseCaseInput
{
public function __construct(
public CouponCode $code,
public CouponName $name,
public MoneyAmount $amount,
public CouponPicture $picture,

View file

@ -124,4 +124,19 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent
return $this->rejectionReason;
}
public function updateStatusWhenTransformed(): self
{
return new self(
id: $this->id,
brandCode: $this->brandCode,
applicant: $this->applicant,
campaignType: $this->campaignType,
name: $this->name,
requestedCoupon: $this->requestedCoupon,
status: CampaignRequestStatus::TRANSFORMED,
rejectionReason: null,
updatedAt: DatesHelper::now(),
);
}
}

View file

@ -75,7 +75,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier
}
public static function create(
CouponCode $code,
CouponName $name,
MoneyAmount $amount,
CouponPicture $picture,
@ -91,7 +90,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier
): Coupon {
return new self(
id: CouponId::generate(),
code: $code,
code: CouponCode::generate(),
name: $name,
amount: $amount,
picture: $picture,

View file

@ -4,23 +4,13 @@ declare(strict_types=1);
namespace Domain\Models\ValueObjects\Coupon;
final readonly class CouponCode
use Ramsey\Uuid\Uuid;
use Shared\DDD\Models\Uuidv7EntityId;
final readonly class CouponCode extends Uuidv7EntityId
{
private const string REGEX = '/^[A-Z]{3}\d{2}$/';
public function __construct(
public string $value,
) {
if (! preg_match(self::REGEX, $value)) {
throw new \DomainException(
"Invalid code '{$value}' does not match pattern " . self::REGEX,
);
}
if ($value[3] === $value[4]) {
throw new \DomainException(
"Invalid code '{$value}' has the same digit repeated",
);
}
public static function generate(): self
{
return new self(Uuid::uuid7());
}
}

View file

@ -8,6 +8,7 @@ use Domain\Models\Entities\Coupon;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\Coupon\CouponSortCriteria;
use Domain\Models\ValueObjects\ProcessingCode;
use Shared\DDD\Ports\Repository;
use Shared\ValueObjects\Pagination\Paginated;
use Shared\ValueObjects\Pagination\Pagination;
@ -21,6 +22,10 @@ interface CouponRepository extends Repository
public function findByCode(CouponCode $code): ?Coupon;
public function existsByProcessingCode(ProcessingCode $code): bool;
public function findByProcessingCode(ProcessingCode $code): bool;
/**
* @param array<CouponCode> $code
*
@ -37,5 +42,5 @@ interface CouponRepository extends Repository
?BrandCode $brandCode = null,
): Paginated;
public function save(Coupon $brand): void;
public function save(Coupon $coupon): void;
}

View file

@ -94,10 +94,10 @@ final class CampaignFactory
return new CampaignCouponArray(array_map(
static fn (string | CouponCode $code): CampaignCoupon => new CampaignCoupon(
$code instanceof CouponCode ? $code : new CouponCode($code),
$code instanceof CouponCode ? $code : CouponCode::generate(),
$printRules,
),
$couponCodes ?? ['COU01'],
$couponCodes ?? [CouponCode::generate()],
));
}
}

View file

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Tests\Fixtures\CampaignRequest;
use Domain\Models\Entities\CampaignRequest;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Campaign\CampaignName;
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponName;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\ProcessingCodeArray;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
use Shared\Enums\Country;
use Shared\Enums\Currency;
use Shared\ValueObjects\Email;
use Shared\ValueObjects\Money;
final class CampaignRequestFactory
{
/** Demande neuve : id généré, statut CREATED, updatedAt = null. */
public static function make(
string $brandCode = 'AAA01',
string $name = 'Campaign request',
?Applicant $applicant = null,
CampaignRequestType $campaignType = CampaignRequestType::CLUB,
?RequestedCoupon $requestedCoupon = null,
): CampaignRequest {
return CampaignRequest::create(
brandCode: new BrandCode($brandCode),
name: new CampaignName($name),
applicant: $applicant ?? self::applicant(),
campaignType: $campaignType,
requestedCoupon: $requestedCoupon ?? self::requestedCoupon(),
);
}
/** Demande reconstruite comme depuis la base (id et statut contrôlables). */
public static function hydrated(
?CampaignRequestId $id = null,
CampaignRequestStatus $status = CampaignRequestStatus::CREATED,
?RejectionReason $rejectionReason = null,
string $brandCode = 'AAA01',
string $name = 'Campaign request',
?Applicant $applicant = null,
CampaignRequestType $campaignType = CampaignRequestType::CLUB,
?RequestedCoupon $requestedCoupon = null,
): CampaignRequest {
if ($status === CampaignRequestStatus::REJECTED) {
$rejectionReason ??= self::rejectionReason();
}
return CampaignRequest::hydrate(
id: $id ?? CampaignRequestId::generate(),
brandCode: new BrandCode($brandCode),
name: new CampaignName($name),
applicant: $applicant ?? self::applicant(),
campaignType: $campaignType,
requestedCoupon: $requestedCoupon ?? self::requestedCoupon(),
status: $status,
rejectionReason: $rejectionReason,
);
}
/** Demande rejetée, obtenue par la vraie transition métier. */
public static function rejected(?RejectionReason $reason = null): CampaignRequest
{
return self::make()->reject($reason ?? self::rejectionReason());
}
/** Demande transformée en campagne, obtenue par la vraie transition métier. */
public static function transformed(): CampaignRequest
{
return self::make()->updateStatusWhenTransformed();
}
public static function applicant(
string $firstname = 'Jane',
string $lastname = 'Doe',
string $email = 'jane.doe@example.com',
): Applicant {
return new Applicant($firstname, $lastname, new Email($email)); // Email : À VÉRIFIER
}
public static function rejectionReason(string $reason = 'Not eligible'): RejectionReason
{
return new RejectionReason($reason);
}
public static function requestedCoupon(
string $couponName = 'Coupon name',
int $quantity = 1000,
?PrintRules $printRules = null,
): RequestedCoupon {
return new RequestedCoupon(
couponName: new CouponName($couponName),
quantity: new RequestedCouponQuantity($quantity), // À VÉRIFIER
diffusion: new Period(new \DateTime()->modify('+1 month'), new \DateTime()->modify('+2 months')),
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 100)),
couponPicture: new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobile_thumbnail.jpg'),
),
couponProcessingCodes: new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur.', Country::FRANCE),
]),
couponLegalInformations: new CouponLegalInformationArray([
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur.', Country::FRANCE),
]),
couponProducts: new CouponProductArray([new IncoCode(35784)]),
printRules: $printRules,
);
}
}

View file

@ -10,6 +10,7 @@ use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
use Domain\Models\ValueObjects\Coupon\CouponId;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponMovement;
@ -32,65 +33,74 @@ use Shared\ValueObjects\Money;
final class CouponFactory
{
public static function make(
string | CouponCode $code = 'COU01',
?int $amount = 100,
?\DateTime $diffusionStart = null,
?\DateTime $diffusionEnd = null,
?CouponAvailabilityCalendar $availabilityCalendar = null,
string $brandCode = 'AAA01',
?CouponCode $code = null,
): Coupon {
return Coupon::create(
code: $code instanceof CouponCode ? $code : new CouponCode($code),
name: new CouponName('Coupon name'),
amount: new MoneyAmount(new Money(Currency::EUR, $amount)),
picture: new CouponPicture(
$arguments = [
'name' => new CouponName('Coupon name'),
'amount' => new MoneyAmount(new Money(Currency::EUR, $amount)),
'picture' => new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobile_thumbnail.jpg'),
),
diffusion: new CouponDiffusion(
'diffusion' => new CouponDiffusion(
new Period(
start: $diffusionStart ?? new \DateTime()->modify('-1 month'),
end: $diffusionEnd ?? new \DateTime()->modify('+1 month'),
),
),
validityInformations: new CouponValidityInformationArray([
'validityInformations' => new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
legalInformations: new CouponLegalInformationArray([
'legalInformations' => new CouponLegalInformationArray([
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
processingCode: new ProcessingCodeArray([
'processingCode' => new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
new ProcessingCode('4006381333931', Country::BENELUX),
]),
brandCode: new BrandCode($brandCode),
createdBy: ActorId::generate(),
products: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]),
movements: new CouponMovementArray([new CouponMovement(1000)]),
availabilityCalendar: $availabilityCalendar,
);
'brandCode' => new BrandCode($brandCode),
'createdBy' => ActorId::generate(),
'products' => new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]),
'movements' => new CouponMovementArray([new CouponMovement(1000)]),
'availabilityCalendar' => $availabilityCalendar,
];
if ($code instanceof CouponCode) {
return Coupon::hydrate(...[
'id' => CouponId::generate(),
'code' => $code,
...$arguments,
]);
}
return Coupon::create(...$arguments);
}
/** Coupon dont la diffusion est terminée. */
public static function expired(string | CouponCode $code = 'COU01'): Coupon
public static function expired(?CouponCode $code = null): Coupon
{
return self::make(
code: $code,
diffusionStart: new \DateTime()->modify('-2 months'),
diffusionEnd: new \DateTime()->modify('-1 day'),
code: $code,
);
}
/** Coupon dont la diffusion n'a pas commencé. */
public static function upcoming(string | CouponCode $code = 'COU01'): Coupon
public static function upcoming(?CouponCode $code = null): Coupon
{
return self::make(
code: $code,
diffusionStart: new \DateTime()->modify('+1 day'),
diffusionEnd: new \DateTime()->modify('+2 months'),
code: $code,
);
}
}

View file

@ -27,8 +27,8 @@ describe('create campaign use case', function (): void {
'brandCode' => new BrandCode('AAA01'),
'printAllAtOnce' => false,
'coupons' => new CampaignCouponArray([
new CampaignCoupon(new CouponCode('COU01'), new PrintRules(maxPerCustomer: 1)),
new CampaignCoupon(new CouponCode('COU02'), new PrintRules(cooldownDays: 30)),
new CampaignCoupon(CouponCode::generate(), new PrintRules(maxPerCustomer: 1)),
new CampaignCoupon(CouponCode::generate(), new PrintRules(cooldownDays: 30)),
]),
'createdBy' => ActorId::generate(),
], $overrides));

View file

@ -10,6 +10,7 @@ use Application\UseCases\Campaign\Find\Outputs\CampaignFound;
use Application\UseCases\Campaign\Outputs\CampaignNotFound;
use Domain\Models\Entities\Campaign;
use Domain\Models\ValueObjects\Campaign\CampaignCode;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Ports\Repositories\CampaignRepository;
use Domain\Models\ValueObjects\PrintRules;
use Tests\Fixtures\Campaign\CampaignFactory;
@ -17,15 +18,16 @@ use Tests\Fixtures\Campaign\CampaignFactory;
covers(FindCampaign::class, Input::class, CampaignFound::class, CampaignNotFound::class);
describe('find campaign use case', function (): void {
$makeCampaign = static fn (array $couponCodes = ['COU01']): Campaign => CampaignFactory::hydrated(
$couponCode = CouponCode::generate();
$makeCampaign = static fn (?array $couponCodes = null): Campaign => CampaignFactory::hydrated(
published: true,
name: 'Campaign A',
printAllAtOnce: false,
couponCodes: $couponCodes,
couponCodes: $couponCodes ?? [$couponCode],
printRules: new PrintRules(maxPerCustomer: 1),
);
it('finds a campaign by its code with paginated coupons', function () use ($makeCampaign): void {
it('finds a campaign by its code with coupons array', function () use ($makeCampaign, $couponCode): void {
// GIVEN
$campaign = $makeCampaign();
$repository = \Mockery::mock(CampaignRepository::class);
@ -44,14 +46,14 @@ describe('find campaign use case', function (): void {
->and($result->publication)->toBe($campaign->publication)
->and($result->printAllAtOnce)->toBe($campaign->printAllAtOnce)
->and($result->coupons)->toBe($campaign->coupons)
->and($result->coupons->toArray()[0]->couponCode->value)->toBe('COU01')
->and($result->coupons->toArray()[0]->couponCode->value)->toBe($couponCode->value)
->and($result->createdAt)->toEqual($campaign->createdAt())
->and($result->updatedAt)->toEqual($campaign->updatedAt());
});
it('returns CampaignNotFound when the code is not found', function (): void {
// GIVEN
$code = CampaignCode::generate();
$code = CampaignCode::generate();
$repository = \Mockery::mock(CampaignRepository::class);
$repository->shouldReceive('findByCode')->once()->with($code)->andReturn(null);

View file

@ -11,7 +11,6 @@ use Application\UseCases\Campaign\Update\Outputs\CampaignUpdated;
use Application\UseCases\Campaign\Update\Outputs\CouponAlreadyInCampaign;
use Application\UseCases\Campaign\Update\Outputs\CouponNotActive;
use Application\UseCases\Campaign\Update\Outputs\CouponNotFound;
use DateTime;
use Domain\Models\Entities\Campaign;
use Domain\Models\ValueObjects\Actor\ActorId;
use Domain\Models\ValueObjects\Brand\BrandCode;
@ -24,6 +23,7 @@ use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Ports\Repositories\CampaignRepository;
use Domain\Ports\Repositories\CouponRepository;
use Shared\Helpers\DatesHelper;
use Tests\Fixtures\Campaign\CampaignFactory;
use Tests\Fixtures\Coupon\CouponFactory;
@ -38,9 +38,9 @@ covers(
);
describe('add coupons to campaign use case', function (): void {
$coupon1 = CouponFactory::make(code: 'COU01');
$coupon2 = CouponFactory::make(code: 'COU02');
$coupon3 = CouponFactory::make(code: 'COU03');
$coupon1 = CouponFactory::make();
$coupon2 = CouponFactory::make();
$coupon3 = CouponFactory::make();
$campaignCoupon1 = new CampaignCoupon($coupon1->code, new PrintRules(maxPerCustomer: 1));
$campaignCoupon2 = new CampaignCoupon($coupon2->code, new PrintRules(maxPerCustomer: 1));
$existingCampaign = static fn (CampaignCode $code, bool $basePublicationStatus): Campaign => Campaign::hydrate(
@ -114,11 +114,11 @@ describe('add coupons to campaign use case', function (): void {
it('returns a specific error when the coupon is not printable', function (): void {
// WHEN
$campaign = CampaignFactory::hydrated(couponCodes: ['COU01']);
$coupon = CouponFactory::make(
code: 'COU02',
diffusionStart: new DateTime('2026-01-01'),
diffusionEnd: new DateTime('2026-01-31')
$baseCoupon = CouponFactory::make();
$campaign = CampaignFactory::hydrated(couponCodes: [$baseCoupon->code]);
$couponToAdd = CouponFactory::make(
diffusionStart: DatesHelper::now()->subYear()->subMonth()->toDateTime(),
diffusionEnd: DatesHelper::now()->subYear()->toDateTime()
);
$repository = \Mockery::mock(CampaignRepository::class);
@ -126,9 +126,9 @@ describe('add coupons to campaign use case', function (): void {
$repository->shouldNotReceive('save');
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('findByCode')->once()->andReturn($coupon);
$couponRepository->shouldReceive('findByCode')->once()->andReturn($couponToAdd);
$input = new AddCouponToCampaignInput(code: $campaign->code, couponCode: $coupon->code, printRules: null);
$input = new AddCouponToCampaignInput(code: $campaign->code, couponCode: $couponToAdd->code, printRules: null);
$useCase = new AddCouponToCampaign($repository, $couponRepository);
@ -160,16 +160,17 @@ describe('add coupons to campaign use case', function (): void {
it('returns a specific error when the coupon is not found', function (): void {
// GIVEN
$campaign = CampaignFactory::hydrated();
$campaign = CampaignFactory::hydrated();
$couponCode = CouponCode::generate();
$repository = \Mockery::mock(CampaignRepository::class);
$repository->shouldReceive('findByCode')->once()->andReturn($campaign);
$repository->shouldNotReceive('save');
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('findByCode')->once()->andReturn(null);
$couponRepository->shouldReceive('findByCode')->once()->with($couponCode)->andReturn(null);
$input = new AddCouponToCampaignInput(code: $campaign->code, couponCode: new CouponCode('ZZZ01'), printRules: null);
$input = new AddCouponToCampaignInput(code: $campaign->code, couponCode: $couponCode, printRules: null);
$useCase = new AddCouponToCampaign($repository, $couponRepository);

View file

@ -11,6 +11,7 @@ use Application\UseCases\Campaign\Update\Outputs\CampaignStatusUpdated;
use Application\UseCases\Campaign\Update\Outputs\CampaignUnpublicationSkipped;
use Domain\Models\Entities\Campaign;
use Domain\Models\ValueObjects\Campaign\CampaignCode;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Ports\Repositories\CampaignRepository;
use Domain\Ports\Repositories\CouponRepository;
use Domain\Services\Campaign\CampaignAvailabilityChecker;
@ -68,14 +69,15 @@ describe('auto unpublish campaign use case', function (): void {
it('skips a published campaign that still has an active coupon', function (): void {
// GIVEN
$campaign = CampaignFactory::hydrated(published: true, couponCodes: ['COU01']);
$couponCode = CouponCode::generate();
$campaign = CampaignFactory::hydrated(published: true, couponCodes: [$couponCode]);
$repository = \Mockery::mock(CampaignRepository::class);
$repository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
$repository->shouldNotReceive('save');
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('findByCodes')->once()->andReturn([CouponFactory::make('COU01')]);
$couponRepository->shouldReceive('findByCodes')->once()->andReturn([CouponFactory::make()]);
$useCase = new AutoUnpublishCampaign($repository, new CampaignAvailabilityChecker($couponRepository));
@ -88,7 +90,7 @@ describe('auto unpublish campaign use case', function (): void {
it('skips a published campaign with one active and one expired coupon', function (): void {
// GIVEN
$campaign = CampaignFactory::hydrated(published: true, couponCodes: ['COU01', 'COU02']);
$campaign = CampaignFactory::hydrated(published: true, couponCodes: [CouponCode::generate(), CouponCode::generate()]);
$repository = \Mockery::mock(CampaignRepository::class);
$repository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
@ -96,8 +98,8 @@ describe('auto unpublish campaign use case', function (): void {
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('findByCodes')->once()->andReturn([
CouponFactory::expired('COU01'),
CouponFactory::make('COU02'),
CouponFactory::expired(),
CouponFactory::make(),
]);
$useCase = new AutoUnpublishCampaign($repository, new CampaignAvailabilityChecker($couponRepository));
@ -111,7 +113,9 @@ describe('auto unpublish campaign use case', function (): void {
it('unpublishes a published campaign when every coupon is not available', function (): void {
// GIVEN
$campaign = CampaignFactory::hydrated(published: true, couponCodes: ['COU01', 'COU02']);
$couponCode1 = CouponCode::generate();
$couponCode2 = CouponCode::generate();
$campaign = CampaignFactory::hydrated(published: true, couponCodes: [$couponCode1->value, $couponCode2->value]);
$repository = \Mockery::mock(CampaignRepository::class);
$repository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
@ -123,8 +127,8 @@ describe('auto unpublish campaign use case', function (): void {
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('findByCodes')->once()->andReturn([
CouponFactory::expired('COU01'),
CouponFactory::upcoming('COU02'),
CouponFactory::expired(),
CouponFactory::upcoming(),
]);
$useCase = new AutoUnpublishCampaign($repository, new CampaignAvailabilityChecker($couponRepository));
@ -143,7 +147,8 @@ describe('auto unpublish campaign use case', function (): void {
it('unpublishes a published campaign when no coupon is found', function (): void {
// GIVEN
$campaign = CampaignFactory::hydrated(published: true, couponCodes: ['COU01']);
$couponCode = CouponCode::generate();
$campaign = CampaignFactory::hydrated(published: true, couponCodes: [$couponCode]);
$repository = \Mockery::mock(CampaignRepository::class);
$repository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);

View file

@ -27,8 +27,8 @@ use Tests\Fixtures\Coupon\CouponFactory;
covers(PublishCampaign::class, PublishCampaignInput::class, CampaignStatusUpdated::class, CampaignNotFound::class);
describe('update campaign publication use case', function (): void {
$campaignCoupon1 = new CampaignCoupon(new CouponCode('COU01'), new PrintRules(maxPerCustomer: 1));
$campaignCoupon2 = new CampaignCoupon(new CouponCode('COU02'), new PrintRules(maxPerCustomer: 1));
$campaignCoupon1 = new CampaignCoupon(CouponCode::generate(), new PrintRules(maxPerCustomer: 1));
$campaignCoupon2 = new CampaignCoupon(CouponCode::generate(), new PrintRules(maxPerCustomer: 1));
$existingCampaign = static fn (CampaignCode $code, bool $basePublicationStatus): Campaign => Campaign::hydrate(
CampaignId::generate(),
$code,
@ -77,9 +77,9 @@ describe('update campaign publication use case', function (): void {
$publication = true;
$input = new PublishCampaignInput(code: $campaign->code, publication: $publication);
$expectedCoupons = [
CouponFactory::make('COU01'),
CouponFactory::make('COU02'),
CouponFactory::expired('COU03'),
CouponFactory::make(),
CouponFactory::make(),
CouponFactory::expired(),
];
$repository = \Mockery::mock(CampaignRepository::class);
@ -130,8 +130,8 @@ describe('update campaign publication use case', function (): void {
$publication = true;
$input = new PublishCampaignInput(code: $campaign->code, publication: $publication);
$expectedCoupons = [
CouponFactory::expired('COU01'),
CouponFactory::upcoming('COU02'),
CouponFactory::expired(),
CouponFactory::upcoming(),
];
$repository = \Mockery::mock(CampaignRepository::class);

View file

@ -37,9 +37,9 @@ covers(
);
describe('remove coupons to campaign use case', function (): void {
$coupon1 = CouponFactory::make(code: 'COU01');
$coupon2 = CouponFactory::make(code: 'COU02');
$coupon3 = CouponFactory::make(code: 'COU03');
$coupon1 = CouponFactory::make();
$coupon2 = CouponFactory::make();
$coupon3 = CouponFactory::make();
$campaignCoupon1 = new CampaignCoupon($coupon1->code, new PrintRules(maxPerCustomer: 1));
$campaignCoupon2 = new CampaignCoupon($coupon2->code, new PrintRules(maxPerCustomer: 1));
$campaignCoupon3 = new CampaignCoupon($coupon3->code, new PrintRules(maxPerCustomer: 1));
@ -92,7 +92,7 @@ describe('remove coupons to campaign use case', function (): void {
it('returns a specific output when the coupon is not in the campaign', function () use ($existingCampaign): void {
// GIVEN
$campaign = $existingCampaign(CampaignCode::generate(), true);
$coupon = CouponFactory::make(code: 'COU04');
$coupon = CouponFactory::make();
$repository = \Mockery::mock(CampaignRepository::class);
$repository->shouldReceive('findByCode')->once()->andReturn($campaign);
@ -164,7 +164,7 @@ describe('remove coupons to campaign use case', function (): void {
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('findByCode')->once()->andReturn(null);
$input = new RemoveCouponToCampaignInput(code: $campaign->code, couponCode: new CouponCode('ZZZ01'));
$input = new RemoveCouponToCampaignInput(code: $campaign->code, couponCode: CouponCode::generate());
$useCase = new RemoveCouponToCampaign($repository, $couponRepository);
// WHEN

View file

@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Application\UseCases\CampaignRequest\Transform;
use Application\UseCases\CampaignRequest\Transform\TransformCampaignRequest;
use Application\UseCases\CampaignRequest\Transform\Input;
use Application\UseCases\CampaignRequest\Transform\Outputs\CampaignRequestTransformed;
use Application\UseCases\CampaignRequest\Transform\Outputs\CampaignRequestAlreadyTranformed;
use Application\UseCases\CampaignRequest\Transform\Outputs\CampaignRequestNotFound;
use DateTime;
use Domain\Models\Entities\Campaign;
use Domain\Models\Entities\CampaignRequest;
use Domain\Models\Entities\Coupon;
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponName;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\ProcessingCodeArray;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
use Domain\Ports\Repositories\CampaignRepository;
use Domain\Ports\Repositories\CampaignRequestRepository;
use Domain\Ports\Repositories\CouponRepository;
use Shared\Enums\Country;
use Shared\Enums\Currency;
use Shared\ValueObjects\Email;
use Shared\ValueObjects\Money;
use Tests\Fixtures\CampaignRequest\CampaignRequestFactory;
covers(TransformCampaignRequest::class, Input::class, CampaignRequestTransformed::class);
describe('transform campaign request use case', function (): void {
$brandCode = 'AAA01';
$campaignName = 'Name';
$requestedCoupon = new RequestedCoupon(
couponName: new CouponName('Coupon A'),
quantity: new RequestedCouponQuantity(100),
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
couponPicture: new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobileThumbnail.jpg')
),
couponProcessingCodes: new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
new ProcessingCode('4006381333931', Country::BENELUX),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('FR - validity information.', Country::FRANCE),
new CouponValidityInformation('BE - validity information.', Country::BENELUX)
]),
couponLegalInformations: new CouponLegalInformationArray([
new CouponLegalInformation('FR - legal information.', Country::FRANCE),
new CouponLegalInformation('BE - legal information.', Country::BENELUX)
]),
couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]),
printRules: new PrintRules(maxPerCustomer: 3)
);
$existingCampaignRequest = fn (array $overrides = []): CampaignRequest => CampaignRequestFactory::hydrated(
status: $overrides['status'] ?? CampaignRequestStatus::CREATED,
brandCode: $brandCode,
name: $campaignName,
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
campaignType: $overrides['campaignType'] ?? CampaignRequestType::CLUB,
requestedCoupon: $overrides['coupon'] ?? $requestedCoupon,
);
it('transforms a campaign request to a campaign and a coupon', function () use ($existingCampaignRequest, $campaignName): void {
// GIVEN
$campaignRequest = $existingCampaignRequest();
$campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class);
$campaignRequestRepository->shouldReceive('findById')->once()->with($campaignRequest->id)->andReturn($campaignRequest);
$campaignRequestRepository->shouldReceive('save')->once()->with(\Mockery::type(CampaignRequest::class));
$campaignRepository = \Mockery::mock(CampaignRepository::class);
$campaignRepository->shouldReceive('save')->once()->with(\Mockery::type(Campaign::class));
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('save')->once()->with(\Mockery::type(Coupon::class));
$input = new Input($campaignRequest->id);
$useCase = new TransformCampaignRequest($campaignRequestRepository, $campaignRepository, $couponRepository);
// WHEN
$result = $useCase->execute($input);
// THEN
expect($result)->toBeInstanceOf(CampaignRequestTransformed::class)
->and($result->campaign->name->value)->toBe($campaignName)
->and($result->status)->toBe(CampaignRequestStatus::TRANSFORMED);
});
it('can not tranform a campaign request which has already been transformed', function () use ($existingCampaignRequest): void {
// GIVEN
$campaignRequest = $existingCampaignRequest(['status' => CampaignRequestStatus::TRANSFORMED]);
$campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class);
$campaignRequestRepository->shouldReceive('findById')->once()->with($campaignRequest->id)->andReturn($campaignRequest);
$campaignRequestRepository->shouldNotReceive('save');
$campaignRepository = \Mockery::mock(CampaignRepository::class);
$campaignRepository->shouldNotReceive('save');
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldNotReceive('save');
$input = new Input($campaignRequest->id);
$useCase = new TransformCampaignRequest($campaignRequestRepository, $campaignRepository, $couponRepository);
// WHEN
$result = $useCase->execute($input);
// THEN
expect($result)->toBeInstanceOf(CampaignRequestAlreadyTranformed::class);
});
it('returns CampaignRequestNotFound when the code is not found', function () use ($existingCampaignRequest): void {
// GIVEN
$campaignRequest = $existingCampaignRequest(['status' => CampaignRequestStatus::CREATED]);
$campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class);
$campaignRequestRepository->shouldReceive('findById')->once()->with($campaignRequest->id)->andReturn(null);
$campaignRequestRepository->shouldNotReceive('save');
$campaignRepository = \Mockery::mock(CampaignRepository::class);
$campaignRepository->shouldNotReceive('save');
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldNotReceive('save');
$input = new Input($campaignRequest->id);
$useCase = new TransformCampaignRequest($campaignRequestRepository, $campaignRepository, $couponRepository);
// WHEN
$result = $useCase->execute($input);
// THEN
expect($result)->toBeInstanceOf(CampaignRequestNotFound::class);
});
});

View file

@ -42,7 +42,6 @@ covers(CreateCoupon::class, Input::class, CouponCreated::class, CouponCodeAlread
describe('create coupon use case', function (): void {
$makeInput = fn (array $overrides = []): Input => new Input(...array_merge([
'code' => new CouponCode('COU01'),
'name' => new CouponName('Coupon name'),
'amount' => new MoneyAmount(new Money(Currency::EUR, 100)),
'picture' => new CouponPicture(
@ -79,7 +78,7 @@ describe('create coupon use case', function (): void {
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true);
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('existsByCode')->once()->with($input->code)->andReturn(false);
$couponRepository->shouldReceive('existsByProcessingCode')->once()->with($input->processingCode->toArray()[0])->andReturn(false);
$couponRepository->shouldReceive('save')->once()->with(\Mockery::type(Coupon::class));
$useCase = new CreateCoupon($couponRepository, $brandRepository);
@ -90,7 +89,7 @@ describe('create coupon use case', function (): void {
// THEN
expect($result)->toBeInstanceOf(CouponCreated::class)
->and($result->id)->toBeInstanceOf(CouponId::class)
->and($result->code)->toEqual($input->code)
->and($result->code)->toBeInstanceOf(CouponCode::class)
->and($result->name)->toEqual($input->name)
->and($result->amount)->toEqual($input->amount)
->and($result->picture)->toEqual($input->picture)
@ -113,7 +112,7 @@ describe('create coupon use case', function (): void {
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(false);
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldNotReceive('existsByCode');
$couponRepository->shouldNotReceive('existsByProcessingCode');
$couponRepository->shouldNotReceive('save');
$useCase = new CreateCoupon($couponRepository, $brandRepository);
@ -133,7 +132,7 @@ describe('create coupon use case', function (): void {
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true);
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('existsByCode')->once()->with($input->code)->andReturn(true);
$couponRepository->shouldReceive('existsByProcessingCode')->once()->with($input->processingCode->toArray()[0])->andReturn(true);
$couponRepository->shouldNotReceive('save');
$useCase = new CreateCoupon($couponRepository, $brandRepository);
@ -157,7 +156,7 @@ describe('create coupon use case', function (): void {
$brandRepository->shouldReceive('existsByCode')->once()->andReturn(true);
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('existsByCode')->once()->andReturn(false);
$couponRepository->shouldReceive('existsByProcessingCode')->once()->andReturn(false);
$couponRepository->shouldReceive('save')->once()->with(\Mockery::on(
fn (Coupon $saved): bool => $saved->availabilityCalendar === $calendar,
));
@ -195,7 +194,7 @@ describe('create coupon use case', function (): void {
$brandRepository->shouldReceive('existsByCode')->once()->andReturn(true);
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('existsByCode')->once()->andReturn(false);
$couponRepository->shouldReceive('existsByProcessingCode')->andReturn(false);
$couponRepository->shouldReceive('save')->once()->with(\Mockery::type(Coupon::class));
$useCase = new CreateCoupon($couponRepository, $brandRepository);
@ -244,7 +243,7 @@ describe('create coupon use case', function (): void {
$brandRepository->shouldReceive('existsByCode')->andReturn(true);
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('existsByCode')->andReturn(false);
$couponRepository->shouldReceive('existsByProcessingCode')->andReturn(false);
$couponRepository->shouldNotReceive('save');
$useCase = new CreateCoupon($couponRepository, $brandRepository);
@ -273,7 +272,7 @@ describe('create coupon use case', function (): void {
$brandRepository->shouldReceive('existsByCode')->andReturn(true);
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('existsByCode')->andReturn(false);
$couponRepository->shouldReceive('existsByProcessingCode')->andReturn(false);
$couponRepository->shouldNotReceive('save');
$useCase = new CreateCoupon($couponRepository, $brandRepository);
@ -285,6 +284,43 @@ describe('create coupon use case', function (): void {
expect($action)->toThrow(\DomainException::class, 'A coupon requires validity information for BENELUX.');
});
it('rejects a coupon with an already used processingCode (with 2 locales)', function () use ($makeInput): void {
// GIVEN
$processingCode = new ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
new ProcessingCode('5425003944147', Country::BENELUX),
]);
$validityInformations = new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
]);
$legalInformations = new CouponLegalInformationArray([
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
]);
$input = $makeInput([
'processingCode' => $processingCode,
'validityInformations' => $validityInformations,
'legalInformations' => $legalInformations,
]);
$brandRepository = \Mockery::mock(BrandRepository::class);
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true);
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('existsByProcessingCode')->with($input->processingCode->toArray()[0])->andReturn(false);
$couponRepository->shouldReceive('existsByProcessingCode')->with($input->processingCode->toArray()[1])->andReturn(true);
$couponRepository->shouldNotReceive('save');
$useCase = new CreateCoupon($couponRepository, $brandRepository);
// WHEN
$result = $useCase->execute($input);
// THEN
expect($result)->toBeInstanceOf(CouponCodeAlreadyExists::class);
});
it('rejects two processing codes for the same country', function () use ($makeInput): void {
// GIVEN / WHEN
$action = fn (): Input => $makeInput([

View file

@ -56,7 +56,6 @@ describe('find coupon use case', function (): void {
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(
@ -144,7 +143,7 @@ describe('find coupon use case', function (): void {
it('returns CouponNotFound when the code is not found', function (): void {
// GIVEN
$code = new CouponCode('COU02');
$code = CouponCode::generate();
$couponRepository = \Mockery::mock(CouponRepository::class);
$couponRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(null);

View file

@ -46,7 +46,7 @@ covers(ListCoupons::class, Input::class, Output::class, CouponListItem::class);
describe('list coupons use case', function (): void {
$coupon1 = Coupon::hydrate(
id: CouponId::generate(),
code: new CouponCode('AAA01'),
code: CouponCode::generate(),
name: new CouponName('Coupon 1'),
amount: new MoneyAmount(new Money(Currency::EUR, 300)),
picture: new CouponPicture(
@ -77,7 +77,7 @@ describe('list coupons use case', function (): void {
);
$coupon2 = Coupon::hydrate(
id: CouponId::generate(),
code: new CouponCode('BBB02'),
code: CouponCode::generate(),
name: new CouponName('Coupon 2'),
amount: new MoneyAmount(new Money(Currency::EUR, 300)),
picture: new CouponPicture(

View file

@ -40,7 +40,7 @@ covers(UpdateCoupon::class, Input::class, CouponUpdated::class, CouponNotFound::
describe('update coupon use case', function (): void {
$makeCoupon = static fn (): Coupon => Coupon::hydrate(
id: CouponId::generate(),
code: new CouponCode('COU01'),
code: CouponCode::generate(),
name: new CouponName('Coupon name'),
amount: new MoneyAmount(new Money(Currency::EUR, 100)),
picture: new CouponPicture(
@ -115,7 +115,7 @@ describe('update coupon use case', function (): void {
it('returns CouponNotFound when the coupon does not exist', function (): void {
// GIVEN
$code = new CouponCode('COU01');
$code = CouponCode::generate();
$input = new Input(
code: $code,
picture: new CouponPicture(
@ -180,7 +180,7 @@ describe('update coupon use case', function (): void {
it('rejects an input with nothing to update', function (): void {
// GIVEN
$code = new CouponCode('COU01');
$code = CouponCode::generate();
// WHEN
$action = fn (): Input => new Input(code: $code);

View file

@ -24,37 +24,21 @@ use Domain\Models\ValueObjects\Campaign\CampaignId;
use Domain\Models\ValueObjects\Campaign\CampaignName;
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
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\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\CrmHashcode;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintableCoupon\MarketingDirectCode;
use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponCode;
use Domain\Models\ValueObjects\PrintableCoupon\PrintedCoupon;
use Domain\Models\ValueObjects\PrintableCoupon\PrintedCouponId;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\ProcessingCodeArray;
use Domain\Ports\Repositories\CampaignRepository;
use Domain\Ports\Repositories\CouponRepository;
use Domain\Ports\Repositories\PrintableCouponRepository;
use Domain\Services\PrintableCoupon\PrintEligibilityChecker;
use Ramsey\Uuid\Uuid;
use Shared\Enums\Country;
use Shared\Enums\Currency;
use Shared\Helpers\DatesHelper;
use Shared\ValueObjects\Money;
use Tests\Fixtures\Coupon\CouponFactory;
covers(CreatePrintableCoupon::class, Input::class, PrintableCouponCreated::class);
@ -62,60 +46,24 @@ describe('create coupon use case', function (): void {
$crmCustomerHashcode = new CrmHashcode(Uuid::uuid4());
$brandCode = new BrandCode('AAA01');
$campaignCode = CampaignCode::generate();
$couponCode = new CouponCode('COU01');
$couponName = new CouponName('Coupon name');
$couponDiffusionStart = new DateTime()->modify('-1 month');
$couponDiffusionEnd = new DateTime()->modify('+1 month');
$makeCoupon = fn (array $overrides = []): Coupon => Coupon::create(
code: $couponCode,
name: $couponName,
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(
$overrides['diffusionStart'] ?? $couponDiffusionStart,
$overrides['diffusionEnd'] ?? $couponDiffusionEnd,
),
),
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 ProcessingCodeArray([
new ProcessingCode('5425003944130', Country::FRANCE),
]),
brandCode: $brandCode,
createdBy: ActorId::generate(),
products: new CouponProductArray([
new IncoCode(35784),
new IncoCode(15522),
]),
movements: new CouponMovementArray([
new CouponMovement(1000)
]),
availabilityCalendar: $overrides['availabilityCalendar'] ?? null,
);
$couponCode = CouponCode::generate();
$printRules = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null): ?PrintRules => ($maxPerCustomer || $cooldownDays) ? new PrintRules(
maxPerCustomer: $maxPerCustomer,
cooldownDays: $cooldownDays,
) : null;
$campaignCoupon = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null): CampaignCoupon => new CampaignCoupon(
$makeCoupon = fn (array $overrides = []): Coupon => CouponFactory::make(
diffusionStart: $overrides['diffusionStart'] ?? null,
diffusionEnd: $overrides['diffusionEnd'] ?? null,
availabilityCalendar: $overrides['availabilityCalendar'] ?? null,
brandCode: $brandCode->value,
code: $couponCode,
);
$campaignCoupon = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null): CampaignCoupon => new CampaignCoupon(
$couponCode,
$printRules($maxPerCustomer, $cooldownDays),
);
$makeCampaign = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null, $published = true): Campaign => Campaign::hydrate(
$makeCampaign = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null, $published = true): Campaign => Campaign::hydrate(
id: CampaignId::generate(),
code: $campaignCode,
name: new CampaignName('Campaign name'),
@ -127,7 +75,7 @@ describe('create coupon use case', function (): void {
brandCode: $brandCode,
actorId: ActorId::generate(),
);
$printedCoupon = new PrintedCoupon(
$printedCoupon = new PrintedCoupon(
id: PrintedCouponId::generate(),
marketingCode: new MarketingDirectCode('0000000000001'),
printableCode: PrintableCouponCode::generate(),
@ -136,7 +84,7 @@ describe('create coupon use case', function (): void {
customer: $crmCustomerHashcode,
printedAt: DatesHelper::now(),
);
$makeInput = fn (Campaign $campaign): Input => new Input(
$makeInput = fn (Campaign $campaign): Input => new Input(
campaignCode: $campaign->code,
couponCode: $couponCode,
crmCustomerHashcode: $crmCustomerHashcode,
@ -159,9 +107,7 @@ describe('create coupon use case', function (): void {
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
$printableCouponRepository->shouldReceive('save')->once()->with(\Mockery::type(PrintableCoupon::class));
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,
@ -193,9 +139,7 @@ describe('create coupon use case', function (): void {
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,
@ -223,9 +167,7 @@ describe('create coupon use case', function (): void {
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,
@ -256,9 +198,7 @@ describe('create coupon use case', function (): void {
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,
@ -291,9 +231,7 @@ describe('create coupon use case', function (): void {
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,
@ -326,9 +264,7 @@ describe('create coupon use case', function (): void {
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,
@ -360,9 +296,7 @@ describe('create coupon use case', function (): void {
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,
@ -410,9 +344,7 @@ describe('create coupon use case', function (): void {
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
$printableCouponRepository->shouldReceive('save')->once()->with(\Mockery::type(PrintableCoupon::class));
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,
@ -458,9 +390,7 @@ describe('create coupon use case', function (): void {
$crmCustomerHashcode,
)->andReturn(5);
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,
@ -509,9 +439,7 @@ describe('create coupon use case', function (): void {
$crmCustomerHashcode,
)->andReturn($printedCoupon);
$printEligibilityChecker = new PrintEligibilityChecker(
$printableCouponRepository,
);
$printEligibilityChecker = new PrintEligibilityChecker($printableCouponRepository);
$useCase = new CreatePrintableCoupon(
$printableCouponRepository,

View file

@ -74,7 +74,6 @@ describe('list printable coupons use case', function (): void {
);
$createCoupon = fn (CouponCode $code, CouponPicture $picture): Coupon => Coupon::create(
code: $code,
name: new CouponName('Coupon name'),
amount: new MoneyAmount(new Money(Currency::EUR, 100)),
picture: $picture,
@ -119,7 +118,7 @@ describe('list printable coupons use case', function (): void {
// GIVEN
$input = $createInput();
$picture = $createPicture('picture');
$couponCode = new CouponCode('AAA01');
$couponCode = CouponCode::generate();
$printable = $createPrintableCoupon($couponCode);
$coupon = $createCoupon($couponCode, $picture);
@ -167,8 +166,8 @@ describe('list printable coupons use case', function (): void {
);
$input = $createInput($sortCriteria);
$code1 = new CouponCode('AAA01');
$code2 = new CouponCode('BBB02');
$code1 = CouponCode::generate();
$code2 = CouponCode::generate();
$picture1 = $createPicture('picture-1');
$picture2 = $createPicture('picture-2');
@ -232,7 +231,7 @@ describe('list printable coupons use case', function (): void {
// GIVEN
$input = $createInput();
$picture = $createPicture('picture');
$couponCode = new CouponCode('AAA01');
$couponCode = CouponCode::generate();
$items = [$createPrintableCoupon($couponCode), $createPrintableCoupon($couponCode)];
$repository = \Mockery::mock(PrintableCouponRepository::class);
@ -257,7 +256,7 @@ describe('list printable coupons use case', function (): void {
it('throws when the coupon of a printable coupon does not exist', function () use ($createInput, $createPrintableCoupon): void {
// GIVEN
$input = $createInput();
$printable = $createPrintableCoupon(new CouponCode('BBB02'));
$printable = $createPrintableCoupon(CouponCode::generate());
$repository = \Mockery::mock(PrintableCouponRepository::class);
$repository->shouldReceive('findAllAvailable')

View file

@ -33,6 +33,7 @@ use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\ProcessingCodeArray;
use Ramsey\Uuid\Uuid;
use Shared\Enums\Country;
use Shared\Enums\Currency;
use Shared\ValueObjects\Money;
@ -47,19 +48,17 @@ describe('campaign', function (): void {
$printAllAtOnce = \boolval(1);
$coupons = new CampaignCouponArray([
new CampaignCoupon(
new CouponCode('AAA01'),
CouponCode::generate(),
new PrintRules(maxPerCustomer: 3)
)
]);
$brandCode = new BrandCode('AAA01');
$actorId = ActorId::generate();
$couponCode = new CouponCode('COU01');
$couponName = new CouponName('Coupon name');
$couponDiffusionStart = new \DateTime()->modify('-1 month');
$couponDiffusionEnd = new \DateTime()->modify('+1 month');
$makeCoupon = fn (array $overrides = []): Coupon => Coupon::create(
code: $couponCode,
name: $couponName,
amount: new MoneyAmount(new Money(Currency::EUR, 100)),
picture: new CouponPicture(
@ -109,7 +108,7 @@ describe('campaign', function (): void {
$name = new CampaignName('Name');
$coupons = new CampaignCouponArray([
new CampaignCoupon(
new CouponCode('AAA01'),
CouponCode::generate(),
new PrintRules(maxPerCustomer: 3)
)
]);
@ -173,10 +172,13 @@ describe('campaign', function (): void {
$actorId,
): void {
// GIVEN
$expected = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$campaign = Campaign::create(
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
$id = new CampaignId(Uuid::uuid7($expected));
$campaign = Campaign::hydrate(
$id,
CampaignCode::generate(),
$name,
false,
$printAllAtOnce,
$coupons,
$brandCode,
@ -371,8 +373,10 @@ describe('campaign', function (): void {
it('gets the codes of the associated coupons', function (): void {
// GIVEN
$expectedCoupons = ['COU01', 'COU02'];
$campaign = CampaignFactory::hydrated(couponCodes: $expectedCoupons);
$couponCode1 = CouponCode::generate();
$couponCode2 = CouponCode::generate();
$expectedCoupons = [$couponCode1->value, $couponCode2->value];
$campaign = CampaignFactory::hydrated(couponCodes: [$couponCode1, $couponCode2]);
// WHEN
$coupons = array_map(static fn (CouponCode $c): string => $c->value, $campaign->couponCodes());
@ -429,7 +433,7 @@ describe('campaign', function (): void {
// GIVEN
CarbonImmutable::setTestNow('2026-06-15 16:45:00 UTC');
$id = CampaignId::generate();
$couponCode = new CouponCode('COU09');
$couponCode = CouponCode::generate();
$campaign = Campaign::hydrate(
$id,
$code,

View file

@ -37,7 +37,7 @@ use Shared\ValueObjects\Money;
covers(Coupon::class);
describe('coupon', function (): void {
$code = new CouponCode('AAA01');
$code = CouponCode::generate();
$name = new CouponName('Name');
$amount = new MoneyAmount(new Money(Currency::EUR, 1));
$picture = new CouponPicture(
@ -288,7 +288,6 @@ describe('coupon', function (): void {
});
it('creates a new coupon with a freshly generated id', function () use (
$code,
$name,
$amount,
$picture,
@ -304,7 +303,6 @@ describe('coupon', function (): void {
): void {
// GIVEN / WHEN
$coupon = Coupon::create(
code: $code,
name: $name,
amount: $amount,
picture: $picture,
@ -321,7 +319,6 @@ describe('coupon', function (): void {
// THEN
expect($coupon->id())->toBeInstanceOf(CouponId::class)
->and($coupon->code)->toBe($code)
->and($coupon->name)->toBe($name)
->and($coupon->amount)->toBe($amount)
->and($coupon->picture)->toBe($picture)
@ -339,7 +336,6 @@ describe('coupon', function (): void {
});
it('generates a different id for each created coupon', function () use (
$code,
$name,
$amount,
$picture,
@ -355,7 +351,6 @@ describe('coupon', function (): void {
): void {
// GIVEN / WHEN
$firstCoupon = Coupon::create(
code: $code,
name: $name,
amount: $amount,
picture: $picture,
@ -370,7 +365,6 @@ describe('coupon', function (): void {
availabilityCalendar: $availabilityCalendar,
);
$secondCoupon = Coupon::create(
code: $code,
name: $name,
amount: $amount,
picture: $picture,
@ -432,7 +426,6 @@ describe('coupon', function (): void {
});
it('computes the quantity from its movements', function () use (
$code,
$name,
$amount,
$picture,
@ -446,7 +439,6 @@ describe('coupon', function (): void {
): void {
// GIVEN
$coupon = Coupon::create(
code: $code,
name: $name,
amount: $amount,
picture: $picture,

View file

@ -17,7 +17,7 @@ covers(PrintableCoupon::class);
describe('printable coupon', function (): void {
$campaignCode = CampaignCode::generate();
$couponCode = new CouponCode('AAA01');
$couponCode = CouponCode::generate();
$couponName = new CouponName('Coupon name');
$crmCustomerHashcode = new CrmHashcode(Uuid::uuid4());
$distributableUntil = new \DateTimeImmutable('2027-01-01');

View file

@ -19,7 +19,7 @@ covers(Stock::class);
describe('stock', function (): void {
$stockAmount = new StockAmount(10);
$campaignCode = CampaignCode::generate();
$couponCode = new CouponCode('AAA01');
$couponCode = CouponCode::generate();
$stockAdjustment = [new StockAdjustment(5, 'Inventory correction', ActorId::generate())];
it('hydrates a stock', function () use ($stockAmount, $campaignCode, $couponCode, $stockAdjustment): void {

View file

@ -13,12 +13,12 @@ covers(CampaignCouponArray::class);
describe('campaign coupon array', function (): void {
$coupon = new CampaignCoupon(
new CouponCode('AAA01'),
CouponCode::generate(),
new PrintRules(maxPerCustomer: 3),
);
$coupon2 = new CampaignCoupon(
new CouponCode('AAA02'),
CouponCode::generate(),
new PrintRules(maxPerCustomer: 3),
);
@ -69,28 +69,32 @@ describe('campaign coupon array', function (): void {
it('rejects the same coupon twice at initialisation', function (): void {
// GIVEN
$coupons = [
new CampaignCoupon(new CouponCode('AAA01')),
new CampaignCoupon(new CouponCode('AAA01')),
$couponCode = CouponCode::generate();
$coupons = [
new CampaignCoupon($couponCode),
new CampaignCoupon($couponCode),
];
// WHEN
$action = fn (): CampaignCouponArray => new CampaignCouponArray($coupons);
//WHEN / THEN
expect($action)->toThrow(\DomainException::class, '"AAA01" is present more than once.');
expect($action)->toThrow(\DomainException::class, sprintf(
'"%s" is present more than once.',
$couponCode
));
});
it('adds a coupon to a campaign coupon array', function (): void {
// GIVEN
$coupons = [
new CampaignCoupon(new CouponCode('AAA01')),
new CampaignCoupon(new CouponCode('AAA02')),
new CampaignCoupon(CouponCode::generate()),
new CampaignCoupon(CouponCode::generate()),
];
$array = new CampaignCouponArray($coupons);
// WHEN
$array = $array->addCoupon(new CampaignCoupon(new CouponCode('AAA03')));
$array = $array->addCoupon(new CampaignCoupon(CouponCode::generate()));
// THEN
expect($array->toArray())->toHaveCount(3);
@ -110,17 +114,21 @@ describe('campaign coupon array', function (): void {
it('cant remove a coupon which is not on the campaign', function (): void {
// GIVEN
$coupons = [
new CampaignCoupon(new CouponCode('AAA01')),
new CampaignCoupon(new CouponCode('AAA02')),
$coupons = [
new CampaignCoupon(CouponCode::generate()),
new CampaignCoupon(CouponCode::generate()),
];
$array = new CampaignCouponArray($coupons);
$array = new CampaignCouponArray($coupons);
$couponCodeToRemove = CouponCode::generate();
// WHEN
$action = fn () => $array->removeCoupon(new CouponCode('AAA03'));
$action = fn () => $array->removeCoupon($couponCodeToRemove);
// THEN
expect($action)->toThrow(\DomainException::class, '"AAA03" is not in the set.');
expect($action)->toThrow(\DomainException::class, sprintf(
'"%s" is not in the set.',
$couponCodeToRemove
));
});
it('returns all coupon codes', function () use ($coupon, $coupon2): void {

View file

@ -11,7 +11,7 @@ use Domain\Models\ValueObjects\PrintRules;
covers(CampaignCoupon::class);
describe('campaign coupon', function (): void {
$couponCode = new CouponCode('AAA01');
$couponCode = CouponCode::generate();
$printRules = new PrintRules(maxPerCustomer: 3);
it('creates a valid campaign coupon', function () use ($couponCode, $printRules): void {
@ -45,7 +45,7 @@ describe('campaign coupon', function (): void {
// WHEN
$result = $campaignCoupon->hasCouponCode(
new CouponCode('AAA01'),
$couponCode,
);
// THEN
@ -61,7 +61,7 @@ describe('campaign coupon', function (): void {
// WHEN
$result = $campaignCoupon->hasCouponCode(
new CouponCode('BBB02'),
CouponCode::generate(),
);
// THEN

View file

@ -5,71 +5,43 @@ declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Ramsey\Uuid\Uuid;
covers(CouponCode::class);
describe('coupon code', function (): void {
it('creates a valid coupon code', function (): void {
// GIVEN
$value = 'AAA01';
// WHEN
$code = new CouponCode($value);
it('generates a valid uuid v7 code', function (): void {
// GIVEN / WHEN
$code = CouponCode::generate();
// THEN
expect($code)->toBeInstanceOf(CouponCode::class)
->and($code->value)->toBe($value);
expect(Uuid::isValid($code->value))->toBeTrue()
->and(Uuid::fromString($code->value)->getVersion())->toBe(7);
});
it('generates a different code each time', function (): void {
// GIVEN / WHEN
$first = CouponCode::generate();
$second = CouponCode::generate();
// THEN
expect($first->value)->not->toBe($second->value);
});
it('wraps a given uuid v7', function (): void {
// GIVEN
$uuid = Uuid::uuid7();
// WHEN
$code = new CouponCode($uuid);
// THEN
expect($code->value)->toBe($uuid->toString());
});
it('rejects a uuid that is not v7', function (): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponCode => new CouponCode(Uuid::uuid4()))
->toThrow(\DomainException::class);
});
});
describe('coupon code validation rules', function (): void {
it('rejects invalid coupon codes', function (string $invalid): void {
// WHEN / THEN
expect(fn (): CouponCode => new CouponCode($invalid))->toThrow(\DomainException::class);
// GIVEN
})->with([
// rejects empty strings
'',
' ',
"\t",
"\n",
// rejects numbers only
'12345',
'67890',
// rejects letters only
'ABCDE',
'FGHIJ',
// rejects codes with special characters
'AB@12',
'CD#34',
'EF$56',
// rejects codes that have the same digit repeated
'ABC11',
'DEF22',
'GHI33',
// rejects codes with lowercase letters
'AbC12',
'dEf34',
'GhI56',
]);
it('rejects codes with more/less than 3 letters or more/less than 2 digits', function (string $invalid): void {
// WHEN / THEN
expect(fn (): CouponCode => new CouponCode($invalid))->toThrow(\DomainException::class);
// GIVEN
})->with([
'ABCD12',
'ABC123',
'ABCDE123',
'AB12',
'A1',
'AB1',
'A12',
'AB123',
'A123',
'ABC1',
'AB12C',
]);
});

View file

@ -18,7 +18,7 @@ covers(PrintedCoupon::class);
describe('printed coupon', function (): void {
$printableCode = PrintableCouponCode::generate();
$marketingCode = new MarketingDirectCode('0000000000001');
$couponCode = new CouponCode('AAA01');
$couponCode = CouponCode::generate();
$campaignCode = CampaignCode::generate();
$customer = new CrmHashcode(Uuid::uuid4());

View file

@ -16,7 +16,7 @@ covers(UnprintedCoupon::class);
describe('unprinted coupon', function (): void {
$printableCode = PrintableCouponCode::generate();
$couponCode = new CouponCode('AAA01');
$couponCode = CouponCode::generate();
$campaignCode = CampaignCode::generate();
$customer = new CrmHashcode(Uuid::uuid4());

View file

@ -12,25 +12,16 @@ use Tests\Fixtures\Coupon\CouponFactory;
covers(CampaignAvailabilityChecker::class);
describe('campaign availability checker', function (): void {
$makeRepository = static function (array $campaignCodes, array $coupons): CouponRepository {
it('is available with an active coupon', function (): void {
// GIVEN
$coupon = CouponFactory::make();
$campaign = CampaignFactory::hydrated(couponCodes: [$coupon->code]);
$repository = \Mockery::mock(CouponRepository::class);
$repository->shouldReceive('findByCodes')
->once()
->with(\Mockery::on(
static fn (array $codes): bool => array_map(
static fn ($code): string => $code->value,
$codes,
) === $campaignCodes,
))
->andReturn($coupons);
$repository->shouldReceive('findByCodes')->once()->with($campaign->couponCodes())->andReturn([
$coupon
]);
return $repository;
};
it('is available with an active coupon', function () use ($makeRepository): void {
// GIVEN
$campaign = CampaignFactory::hydrated(couponCodes: ['COU01']);
$repository = $makeRepository(['COU01'], [CouponFactory::make('COU01')]);
$service = new CampaignAvailabilityChecker($repository);
// WHEN
@ -40,14 +31,19 @@ describe('campaign availability checker', function (): void {
expect($result)->toBeTrue();
});
it('is available with one active and one expired coupon', function () use ($makeRepository): void {
it('is available with one active and one expired coupon', function (): void {
// GIVEN
$campaign = CampaignFactory::hydrated(couponCodes: ['COU01', 'COU02']);
$repository = $makeRepository(['COU01', 'COU02'], [
CouponFactory::expired('COU01'),
CouponFactory::make('COU02'),
$expiredCoupon = CouponFactory::expired();
$validCoupon = CouponFactory::make();
$campaign = CampaignFactory::hydrated(couponCodes: [$expiredCoupon->code, $validCoupon->code]);
$repository = \Mockery::mock(CouponRepository::class);
$repository->shouldReceive('findByCodes')->once()->with($campaign->couponCodes())->andReturn([
$expiredCoupon,
$validCoupon
]);
$service = new CampaignAvailabilityChecker($repository);
$service = new CampaignAvailabilityChecker($repository);
// WHEN
$result = $service->hasActiveCoupon($campaign);
@ -56,14 +52,19 @@ describe('campaign availability checker', function (): void {
expect($result)->toBeTrue();
});
it('is not available when every coupon is expired or upcoming', function () use ($makeRepository): void {
it('is not available when every coupon is expired or upcoming', function (): void {
// GIVEN
$campaign = CampaignFactory::hydrated(couponCodes: ['COU01', 'COU02']);
$repository = $makeRepository(['COU01', 'COU02'], [
CouponFactory::expired('COU01'),
CouponFactory::upcoming('COU02'),
$expiredCoupon = CouponFactory::expired();
$upcomingCoupon = CouponFactory::upcoming();
$campaign = CampaignFactory::hydrated(couponCodes: [$expiredCoupon->code, $upcomingCoupon->code]);
$repository = \Mockery::mock(CouponRepository::class);
$repository->shouldReceive('findByCodes')->once()->with($campaign->couponCodes())->andReturn([
$expiredCoupon,
$upcomingCoupon
]);
$service = new CampaignAvailabilityChecker($repository);
$service = new CampaignAvailabilityChecker($repository);
// WHEN
$result = $service->hasActiveCoupon($campaign);
@ -72,11 +73,14 @@ describe('campaign availability checker', function (): void {
expect($result)->toBeFalse();
});
it('is not available when no coupon is found', function () use ($makeRepository): void {
it('is not available when no coupon is found', function (): void {
// GIVEN
$campaign = CampaignFactory::hydrated(couponCodes: ['COU01']);
$repository = $makeRepository(['COU01'], []);
$service = new CampaignAvailabilityChecker($repository);
$campaign = CampaignFactory::hydrated();
$repository = \Mockery::mock(CouponRepository::class);
$repository->shouldReceive('findByCodes')->once()->with($campaign->couponCodes())->andReturn([]);
$service = new CampaignAvailabilityChecker($repository);
// WHEN
$result = $service->hasActiveCoupon($campaign);