user = User::factory()->create(); $this->trip = Trip::factory()->create([ 'created_by_user_id' => $this->user->id, 'start_date' => '2025-01-15', 'end_date' => '2025-01-17', ]); $this->plannableItem = PlannableItem::factory()->create([ 'trip_id' => $this->trip->id, 'name' => 'Eiffel Tower', ]); } /** @test */ public function it_creates_calendar_slot_when_scheduling_item_with_datetime() { $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $this->plannableItem->id, 'trip_id' => $this->trip->id, 'start_datetime' => '2025-01-15 14:00:00', 'end_datetime' => '2025-01-15 16:00:00', ]); $response->assertStatus(201); $this->assertDatabaseHas('calendar_slots', [ 'trip_id' => $this->trip->id, 'name' => 'Eiffel Tower', 'slot_date' => '2025-01-15', ]); $this->assertDatabaseHas('planned_items', [ 'plannable_item_id' => $this->plannableItem->id, ]); } /** @test */ public function it_calculates_slot_order_correctly() { // Create first slot at 10:00 $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $this->plannableItem->id, 'trip_id' => $this->trip->id, 'start_datetime' => '2025-01-15 10:00:00', 'end_datetime' => '2025-01-15 11:00:00', ]); // Create another plannable item $secondItem = PlannableItem::factory()->create([ 'trip_id' => $this->trip->id, 'name' => 'Louvre', ]); // Create second slot at 14:00 $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $secondItem->id, 'trip_id' => $this->trip->id, 'start_datetime' => '2025-01-15 14:00:00', 'end_datetime' => '2025-01-15 16:00:00', ]); // Create third slot at 08:00 (should have slot_order 0 after recalculation) $thirdItem = PlannableItem::factory()->create([ 'trip_id' => $this->trip->id, 'name' => 'Breakfast', ]); $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $thirdItem->id, 'trip_id' => $this->trip->id, 'start_datetime' => '2025-01-15 08:00:00', 'end_datetime' => '2025-01-15 09:00:00', ]); $slots = CalendarSlot::where('trip_id', $this->trip->id) ->where('slot_date', '2025-01-15') ->orderBy('slot_order') ->get(); // After recalculation, order should be by datetime_start $this->assertEquals('Breakfast', $slots[0]->name); $this->assertEquals(0, $slots[0]->slot_order); $this->assertEquals('Eiffel Tower', $slots[1]->name); $this->assertEquals(1, $slots[1]->slot_order); $this->assertEquals('Louvre', $slots[2]->name); $this->assertEquals(2, $slots[2]->slot_order); } /** @test */ public function it_validates_end_time_is_after_start_time() { $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $this->plannableItem->id, 'trip_id' => $this->trip->id, 'start_datetime' => '2025-01-15 16:00:00', 'end_datetime' => '2025-01-15 14:00:00', ]); $response->assertStatus(422); } /** @test */ public function it_requires_plannable_item_to_belong_to_trip() { $otherTrip = Trip::factory()->create([ 'created_by_user_id' => $this->user->id, ]); $otherItem = PlannableItem::factory()->create([ 'trip_id' => $otherTrip->id, ]); $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $otherItem->id, 'trip_id' => $this->trip->id, 'start_datetime' => '2025-01-15 14:00:00', 'end_datetime' => '2025-01-15 16:00:00', ]); $response->assertStatus(422); $response->assertJson([ 'error' => 'Plannable item does not belong to this trip' ]); } /** @test */ public function it_supports_backward_compatible_calendar_slot_id_flow() { $calendarSlot = CalendarSlot::factory()->create([ 'trip_id' => $this->trip->id, ]); $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $this->plannableItem->id, 'calendar_slot_id' => $calendarSlot->id, ]); $response->assertStatus(201); $this->assertDatabaseHas('planned_items', [ 'plannable_item_id' => $this->plannableItem->id, 'calendar_slot_id' => $calendarSlot->id, ]); } /** @test */ public function it_requires_authentication_to_create_planned_item() { $response = $this->postJson('/api/planned-items', [ 'plannable_item_id' => $this->plannableItem->id, 'trip_id' => $this->trip->id, 'start_datetime' => '2025-01-15 14:00:00', 'end_datetime' => '2025-01-15 16:00:00', ]); $response->assertStatus(401); } /** @test */ public function it_validates_required_fields() { $response = $this->actingAs($this->user)->postJson('/api/planned-items', []); $response->assertStatus(422); $response->assertJsonValidationErrors(['plannable_item_id', 'trip_id', 'start_datetime', 'end_datetime']); } /** @test */ public function it_validates_plannable_item_exists() { $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => 99999, 'trip_id' => $this->trip->id, 'start_datetime' => '2025-01-15 14:00:00', 'end_datetime' => '2025-01-15 16:00:00', ]); $response->assertStatus(422); $response->assertJsonValidationErrors(['plannable_item_id']); } /** @test */ public function it_validates_trip_exists() { $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $this->plannableItem->id, 'trip_id' => 99999, 'start_datetime' => '2025-01-15 14:00:00', 'end_datetime' => '2025-01-15 16:00:00', ]); $response->assertStatus(422); $response->assertJsonValidationErrors(['trip_id']); } /** @test */ public function it_returns_calendar_slot_with_relationships() { $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $this->plannableItem->id, 'trip_id' => $this->trip->id, 'start_datetime' => '2025-01-15 14:00:00', 'end_datetime' => '2025-01-15 16:00:00', ]); $response->assertStatus(201); $response->assertJsonStructure([ 'id', 'trip_id', 'name', 'datetime_start', 'datetime_end', 'slot_date', 'slot_order', 'planned_items' => [ '*' => [ 'id', 'plannable_item_id', 'calendar_slot_id', 'plannable_item' => [ 'id', 'name', 'trip_id', ] ] ] ]); } /** @test */ public function it_can_update_planned_item_slot() { $calendarSlot1 = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); $calendarSlot2 = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); $plannedItem = PlannedItem::create([ 'plannable_item_id' => $this->plannableItem->id, 'calendar_slot_id' => $calendarSlot1->id, 'sort_order' => 0, ]); $response = $this->actingAs($this->user)->putJson("/api/planned-items/{$plannedItem->id}", [ 'calendar_slot_id' => $calendarSlot2->id, 'sort_order' => 5, ]); $response->assertStatus(200); $this->assertDatabaseHas('planned_items', [ 'id' => $plannedItem->id, 'calendar_slot_id' => $calendarSlot2->id, 'sort_order' => 5, ]); } /** @test */ public function it_can_delete_planned_item() { $calendarSlot = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); $plannedItem = PlannedItem::create([ 'plannable_item_id' => $this->plannableItem->id, 'calendar_slot_id' => $calendarSlot->id, 'sort_order' => 0, ]); $response = $this->actingAs($this->user)->deleteJson("/api/planned-items/{$plannedItem->id}"); $response->assertStatus(204); $this->assertDatabaseMissing('planned_items', [ 'id' => $plannedItem->id, ]); } /** @test */ public function it_requires_authentication_to_update_planned_item() { $calendarSlot = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); $plannedItem = PlannedItem::create([ 'plannable_item_id' => $this->plannableItem->id, 'calendar_slot_id' => $calendarSlot->id, ]); $response = $this->putJson("/api/planned-items/{$plannedItem->id}", [ 'sort_order' => 1, ]); $response->assertStatus(401); } /** @test */ public function it_requires_authentication_to_delete_planned_item() { $calendarSlot = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); $plannedItem = PlannedItem::create([ 'plannable_item_id' => $this->plannableItem->id, 'calendar_slot_id' => $calendarSlot->id, ]); $response = $this->deleteJson("/api/planned-items/{$plannedItem->id}"); $response->assertStatus(401); } }