From 8d920d5f65761600371c86a86aeaef9289e7bb6a Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 23 Mar 2026 15:14:33 +0100 Subject: [PATCH] feat(documents) : add migration for type column with data classification Co-Authored-By: Claude Opus 4.6 (1M context) --- Inventory_frontend | 2 +- migrations/Version20260323141052.php | 31 ++++++++++ .../SearchByNameOrReferenceExtension.php | 61 ++++++++++++++++++ tests/AbstractApiTestCase.php | 5 +- .../ModelTypeSyncControllerTest.php | 10 +-- tests/Api/Entity/MachineTest.php | 2 +- tests/Api/FilterTest.php | 62 +++++++++++++++++++ .../Api/Service/ComposantSyncStrategyTest.php | 14 ++--- 8 files changed, 172 insertions(+), 15 deletions(-) create mode 100644 migrations/Version20260323141052.php create mode 100644 src/Doctrine/SearchByNameOrReferenceExtension.php diff --git a/Inventory_frontend b/Inventory_frontend index ac860d3..2e82e85 160000 --- a/Inventory_frontend +++ b/Inventory_frontend @@ -1 +1 @@ -Subproject commit ac860d3165ebb560a7171c3a4e9a6efed0bf2cc1 +Subproject commit 2e82e854bffefce1a3dd63ef222faf718d3b3e4b diff --git a/migrations/Version20260323141052.php b/migrations/Version20260323141052.php new file mode 100644 index 0000000..06cf359 --- /dev/null +++ b/migrations/Version20260323141052.php @@ -0,0 +1,31 @@ +addSql("DO \$\$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'documents' AND column_name = 'type') THEN ALTER TABLE documents ADD COLUMN type VARCHAR(20) NOT NULL DEFAULT 'documentation'; END IF; END \$\$"); + $this->addSql("UPDATE documents SET type = 'photo' WHERE mimetype LIKE 'image/%'"); + $this->addSql("UPDATE documents SET type = 'autre' WHERE type = 'documentation' AND mimetype NOT LIKE 'application/pdf' AND mimetype NOT LIKE 'image/%'"); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE documents DROP COLUMN IF EXISTS type'); + } +} diff --git a/src/Doctrine/SearchByNameOrReferenceExtension.php b/src/Doctrine/SearchByNameOrReferenceExtension.php new file mode 100644 index 0000000..7c7a03f --- /dev/null +++ b/src/Doctrine/SearchByNameOrReferenceExtension.php @@ -0,0 +1,61 @@ +requestStack->getCurrentRequest(); + if (null === $request) { + return; + } + + $q = $request->query->get('q', ''); + if (!is_string($q) || '' === trim($q)) { + return; + } + + $escaped = addcslashes(trim($q), '%_'); + $paramName = $queryNameGenerator->generateParameterName('searchQ'); + $alias = $queryBuilder->getRootAliases()[0]; + + $queryBuilder + ->andWhere(sprintf('LOWER(%s.name) LIKE :%s OR LOWER(%s.reference) LIKE :%s', $alias, $paramName, $alias, $paramName)) + ->setParameter($paramName, '%'.strtolower($escaped).'%') + ; + } +} diff --git a/tests/AbstractApiTestCase.php b/tests/AbstractApiTestCase.php index 6fddab5..2732158 100644 --- a/tests/AbstractApiTestCase.php +++ b/tests/AbstractApiTestCase.php @@ -290,10 +290,13 @@ abstract class AbstractApiTestCase extends ApiTestCase return $machine; } - protected function createComposant(string $name = 'Composant Test', ?ModelType $type = null): Composant + protected function createComposant(string $name = 'Composant Test', ?string $reference = null, ?ModelType $type = null): Composant { $c = new Composant(); $c->setName($name); + if (null !== $reference) { + $c->setReference($reference); + } if (null !== $type) { $c->setTypeComposant($type); } diff --git a/tests/Api/Controller/ModelTypeSyncControllerTest.php b/tests/Api/Controller/ModelTypeSyncControllerTest.php index 33a37b0..09fbf43 100644 --- a/tests/Api/Controller/ModelTypeSyncControllerTest.php +++ b/tests/Api/Controller/ModelTypeSyncControllerTest.php @@ -42,7 +42,7 @@ class ModelTypeSyncControllerTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $this->createComposant('C1', $mt); + $this->createComposant('C1', null, $mt); $client = $this->createGestionnaireClient(); $client->request('POST', '/api/model_types/'.$mt->getId().'/sync-preview', [ @@ -102,7 +102,7 @@ class ModelTypeSyncControllerTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $this->createComposant('C1', $mt); + $this->createComposant('C1', null, $mt); // Add a skeleton requirement (simulates a PATCH that already happened) $em = $this->getEntityManager(); @@ -131,7 +131,7 @@ class ModelTypeSyncControllerTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $composant = $this->createComposant('C1', $mt); + $composant = $this->createComposant('C1', null, $mt); $this->createComposantPieceSlot($composant, $pieceType, null, 1, 0); // No skeleton requirements → slot is orphaned @@ -152,7 +152,7 @@ class ModelTypeSyncControllerTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $composant = $this->createComposant('C1', $mt); + $composant = $this->createComposant('C1', null, $mt); $this->createComposantPieceSlot($composant, $pieceType, null, 1, 0); $client = $this->createGestionnaireClient(); @@ -194,7 +194,7 @@ class ModelTypeSyncControllerTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $this->createComposant('C1', $mt); + $this->createComposant('C1', null, $mt); $em = $this->getEntityManager(); $req = new SkeletonPieceRequirement(); diff --git a/tests/Api/Entity/MachineTest.php b/tests/Api/Entity/MachineTest.php index 579c68a..7e6c36b 100644 --- a/tests/Api/Entity/MachineTest.php +++ b/tests/Api/Entity/MachineTest.php @@ -173,7 +173,7 @@ class MachineTest extends AbstractApiTestCase $productType = $this->createModelType('Huile', 'HUILE-SLOT', ModelCategory::PRODUCT); $compType = $this->createModelType('Pompe', 'POMPE-SLOT', ModelCategory::COMPONENT); - $composant = $this->createComposant('Composant avec slots', $compType); + $composant = $this->createComposant('Composant avec slots', null, $compType); $piece = $this->createPiece('Joint sélectionné', 'REF-JS', $pieceType); $product = $this->createProduct('Huile sélectionnée', 'REF-HS', $productType); diff --git a/tests/Api/FilterTest.php b/tests/Api/FilterTest.php index bbb7be6..e5208dc 100644 --- a/tests/Api/FilterTest.php +++ b/tests/Api/FilterTest.php @@ -109,4 +109,66 @@ class FilterTest extends AbstractApiTestCase $this->assertResponseIsSuccessful(); $this->assertJsonContains(['totalItems' => 1]); } + + public function testOrSearchByNameOnPieces(): void + { + $this->createPiece('Joint torique', 'REF-JT-001'); + $this->createPiece('Roulement', 'REF-RL-002'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/pieces?q=joint'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testOrSearchByReferenceOnPieces(): void + { + $this->createPiece('Joint torique', 'REF-JT-001'); + $this->createPiece('Roulement', 'REF-RL-002'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/pieces?q=RL-002'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testOrSearchMatchesBothNameAndReference(): void + { + $this->createComposant('Pompe REF-X', 'REF-POMPE-01'); + $this->createComposant('Vanne', 'REF-VANNE-01'); + $this->createComposant('Moteur', 'POMPE-MOTEUR'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/composants?q=pompe'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testOrSearchEmptyQueryReturnsAll(): void + { + $this->createProduct('Produit A', 'REF-A'); + $this->createProduct('Produit B', 'REF-B'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/products?q='); + + $this->assertResponseIsSuccessful(); + $data = $client->getResponse()->toArray(); + $this->assertGreaterThanOrEqual(2, $data['totalItems']); + } + + public function testOrSearchOnProducts(): void + { + $this->createProduct('Huile moteur', 'HM-500'); + $this->createProduct('Graisse', 'GR-100'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/products?q=HM-500'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } } diff --git a/tests/Api/Service/ComposantSyncStrategyTest.php b/tests/Api/Service/ComposantSyncStrategyTest.php index 681f731..6d2ac36 100644 --- a/tests/Api/Service/ComposantSyncStrategyTest.php +++ b/tests/Api/Service/ComposantSyncStrategyTest.php @@ -41,7 +41,7 @@ class ComposantSyncStrategyTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $this->createComposant('C1', $mt); + $this->createComposant('C1', null, $mt); $result = $this->strategy->preview($mt, [ 'pieces' => [['typePieceId' => $pieceType->getId(), 'position' => 0]], @@ -58,7 +58,7 @@ class ComposantSyncStrategyTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $composant = $this->createComposant('C1', $mt); + $composant = $this->createComposant('C1', null, $mt); $this->createComposantPieceSlot($composant, $pieceType, null, 1, 0); $result = $this->strategy->preview($mt, [ @@ -75,7 +75,7 @@ class ComposantSyncStrategyTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $composant = $this->createComposant('C1', $mt); + $composant = $this->createComposant('C1', null, $mt); $this->createComposantPieceSlot($composant, $pieceType, null, 1, 0); $result = $this->strategy->preview($mt, [ @@ -92,7 +92,7 @@ class ComposantSyncStrategyTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $composant = $this->createComposant('C1', $mt); + $composant = $this->createComposant('C1', null, $mt); $em = $this->getEntityManager(); $req = new SkeletonPieceRequirement(); @@ -115,7 +115,7 @@ class ComposantSyncStrategyTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $composant = $this->createComposant('C1', $mt); + $composant = $this->createComposant('C1', null, $mt); $piece = $this->createPiece('P1', 'P1-REF', $pieceType); $slot = $this->createComposantPieceSlot($composant, $pieceType, $piece, 5, 0); @@ -142,7 +142,7 @@ class ComposantSyncStrategyTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $composant = $this->createComposant('C1', $mt); + $composant = $this->createComposant('C1', null, $mt); $this->createComposantPieceSlot($composant, $pieceType, null, 1, 0); // No skeleton requirements -> slot should be deleted @@ -158,7 +158,7 @@ class ComposantSyncStrategyTest extends AbstractApiTestCase { $mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE); - $composant = $this->createComposant('C1', $mt); + $composant = $this->createComposant('C1', null, $mt); $em = $this->getEntityManager(); $req = new SkeletonPieceRequirement();