basketService->getBasket()->countItems() > 0; } public function canAccessStep(OrderTunnelStep $step): bool { $builder = $this->orderBuilderSessionManager->get(); $hasValidBasket = $this->hasValidBasket(); return match ($step) { OrderTunnelStep::REVIEW => $hasValidBasket, OrderTunnelStep::BOOKING => $this->hasValidBasket() && $builder !== null && $builder->isFromBasket(), OrderTunnelStep::CONFIRMATION => $this->hasValidBasket() && $builder !== null && $builder->isCompleted(), }; } /** * Resolve the shop assigned to a customer. * * @throws \UnexpectedValueException If no shop can be resolved. */ public function getShopForCustomer(ShopperId $customerId): Shop { return $this->shopperService->getShopFor($customerId) ?? throw new \UnexpectedValueException( "No shop found for customer '{$customerId->value}'" ); } /** @return array */ public function getAvailableTimeSlots(Shop $shop): array { return $this->pickupService->getTimeSlotsFor( $shop, $this->getTimeslotsUntil() ); } public function initializeFromBasket(Basket $basket, ShopperId $customerId): void { /** @var OrderBuilder $builder */ $builder = OrderBuilder::fromBasket($basket, $customerId); $this->orderBuilderSessionManager->save($builder); } /** * @throws \Domain\Exceptions\OrderTunnel\MissingOrderBuilderException */ public function getBuilderFromBasketOrFail(): OrderBuilderWithBasket { $builder = $this->orderBuilderSessionManager->get(); if ($builder === null || ! $builder->isFromBasket()) { throw new MissingOrderBuilderException(); } /** @var OrderBuilderWithBasket $builder */ return $builder; } /** * @throws \Domain\Exceptions\OrderTunnel\MissingOrderBuilderException */ public function getBuilderCompletedOrFail(): OrderBuilderCompleted { $builder = $this->orderBuilderSessionManager->get(); if ($builder === null || ! $builder->isCompleted()) { throw new MissingOrderBuilderException(); } /** @var OrderBuilderCompleted $builder */ return $builder; } /** * Validate that the selected date matches an available slot and apply it to the builder. * * @param array $availableSlots */ public function applyTimeSlot( OrderBuilderWithBasket $builder, Shop $shop, CarbonInterface $selectedDate, array $availableSlots ): ?PickupTimeSlot { $matchedSlot = null; foreach ($availableSlots as $ts) { if ($selectedDate->isSameDay($ts->getAt())) { $matchedSlot = $ts; break; } } if ($matchedSlot === null) { return null; } /** @var OrderBuilder $completed */ $completed = $builder->withPickupTimeSlot($shop->id(), $matchedSlot); $this->orderBuilderSessionManager->save($completed); return $matchedSlot; } /** @throws \Domain\Exceptions\OrderTunnel\OrderLimitExceededException */ public function assertBasketAmountWithinLimit(Basket $basket): void { $this->orderLimitPolicy->assertCanPlaceOrder($basket->totalTaxIncluded()); } /** @throws \Domain\Exceptions\OrderTunnel\WeeklyLimitExceededException */ public function assertBasketWeeklyAmountWithinLimitFor( Shopper $shopper, Basket $withBasket, \DateTimeInterface $atDate ): void { $startOfWeek = CarbonImmutable::instance($atDate)->startOfWeek(CarbonInterface::MONDAY); $endOfWeek = CarbonImmutable::instance($atDate)->endOfWeek(CarbonInterface::SUNDAY); $weekTotal = $this->employeeStatisticsService->getWeeklyTotalForEmployee( $shopper->matricule, $startOfWeek, $endOfWeek ); $this->orderLimitPolicy->assertCanPlaceWeeklyOrder($withBasket->totalTaxIncluded(), $weekTotal); } /** * @throws WeeklyLimitExceededException * @throws OrderLimitExceededException */ public function finalizeOrder(OrderBuilderCompleted $builder): Order { $shopperId = $builder->getShopperId(); $basket = $this->basketService->revalidateBasket($builder->getBasket()); $pickupTimeSlot = $builder->getPickupTimeSlot(); $shopId = $builder->getShopId(); $shopper = $this->shopperRepository->getByIdOrFail($shopperId); $this->assertBasketAmountWithinLimit($basket); $this->assertBasketWeeklyAmountWithinLimitFor( $shopper, $basket, $pickupTimeSlot->getAt() ); return $this->transactionManager->execute(function () use ($basket, $shopId, $shopper, $pickupTimeSlot) { $shop = $this->shopRepository->find($shopId); if (! $shop) { throw new \DomainException("Shop with id '{$shopId->value}' not found."); } $order = $this->orderService->createOrderFromBasket( $basket, $pickupTimeSlot, $shopper, $shop ); $this->orderRepository->persist($order); $this->eventDispatcher->dispatch($order->toPlacedEvent()); $this->basketService->empty(); $this->orderBuilderSessionManager->clear(); return $order; }); } private function getTimeslotsUntil(): CarbonInterface { return CarbonImmutable::now()->addDays(self::TIMESLOTS_DAY_TARGET); } }