ldc-boutique-cnc-sable/domain/Services/PickupSchedulingService.php
Jérémy Munsch c40016bba1
feat(order): implement order placement and related DTOs
- Add methods to convert OrderLine and DeliveryAddress to DTOs.
- Introduce toPlacedEvent method in Order to generate domain event.
- Enhance OrderUnit with Regate code mapping.
- Implement tests for new functionality and ensure correctness.
2026-10-29 20:35:13 +01:00

203 lines
7.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Domain\Services;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\CarbonPeriod;
use Carbon\WeekDay;
use Domain\Models\Entities\PickupExceptionOnDate;
use Domain\Models\Entities\Shop;
use Domain\Models\ValueObjects\EditablePickupExceptionForShop;
use Domain\Models\ValueObjects\PickupTimeSlot;
use Domain\Models\ValueObjects\ShopPickupOverride;
use Domain\Ports\Repositories\PickupExceptionRepository;
class PickupSchedulingService
{
public function __construct(
private readonly PickupExceptionRepository $pickupExceptionRepository
) {
}
private const PICKUP_DAY = WeekDay::Friday;
private const CUTOFF_WEEK_OFFSET = 0;
private const CUTOFF_DAY = WeekDay::Monday;
private const CUTOFF_HOUR = 14;
public function getPickupDay(): WeekDay
{
return self::PICKUP_DAY;
}
public function currentCutoff(): CarbonInterface
{
$nextPickup = $this->nextPickup();
return $this->getDefaultDeadline($nextPickup);
}
public function nextPickup(): CarbonInterface
{
$now = $this->getNow();
$startOfThisDay = $now->copy()->startOfDay();
if ($startOfThisDay->isDayOfWeek(self::PICKUP_DAY->value) && $now->hour < self::CUTOFF_HOUR) {
return $startOfThisDay->addHours(self::CUTOFF_HOUR);
}
return $startOfThisDay
->next(self::PICKUP_DAY->value)
->addHours(self::CUTOFF_HOUR);
}
public function getPeriodFromPickUpDay(): CarbonPeriod
{
$nextPickup = $this->nextPickup();
return $this->getPeriodForPickupDate($nextPickup);
}
public function getPeriodForPickupDate(CarbonInterface $pickupDate): CarbonPeriod
{
$pickupDate = CarbonImmutable::createFromInterface($pickupDate);
$defaultDeadLine = $this->getDefaultDeadline($pickupDate);
return CarbonPeriod::create($defaultDeadLine, $pickupDate->endOfDay());
}
/** @return \Domain\Models\ValueObjects\EditablePickupExceptionForShop[] */
public function getEditablePickupExceptionsFor(Shop $shop): array
{
$allExceptions = $this->pickupExceptionRepository->getAllExceptions();
$shopExceptions = $this->pickupExceptionRepository->getExceptionsAssociatedWithShop($shop->id());
return array_map(function (PickupExceptionOnDate $pickupException) use ($shopExceptions) {
$associated = null;
foreach ($shopExceptions as $shopEx) {
if ($shopEx->exception->id->equals($pickupException->id)) {
$associated = $shopEx;
break;
}
}
$isChecked = $associated !== null;
$isPickupDay = $pickupException->isPickupDay(self::PICKUP_DAY);
$defaultDeadline = $this->getDefaultDeadline($pickupException->date);
$isBlocked = $associated !== null && ! $associated->hasReplacement();
$isPastDeadline = $associated !== null && $associated->isDeadlinePassed($this->getNow(), $defaultDeadline);
return new EditablePickupExceptionForShop(
$pickupException,
$isPickupDay,
$isChecked && $isPickupDay && $isBlocked,
$isChecked && $isPickupDay && $isPastDeadline
);
}, $allExceptions);
}
/** @return \Domain\Models\ValueObjects\PickupTimeSlot[] */
public function getTimeSlotsFor(Shop $shop, \DateTimeInterface $until): array
{
$todayDate = $this->getNow()->startOfDay();
$untilDayDate = CarbonImmutable::createFromInterface($until)->startOfDay();
$coveredPeriod = CarbonPeriod::create($todayDate, $untilDayDate);
if ($untilDayDate->lessThanOrEqualTo($todayDate)) {
throw new \InvalidArgumentException('Cannot get time slots for past days');
}
$pickupExceptionList = $this->pickupExceptionRepository->getExceptionsForShop($shop->id(), $coveredPeriod);
$filteredDates = array_filter(
$coveredPeriod->toArray(),
fn (CarbonInterface $date) => $date->isDayOfWeek(self::PICKUP_DAY->value)
);
$mappedSlots = array_map(function (CarbonInterface $date) use ($pickupExceptionList) {
$pickupDayDate = CarbonImmutable::createFromInterface($date);
$shopPickupExceptionForTheDate = null;
foreach ($pickupExceptionList as $ex) {
if ($pickupDayDate->isSameDay($ex->exception->date)) {
$shopPickupExceptionForTheDate = $ex;
break;
}
}
return $shopPickupExceptionForTheDate !== null
? $this->getTimeSlotForException($shopPickupExceptionForTheDate)
: $this->getTimeSlotForPickupDay($pickupDayDate);
}, $filteredDates);
$validSlots = array_filter($mappedSlots);
usort($validSlots, fn (PickupTimeSlot $a, PickupTimeSlot $b) => $a->getAt() <=> $b->getAt());
return $validSlots;
}
protected function getTimeSlotForPickupDay(CarbonInterface $pickupDayDate): ?PickupTimeSlot
{
$defaultDeadline = $this->getDefaultDeadline($pickupDayDate);
// ? Have we reached the default standard deadline ?
return $this->isBefore($defaultDeadline)
// * Nominal pipeline: timeslot matches standard pickup day
? $this->createTimeSlot($pickupDayDate)
: null;
}
protected function getTimeSlotForException(ShopPickupOverride $shopPickupException): ?PickupTimeSlot
{
if (! $shopPickupException->hasReplacement() || $shopPickupException->replacementDate === null) {
return null;
}
$defaultDeadline = $this->getDefaultDeadline(CarbonImmutable::createFromInterface($shopPickupException->exception->date));
$deadline = $shopPickupException->effectiveDeadline($defaultDeadline);
return $this->isBefore($deadline)
? $this->createTimeSlot($shopPickupException->replacementDate->toImmutable())
: null;
}
protected function createTimeSlot(CarbonInterface $forDate): PickupTimeSlot
{
return new PickupTimeSlot(
$forDate->startOfDay(),
\DateInterval::createFromDateString('23 hours 59 minutes 59 seconds')
);
}
protected function getDefaultDeadline(CarbonInterface $pickupDate): CarbonImmutable
{
$deadline = CarbonImmutable::createFromInterface($pickupDate)
->startOfWeek(CarbonInterface::MONDAY)
->addDays(self::CUTOFF_DAY->value - 1)
->addHours(self::CUTOFF_HOUR);
return $this->getCutoffWeekOffset() !== 0
? $deadline->addWeeks($this->getCutoffWeekOffset())
: $deadline;
}
private function getCutoffWeekOffset(): int
{
return self::CUTOFF_WEEK_OFFSET;
}
protected function isBefore(CarbonInterface $date): bool
{
return $this->getNow()->lessThanOrEqualTo($date);
}
protected function getNow(): CarbonImmutable
{
return CarbonImmutable::createFromInterface(new \DateTimeImmutable());
}
}