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']); } }