# Isolation Média par Company - POS

## Problème Actuel

Le système média actuel utilise `relatedCompany` et `documentCategory.company` pour déterminer l'appartenance d'un média à une company. Cependant, la structure de stockage physique ne garantit pas l'isolation :

```
var/media/
├── profile_pictures/    ← Toutes companies mélangées
├── company_logos/       ← Toutes companies mélangées
└── documents/           ← Toutes companies mélangées
```

## Solution : Stockage Isolé par Company

### Nouvelle Structure Physique

```
var/media/
├── shared/                          ← Fichiers système (logos app, etc.)
├── companies/
│   ├── {company_uuid}/
│   │   ├── logos/                   ← Logo de l'entreprise
│   │   ├── documents/
│   │   │   ├── {category_slug}/     ← Par catégorie de document
│   │   │   └── uncategorized/
│   │   ├── pos/
│   │   │   ├── products/            ← Images produits
│   │   │   ├── categories/          ← Images catégories produits
│   │   │   ├── receipts/            ← Tickets de caisse PDF
│   │   │   ├── reports/             ← Rapports générés
│   │   │   └── imports/             ← Fichiers d'import CSV
│   │   └── exports/                 ← Fichiers d'export
│   ├── {company_uuid_2}/
│   │   └── ... (même structure)
│   └── ...
└── users/
    └── {user_uuid}/
        └── profile/                 ← Photos de profil
```

### Avantages
1. **Isolation physique** : impossible d'accéder aux fichiers d'une autre company par chemin
2. **Backup sélectif** : sauvegarder/restaurer par company
3. **Quotas** : calculer l'espace disque par company facilement
4. **Suppression** : nettoyer tous les fichiers d'une company en une opération
5. **Migration** : déplacer une company vers un autre serveur

---

## Implémentation

### 1. Nouveau Type Media pour POS

Ajouter dans `Media.php` :

```php
// Nouveaux types
const TYPE_POS_PRODUCT = 'pos_product';
const TYPE_POS_CATEGORY = 'pos_category';
const TYPE_POS_RECEIPT = 'pos_receipt';
const TYPE_POS_REPORT = 'pos_report';
const TYPE_POS_IMPORT = 'pos_import';
```

### 2. Service de Chemin Média

Créer `src/Service/Media/CompanyMediaPathService.php` :

```php
class CompanyMediaPathService
{
    public function __construct(
        private string $mediaDirectory
    ) {}
    
    /**
     * Retourne le chemin racine média d'une company.
     */
    public function getCompanyMediaPath(Company $company): string
    {
        return sprintf('%s/companies/%s', $this->mediaDirectory, $company->getUuid());
    }
    
    /**
     * Retourne le chemin pour un type de média POS.
     */
    public function getPosMediaPath(Company $company, string $type): string
    {
        $subPath = match ($type) {
            'product'  => 'pos/products',
            'category' => 'pos/categories',
            'receipt'  => 'pos/receipts',
            'report'   => 'pos/reports',
            'import'   => 'pos/imports',
            default    => 'pos/other',
        };
        
        return sprintf('%s/%s', $this->getCompanyMediaPath($company), $subPath);
    }
    
    /**
     * Retourne le chemin pour un document catégorisé.
     */
    public function getDocumentPath(Company $company, ?DocumentCategory $category): string
    {
        $basePath = $this->getCompanyMediaPath($company) . '/documents';
        return $category 
            ? sprintf('%s/%s', $basePath, $category->getSlug())
            : $basePath . '/uncategorized';
    }
    
    /**
     * Retourne le chemin pour le logo company.
     */
    public function getCompanyLogoPath(Company $company): string
    {
        return $this->getCompanyMediaPath($company) . '/logos';
    }
    
    /**
     * Vérifie qu'un chemin appartient bien à une company.
     */
    public function isPathOwnedByCompany(string $filePath, Company $company): bool
    {
        $companyPath = $this->getCompanyMediaPath($company);
        return str_starts_with(realpath($filePath), realpath($companyPath));
    }
    
    /**
     * Calcule l'espace disque utilisé par une company.
     */
    public function getCompanyDiskUsage(Company $company): int
    {
        $path = $this->getCompanyMediaPath($company);
        if (!is_dir($path)) return 0;
        
        $size = 0;
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
        );
        foreach ($iterator as $file) {
            $size += $file->getSize();
        }
        return $size;
    }
    
    /**
     * Assure que le répertoire existe.
     */
    public function ensureDirectoryExists(string $path): void
    {
        if (!is_dir($path)) {
            mkdir($path, 0755, true);
        }
    }
}
```

### 3. Extension de MediaService

Modifier `MediaService` pour utiliser `CompanyMediaPathService` :

```php
// Dans uploadProductImage()
public function uploadProductImage(
    UploadedFile $file, 
    Product $product, 
    User $uploader
): Media {
    $company = $product->getCompany();
    $targetDir = $this->companyMediaPath->getPosMediaPath($company, 'product');
    $this->companyMediaPath->ensureDirectoryExists($targetDir);
    
    // ... upload logic avec targetDir
    
    $media = new Media();
    $media->setType(Media::TYPE_POS_PRODUCT);
    $media->setRelatedCompany($company);
    $media->setUploadedBy($uploader);
    // ...
}
```

### 4. Vérification d'Accès Renforcée

Dans `MediaController` ou un listener, ajouter une vérification de chemin :

```php
// Avant de servir un fichier
if ($media->getRelatedCompany() && !$this->companyMediaPath->isPathOwnedByCompany(
    $fullPath, $media->getRelatedCompany()
)) {
    throw new AccessDeniedHttpException('Accès interdit.');
}
```

### 5. Quotas par Company (optionnel)

Ajouter dans `PosSettings` :

```php
// Quota de stockage
private ?int $storageQuotaMb = null;  // null = illimité
```

Vérification avant upload :

```php
public function checkStorageQuota(Company $company, int $newFileSize): bool
{
    $settings = $this->posSettingsService->getSettings($company);
    $quotaMb = $settings->getStorageQuotaMb();
    
    if ($quotaMb === null) return true; // Pas de limite
    
    $currentUsage = $this->companyMediaPath->getCompanyDiskUsage($company);
    $quotaBytes = $quotaMb * 1024 * 1024;
    
    return ($currentUsage + $newFileSize) <= $quotaBytes;
}
```

---

## Migration des Fichiers Existants

### Commande de Migration

Créer `src/Command/Pos/MigrateMediaToCompanyStructureCommand.php` :

```php
// Logique de migration
// 1. Lister tous les Media avec relatedCompany != null
// 2. Pour chaque media, déplacer le fichier vers companies/{uuid}/{type}/
// 3. Mettre à jour le filePath en BDD
// 4. Vérifier l'intégrité (md5 avant/après)
// 5. Mode dry-run par défaut
```

### Étapes de Migration
1. **Dry-run** : `php bin/console pos:migrate-media --dry-run`
2. **Exécution** : `php bin/console pos:migrate-media`
3. **Validation** : `php bin/console pos:validate-media-structure`
4. **Nettoyage** : supprimer les anciens répertoires vides

---

## Matrice d'Accès Média POS

| Type Média | ADMIN | MANAGER (sa company) | EMPLOYER (sa company) | Autre Company |
|-----------|-------|---------------------|----------------------|---------------|
| pos_product | Lecture | CRUD | Lecture | BLOQUE |
| pos_category | Lecture | CRUD | Lecture | BLOQUE |
| pos_receipt | Lecture | Lecture | Ses tickets | BLOQUE |
| pos_report | Lecture | Lecture | - | BLOQUE |
| pos_import | Lecture | CRUD | - | BLOQUE |
| company_logo | Lecture | CRUD | Lecture | BLOQUE |
| document | Lecture | CRUD | Selon MediaAccess | BLOQUE |

---

## Configuration PosSettings Liée

```php
// Champs à ajouter dans PosSettings
private ?int $storageQuotaMb = null;           // Quota stockage (MB)
private int $maxProductImages = 5;              // Max images par produit
private int $maxImageSizeMb = 5;               // Taille max image (MB)
private bool $autoCompressImages = true;        // Compression auto
private int $compressQuality = 80;             // Qualité compression (%)
private bool $generateThumbnails = true;        // Miniatures auto
private int $thumbnailWidth = 300;             // Largeur miniature
private int $thumbnailHeight = 300;            // Hauteur miniature
private string $allowedImageFormats = 'jpg,png,webp'; // Formats autorisés
```

---

## Tests d'Isolation

### Scénarios à Tester

1. **Upload** : un manager uploade une image → stockée dans `companies/{sa_company_uuid}/`
2. **Accès** : un manager tente d'accéder à un média d'une autre company → 403
3. **Chemin** : tenter un path traversal (`../../other_company/`) → bloqué
4. **API** : endpoint API média filtre par company du JWT
5. **Quota** : upload dépassant le quota → rejeté avec message clair
6. **Suppression** : soft-delete du média → fichier archivé dans la même structure company
7. **Migration** : fichiers existants correctement déplacés et accessibles après migration
