diff --git a/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js b/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js
index 1b73960..427b374 100644
--- a/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js
+++ b/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js
@@ -23,8 +23,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
url += `&status=${status.join(',')}`;
}
- console.log('Fetching jobs from URL:', url);
-
const response = await fetch(url);
if (!response.ok) {
@@ -32,7 +30,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
}
const data = await response.json();
- console.log('API Response:', data);
// Gérer différents formats de réponse API
let jobs, total, currentPage, limit_returned, hasNext, hasPrev;
@@ -63,15 +60,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
hasPrev = !!data.hasPreviousPage;
}
- console.log('Processed data:', {
- jobs: jobs.length,
- total,
- currentPage,
- limit_returned,
- hasNext,
- hasPrev
- });
-
return new JobCollection(
jobs,
total,
@@ -81,7 +69,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
hasPrev
);
} catch (error) {
- console.error('API Error:', error);
throw error;
}
}
@@ -102,7 +89,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
const data = await response.json();
return Job.create(data);
} catch (error) {
- console.error('API Error:', error);
throw error;
}
}
@@ -124,7 +110,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
return true;
} catch (error) {
- console.error('API Error:', error);
throw error;
}
}
@@ -158,7 +143,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
const data = await response.json();
return data.deleted || 0;
} catch (error) {
- console.error('API Error:', error);
throw error;
}
}
diff --git a/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue b/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue
index a5459fb..c298851 100644
--- a/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue
+++ b/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue
@@ -11,16 +11,6 @@
-
-
- Debug Pagination:
- Total: {{ activityStore.total }},
- Limit: {{ activityStore.limit }},
- Pages: {{ activityStore.totalPages }},
- Page courante: {{ activityStore.currentPage }},
- Condition: {{ activityStore.total > activityStore.limit }}
-
-
diff --git a/src/Domain/Conversion/Infrastructure/ApiPlatform/Controller/ConvertFileController.php b/src/Domain/Conversion/Infrastructure/ApiPlatform/Controller/ConvertFileController.php
index 6ff189f..7517aa5 100644
--- a/src/Domain/Conversion/Infrastructure/ApiPlatform/Controller/ConvertFileController.php
+++ b/src/Domain/Conversion/Infrastructure/ApiPlatform/Controller/ConvertFileController.php
@@ -48,6 +48,7 @@ final class ConvertFileController extends AbstractController
// Retourner le fichier converti
$fileContent = file_get_contents($response->convertedFilePath);
+ @unlink($response->convertedFilePath);
return new Response(
content: $fileContent,
diff --git a/src/Domain/Shared/Application/Command/DeleteJob.php b/src/Domain/Shared/Application/Command/DeleteJob.php
new file mode 100644
index 0000000..cfe46d8
--- /dev/null
+++ b/src/Domain/Shared/Application/Command/DeleteJob.php
@@ -0,0 +1,15 @@
+jobRepository->get($command->id);
+ $this->jobRepository->delete($command->id);
+ }
+}
diff --git a/src/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandler.php b/src/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandler.php
new file mode 100644
index 0000000..1162547
--- /dev/null
+++ b/src/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandler.php
@@ -0,0 +1,49 @@
+statuses) {
+ $criteria['statuses'] = $command->statuses;
+ }
+
+ if (null !== $command->type) {
+ $criteria['type'] = $command->type;
+ }
+
+ if (null !== $command->createdAfter) {
+ $criteria['createdAfter'] = $command->createdAfter;
+ }
+
+ if (null !== $command->createdBefore) {
+ $criteria['createdBefore'] = $command->createdBefore;
+ }
+
+ $this->jobRepository->deleteByCriteria($criteria);
+ }
+}
diff --git a/src/Domain/Shared/Application/Query/GetJobById.php b/src/Domain/Shared/Application/Query/GetJobById.php
new file mode 100644
index 0000000..19fa20f
--- /dev/null
+++ b/src/Domain/Shared/Application/Query/GetJobById.php
@@ -0,0 +1,15 @@
+jobRepository->get($query->id);
+
+ return JobResponse::fromJob($job);
+ }
+}
diff --git a/src/Domain/Shared/Application/Response/JobResponse.php b/src/Domain/Shared/Application/Response/JobResponse.php
new file mode 100644
index 0000000..4d51006
--- /dev/null
+++ b/src/Domain/Shared/Application/Response/JobResponse.php
@@ -0,0 +1,41 @@
+id,
+ type: $job->type,
+ status: $job->status->value,
+ createdAt: $job->createdAt,
+ startedAt: $job->startedAt,
+ completedAt: $job->completedAt,
+ failureReason: $job->failureReason,
+ attempts: $job->attempts,
+ maxAttempts: $job->maxAttempts,
+ context: $job->context
+ );
+ }
+}
diff --git a/src/Domain/Shared/Domain/Contract/JobRepositoryInterface.php b/src/Domain/Shared/Domain/Contract/JobRepositoryInterface.php
index 029357e..dcff019 100644
--- a/src/Domain/Shared/Domain/Contract/JobRepositoryInterface.php
+++ b/src/Domain/Shared/Domain/Contract/JobRepositoryInterface.php
@@ -41,4 +41,17 @@ interface JobRepositoryInterface
* } $criteria
*/
public function countByCriteria(array $criteria): int;
+
+ public function delete(string $id): void;
+
+ /**
+ * @param array{
+ * statuses?: ?array,
+ * type?: ?string,
+ * createdAfter?: ?\DateTimeImmutable,
+ * createdBefore?: ?\DateTimeImmutable
+ * } $criteria
+ * @return int Nombre de jobs supprimés
+ */
+ public function deleteByCriteria(array $criteria): int;
}
diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/Resource/GetJobListResource.php b/src/Domain/Shared/Infrastructure/ApiPlatform/Resource/GetJobListResource.php
index f733596..e24ea57 100644
--- a/src/Domain/Shared/Infrastructure/ApiPlatform/Resource/GetJobListResource.php
+++ b/src/Domain/Shared/Infrastructure/ApiPlatform/Resource/GetJobListResource.php
@@ -7,9 +7,16 @@ namespace App\Domain\Shared\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Delete;
+use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
+use App\Domain\Shared\Application\Response\JobResponse;
use App\Domain\Shared\Domain\Model\JobStatus;
+use App\Domain\Shared\Infrastructure\ApiPlatform\State\Processor\DeleteJobProcessor;
+use App\Domain\Shared\Infrastructure\ApiPlatform\State\Processor\DeleteJobsByCriteriaProcessor;
+use App\Domain\Shared\Infrastructure\ApiPlatform\State\Provider\DeleteJobProvider;
use App\Domain\Shared\Infrastructure\ApiPlatform\State\Provider\GetJobListStateProvider;
+use App\Domain\Shared\Infrastructure\ApiPlatform\State\Provider\GetJobStateProvider;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
@@ -105,7 +112,56 @@ use Symfony\Component\Validator\Constraints as Assert;
]
]
]
- )
+ ),
+ new Get(
+ uriTemplate: '/jobs/{id}',
+ provider: GetJobStateProvider::class,
+ output: GetJobListResource::class,
+ description: 'Récupère un job par son identifiant',
+ ),
+ new Delete(
+ uriTemplate: '/jobs/{id}',
+ provider: DeleteJobProvider::class,
+ processor: DeleteJobProcessor::class,
+ description: 'Supprime un job par son identifiant',
+ ),
+ new Delete(
+ uriTemplate: '/jobs',
+ processor: DeleteJobsByCriteriaProcessor::class,
+ description: 'Supprime les jobs correspondant aux critères',
+ openapiContext: [
+ 'parameters' => [
+ [
+ 'name' => 'status',
+ 'in' => 'query',
+ 'description' => 'Filtrer par statut(s) (virgule-séparés ou tableau)',
+ 'required' => false,
+ 'schema' => ['type' => 'string'],
+ ],
+ [
+ 'name' => 'type',
+ 'in' => 'query',
+ 'description' => 'Filtrer par type de job',
+ 'required' => false,
+ 'schema' => ['type' => 'string'],
+ ],
+ [
+ 'name' => 'createdAfter',
+ 'in' => 'query',
+ 'description' => 'Date de création minimum (ISO8601)',
+ 'required' => false,
+ 'schema' => ['type' => 'string', 'format' => 'date-time'],
+ ],
+ [
+ 'name' => 'createdBefore',
+ 'in' => 'query',
+ 'description' => 'Date de création maximum (ISO8601)',
+ 'required' => false,
+ 'schema' => ['type' => 'string', 'format' => 'date-time'],
+ ],
+ ]
+ ]
+ ),
]
)]
class GetJobListResource
@@ -157,4 +213,20 @@ class GetJobListResource
context: $job->context
);
}
+
+ public static function fromJobResponse(JobResponse $response): self
+ {
+ return new self(
+ id: $response->id,
+ type: $response->type,
+ status: $response->status,
+ createdAt: $response->createdAt,
+ startedAt: $response->startedAt,
+ completedAt: $response->completedAt,
+ failureReason: $response->failureReason,
+ attempts: $response->attempts,
+ maxAttempts: $response->maxAttempts,
+ context: $response->context
+ );
+ }
}
diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobProcessor.php b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobProcessor.php
new file mode 100644
index 0000000..0a56fc9
--- /dev/null
+++ b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobProcessor.php
@@ -0,0 +1,29 @@
+handler->handle(new DeleteJob($uriVariables['id']));
+ } catch (JobNotFoundException $e) {
+ throw new NotFoundHttpException($e->getMessage(), $e);
+ }
+ }
+}
diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobsByCriteriaProcessor.php b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobsByCriteriaProcessor.php
new file mode 100644
index 0000000..f8e348d
--- /dev/null
+++ b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobsByCriteriaProcessor.php
@@ -0,0 +1,54 @@
+requestStack->getCurrentRequest();
+ $filters = $request ? $request->query->all() : [];
+
+ $statuses = null;
+ if (isset($filters['status'])) {
+ $statusValues = is_array($filters['status'])
+ ? $filters['status']
+ : explode(',', $filters['status']);
+
+ $statuses = [];
+ foreach ($statusValues as $value) {
+ try {
+ $statuses[] = JobStatus::from($value);
+ } catch (\ValueError) {
+ // ignore invalid values
+ }
+ }
+
+ if (empty($statuses)) {
+ $statuses = null;
+ }
+ }
+
+ $this->handler->handle(new DeleteJobsByCriteria(
+ statuses: $statuses,
+ type: $filters['type'] ?? null,
+ createdAfter: isset($filters['createdAfter']) ? new \DateTimeImmutable($filters['createdAfter']) : null,
+ createdBefore: isset($filters['createdBefore']) ? new \DateTimeImmutable($filters['createdBefore']) : null,
+ ));
+ }
+}
diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/DeleteJobProvider.php b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/DeleteJobProvider.php
new file mode 100644
index 0000000..048c330
--- /dev/null
+++ b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/DeleteJobProvider.php
@@ -0,0 +1,32 @@
+handler->handle(new GetJobById($uriVariables['id']));
+ } catch (JobNotFoundException $e) {
+ throw new NotFoundHttpException($e->getMessage(), $e);
+ }
+
+ return GetJobListResource::fromJobResponse($response);
+ }
+}
diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/GetJobStateProvider.php b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/GetJobStateProvider.php
new file mode 100644
index 0000000..c5e3ad9
--- /dev/null
+++ b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/GetJobStateProvider.php
@@ -0,0 +1,32 @@
+handler->handle(new GetJobById($uriVariables['id']));
+ } catch (JobNotFoundException $e) {
+ throw new NotFoundHttpException($e->getMessage(), $e);
+ }
+
+ return GetJobListResource::fromJobResponse($response);
+ }
+}
diff --git a/src/Domain/Shared/Infrastructure/Persistence/Repository/DoctrineJobRepository.php b/src/Domain/Shared/Infrastructure/Persistence/Repository/DoctrineJobRepository.php
index 65814fe..f875340 100644
--- a/src/Domain/Shared/Infrastructure/Persistence/Repository/DoctrineJobRepository.php
+++ b/src/Domain/Shared/Infrastructure/Persistence/Repository/DoctrineJobRepository.php
@@ -188,4 +188,52 @@ readonly class DoctrineJobRepository implements JobRepositoryInterface
return (int) $qb->getQuery()->getSingleScalarResult();
}
+
+ public function delete(string $id): void
+ {
+ $entity = $this->entityManager->find(JobEntity::class, $id);
+
+ if (null === $entity) {
+ throw JobNotFoundException::withId($id);
+ }
+
+ $this->entityManager->remove($entity);
+ $this->entityManager->flush();
+ }
+
+ public function deleteByCriteria(array $criteria): int
+ {
+ $qb = $this->entityManager->createQueryBuilder()
+ ->delete(JobEntity::class, 'j');
+
+ if (isset($criteria['statuses']) && is_array($criteria['statuses']) && !empty($criteria['statuses'])) {
+ $expr = $qb->expr()->orX();
+ foreach ($criteria['statuses'] as $key => $status) {
+ $paramName = 'status' . $key;
+ $expr->add($qb->expr()->eq('j.status', ':' . $paramName));
+ $qb->setParameter($paramName, $status->value);
+ }
+ $qb->andWhere($expr);
+ } elseif (isset($criteria['status'])) {
+ $qb->andWhere('j.status = :status')
+ ->setParameter('status', $criteria['status']->value);
+ }
+
+ if (isset($criteria['type'])) {
+ $qb->andWhere('j.type = :type')
+ ->setParameter('type', $criteria['type']);
+ }
+
+ if (isset($criteria['createdAfter'])) {
+ $qb->andWhere('j.createdAt >= :createdAfter')
+ ->setParameter('createdAfter', $criteria['createdAfter']);
+ }
+
+ if (isset($criteria['createdBefore'])) {
+ $qb->andWhere('j.createdAt <= :createdBefore')
+ ->setParameter('createdBefore', $criteria['createdBefore']);
+ }
+
+ return (int) $qb->getQuery()->execute();
+ }
}
diff --git a/tests/Domain/Shared/Adapter/InMemoryJobRepository.php b/tests/Domain/Shared/Adapter/InMemoryJobRepository.php
index 7a402ff..118bf0b 100644
--- a/tests/Domain/Shared/Adapter/InMemoryJobRepository.php
+++ b/tests/Domain/Shared/Adapter/InMemoryJobRepository.php
@@ -3,6 +3,7 @@
namespace App\Tests\Domain\Shared\Adapter;
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
+use App\Domain\Shared\Domain\Exception\JobNotFoundException;
use App\Domain\Shared\Domain\Model\Job;
use App\Domain\Shared\Domain\Model\JobStatus;
@@ -97,6 +98,26 @@ class InMemoryJobRepository implements JobRepositoryInterface
return count($this->findByCriteria($criteria));
}
+ public function delete(string $id): void
+ {
+ if (!isset($this->jobs[$id])) {
+ throw JobNotFoundException::withId($id);
+ }
+
+ unset($this->jobs[$id]);
+ }
+
+ public function deleteByCriteria(array $criteria): int
+ {
+ $toDelete = $this->findByCriteria($criteria);
+
+ foreach ($toDelete as $job) {
+ unset($this->jobs[$job->id]);
+ }
+
+ return count($toDelete);
+ }
+
public function clear(): void
{
$this->jobs = [];
diff --git a/tests/Domain/Shared/Application/CommandHandler/DeleteJobHandlerTest.php b/tests/Domain/Shared/Application/CommandHandler/DeleteJobHandlerTest.php
new file mode 100644
index 0000000..f7a10a2
--- /dev/null
+++ b/tests/Domain/Shared/Application/CommandHandler/DeleteJobHandlerTest.php
@@ -0,0 +1,41 @@
+repository = new InMemoryJobRepository();
+ $this->handler = new DeleteJobHandler($this->repository);
+ }
+
+ public function test_it_deletes_existing_job(): void
+ {
+ $job = new ScrapingJob('job-1', 'manga-1', 1.0, 'source-1');
+ $this->repository->save($job);
+
+ $this->handler->handle(new DeleteJob('job-1'));
+
+ $this->assertNull($this->repository->get('job-1'));
+ }
+
+ public function test_it_throws_when_job_not_found(): void
+ {
+ $this->expectException(JobNotFoundException::class);
+
+ $this->handler->handle(new DeleteJob('non-existent-id'));
+ }
+}
diff --git a/tests/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandlerTest.php b/tests/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandlerTest.php
new file mode 100644
index 0000000..bf68208
--- /dev/null
+++ b/tests/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandlerTest.php
@@ -0,0 +1,72 @@
+repository = new InMemoryJobRepository();
+ $this->handler = new DeleteJobsByCriteriaHandler($this->repository);
+ }
+
+ public function test_it_deletes_jobs_by_status(): void
+ {
+ $job1 = new ScrapingJob('job-1', 'manga-1', 1.0, 'source-1');
+ $job2 = new ScrapingJob('job-2', 'manga-1', 2.0, 'source-1');
+ $job3 = new ScrapingJob('job-3', 'manga-1', 3.0, 'source-1');
+ $job2->complete();
+ $job3->complete();
+
+ $this->repository->save($job1);
+ $this->repository->save($job2);
+ $this->repository->save($job3);
+
+ $this->handler->handle(new DeleteJobsByCriteria(
+ statuses: [JobStatus::COMPLETED]
+ ));
+
+ $this->assertNull($this->repository->get('job-2'));
+ $this->assertNull($this->repository->get('job-3'));
+ $this->assertNotNull($this->repository->get('job-1'));
+ }
+
+ public function test_it_deletes_jobs_by_type(): void
+ {
+ $job1 = new ScrapingJob('job-1', 'manga-1', 1.0, 'source-1');
+ $job2 = new ScrapingJob('job-2', 'manga-1', 2.0, 'source-1');
+ $this->repository->save($job1);
+ $this->repository->save($job2);
+
+ $this->handler->handle(new DeleteJobsByCriteria(
+ type: 'scraping_job'
+ ));
+
+ $this->assertNull($this->repository->get('job-1'));
+ $this->assertNull($this->repository->get('job-2'));
+ }
+
+ public function test_it_does_nothing_when_no_criteria_match(): void
+ {
+ $job = new ScrapingJob('job-1', 'manga-1', 1.0, 'source-1');
+ $this->repository->save($job);
+
+ $this->handler->handle(new DeleteJobsByCriteria(
+ statuses: [JobStatus::FAILED]
+ ));
+
+ $this->assertNotNull($this->repository->get('job-1'));
+ }
+}
diff --git a/tests/Feature/Manga/DeleteMangaTest.php b/tests/Feature/Manga/DeleteMangaTest.php
index 59b223d..d6832d5 100644
--- a/tests/Feature/Manga/DeleteMangaTest.php
+++ b/tests/Feature/Manga/DeleteMangaTest.php
@@ -75,12 +75,4 @@ class DeleteMangaTest extends AbstractApiTestCase
$this->assertResponseStatusCodeSame(404);
}
- public function test_it_returns_404_for_missing_id(): void
- {
- // When
- static::createClient()->request('DELETE', '/api/mangas/');
-
- // Then
- $this->assertResponseStatusCodeSame(404);
- }
}