feat(catalog) : M6 — StorageType référentiel plat + seed migration (drop storage_type_site)

La disponibilité « type de stockage par site » relèvera de la future entité
Stockage (site + type), pas du référentiel. On retire donc la jointure M2M
storage_type_site et le filtrage du multi-select par site (RG-6.06 revue) :

- migration : DROP storage_type_site + seed idempotent des 10 types (prod-safe,
  ON CONFLICT) ;
- StorageType : référentiel plat (plus de relation sites) ;
- Product : suppression du Assert\Callback de disponibilité par site ;
- provider/repository : /storage_types renvoie tous les types (plus de ?siteId[]) ;
- front : useStorageTypeOptions charge tout dans loadReferentials, setSites sans
  cascade/purge ;
- fixture, ColumnCommentsCatalog, tests et spec-back M6 alignés.
This commit is contained in:
2026-06-26 15:39:11 +02:00
parent a6b8e7145e
commit fced2c2cfd
16 changed files with 235 additions and 422 deletions
@@ -24,8 +24,8 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
* volee pour que les POST passent RG-6.05.
* - `productCategory()` / `nonProductCategory()` : categories de test rattachees
* (ou non) au type PRODUIT.
* - `seedStorageType()` : type de stockage de test (prefixe code pour cleanup),
* rattachable a des sites precis (RG-6.06).
* - `seedStorageType()` : type de stockage de test (prefixe code pour cleanup ;
* referentiel plat, plus de rattachement par site).
* - `siteByCode()` / `firstSite()` : sites fixtures (86 / 17 / 82).
* - `authView()` : user non-admin portant la permission `catalog.products.view`.
* - `validProductPayload()` : payload POST de reference (IRIs category/sites/
@@ -60,7 +60,7 @@ abstract class AbstractProductApiTestCase extends AbstractCatalogApiTestCase
// product_storage_type cascadent au niveau base (ON DELETE CASCADE).
$em->createQuery('DELETE FROM '.Product::class)->execute();
// Types de stockage de test (prefixe code) — libere storage_type_site.
// Types de stockage de test (prefixe code).
$em->createQuery('DELETE FROM '.StorageType::class.' s WHERE s.code LIKE :prefix')
->setParameter('prefix', self::TEST_STORAGE_TYPE_PREFIX.'%')
->execute()
@@ -111,19 +111,16 @@ abstract class AbstractProductApiTestCase extends AbstractCatalogApiTestCase
}
/**
* Cree un type de stockage de test (code prefixe TESTPRD pour le cleanup),
* rattache aux sites passes (disponibilite — RG-6.06).
* Cree un type de stockage de test (code prefixe TESTPRD pour le cleanup).
* Referentiel plat : plus de rattachement a des sites (RG-6.06 revue).
*/
protected function seedStorageType(string $label = 'Tas de test', Site ...$sites): StorageType
protected function seedStorageType(string $label = 'Tas de test'): StorageType
{
$em = $this->getEm();
$storageType = new StorageType();
$storageType->setCode($this->uniqueCode(self::TEST_STORAGE_TYPE_PREFIX));
$storageType->setLabel($label);
foreach ($sites as $site) {
$storageType->addSite($em->getReference(Site::class, (int) $site->getId()));
}
$em->persist($storageType);
$em->flush();
@@ -169,7 +166,7 @@ abstract class AbstractProductApiTestCase extends AbstractCatalogApiTestCase
protected function validProductPayload(array $overrides = []): array
{
$site = $this->firstSite();
$storageType = $this->seedStorageType('Tas test', $site);
$storageType = $this->seedStorageType('Tas test');
$category = $this->productCategory();
$base = [
@@ -213,7 +210,7 @@ abstract class AbstractProductApiTestCase extends AbstractCatalogApiTestCase
$product->setContainsMolasses(false);
$product->setCategory($category ?? $this->productCategory());
$product->addSite($em->getReference(Site::class, (int) $site->getId()));
$product->addStorageType($storageType ?? $this->seedStorageType('Seed', $site));
$product->addStorageType($storageType ?? $this->seedStorageType('Seed'));
$product->setDeletedAt($deletedAt);
$em->persist($product);
@@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Tests\Module\Catalog\Api;
/**
* RG-6.06 : chaque type de stockage retenu doit etre disponible sur au moins un
* des sites selectionnes. Un type de stockage hors des sites du produit est
* rejete en 422 (Assert\Callback, propertyPath `storageTypes`).
*
* @internal
*/
final class ProductStorageTypeBySiteTest extends AbstractProductApiTestCase
{
public function testStorageTypeNotOnSelectedSitesIsRejected(): void
{
$client = $this->createAdminClient();
$siteA = $this->siteByCode('86');
$siteB = $this->siteByCode('17');
// Type de stockage disponible uniquement sur le site B...
$storageType = $this->seedStorageType('Cellule site B', $siteB);
// ... mais produit declare sur le site A seulement -> 422.
$response = $client->request('POST', '/api/products', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validProductPayload([
'sites' => [$this->iri('sites', (int) $siteA->getId())],
'storageTypes' => [$this->iri('storage_types', (int) $storageType->getId())],
]),
]);
self::assertResponseStatusCodeSame(422);
self::assertContains('storageTypes', $this->violationPaths($response));
}
public function testStorageTypeOnSelectedSiteIsAccepted(): void
{
$client = $this->createAdminClient();
$siteA = $this->siteByCode('86');
$storageType = $this->seedStorageType('Tas site A', $siteA);
$client->request('POST', '/api/products', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validProductPayload([
'sites' => [$this->iri('sites', (int) $siteA->getId())],
'storageTypes' => [$this->iri('storage_types', (int) $storageType->getId())],
]),
]);
self::assertResponseStatusCodeSame(201);
}
}