availability = Mockery::mock(AvailabilityRepository::class); $this->reconciler = new ClassSlotReconciler($this->availability); } private function groupClass(?string $termEnd = null): Offering { return new Offering( instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Choir', durationMinutes: 60, termStart: '2026-09-08', termEnd: $termEnd ?? '2026-09-08', classTime: '16:00:00', id: 8, ); } public function testRemovesOpenSlotsOverlappingASession(): void { $open = new AvailabilitySlot(3, '2026-09-08 16:00:00', '2026-09-08 17:00:00', 60, id: 12); $this->availability->shouldReceive('findOverlapping') ->once()->with(3, '2026-09-08 16:00:00', '2026-09-08 17:00:00') ->andReturn([$open]); $this->availability->shouldReceive('delete')->once()->with(12)->andReturn(true); $result = $this->reconciler->reconcile($this->groupClass()); self::assertSame(1, $result['removed']); self::assertSame([], $result['conflicts']); } public function testReportsBookedSlotAsConflictWithoutDeleting(): void { $booked = new AvailabilitySlot(3, '2026-09-08 16:00:00', '2026-09-08 17:00:00', 60, isBooked: true, id: 12); $this->availability->shouldReceive('findOverlapping')->once()->andReturn([$booked]); // A booked lesson is never deleted. $this->availability->shouldReceive('delete')->never(); $result = $this->reconciler->reconcile($this->groupClass()); self::assertSame(0, $result['removed']); self::assertSame(['2026-09-08 16:00:00'], $result['conflicts']); } public function testWeeklyClassReconcilesEverySession(): void { // Three weekly sessions from Sep 8 to Sep 22. $this->availability->shouldReceive('findOverlapping') ->once()->with(3, '2026-09-08 16:00:00', '2026-09-08 17:00:00')->andReturn([]); $this->availability->shouldReceive('findOverlapping') ->once()->with(3, '2026-09-15 16:00:00', '2026-09-15 17:00:00')->andReturn([]); $this->availability->shouldReceive('findOverlapping') ->once()->with(3, '2026-09-22 16:00:00', '2026-09-22 17:00:00')->andReturn([]); $result = $this->reconciler->reconcile($this->groupClass('2026-09-22')); self::assertSame(0, $result['removed']); } public function testUnscheduledClassIsSkipped(): void { // No class time set — nothing to reconcile. $offering = new Offering( instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Choir', durationMinutes: 60, termStart: '2026-09-08', id: 8, ); $this->availability->shouldReceive('findOverlapping')->never(); $result = $this->reconciler->reconcile($offering); self::assertSame(['removed' => 0, 'conflicts' => []], $result); } public function testPrivateLessonOfferingIsSkipped(): void { $offering = new Offering( instructorId: 3, kind: Offering::KIND_PRIVATE_LESSON, title: '30 min', durationMinutes: 30, termStart: '2026-09-08', termEnd: '2026-09-08', classTime: '16:00:00', id: 8, ); $this->availability->shouldReceive('findOverlapping')->never(); $result = $this->reconciler->reconcile($offering); self::assertSame(['removed' => 0, 'conflicts' => []], $result); } }