trip-planner/backend/app/Domain/PlannedItem/Actions/CreatePlannedItemAction.php

105 lines
3.6 KiB
PHP
Raw Normal View History

<?php
namespace App\Domain\PlannedItem\Actions;
use App\Domain\Trip\Services\CalendarSlotService;
use App\Models\PlannableItem;
use App\Models\CalendarSlot;
use App\Models\PlannedItem;
use Carbon\Carbon;
class CreatePlannedItemAction
{
public function __construct(
private CalendarSlotService $calendarSlotService
) {}
/**
* Create a new planned item from datetime parameters
*
* @param array $data ['plannable_item_id', 'trip_id', 'start_datetime', 'end_datetime', 'sort_order'?]
* @return CalendarSlot The calendar slot with loaded planned items
* @throws \Exception
*/
public function execute(array $data): CalendarSlot
{
$startDatetime = Carbon::parse($data['start_datetime']);
$endDatetime = Carbon::parse($data['end_datetime']);
$slotDate = $startDatetime->toDateString();
// Verify the plannable item belongs to the trip
$plannableItem = PlannableItem::findOrFail($data['plannable_item_id']);
if ($plannableItem->trip_id !== $data['trip_id']) {
throw new \Exception('Plannable item does not belong to this trip');
}
// Use database transaction to prevent race conditions
$calendarSlot = \DB::transaction(function () use ($data, $plannableItem, $startDatetime, $endDatetime, $slotDate) {
// Use firstOrCreate to prevent duplicate slots with same times
$calendarSlot = CalendarSlot::firstOrCreate(
[
'trip_id' => $data['trip_id'],
'datetime_start' => $startDatetime,
'datetime_end' => $endDatetime,
'slot_date' => $slotDate,
],
[
'name' => $plannableItem->name,
'slot_order' => 0, // Will be recalculated
]
);
// Recalculate slot orders to ensure consistency
$this->calendarSlotService->recalculateSlotOrdersForDate(
$data['trip_id'],
$slotDate
);
// Create PlannedItem link (use firstOrCreate to prevent race condition duplicates)
PlannedItem::firstOrCreate(
[
'plannable_item_id' => $data['plannable_item_id'],
'calendar_slot_id' => $calendarSlot->id,
],
[
'sort_order' => $data['sort_order'] ?? 0,
]
);
return $calendarSlot;
});
return $calendarSlot->load(['plannedItems.plannableItem']);
}
/**
* Create planned item from existing calendar slot (legacy flow)
*
* @param array $data ['plannable_item_id', 'calendar_slot_id', 'sort_order'?]
* @return PlannedItem
* @throws \Exception
*/
public function executeFromSlot(array $data): PlannedItem
{
// Validate that calendar slot and plannable item belong to the same trip
$plannableItem = PlannableItem::findOrFail($data['plannable_item_id']);
$calendarSlot = CalendarSlot::findOrFail($data['calendar_slot_id']);
if ($plannableItem->trip_id !== $calendarSlot->trip_id) {
throw new \Exception('Calendar slot and plannable item must belong to the same trip');
}
$plannedItem = PlannedItem::updateOrCreate(
[
'plannable_item_id' => $data['plannable_item_id'],
'calendar_slot_id' => $data['calendar_slot_id'],
],
[
'sort_order' => $data['sort_order'] ?? 0,
]
);
return $plannedItem->load(['plannableItem', 'calendarSlot']);
}
}