# Comment Document Attachments — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Allow users to attach one or more documents when creating a comment, via a single multipart/form-data request.

**Architecture:** Add a `comment` ManyToOne on Document entity (same pattern as machine/site/etc.), modify `CommentController::create()` to accept multipart/form-data with files + text fields, store files via existing `DocumentStorageService`, and update the frontend `CommentSection.vue` to include a file picker.

**Tech Stack:** Symfony 8, Doctrine, API Platform, Vue 3 Composition API, TypeScript, TailwindCSS/DaisyUI

---

### Task 1: Migration — add `comment_id` FK on `documents`

**Files:**
- Create: `migrations/Version20260323160000.php`

- [ ] **Step 1: Create the migration**

```php
<?php
declare(strict_types=1);
namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20260323160000 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Add comment_id FK on documents table';
    }

    public function up(Schema $schema): void
    {
        $this->addSql("DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'documents' AND column_name = 'comment_id') THEN ALTER TABLE documents ADD COLUMN comment_id VARCHAR(36) DEFAULT NULL; END IF; END $$");
        $this->addSql("DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_documents_comment') THEN ALTER TABLE documents ADD CONSTRAINT fk_documents_comment FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE; END IF; END $$");
        $this->addSql("CREATE INDEX IF NOT EXISTS idx_documents_comment_id ON documents(comment_id)");
    }

    public function down(Schema $schema): void
    {
        $this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS fk_documents_comment');
        $this->addSql('DROP INDEX IF EXISTS idx_documents_comment_id');
        $this->addSql('ALTER TABLE documents DROP COLUMN IF EXISTS comment_id');
    }
}
```

- [ ] **Step 2: Run the migration**

Run: `docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate --no-interaction`
Expected: Migration executes successfully.

- [ ] **Step 3: Update test schema**

Run: `make test-setup`

- [ ] **Step 4: Commit**

```bash
git add migrations/Version20260323160000.php
git commit -m "feat(documents) : add comment_id FK on documents table"
```

---

### Task 2: Entity updates — Document.comment + Comment.documents

**Files:**
- Modify: `src/Entity/Document.php`
- Modify: `src/Entity/Comment.php`

- [ ] **Step 1: Add `comment` ManyToOne on Document entity**

In `src/Entity/Document.php`, add after the `$site` property (around line 109):

```php
#[ORM\ManyToOne(targetEntity: Comment::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'comment_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
#[Groups(['document:list'])]
private ?Comment $comment = null;
```

And add getter/setter:

```php
public function getComment(): ?Comment
{
    return $this->comment;
}

public function setComment(?Comment $comment): static
{
    $this->comment = $comment;

    return $this;
}
```

- [ ] **Step 2: Add `documents` OneToMany on Comment entity**

In `src/Entity/Comment.php`, add the import:
```php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
```

Add property after `$updatedAt`:
```php
/** @var Collection<int, Document> */
#[ORM\OneToMany(targetEntity: Document::class, mappedBy: 'comment', cascade: ['remove'])]
private Collection $documents;
```

Initialize in constructor:
```php
public function __construct()
{
    $this->createdAt = new DateTimeImmutable();
    $this->updatedAt = new DateTimeImmutable();
    $this->documents = new ArrayCollection();
}
```

Add getter:
```php
/** @return Collection<int, Document> */
public function getDocuments(): Collection
{
    return $this->documents;
}
```

- [ ] **Step 3: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 4: Run tests to check nothing broke**

Run: `make test`
Expected: All existing tests pass.

- [ ] **Step 5: Commit**

```bash
git add src/Entity/Document.php src/Entity/Comment.php
git commit -m "feat(documents) : add Comment-Document relationship (ManyToOne/OneToMany)"
```

---

### Task 3: Update CommentController to accept multipart/form-data with files

**Files:**
- Modify: `src/Controller/CommentController.php`

- [ ] **Step 1: Add DocumentStorageService dependency and update create() method**

Update constructor to inject `DocumentStorageService`:
```php
use App\Entity\Document;
use App\Enum\DocumentType;
use App\Service\DocumentStorageService;
use Symfony\Component\HttpFoundation\File\UploadedFile;
```

```php
public function __construct(
    private readonly EntityManagerInterface $entityManager,
    private readonly ProfileRepository $profiles,
    private readonly DocumentStorageService $storageService,
) {}
```

Replace the `create()` method body to handle both JSON and multipart:

```php
#[Route('', name: 'api_comments_create', methods: ['POST'])]
public function create(Request $request): JsonResponse
{
    $this->denyAccessUnlessGranted('ROLE_VIEWER');

    $session   = $request->getSession();
    $profileId = $session->get('profileId');
    if (!$profileId) {
        return $this->json(['message' => 'Aucun profil actif.'], 401);
    }

    $profile = $this->profiles->find($profileId);
    if (!$profile) {
        return $this->json(['message' => 'Profil introuvable.'], 401);
    }

    // Parse fields from JSON or form-data
    $contentType = $request->headers->get('Content-Type', '');
    if (str_contains($contentType, 'multipart/form-data')) {
        $content    = trim((string) $request->request->get('content', ''));
        $entityType = trim((string) $request->request->get('entityType', ''));
        $entityId   = trim((string) $request->request->get('entityId', ''));
        $entityName = $request->request->get('entityName') ? trim((string) $request->request->get('entityName')) : null;
    } else {
        $payload = json_decode($request->getContent(), true);
        if (!is_array($payload)) {
            return $this->json(['message' => 'Payload JSON invalide.'], 400);
        }
        $content    = trim((string) ($payload['content'] ?? ''));
        $entityType = trim((string) ($payload['entityType'] ?? ''));
        $entityId   = trim((string) ($payload['entityId'] ?? ''));
        $entityName = isset($payload['entityName']) ? trim((string) $payload['entityName']) : null;
    }

    if ('' === $content) {
        return $this->json(['message' => 'Le contenu est requis.'], 400);
    }

    $allowedTypes = ['machine', 'piece', 'composant', 'product', 'piece_category', 'component_category', 'product_category', 'machine_skeleton'];
    if (!in_array($entityType, $allowedTypes, true)) {
        return $this->json(['message' => 'Type d\'entité invalide.'], 400);
    }

    if ('' === $entityId) {
        return $this->json(['message' => 'L\'identifiant de l\'entité est requis.'], 400);
    }

    $authorName = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
    if ('' === $authorName) {
        $authorName = $profile->getEmail() ?? 'Inconnu';
    }

    $comment = new Comment();
    $comment->setContent($content);
    $comment->setEntityType($entityType);
    $comment->setEntityId($entityId);
    $comment->setEntityName($entityName);
    $comment->setAuthorId($profileId);
    $comment->setAuthorName($authorName);

    $this->entityManager->persist($comment);

    // Handle file uploads
    /** @var UploadedFile[] $files */
    $files = $request->files->all('files');
    foreach ($files as $file) {
        if (!$file instanceof UploadedFile || !$file->isValid()) {
            continue;
        }

        $document   = new Document();
        $documentId = 'cl'.bin2hex(random_bytes(12));
        $document->setId($documentId);
        $document->setName($file->getClientOriginalName());
        $document->setFilename($file->getClientOriginalName());
        $document->setMimeType($file->getMimeType() ?: 'application/octet-stream');
        $document->setSize((int) $file->getSize());
        $document->setType(DocumentType::DOCUMENTATION);
        $document->setComment($comment);

        $extension    = $this->storageService->extensionFromFilename($file->getClientOriginalName());
        $relativePath = $this->storageService->storeFromPath(
            $file->getPathname(),
            $documentId,
            $extension,
        );
        $document->setPath($relativePath);

        $this->entityManager->persist($document);
    }

    $this->entityManager->flush();

    return $this->json($this->normalize($comment), 201);
}
```

- [ ] **Step 2: Update normalize() to include documents**

```php
private function normalize(Comment $comment): array
{
    $documents = [];
    foreach ($comment->getDocuments() as $document) {
        $documents[] = [
            'id'          => $document->getId(),
            'name'        => $document->getName(),
            'filename'    => $document->getFilename(),
            'mimeType'    => $document->getMimeType(),
            'size'        => $document->getSize(),
            'type'        => $document->getType()->value,
            'fileUrl'     => '/api/documents/'.$document->getId().'/file',
            'downloadUrl' => '/api/documents/'.$document->getId().'/download',
            'createdAt'   => $document->getCreatedAt()->format(DateTimeInterface::ATOM),
        ];
    }

    return [
        'id'             => $comment->getId(),
        'content'        => $comment->getContent(),
        'entityType'     => $comment->getEntityType(),
        'entityId'       => $comment->getEntityId(),
        'entityName'     => $comment->getEntityName(),
        'authorId'       => $comment->getAuthorId(),
        'authorName'     => $comment->getAuthorName(),
        'status'         => $comment->getStatus(),
        'resolvedById'   => $comment->getResolvedById(),
        'resolvedByName' => $comment->getResolvedByName(),
        'resolvedAt'     => $comment->getResolvedAt()?->format(DateTimeInterface::ATOM),
        'createdAt'      => $comment->getCreatedAt()->format(DateTimeInterface::ATOM),
        'updatedAt'      => $comment->getUpdatedAt()->format(DateTimeInterface::ATOM),
        'documents'      => $documents,
    ];
}
```

- [ ] **Step 3: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 4: Run tests**

Run: `make test`
Expected: All existing tests still pass (they use JSON, not multipart).

- [ ] **Step 5: Commit**

```bash
git add src/Controller/CommentController.php
git commit -m "feat(comments) : accept multipart/form-data with file uploads on create"
```

---

### Task 4: Update DocumentUploadProcessor and DocumentQueryController

**Files:**
- Modify: `src/State/DocumentUploadProcessor.php`
- Modify: `src/Controller/DocumentQueryController.php`

- [ ] **Step 1: Add `commentId` to DocumentUploadProcessor relation map**

In `src/State/DocumentUploadProcessor.php`, update `$relationMap` in `setRelationsFromRequest()`:

```php
$relationMap = [
    'machineId'   => 'Machine',
    'composantId' => 'Composant',
    'pieceId'     => 'Piece',
    'productId'   => 'Product',
    'siteId'      => 'Site',
    'commentId'   => 'Comment',
];
```

- [ ] **Step 2: Add comment route to DocumentQueryController**

Add `CommentRepository` import and inject it, then add the route:

```php
use App\Repository\CommentRepository;
```

Add to constructor:
```php
private readonly CommentRepository $commentRepository,
```

Wait — `Comment` has no repository. Use the EntityManager instead. Add the route method:

```php
#[Route('/comment/{id}', name: 'documents_by_comment', methods: ['GET'])]
public function listByComment(string $id): JsonResponse
{
    $this->denyAccessUnlessGranted('ROLE_VIEWER');

    $comment = $this->getEntityManager()->getRepository(\App\Entity\Comment::class)->find($id);
    if (!$comment) {
        return $this->json(['success' => false, 'error' => 'Comment not found.'], 404);
    }

    $documents = $this->documentRepository->findBy(['comment' => $comment]);

    return $this->json($this->normalizeDocuments($documents));
}
```

Actually, the controller doesn't have `getEntityManager()`. Use `DocumentRepository` directly:

```php
#[Route('/comment/{id}', name: 'documents_by_comment', methods: ['GET'])]
public function listByComment(string $id): JsonResponse
{
    $this->denyAccessUnlessGranted('ROLE_VIEWER');

    $documents = $this->documentRepository->findBy(['comment' => $id]);

    return $this->json($this->normalizeDocuments($documents));
}
```

Wait — `findBy(['comment' => $id])` won't work with a string ID directly on a relation. Let me use the pattern from the existing code and add the Comment entity lookup. The simplest approach: inject `EntityManagerInterface`.

Actually, looking at the existing pattern more carefully, the other methods fetch the entity first and pass the object. We can use the documentRepository's entity manager. Let's just follow the exact same pattern and add a dependency. But actually, let's keep it simple — the documents table has `comment_id` column, so we can use a custom query. The simplest: just inject EntityManagerInterface.

```php
use Doctrine\ORM\EntityManagerInterface;
```

Add to constructor: `private readonly EntityManagerInterface $em,`

```php
#[Route('/comment/{id}', name: 'documents_by_comment', methods: ['GET'])]
public function listByComment(string $id): JsonResponse
{
    $this->denyAccessUnlessGranted('ROLE_VIEWER');

    $comment = $this->em->find(\App\Entity\Comment::class, $id);
    if (!$comment) {
        return $this->json(['success' => false, 'error' => 'Comment not found.'], 404);
    }

    $documents = $this->documentRepository->findBy(['comment' => $comment]);

    return $this->json($this->normalizeDocuments($documents));
}
```

- [ ] **Step 3: Update normalizeDocuments to include commentId**

Add to the normalizeDocuments return array:
```php
'commentId' => $document->getComment()?->getId(),
```

- [ ] **Step 4: Run php-cs-fixer + tests**

Run: `make php-cs-fixer-allow-risky && make test`

- [ ] **Step 5: Commit**

```bash
git add src/State/DocumentUploadProcessor.php src/Controller/DocumentQueryController.php
git commit -m "feat(documents) : add comment support in upload processor and query controller"
```

---

### Task 5: Backend tests — comment with documents

**Files:**
- Modify: `tests/Api/Controller/CommentControllerTest.php`

- [ ] **Step 1: Add test for creating comment with files**

```php
public function testCreateCommentWithFiles(): void
{
    $machine = $this->createMachine('Machine A');

    $client = $this->createViewerClient();

    // Create a temporary file for upload
    $tmpFile = tempnam(sys_get_temp_dir(), 'test_');
    file_put_contents($tmpFile, 'test file content');

    $uploadedFile = new \Symfony\Component\HttpFoundation\File\UploadedFile(
        $tmpFile,
        'test-doc.pdf',
        'application/pdf',
        null,
        true,
    );

    $client->request('POST', '/api/comments', [
        'headers' => ['Content-Type' => 'multipart/form-data'],
        'extra'   => [
            'parameters' => [
                'content'    => 'Comment with file',
                'entityType' => 'machine',
                'entityId'   => $machine->getId(),
                'entityName' => 'Machine A',
            ],
            'files' => [
                'files' => [$uploadedFile],
            ],
        ],
    ]);

    $this->assertResponseStatusCodeSame(201);
    $data = json_decode($client->getResponse()->getContent(), true);
    $this->assertSame('Comment with file', $data['content']);
    $this->assertCount(1, $data['documents']);
    $this->assertSame('test-doc.pdf', $data['documents'][0]['filename']);

    @unlink($tmpFile);
}
```

- [ ] **Step 2: Add test for creating comment with multiple files**

```php
public function testCreateCommentWithMultipleFiles(): void
{
    $machine = $this->createMachine('Machine A');

    $client = $this->createViewerClient();

    $tmpFile1 = tempnam(sys_get_temp_dir(), 'test_');
    file_put_contents($tmpFile1, 'content 1');
    $tmpFile2 = tempnam(sys_get_temp_dir(), 'test_');
    file_put_contents($tmpFile2, 'content 2');

    $file1 = new \Symfony\Component\HttpFoundation\File\UploadedFile($tmpFile1, 'doc1.pdf', 'application/pdf', null, true);
    $file2 = new \Symfony\Component\HttpFoundation\File\UploadedFile($tmpFile2, 'doc2.png', 'image/png', null, true);

    $client->request('POST', '/api/comments', [
        'extra' => [
            'parameters' => [
                'content'    => 'Multiple files',
                'entityType' => 'machine',
                'entityId'   => $machine->getId(),
            ],
            'files' => [
                'files' => [$file1, $file2],
            ],
        ],
    ]);

    $this->assertResponseStatusCodeSame(201);
    $data = json_decode($client->getResponse()->getContent(), true);
    $this->assertCount(2, $data['documents']);

    @unlink($tmpFile1);
    @unlink($tmpFile2);
}
```

- [ ] **Step 3: Add test that existing JSON create still works and returns empty documents array**

```php
public function testCreateCommentJsonStillReturnsDocuments(): void
{
    $machine = $this->createMachine('Machine A');

    $client = $this->createViewerClient();
    $client->request('POST', '/api/comments', [
        'json' => [
            'content'    => 'No files',
            'entityType' => 'machine',
            'entityId'   => $machine->getId(),
        ],
    ]);

    $this->assertResponseStatusCodeSame(201);
    $data = json_decode($client->getResponse()->getContent(), true);
    $this->assertSame([], $data['documents']);
}
```

- [ ] **Step 4: Run tests**

Run: `make test`
Expected: All tests pass.

- [ ] **Step 5: Commit**

```bash
git add tests/Api/Controller/CommentControllerTest.php
git commit -m "test(comments) : add tests for comment creation with file attachments"
```

---

### Task 6: Frontend — update useComments composable

**Files:**
- Modify: `frontend/app/composables/useComments.ts`

- [ ] **Step 1: Add document type to Comment interface**

```typescript
export interface CommentDocument {
  id: string
  name: string
  filename: string
  mimeType: string
  size: number
  type: string
  fileUrl: string
  downloadUrl: string
  createdAt: string
}

export interface Comment {
  id: string
  content: string
  entityType: string
  entityId: string
  entityName?: string | null
  authorId: string
  authorName: string
  status: 'open' | 'resolved'
  resolvedById?: string | null
  resolvedByName?: string | null
  resolvedAt?: string | null
  createdAt: string
  updatedAt: string
  documents: CommentDocument[]
}
```

- [ ] **Step 2: Update createComment to accept files and use FormData**

Add `postFormData` to the destructured `useApi()` call:
```typescript
const { get, post, patch, postFormData, delete: del } = useApi()
```

Update `createComment`:
```typescript
const createComment = async (
  entityType: string,
  entityId: string,
  content: string,
  entityName?: string,
  files?: File[],
): Promise<CommentResult> => {
  loading.value = true
  try {
    let result
    if (files && files.length > 0) {
      const formData = new FormData()
      formData.append('content', content)
      formData.append('entityType', entityType)
      formData.append('entityId', entityId)
      if (entityName) formData.append('entityName', entityName)
      for (const file of files) {
        formData.append('files[]', file)
      }
      result = await postFormData('/comments', formData)
    } else {
      const payload: Record<string, string> = { entityType, entityId, content }
      if (entityName) payload.entityName = entityName
      result = await post('/comments', payload)
    }
    if (result.success) {
      showSuccess('Commentaire ajouté')
      return { success: true, data: result.data as Comment }
    }
    if (result.error) showError(result.error)
    return { success: false, error: result.error }
  } catch (error) {
    const err = error as Error
    showError('Impossible d\'ajouter le commentaire')
    return { success: false, error: err.message }
  } finally {
    loading.value = false
  }
}
```

- [ ] **Step 3: Run lint + typecheck**

Run: `cd frontend && npm run lint:fix && npx nuxi typecheck`

- [ ] **Step 4: Commit (in frontend submodule)**

```bash
cd frontend
git add app/composables/useComments.ts
git commit -m "feat(comments) : support file attachments in createComment"
```

---

### Task 7: Frontend — update CommentSection.vue

**Files:**
- Modify: `frontend/app/components/CommentSection.vue`

- [ ] **Step 1: Add file input and file list display to the template**

Replace the form section (lines 22-40) with:

```vue
<!-- Formulaire d'ajout -->
<div class="space-y-2">
  <div class="flex gap-2">
    <textarea
      v-model="newContent"
      class="textarea textarea-bordered flex-1 text-sm"
      rows="2"
      placeholder="Ajouter un commentaire..."
      :disabled="submitting"
      @keydown.ctrl.enter="handleSubmit"
    />
    <div class="flex flex-col gap-1 self-end">
      <label
        class="btn btn-ghost btn-sm btn-square tooltip tooltip-left"
        data-tip="Joindre des fichiers"
      >
        <IconLucidePaperclip class="w-4 h-4" />
        <input
          ref="fileInputRef"
          type="file"
          multiple
          class="hidden"
          @change="handleFilesSelected"
        />
      </label>
      <button
        type="button"
        class="btn btn-primary btn-sm btn-square"
        :disabled="!newContent.trim() || submitting"
        @click="handleSubmit"
      >
        <span v-if="submitting" class="loading loading-spinner loading-xs" />
        <IconLucideSend v-else class="w-4 h-4" />
      </button>
    </div>
  </div>
  <!-- Selected files preview -->
  <div v-if="selectedFiles.length" class="flex flex-wrap gap-1">
    <span
      v-for="(file, i) in selectedFiles"
      :key="i"
      class="badge badge-sm badge-outline gap-1"
    >
      <IconLucideFile class="w-3 h-3" />
      {{ file.name }}
      <button type="button" class="ml-1" @click="removeFile(i)">
        <IconLucideX class="w-3 h-3" />
      </button>
    </span>
  </div>
</div>
```

Add after each comment's content (`<p class="text-sm whitespace-pre-wrap">`) in both open and resolved sections:

```vue
<!-- Documents attachés -->
<div v-if="comment.documents?.length" class="flex flex-wrap gap-1 mt-1">
  <a
    v-for="doc in comment.documents"
    :key="doc.id"
    :href="`${apiBase}${doc.downloadUrl}`"
    target="_blank"
    class="badge badge-sm badge-ghost gap-1 hover:badge-primary"
  >
    <IconLucideFile class="w-3 h-3" />
    {{ doc.filename }}
  </a>
</div>
```

- [ ] **Step 2: Update script setup**

Add new imports:
```typescript
import IconLucidePaperclip from '~icons/lucide/paperclip'
import IconLucideFile from '~icons/lucide/file'
import IconLucideX from '~icons/lucide/x'
```

Add after existing refs:
```typescript
const selectedFiles = ref<File[]>([])
const fileInputRef = ref<HTMLInputElement | null>(null)
const apiBase = useRuntimeConfig().public.apiBase || ''
```

Add file management functions:
```typescript
const handleFilesSelected = (e: Event) => {
  const input = e.target as HTMLInputElement
  if (input.files) {
    selectedFiles.value.push(...Array.from(input.files))
  }
  // Reset input so the same file can be re-selected
  input.value = ''
}

const removeFile = (index: number) => {
  selectedFiles.value.splice(index, 1)
}
```

Update `handleSubmit`:
```typescript
const handleSubmit = async () => {
  const content = newContent.value.trim()
  if (!content) return
  submitting.value = true
  const result = await createComment(
    props.entityType,
    props.entityId,
    content,
    props.entityName,
    selectedFiles.value.length > 0 ? selectedFiles.value : undefined,
  )
  submitting.value = false
  if (result.success) {
    newContent.value = ''
    selectedFiles.value = []
    await loadComments()
  }
}
```

- [ ] **Step 3: Run lint + typecheck**

Run: `cd frontend && npm run lint:fix && npx nuxi typecheck`

- [ ] **Step 4: Commit (in frontend submodule)**

```bash
cd frontend
git add app/components/CommentSection.vue
git commit -m "feat(comments) : add file attachment UI to CommentSection"
```

---

### Task 8: Update API Platform filter and submodule pointer

**Files:**
- Modify: `src/Entity/Document.php` (add ExistsFilter for comment)

- [ ] **Step 1: Add comment to ExistsFilter on Document entity**

Update the `ApiFilter(ExistsFilter...)` line in `Document.php`:
```php
#[ApiFilter(ExistsFilter::class, properties: ['site', 'machine', 'composant', 'piece', 'product', 'comment'])]
```

- [ ] **Step 2: Run php-cs-fixer + all backend tests**

Run: `make php-cs-fixer-allow-risky && make test`

- [ ] **Step 3: Commit backend**

```bash
git add src/Entity/Document.php
git commit -m "feat(documents) : add comment ExistsFilter"
```

- [ ] **Step 4: Update submodule pointer**

```bash
git add frontend
git commit -m "chore(submodule) : update frontend pointer (comment documents feature)"
```

---

### Task 9: Manual verification

- [ ] **Step 1: Start the app**

Run: `make start`

- [ ] **Step 2: Test creating a comment without files** — should work exactly as before, response now includes `"documents": []`

- [ ] **Step 3: Test creating a comment with files** — use the paperclip button, select 1-2 files, submit. Files should appear as badges on the comment.

- [ ] **Step 4: Click a file badge** — should download the file.

- [ ] **Step 5: Run full test suite one last time**

Run: `make test`
