user(1, 'reader@example.com'); $repository = new FakeScheduleRepository; $schedule = $repository->create($this->schedule($user)); $assignments = $schedule->getAssignments(); $firstCompletion = $this->utc('2026-08-15T10:00:00'); $secondCompletion = $this->utc('2026-08-16T11:00:00'); $assignments[1]->complete($firstCompletion); $assignments[3]->complete($secondCompletion); $repository->updateAssignment($assignments[1]); $repository->updateAssignment($assignments[3]); $rescheduled = $this->useCase($repository)->execute( new RescheduleScheduleRequest( scheduleId: $schedule->getId(), user: $user, startDate: '2026-08-20', targetDate: '2026-08-21', workloadPlacement: 'end', ), ); $this->assertSame('2026-08-20', $rescheduled->getStartDate() ->format('Y-m-d')); $this->assertSame('2026-08-21', $rescheduled->getTargetDate() ->format('Y-m-d')); $this->assertSame( [ '2026-08-20', '2026-08-12', '2026-08-21', '2026-08-14', '2026-08-21', ], array_map(function (ScheduleAssignment $assignment): string { return $assignment->getScheduledDate()->format('Y-m-d'); }, $rescheduled->getAssignments()), ); $this->assertSame( [null, $firstCompletion, null, $secondCompletion, null], array_map(function ( ScheduleAssignment $assignment, ): ?DateTimeImmutable { return $assignment->getCompletedAt(); }, $rescheduled->getAssignments()), ); $this->assertSame( [1, 2, 3, 4, 5], array_map(function (ScheduleAssignment $assignment): int { return $assignment->getId(); }, $rescheduled->getAssignments()), ); $this->assertSame( ['Lesson 1', 'Lesson 2', 'Lesson 3', 'Lesson 4', 'Lesson 5'], array_map(function (ScheduleAssignment $assignment): string { return $assignment->getName(); }, $rescheduled->getAssignments()), ); } public function test_it_rejects_a_target_before_the_start(): void { $user = $this->user(1, 'reader@example.com'); $repository = new FakeScheduleRepository; $schedule = $repository->create($this->schedule($user)); $this->expectException(BadRequestException::class); $this->expectExceptionMessage( 'targetDate must not be before startDate', ); $this->useCase($repository)->execute( new RescheduleScheduleRequest( scheduleId: $schedule->getId(), user: $user, startDate: '2026-08-21', targetDate: '2026-08-20', workloadPlacement: 'middle', ), ); } public function test_it_rejects_an_unknown_workload_placement(): void { $user = $this->user(1, 'reader@example.com'); $repository = new FakeScheduleRepository; $schedule = $repository->create($this->schedule($user)); $this->expectException(BadRequestException::class); $this->expectExceptionMessage( 'workloadPlacement must be start, middle, or end', ); $this->useCase($repository)->execute( new RescheduleScheduleRequest( scheduleId: $schedule->getId(), user: $user, startDate: '2026-08-20', targetDate: '2026-08-21', workloadPlacement: 'sideways', ), ); } public function test_it_rejects_a_schedule_without_unfinished_work(): void { $user = $this->user(1, 'reader@example.com'); $repository = new FakeScheduleRepository; $schedule = $repository->create($this->schedule($user)); foreach ($schedule->getAssignments() as $assignment) { $assignment->complete($this->utc('2026-08-15T10:00:00')); $repository->updateAssignment($assignment); } $this->expectException(BadRequestException::class); $this->expectExceptionMessage( 'schedule has no unfinished assignments', ); $this->useCase($repository)->execute( new RescheduleScheduleRequest( scheduleId: $schedule->getId(), user: $user, startDate: '2026-08-20', targetDate: '2026-08-21', workloadPlacement: 'middle', ), ); } public function test_it_hides_another_users_schedule(): void { $repository = new FakeScheduleRepository; $owner = $this->user(1, 'owner@example.com'); $schedule = $repository->create($this->schedule($owner)); $this->expectException(NotFoundException::class); $this->expectExceptionMessage('schedule not found'); $this->useCase($repository)->execute( new RescheduleScheduleRequest( scheduleId: $schedule->getId(), user: $this->user(2, 'viewer@example.com'), startDate: '2026-08-20', targetDate: '2026-08-21', workloadPlacement: 'middle', ), ); } private function useCase( FakeScheduleRepository $repository, ): RescheduleSchedule { return new RescheduleSchedule( $repository, new EvenDistributionScheduler, ); } private function schedule(User $user): CreateScheduleDto { $assignments = []; foreach (range(1, 5) as $number) { $assignments[] = new CreateScheduleAssignmentDto( name: "Lesson {$number}", kind: 'lesson', path: ['Course', "Lesson {$number}"], scheduledDate: $this->utc("2026-08-1{$number}"), position: $number, ); } return new CreateScheduleDto( user: $user, setName: 'Course', elementKind: 'lesson', startDate: $this->utc('2026-08-10'), targetDate: $this->utc('2026-08-15'), assignments: $assignments, ); } private function user(int $id, string $email): User { return new User( id: $id, email: new EmailAddress($email), passwordHash: 'hashed-password', ); } private function utc(string $value): DateTimeImmutable { return new DateTimeImmutable($value, new DateTimeZone('UTC')); } }