user(); $setRepository = new FakeSetRepository; $elementRepository = new FakeElementRepository; $scheduleRepository = new FakeScheduleRepository; $set = $setRepository->create(new CreateSetDto( name: 'Bible', creator: $user, )); $genesis = $elementRepository->create(new CreateElementDto( set: $set, name: 'Genesis', kind: 'book', parentElement: null, )); $exodus = $elementRepository->create(new CreateElementDto( set: $set, name: 'Exodus', kind: 'book', parentElement: null, )); $exodusChapterOne = $elementRepository->create( new CreateElementDto( set: $set, name: 'Exodus 1', kind: 'chapter', parentElement: $exodus, ), ); $creation = $elementRepository->create(new CreateElementDto( set: $set, name: 'Creation', kind: 'portion', parentElement: $genesis, )); $genesisChapterOne = $elementRepository->create( new CreateElementDto( set: $set, name: 'Genesis 1', kind: 'chapter', parentElement: $creation, ), ); $genesisChapterTwo = $elementRepository->create( new CreateElementDto( set: $set, name: 'Genesis 2', kind: 'chapter', parentElement: $creation, ), ); $genesisChapterThree = $elementRepository->create( new CreateElementDto( set: $set, name: 'Genesis 3', kind: 'chapter', parentElement: $creation, ), ); $exodusChapterTwo = $elementRepository->create( new CreateElementDto( set: $set, name: 'Exodus 2', kind: 'chapter', parentElement: $exodus, ), ); $schedule = (new CreateSchedule( $setRepository, $elementRepository, $scheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), elementKind: 'chapter', startDate: '2026-08-10', targetDate: '2026-08-12', )); $this->assertSame('chapter', $schedule->getElementKind()); $this->assertSame('2026-08-10', $schedule->getStartDate()->format( 'Y-m-d', )); $this->assertSame('2026-08-12', $schedule->getTargetDate()->format( 'Y-m-d', )); $this->assertSame( [ $genesisChapterOne->getId(), $genesisChapterTwo->getId(), $genesisChapterThree->getId(), $exodusChapterOne->getId(), $exodusChapterTwo->getId(), ], array_map(function ($assignment): int { return $assignment->getElement()->getId(); }, $schedule->getAssignments()), ); $this->assertSame( [ '2026-08-10', '2026-08-10', '2026-08-11', '2026-08-11', '2026-08-12', ], array_map(function ($assignment): string { return $assignment->getScheduledDate()->format('Y-m-d'); }, $schedule->getAssignments()), ); } public function test_it_spreads_sparse_work_across_the_full_range(): void { $user = $this->user(); $setRepository = new FakeSetRepository; $elementRepository = new FakeElementRepository; $set = $setRepository->create(new CreateSetDto( name: 'Course', creator: $user, )); foreach (['First', 'Second', 'Third'] as $name) { $elementRepository->create(new CreateElementDto( set: $set, name: $name, kind: 'lesson', parentElement: null, )); } $schedule = (new CreateSchedule( $setRepository, $elementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), elementKind: 'lesson', startDate: '2026-08-10', targetDate: '2026-08-16', )); $this->assertSame( ['2026-08-10', '2026-08-13', '2026-08-16'], array_map(function ($assignment): string { return $assignment->getScheduledDate()->format('Y-m-d'); }, $schedule->getAssignments()), ); } public function test_it_places_one_element_on_the_start_date(): void { $user = $this->user(); $setRepository = new FakeSetRepository; $elementRepository = new FakeElementRepository; $set = $setRepository->create(new CreateSetDto( name: 'Project', creator: $user, )); $elementRepository->create(new CreateElementDto( set: $set, name: 'Ship it', kind: 'milestone', parentElement: null, )); $schedule = (new CreateSchedule( $setRepository, $elementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), elementKind: 'milestone', startDate: '2020-01-01', targetDate: '2030-01-01', )); $this->assertSame( '2020-01-01', $schedule->getAssignments()[0] ->getScheduledDate() ->format('Y-m-d'), ); } public function test_it_rejects_an_unknown_set(): void { $this->expectException(NotFoundException::class); $this->expectExceptionMessage('set not found'); (new CreateSchedule( new FakeSetRepository, new FakeElementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $this->user(), setId: 999, elementKind: 'chapter', startDate: '2026-08-10', targetDate: '2026-08-12', )); } public function test_it_rejects_an_unavailable_element_kind(): void { $user = $this->user(); $setRepository = new FakeSetRepository; $set = $setRepository->create(new CreateSetDto( name: 'Bible', creator: $user, )); $this->expectException(BadRequestException::class); $this->expectExceptionMessage( 'elementKind is not available for set', ); (new CreateSchedule( $setRepository, new FakeElementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), elementKind: 'Chapter', startDate: '2026-08-10', targetDate: '2026-08-12', )); } public function test_it_rejects_invalid_dates(): void { $user = $this->user(); $setRepository = new FakeSetRepository; $set = $setRepository->create(new CreateSetDto( name: 'Bible', creator: $user, )); $this->expectException(BadRequestException::class); $this->expectExceptionMessage('startDate must be a valid date'); (new CreateSchedule( $setRepository, new FakeElementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), elementKind: 'chapter', startDate: '2026-02-30', targetDate: '2026-08-12', )); } public function test_it_rejects_a_target_before_the_start(): void { $user = $this->user(); $setRepository = new FakeSetRepository; $set = $setRepository->create(new CreateSetDto( name: 'Bible', creator: $user, )); $this->expectException(BadRequestException::class); $this->expectExceptionMessage( 'targetDate must not be before startDate', ); (new CreateSchedule( $setRepository, new FakeElementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), elementKind: 'chapter', startDate: '2026-08-12', targetDate: '2026-08-10', )); } private function user(): User { return new User( id: 7, email: new EmailAddress('reader@example.com'), passwordHash: 'hashed-password', ); } }