diff --git a/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php b/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php index 755fffa..05641e2 100644 --- a/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php +++ b/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php @@ -204,8 +204,9 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz } } - // Si on a trouvé un volume précédent et que le suivant est le même ou null, alors utilise le précédent - if ($prevVolume !== null && ($nextVolume === null || $nextVolume === $prevVolume)) { + // Priorité au volume précédent : le chapitre appartient à la fin du volume en cours + // Couvre les cas : milieu de volume (prev=next), transition entre deux volumes (prev≠next) + if ($prevVolume !== null) { $chaptersByNumber[$currentChapterNum] = new Chapter( new ChapterId($currentChapter->getId()), $currentChapter->getMangaId(), @@ -218,8 +219,8 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz $currentChapter->getCreatedAt() ); } - // Si on a trouvé un volume suivant mais pas de précédent, utilise le suivant - elseif ($nextVolume !== null && $prevVolume === null) { + // Sinon utilise le volume suivant (chapitres en début de série) + elseif ($nextVolume !== null) { $chaptersByNumber[$currentChapterNum] = new Chapter( new ChapterId($currentChapter->getId()), $currentChapter->getMangaId(), diff --git a/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php b/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php index 0745ef2..6174506 100644 --- a/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php +++ b/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php @@ -267,10 +267,14 @@ class InMemoryMangaRepository implements MangaRepositoryInterface return []; } - return array_filter( - $this->chapters[$mangaId], - fn (Chapter $chapter) => in_array($chapter->getNumber(), $chapterNumbers) - ); + $result = []; + foreach ($this->chapters[$mangaId] as $chapter) { + if (in_array($chapter->getNumber(), $chapterNumbers)) { + $result[$chapter->getNumber()] = $chapter; + } + } + + return $result; } public function findByMonitoringCriteria(MonitoringCriteria $criteria): array diff --git a/tests/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationServiceTest.php b/tests/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationServiceTest.php new file mode 100644 index 0000000..1523e93 --- /dev/null +++ b/tests/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationServiceTest.php @@ -0,0 +1,201 @@ +client = new InMemoryMangadexClient(); + $this->repository = new InMemoryMangaRepository(); + $this->service = new MangadxChapterSynchronizationService($this->client, $this->repository); + } + + private function makeManga(string $externalId = 'manga-123'): Manga + { + return new Manga( + new MangaId('manga-id'), + new MangaTitle('Test'), + new MangaSlug('test'), + 'Desc', + 'Author', + 2024, + [], + 'ongoing', + new ExternalId($externalId) + ); + } + + private function chapterEntry(string $number, string $lang, ?string $volume): array + { + return [ + 'attributes' => [ + 'chapter' => $number, + 'translatedLanguage' => $lang, + 'title' => "Chapter $number", + 'volume' => $volume, + ], + ]; + } + + /** + * Chapitres sans volume entre deux volumes différents → assignés au volume précédent + * + * Ch1→Vol1, Ch2→null, Ch3→null, Ch4→Vol2 + * Après sync : Ch2 et Ch3 doivent avoir Vol1 + */ + public function testVolumeTransitionIsAssignedToPreviousVolume(): void + { + $manga = $this->makeManga(); + $this->client->addFeed('manga-123', [ + $this->chapterEntry('1', 'en', '1'), + $this->chapterEntry('2', 'en', null), + $this->chapterEntry('3', 'en', null), + $this->chapterEntry('4', 'en', '2'), + ]); + + $this->service->synchronizeChapters($manga); + + $chapters = $this->indexedByNumber($manga->pullNewChapters()); + + $this->assertSame(1, $chapters[1.0]->getVolume(), 'Ch1 doit rester Vol1'); + $this->assertSame(1, $chapters[2.0]->getVolume(), 'Ch2 (transition) doit être assigné à Vol1'); + $this->assertSame(1, $chapters[3.0]->getVolume(), 'Ch3 (transition) doit être assigné à Vol1'); + $this->assertSame(2, $chapters[4.0]->getVolume(), 'Ch4 doit rester Vol2'); + } + + /** + * Chapitres en début de série sans volume → assignés au premier volume trouvé + * + * Ch1→null, Ch2→null, Ch3→Vol1 + * Après sync : Ch1 et Ch2 doivent avoir Vol1 + */ + public function testChaptersWithoutVolumeAtStartGetNextVolume(): void + { + $manga = $this->makeManga(); + $this->client->addFeed('manga-123', [ + $this->chapterEntry('1', 'en', null), + $this->chapterEntry('2', 'en', null), + $this->chapterEntry('3', 'en', '1'), + ]); + + $this->service->synchronizeChapters($manga); + + $chapters = $this->indexedByNumber($manga->pullNewChapters()); + + $this->assertSame(1, $chapters[1.0]->getVolume(), 'Ch1 (début de série) doit prendre Vol1'); + $this->assertSame(1, $chapters[2.0]->getVolume(), 'Ch2 (début de série) doit prendre Vol1'); + $this->assertSame(1, $chapters[3.0]->getVolume(), 'Ch3 doit rester Vol1'); + } + + /** + * Chapitres avec volumes explicites ne sont pas modifiés + * + * Ch1→Vol1, Ch2→Vol1, Ch3→Vol2 → inchangé + */ + public function testChaptersWithExplicitVolumesArePreserved(): void + { + $manga = $this->makeManga(); + $this->client->addFeed('manga-123', [ + $this->chapterEntry('1', 'en', '1'), + $this->chapterEntry('2', 'en', '1'), + $this->chapterEntry('3', 'en', '2'), + ]); + + $this->service->synchronizeChapters($manga); + + $chapters = $this->indexedByNumber($manga->pullNewChapters()); + + $this->assertSame(1, $chapters[1.0]->getVolume()); + $this->assertSame(1, $chapters[2.0]->getVolume()); + $this->assertSame(2, $chapters[3.0]->getVolume()); + } + + /** + * La version française est prioritaire sur l'anglaise + * + * Même chapitre disponible EN (volume 1) et FR (volume 2) → FR gagne + */ + public function testFrenchChaptersTakePriorityOverEnglish(): void + { + $manga = $this->makeManga(); + $this->client->addFeed('manga-123', [ + $this->chapterEntry('1', 'en', '1'), + $this->chapterEntry('1', 'fr', '2'), + ]); + + $this->service->synchronizeChapters($manga); + + $chapters = $this->indexedByNumber($manga->pullNewChapters()); + + $this->assertCount(1, $chapters, 'Un seul chapitre 1 doit exister'); + $this->assertSame(2, $chapters[1.0]->getVolume(), 'La version FR (Vol2) doit prendre la priorité'); + } + + /** + * Seuls les nouveaux chapitres sont sauvegardés (pas les doublons) + * + * Ch1 déjà en DB + Ch2 nouveau → seul Ch2 est retourné + */ + public function testOnlyNewChaptersAreSaved(): void + { + $manga = $this->makeManga(); + + // Pré-peupler la DB avec Ch1 + $existingChapter = new Chapter( + new ChapterId('existing-uuid'), + new MangaId('manga-id'), + 1.0, + 'Chapter 1', + 1, + true + ); + $manga->addChapter($existingChapter); + $this->repository->save($manga); + + // Feed contient Ch1 (déjà en DB) et Ch2 (nouveau) + $this->client->addFeed('manga-123', [ + $this->chapterEntry('1', 'en', '1'), + $this->chapterEntry('2', 'en', '1'), + ]); + + $newChapterIds = $this->service->synchronizeChapters($manga); + + $this->assertCount(1, $newChapterIds, 'Seul Ch2 doit être retourné comme nouveau'); + + $newChapters = $manga->pullNewChapters(); + $this->assertCount(1, $newChapters); + $this->assertSame(2.0, $newChapters[0]->getNumber(), 'Le nouveau chapitre doit être Ch2'); + } + + /** + * @param Chapter[] $chapters + * @return array + */ + private function indexedByNumber(array $chapters): array + { + $result = []; + foreach ($chapters as $chapter) { + $result[$chapter->getNumber()] = $chapter; + } + return $result; + } +}