726 lines
27 KiB
PHP
726 lines
27 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Domain\Services;
|
|
|
|
use Carbon\CarbonImmutable;
|
|
use DateInterval;
|
|
use Domain\Builders\OrderBuilder;
|
|
use Domain\Enums\OrderTunnelStep;
|
|
use Domain\Enums\TaxRate;
|
|
use Domain\Enums\Units\OrderUnit;
|
|
use Domain\Exceptions\OrderTunnel\MissingOrderBuilderException;
|
|
use Domain\Models\Entities\Basket;
|
|
use Domain\Models\Entities\Order;
|
|
use Domain\Models\Entities\Shop;
|
|
use Domain\Models\Entities\Shopper;
|
|
use Domain\Models\ValueObjects\BasketProduct;
|
|
use Domain\Models\ValueObjects\DiscountRate;
|
|
use Domain\Models\ValueObjects\Email;
|
|
use Domain\Models\ValueObjects\Entities\BasketId;
|
|
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
use Domain\Models\ValueObjects\Entities\ShopId;
|
|
use Domain\Models\ValueObjects\Entities\ShopperId;
|
|
use Domain\Models\ValueObjects\IncoId;
|
|
use Domain\Models\ValueObjects\Matricule;
|
|
use Domain\Models\ValueObjects\PickupTimeSlot;
|
|
use Domain\Models\ValueObjects\SessionId;
|
|
use Domain\Models\ValueObjects\ShopperEstablishmentCode;
|
|
use Domain\Models\ValueObjects\ShopperServiceCode;
|
|
use Domain\Models\ValueObjects\ShopperSiteCode;
|
|
use Domain\Policies\CustomerOrderLimitPolicy;
|
|
use Domain\Ports\Repositories\BasketRepository;
|
|
use Domain\Ports\Repositories\OrderRepository;
|
|
use Domain\Ports\Repositories\PickupExceptionRepository;
|
|
use Domain\Ports\Repositories\ProductRepository;
|
|
use Domain\Ports\Repositories\ShopperRepository;
|
|
use Domain\Ports\Repositories\ShopRepository;
|
|
use Domain\Ports\Services\EmployeeStatisticsService;
|
|
use Domain\Ports\Services\OrderBuilderSessionManager;
|
|
use Domain\Ports\Services\SessionService;
|
|
use Domain\Ports\Services\TransactionManager;
|
|
use Domain\Services\BasketService;
|
|
use Domain\Services\OrderService;
|
|
use Domain\Services\OrderTunnelService;
|
|
use Domain\Services\PickupSchedulingService;
|
|
use Domain\Services\ShopperService;
|
|
use Money\Money;
|
|
use Ramsey\Uuid\Uuid;
|
|
use Shared\DDD\DomainEventDispatcher;
|
|
|
|
function createTestBasket(int $qty = 0): Basket
|
|
{
|
|
$items = [];
|
|
|
|
if ($qty > 0) {
|
|
$productId = new ProductId(Uuid::fromString('019e9e37-f2a1-74ef-b9c2-66d222470128'));
|
|
$basketProduct = new BasketProduct(
|
|
id: $productId,
|
|
name: 'Test Product',
|
|
qty: $qty,
|
|
unitPriceHt: Money::EUR(100),
|
|
unitDiscount: Money::EUR(0),
|
|
taxRate: TaxRate::TVA_20,
|
|
weightGrams: 100,
|
|
brand: null,
|
|
incoId: new IncoId(123),
|
|
orderUnit: OrderUnit::PCE,
|
|
slug: 'test-product',
|
|
picture: null,
|
|
description: null,
|
|
weightVariable: false,
|
|
discountRate: DiscountRate::fromRatio(0.0),
|
|
priceKgTtc: Money::EUR(120)
|
|
);
|
|
$items[(string) $productId->value] = $basketProduct;
|
|
}
|
|
|
|
$basketId = new BasketId(Uuid::fromString('019e9e37-f2a1-74ef-b9c2-66d222470128'));
|
|
|
|
return new Basket($basketId, $items);
|
|
}
|
|
|
|
function createTestBuilder(bool $completed = false): OrderBuilder
|
|
{
|
|
$basket = createTestBasket(1);
|
|
$shopperId = new ShopperId(Uuid::uuid7());
|
|
$builder = OrderBuilder::fromBasket($basket, $shopperId);
|
|
|
|
if ($completed) {
|
|
$shopId = new ShopId(Uuid::uuid7());
|
|
$timeSlot = new PickupTimeSlot(CarbonImmutable::create(2025, 1, 1), new DateInterval('PT1H'));
|
|
$builder = $builder->withPickupTimeSlot($shopId, $timeSlot);
|
|
}
|
|
|
|
/** @var OrderBuilder $builder */
|
|
return $builder;
|
|
}
|
|
|
|
function createOrderTunnelService(
|
|
?BasketRepository $basketRepository = null,
|
|
?ProductRepository $productRepository = null,
|
|
?ShopRepository $shopRepository = null,
|
|
?ShopperRepository $shopperRepository = null,
|
|
?PickupExceptionRepository $pickupExceptionRepository = null,
|
|
?OrderBuilderSessionManager $orderBuilderSessionManager = null,
|
|
?OrderRepository $orderRepository = null,
|
|
?TransactionManager $transactionManager = null,
|
|
?DomainEventDispatcher $eventDispatcher = null,
|
|
): OrderTunnelService {
|
|
if ($basketRepository === null) {
|
|
$basketRepository = mock(BasketRepository::class);
|
|
$basketRepository->shouldReceive('persist')->byDefault();
|
|
}
|
|
if ($productRepository === null) {
|
|
$productRepository = mock(ProductRepository::class);
|
|
$productRepository->shouldReceive('getBasketProduct')->andReturnUsing(function ($id, $qty) {
|
|
return new BasketProduct(
|
|
id: $id,
|
|
name: 'Test Product',
|
|
qty: $qty,
|
|
unitPriceHt: Money::EUR(100),
|
|
unitDiscount: Money::EUR(0),
|
|
taxRate: TaxRate::TVA_20,
|
|
weightGrams: 100,
|
|
brand: null,
|
|
incoId: new IncoId(123),
|
|
orderUnit: OrderUnit::PCE,
|
|
slug: 'test-product',
|
|
picture: null,
|
|
description: null,
|
|
weightVariable: false,
|
|
discountRate: DiscountRate::fromRatio(0.0),
|
|
priceKgTtc: Money::EUR(120)
|
|
);
|
|
})->byDefault();
|
|
}
|
|
$shopRepository ??= mock(ShopRepository::class);
|
|
$shopperRepository ??= mock(ShopperRepository::class);
|
|
$pickupExceptionRepository ??= mock(PickupExceptionRepository::class);
|
|
$orderBuilderSessionManager ??= mock(OrderBuilderSessionManager::class);
|
|
$orderRepository ??= mock(OrderRepository::class);
|
|
|
|
$transactionManager ??= mock(TransactionManager::class);
|
|
$eventDispatcher ??= mock(DomainEventDispatcher::class);
|
|
|
|
$sessionService = \Mockery::mock(SessionService::class);
|
|
$sessionService->shouldReceive('get')->andReturn(new SessionId(Uuid::uuid7()))->byDefault();
|
|
|
|
$employeeStatisticsService = \Mockery::mock(EmployeeStatisticsService::class);
|
|
$employeeStatisticsService->shouldReceive('getWeeklyTotalForEmployee')->andReturn(Money::EUR(0))->byDefault();
|
|
|
|
$basketService = new BasketService($basketRepository, $productRepository, $sessionService);
|
|
$shopperService = new ShopperService($shopRepository, $shopperRepository);
|
|
$pickupService = new PickupSchedulingService($pickupExceptionRepository);
|
|
$orderService = new OrderService($orderRepository);
|
|
|
|
$orderLimitPolicy = new CustomerOrderLimitPolicy(500000, 500000);
|
|
|
|
return new OrderTunnelService(
|
|
$basketService,
|
|
$eventDispatcher,
|
|
$shopperService,
|
|
$orderBuilderSessionManager,
|
|
$pickupService,
|
|
$orderRepository,
|
|
$orderLimitPolicy,
|
|
$transactionManager,
|
|
$shopRepository,
|
|
$shopperRepository,
|
|
$orderService,
|
|
$employeeStatisticsService
|
|
);
|
|
}
|
|
|
|
describe('OrderTunnelService', function (): void {
|
|
it('validates basket is valid when it has items', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->andReturn(createTestBasket(2));
|
|
|
|
$service = createOrderTunnelService(basketRepository: $basketRepo);
|
|
|
|
// WHEN
|
|
$isValid = $service->hasValidBasket();
|
|
|
|
// THEN
|
|
expect($isValid)->toBeTrue();
|
|
});
|
|
|
|
it('invalidates basket when it is empty', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->andReturn(createTestBasket(0));
|
|
|
|
$service = createOrderTunnelService(basketRepository: $basketRepo);
|
|
|
|
// WHEN
|
|
$isValid = $service->hasValidBasket();
|
|
|
|
// THEN
|
|
expect($isValid)->toBeFalse();
|
|
});
|
|
|
|
it('allows access to REVIEW step when basket is valid', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->andReturn(createTestBasket(1));
|
|
|
|
$orderBuilderRepository = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepository->shouldReceive('get')->andReturn(null);
|
|
|
|
$service = createOrderTunnelService(
|
|
basketRepository: $basketRepo,
|
|
orderBuilderSessionManager: $orderBuilderRepository
|
|
);
|
|
|
|
// WHEN
|
|
$canAccess = $service->canAccessStep(OrderTunnelStep::REVIEW);
|
|
|
|
// THEN
|
|
expect($canAccess)->toBeTrue();
|
|
});
|
|
|
|
it('denies access to REVIEW step when basket is empty', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->andReturn(createTestBasket(0));
|
|
|
|
$orderBuilderRepository = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepository->shouldReceive('get')->andReturn(null);
|
|
|
|
$service = createOrderTunnelService(
|
|
basketRepository: $basketRepo,
|
|
orderBuilderSessionManager: $orderBuilderRepository
|
|
);
|
|
|
|
// WHEN
|
|
$canAccess = $service->canAccessStep(OrderTunnelStep::REVIEW);
|
|
|
|
// THEN
|
|
expect($canAccess)->toBeFalse();
|
|
});
|
|
|
|
it('allows access to BOOKING step when builder is from basket and basket is valid', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->andReturn(createTestBasket(1));
|
|
|
|
$builder = createTestBuilder();
|
|
$orderBuilderRepository = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepository->shouldReceive('get')->andReturn($builder);
|
|
|
|
$service = createOrderTunnelService(
|
|
basketRepository: $basketRepo,
|
|
orderBuilderSessionManager: $orderBuilderRepository
|
|
);
|
|
|
|
// WHEN
|
|
$canAccess = $service->canAccessStep(OrderTunnelStep::BOOKING);
|
|
|
|
// THEN
|
|
expect($canAccess)->toBeTrue();
|
|
});
|
|
|
|
it('denies access to BOOKING step when builder is missing', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->andReturn(createTestBasket(1));
|
|
|
|
$orderBuilderRepository = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepository->shouldReceive('get')->andReturn(null);
|
|
|
|
$service = createOrderTunnelService(
|
|
basketRepository: $basketRepo,
|
|
orderBuilderSessionManager: $orderBuilderRepository
|
|
);
|
|
|
|
// WHEN
|
|
$canAccess = $service->canAccessStep(OrderTunnelStep::BOOKING);
|
|
|
|
// THEN
|
|
expect($canAccess)->toBeFalse();
|
|
});
|
|
|
|
it('allows access to CONFIRMATION step when builder is completed and basket is valid', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->andReturn(createTestBasket(1));
|
|
|
|
$builder = createTestBuilder(completed: true);
|
|
$orderBuilderRepository = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepository->shouldReceive('get')->andReturn($builder);
|
|
|
|
$service = createOrderTunnelService(
|
|
basketRepository: $basketRepo,
|
|
orderBuilderSessionManager: $orderBuilderRepository
|
|
);
|
|
|
|
// WHEN
|
|
$canAccess = $service->canAccessStep(OrderTunnelStep::CONFIRMATION);
|
|
|
|
// THEN
|
|
expect($canAccess)->toBeTrue();
|
|
});
|
|
|
|
it('denies access to CONFIRMATION step when builder is not completed', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->andReturn(createTestBasket(1));
|
|
|
|
$builder = createTestBuilder(completed: false);
|
|
$orderBuilderRepository = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepository->shouldReceive('get')->andReturn($builder);
|
|
|
|
$service = createOrderTunnelService(
|
|
basketRepository: $basketRepo,
|
|
orderBuilderSessionManager: $orderBuilderRepository
|
|
);
|
|
|
|
// WHEN
|
|
$canAccess = $service->canAccessStep(OrderTunnelStep::CONFIRMATION);
|
|
|
|
// THEN
|
|
expect($canAccess)->toBeFalse();
|
|
});
|
|
|
|
it('denies access to CONFIRMATION step when builder is missing', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->andReturn(createTestBasket(1));
|
|
|
|
$orderBuilderRepository = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepository->shouldReceive('get')->andReturn(null);
|
|
|
|
$service = createOrderTunnelService(
|
|
basketRepository: $basketRepo,
|
|
orderBuilderSessionManager: $orderBuilderRepository
|
|
);
|
|
|
|
// WHEN
|
|
$canAccess = $service->canAccessStep(OrderTunnelStep::CONFIRMATION);
|
|
|
|
// THEN
|
|
expect($canAccess)->toBeFalse();
|
|
});
|
|
|
|
it('gets shop for customer successfully', function (): void {
|
|
// GIVEN
|
|
$shopperId = new ShopperId(Uuid::uuid7());
|
|
$shopper = new Shopper(
|
|
id: $shopperId,
|
|
email: new Email('test@test.com'),
|
|
matricule: new Matricule('1234567'),
|
|
nom: 'Doe',
|
|
prenom: 'John',
|
|
startDate: CarbonImmutable::now()->subYear(),
|
|
endDate: null,
|
|
etablissement: new ShopperEstablishmentCode('ETAB01'),
|
|
site: new ShopperSiteCode('SITEA'),
|
|
service: new ShopperServiceCode('101')
|
|
);
|
|
|
|
$shopId = new ShopId(Uuid::uuid7());
|
|
$shop = new Shop(
|
|
id: $shopId,
|
|
name: 'Shop',
|
|
address: 'Address',
|
|
postcode: '75000',
|
|
city: 'Paris'
|
|
);
|
|
|
|
$shopperRepo = mock(ShopperRepository::class);
|
|
$shopperRepo->shouldReceive('find')->with($shopperId)->andReturn($shopper);
|
|
|
|
$shopRepo = mock(ShopRepository::class);
|
|
$shopRepo->shouldReceive('findExceptionShopFor')->with($shopper->matricule)->andReturn(null);
|
|
$shopRepo->shouldReceive('findShopForSiteAndService')->with($shopper->site, $shopper->service)->andReturn(null);
|
|
$shopRepo->shouldReceive('findShopForSiteAndService')->with($shopper->site, null)->andReturn(null);
|
|
$shopRepo->shouldReceive('getDefaultShop')->andReturn($shop);
|
|
|
|
$service = createOrderTunnelService(
|
|
shopRepository: $shopRepo,
|
|
shopperRepository: $shopperRepo
|
|
);
|
|
|
|
// WHEN
|
|
$result = $service->getShopForCustomer($shopperId);
|
|
|
|
// THEN
|
|
expect($result)->toBe($shop);
|
|
});
|
|
|
|
it('throws exception when get shop for customer fails', function (): void {
|
|
// GIVEN
|
|
$shopperId = new ShopperId(Uuid::uuid7());
|
|
$shopperRepo = mock(ShopperRepository::class);
|
|
$shopperRepo->shouldReceive('find')->with($shopperId)->andReturn(null);
|
|
|
|
$service = createOrderTunnelService(shopperRepository: $shopperRepo);
|
|
|
|
// WHEN
|
|
$action = fn () => $service->getShopForCustomer($shopperId);
|
|
// THEN
|
|
expect($action)->toThrow(\UnexpectedValueException::class);
|
|
});
|
|
|
|
it('gets available time slots', function (): void {
|
|
// GIVEN
|
|
$shopId = new ShopId(Uuid::uuid7());
|
|
$shop = new Shop($shopId, 'Shop', 'Address', '75000', 'Paris');
|
|
|
|
$pickupExceptionRepo = mock(PickupExceptionRepository::class);
|
|
$pickupExceptionRepo->shouldReceive('getExceptionsForShop')->andReturn([]);
|
|
|
|
$service = createOrderTunnelService(pickupExceptionRepository: $pickupExceptionRepo);
|
|
|
|
// WHEN
|
|
$slots = $service->getAvailableTimeSlots($shop);
|
|
|
|
// THEN
|
|
expect($slots)->toBeArray();
|
|
});
|
|
|
|
it('initializes order from basket', function (): void {
|
|
// GIVEN
|
|
$basket = createTestBasket(1);
|
|
$customerId = new ShopperId(Uuid::uuid7());
|
|
|
|
$orderBuilderRepo = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepo->shouldReceive('save')->once();
|
|
|
|
$service = createOrderTunnelService(orderBuilderSessionManager: $orderBuilderRepo);
|
|
|
|
// WHEN
|
|
$service->initializeFromBasket($basket, $customerId);
|
|
|
|
// THEN
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('gets builder from basket or fail', function (): void {
|
|
// GIVEN
|
|
$builder = createTestBuilder();
|
|
$orderBuilderRepo = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepo->shouldReceive('get')->once()->andReturn($builder);
|
|
|
|
$service = createOrderTunnelService(orderBuilderSessionManager: $orderBuilderRepo);
|
|
|
|
// WHEN
|
|
$result = $service->getBuilderFromBasketOrFail();
|
|
|
|
// THEN
|
|
expect($result)->toBe($builder);
|
|
});
|
|
|
|
it('throws exception if builder not from basket', function (): void {
|
|
// GIVEN
|
|
$orderBuilderRepo = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepo->shouldReceive('get')->once()->andReturn(null);
|
|
|
|
$service = createOrderTunnelService(orderBuilderSessionManager: $orderBuilderRepo);
|
|
|
|
// WHEN
|
|
$action = fn () => $service->getBuilderFromBasketOrFail();
|
|
// THEN
|
|
expect($action)->toThrow(MissingOrderBuilderException::class);
|
|
});
|
|
|
|
it('gets completed builder or fail', function (): void {
|
|
// GIVEN
|
|
$builder = createTestBuilder(completed: true);
|
|
$orderBuilderRepo = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepo->shouldReceive('get')->once()->andReturn($builder);
|
|
|
|
$service = createOrderTunnelService(orderBuilderSessionManager: $orderBuilderRepo);
|
|
|
|
// WHEN
|
|
$result = $service->getBuilderCompletedOrFail();
|
|
|
|
// THEN
|
|
expect($result)->toBe($builder);
|
|
});
|
|
|
|
it('throws exception if builder not completed', function (): void {
|
|
// GIVEN
|
|
$builder = createTestBuilder(completed: false);
|
|
$orderBuilderRepo = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepo->shouldReceive('get')->once()->andReturn($builder);
|
|
|
|
$service = createOrderTunnelService(orderBuilderSessionManager: $orderBuilderRepo);
|
|
|
|
// WHEN
|
|
$action = fn () => $service->getBuilderCompletedOrFail();
|
|
// THEN
|
|
expect($action)->toThrow(MissingOrderBuilderException::class);
|
|
});
|
|
|
|
it('applies time slot', function (): void {
|
|
// GIVEN
|
|
$builder = createTestBuilder(completed: false);
|
|
$shop = new Shop(new ShopId(Uuid::uuid7()), 'Shop', 'Address', '75000', 'Paris');
|
|
$selectedDate = CarbonImmutable::create(2025, 1, 1);
|
|
$ts1 = new PickupTimeSlot($selectedDate, new DateInterval('PT1H'));
|
|
$ts2 = new PickupTimeSlot(CarbonImmutable::create(2025, 1, 2), new DateInterval('PT1H'));
|
|
$availableSlots = [$ts1, $ts2];
|
|
|
|
$orderBuilderRepo = mock(OrderBuilderSessionManager::class);
|
|
$orderBuilderRepo->shouldReceive('save')->once();
|
|
|
|
$service = createOrderTunnelService(orderBuilderSessionManager: $orderBuilderRepo);
|
|
|
|
// WHEN
|
|
$matched = $service->applyTimeSlot($builder, $shop, $selectedDate, $availableSlots);
|
|
|
|
// THEN
|
|
expect($matched)->toBe($ts1);
|
|
});
|
|
|
|
it('returns null if no matching time slot', function (): void {
|
|
// GIVEN
|
|
$builder = createTestBuilder(completed: false);
|
|
$shop = new Shop(new ShopId(Uuid::uuid7()), 'Shop', 'Address', '75000', 'Paris');
|
|
$selectedDate = CarbonImmutable::create(2025, 1, 1);
|
|
$ts1 = new PickupTimeSlot(CarbonImmutable::create(2025, 1, 2), new DateInterval('PT1H'));
|
|
$availableSlots = [$ts1];
|
|
|
|
$service = createOrderTunnelService();
|
|
|
|
// WHEN
|
|
$matched = $service->applyTimeSlot($builder, $shop, $selectedDate, $availableSlots);
|
|
|
|
// THEN
|
|
expect($matched)->toBeNull();
|
|
});
|
|
|
|
it('finalizes order', function (): void {
|
|
// GIVEN
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->once()->andReturn(createTestBasket(1));
|
|
$basketRepo->shouldReceive('persist')->once();
|
|
$basketRepo->shouldReceive('delete')->once();
|
|
|
|
$builder = createTestBuilder(completed: true);
|
|
$shopperId = $builder->getShopperId();
|
|
$shopper = new Shopper(
|
|
id: $shopperId,
|
|
email: new Email('test@test.com'),
|
|
matricule: new Matricule('1234567'),
|
|
nom: 'Doe',
|
|
prenom: 'John',
|
|
startDate: CarbonImmutable::now()->subYear(),
|
|
endDate: null,
|
|
etablissement: new ShopperEstablishmentCode('ETAB01'),
|
|
site: new ShopperSiteCode('SITEA'),
|
|
service: new ShopperServiceCode('101')
|
|
);
|
|
$shopperRepo = mock(ShopperRepository::class);
|
|
$shopperRepo->shouldReceive('getByIdOrFail')->with(\Mockery::type(ShopperId::class))->andReturn($shopper);
|
|
|
|
$shopId = clone clone $builder->getShopId();
|
|
$shop = new Shop($shopId, 'Shop', 'Address', '75000', 'Paris');
|
|
$shopRepo = mock(ShopRepository::class);
|
|
$shopRepo->shouldReceive('find')->andReturn($shop); // any with shopId is fine
|
|
|
|
$orderRepo = mock(OrderRepository::class);
|
|
$orderRepo->shouldReceive('getWeeklyTotalForEmployee')->andReturn(Money::EUR(5000));
|
|
$orderRepo->shouldReceive('persist')->once();
|
|
|
|
$transactionManager = mock(TransactionManager::class);
|
|
$transactionManager->shouldReceive('execute')->once()->andReturnUsing(function ($callback) {
|
|
return $callback();
|
|
});
|
|
|
|
$eventDispatcher = mock(DomainEventDispatcher::class);
|
|
$eventDispatcher->shouldReceive('dispatch')->once();
|
|
|
|
$builderRepo = mock(OrderBuilderSessionManager::class);
|
|
$builderRepo->shouldReceive('clear')->once();
|
|
|
|
$service = createOrderTunnelService(
|
|
basketRepository: $basketRepo,
|
|
shopRepository: $shopRepo,
|
|
shopperRepository: $shopperRepo,
|
|
orderBuilderSessionManager: $builderRepo,
|
|
orderRepository: $orderRepo,
|
|
transactionManager: $transactionManager,
|
|
eventDispatcher: $eventDispatcher
|
|
);
|
|
|
|
// WHEN
|
|
$order = $service->finalizeOrder($builder);
|
|
|
|
// THEN
|
|
expect($order)->toBeInstanceOf(Order::class);
|
|
expect($order->employee->matricule->value)->toBe('1234567');
|
|
expect($order->deliveryAddress->name)->toBe('Shop');
|
|
});
|
|
|
|
it('throws exception if shop not found during finalize order', function (): void {
|
|
// GIVEN
|
|
$builder = createTestBuilder(completed: true);
|
|
$shopperId = clone clone $builder->getShopperId();
|
|
$shopper = new Shopper(
|
|
id: $shopperId,
|
|
email: new Email('test@test.com'),
|
|
matricule: new Matricule('1234567'),
|
|
nom: 'Doe',
|
|
prenom: 'John',
|
|
startDate: CarbonImmutable::now()->subYear(),
|
|
endDate: null,
|
|
etablissement: new ShopperEstablishmentCode('ETAB01'),
|
|
site: new ShopperSiteCode('SITEA'),
|
|
service: new ShopperServiceCode('101')
|
|
);
|
|
$shopperRepo = mock(ShopperRepository::class);
|
|
$shopperRepo->shouldReceive('getByIdOrFail')->with(\Mockery::type(ShopperId::class))->andReturn($shopper);
|
|
|
|
$shopRepo = mock(ShopRepository::class);
|
|
$shopRepo->shouldReceive('find')->andReturn(null);
|
|
|
|
$orderRepo = mock(OrderRepository::class);
|
|
$orderRepo->shouldReceive('getWeeklyTotalForEmployee')->andReturn(Money::EUR(5000));
|
|
|
|
$transactionManager = mock(TransactionManager::class);
|
|
$transactionManager->shouldReceive('execute')->once()->andReturnUsing(function ($callback) {
|
|
return $callback();
|
|
});
|
|
|
|
$service = createOrderTunnelService(
|
|
shopRepository: $shopRepo,
|
|
shopperRepository: $shopperRepo,
|
|
orderRepository: $orderRepo,
|
|
transactionManager: $transactionManager
|
|
);
|
|
|
|
// WHEN
|
|
$action = fn () => $service->finalizeOrder($builder);
|
|
// THEN
|
|
expect($action)->toThrow(\DomainException::class);
|
|
});
|
|
|
|
it('revalidates basket prices from product repository when finalizing order', function (): void {
|
|
// GIVEN
|
|
$builder = createTestBuilder(completed: true);
|
|
$shopperId = $builder->getShopperId();
|
|
$shopper = new Shopper(
|
|
id: $shopperId,
|
|
email: new Email('test@test.com'),
|
|
matricule: new Matricule('1234567'),
|
|
nom: 'Doe',
|
|
prenom: 'John',
|
|
startDate: CarbonImmutable::now()->subYear(),
|
|
endDate: null,
|
|
etablissement: new ShopperEstablishmentCode('ETAB01'),
|
|
site: new ShopperSiteCode('SITEA'),
|
|
service: new ShopperServiceCode('101')
|
|
);
|
|
$shopperRepo = mock(ShopperRepository::class);
|
|
$shopperRepo->shouldReceive('getByIdOrFail')->with(\Mockery::type(ShopperId::class))->andReturn($shopper);
|
|
|
|
$shopId = clone $builder->getShopId();
|
|
$shop = new Shop($shopId, 'Shop', 'Address', '75000', 'Paris');
|
|
$shopRepo = mock(ShopRepository::class);
|
|
$shopRepo->shouldReceive('find')->andReturn($shop);
|
|
|
|
$orderRepo = mock(OrderRepository::class);
|
|
$orderRepo->shouldReceive('getWeeklyTotalForEmployee')->andReturn(Money::EUR(5000));
|
|
$orderRepo->shouldReceive('persist')->once();
|
|
|
|
$transactionManager = mock(TransactionManager::class);
|
|
$transactionManager->shouldReceive('execute')->once()->andReturnUsing(function ($callback) {
|
|
return $callback();
|
|
});
|
|
|
|
$eventDispatcher = mock(DomainEventDispatcher::class);
|
|
$eventDispatcher->shouldReceive('dispatch')->once();
|
|
|
|
$builderRepo = mock(OrderBuilderSessionManager::class);
|
|
$builderRepo->shouldReceive('clear')->once();
|
|
|
|
$basketRepo = mock(BasketRepository::class);
|
|
$basketRepo->shouldReceive('find')->once()->andReturn(createTestBasket(1));
|
|
$basketRepo->shouldReceive('persist')->once();
|
|
$basketRepo->shouldReceive('delete')->once();
|
|
|
|
$productRepo = mock(ProductRepository::class);
|
|
$productRepo->shouldReceive('getBasketProduct')->once()->andReturnUsing(function ($id, $qty) {
|
|
return new BasketProduct(
|
|
id: $id,
|
|
name: 'Updated Product Name',
|
|
qty: $qty,
|
|
unitPriceHt: Money::EUR(250),
|
|
unitDiscount: Money::EUR(50),
|
|
taxRate: TaxRate::TVA_20,
|
|
weightGrams: 200,
|
|
brand: null,
|
|
incoId: new IncoId(123),
|
|
orderUnit: OrderUnit::PCE,
|
|
slug: 'updated-product',
|
|
picture: null,
|
|
description: null,
|
|
weightVariable: false,
|
|
discountRate: DiscountRate::fromRatio(0.0),
|
|
priceKgTtc: Money::EUR(240)
|
|
);
|
|
});
|
|
|
|
$service = createOrderTunnelService(
|
|
basketRepository: $basketRepo,
|
|
productRepository: $productRepo,
|
|
shopRepository: $shopRepo,
|
|
shopperRepository: $shopperRepo,
|
|
orderBuilderSessionManager: $builderRepo,
|
|
orderRepository: $orderRepo,
|
|
transactionManager: $transactionManager,
|
|
eventDispatcher: $eventDispatcher
|
|
);
|
|
|
|
// WHEN
|
|
$order = $service->finalizeOrder($builder);
|
|
|
|
// THEN
|
|
expect($order)->toBeInstanceOf(Order::class);
|
|
});
|
|
});
|