From bd71caa289002d395b39305ea28ad689fd674931 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 22 Jun 2026 09:44:44 +0200 Subject: [PATCH] fix(front+back) : suppression de la categorie du bloc adresse prestataire (ERP-193) La categorie portee par l'ADRESSE du prestataire (M2M provider_address_category) est retiree de toutes les couches : champ + prop du bloc adresse, type/payload/ mapping front, entite ProviderAddress (M2M, Assert\Count, validateCategoryType, accesseurs), fixtures, contexte de serialisation. Nouvelle migration de drop de la table (namespace racine pour l'ordre post-creation). La categorie du PRESTATAIRE lui-meme (provider_category, repertoire, filtre, formulaire principal) est conservee. --- frontend/i18n/locales/fr.json | 1 - .../components/ProviderAddressBlock.vue | 15 ---- .../__tests__/ProviderAddressBlock.spec.ts | 6 +- .../__tests__/useProviderForm.test.ts | 9 +- .../technique/pages/providers/[id]/edit.vue | 1 - .../technique/pages/providers/[id]/index.vue | 6 +- .../modules/technique/pages/providers/new.vue | 1 - .../modules/technique/types/providerForm.ts | 3 - .../forms/__tests__/providerAddress.spec.ts | 15 +--- .../forms/__tests__/providerDetail.spec.ts | 4 +- .../technique/utils/forms/providerAddress.ts | 9 +- .../technique/utils/forms/providerDetail.ts | 2 - migrations/Version20260622100000.php | 74 ++++++++++++++++ .../Technique/Domain/Entity/Provider.php | 3 +- .../Domain/Entity/ProviderAddress.php | 88 ++----------------- .../Processor/ProviderAddressProcessor.php | 3 +- .../DataFixtures/ProviderFixtures.php | 13 +-- .../Database/ColumnCommentsCatalog.php | 6 -- .../Api/AbstractProviderApiTestCase.php | 1 - .../Api/ProviderSerializationContractTest.php | 5 -- .../Api/ProviderSubResourceApiTest.php | 42 ++------- 21 files changed, 109 insertions(+), 198 deletions(-) create mode 100644 migrations/Version20260622100000.php diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index e43e2b3..cc62b73 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -453,7 +453,6 @@ }, "address": { "sites": "Sites", - "categories": "Catégorie", "contacts": "Contact(s) rattaché(s)", "country": "Pays", "postalCode": "Code postal", diff --git a/frontend/modules/technique/components/ProviderAddressBlock.vue b/frontend/modules/technique/components/ProviderAddressBlock.vue index 7e7146c..4b66a97 100644 --- a/frontend/modules/technique/components/ProviderAddressBlock.vue +++ b/frontend/modules/technique/components/ProviderAddressBlock.vue @@ -23,19 +23,6 @@ @update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))" /> - - - = {}, errors?: Record { - it('affiche les erreurs serveur sur sites et categories (RG-3.05 / RG-3.09)', () => { + it('affiche l\'erreur serveur sur sites (RG-3.05)', () => { const wrapper = mountBlock({}, { sites: 'Au moins un site est obligatoire.', - categories: 'Au moins une catégorie est obligatoire.', }) const checkboxes = wrapper.findAll('malio-select-checkbox-stub') const sitesField = checkboxes.find(el => el.attributes('label') === 'technique.providers.form.address.sites') - const categoriesField = checkboxes.find(el => el.attributes('label') === 'technique.providers.form.address.categories') expect(sitesField?.attributes('error')).toBe('Au moins un site est obligatoire.') - expect(categoriesField?.attributes('error')).toBe('Au moins une catégorie est obligatoire.') }) it('affiche l\'erreur serveur sur le code postal', () => { diff --git a/frontend/modules/technique/composables/__tests__/useProviderForm.test.ts b/frontend/modules/technique/composables/__tests__/useProviderForm.test.ts index a0ab65b..7bf9adf 100644 --- a/frontend/modules/technique/composables/__tests__/useProviderForm.test.ts +++ b/frontend/modules/technique/composables/__tests__/useProviderForm.test.ts @@ -330,17 +330,16 @@ describe('useProviderForm — onglet Adresse (ERP-143)', () => { return form } - /** Remplit un bloc adresse valide (site + categorie + scalaires requis). */ + /** Remplit un bloc adresse valide (site + scalaires requis). */ function fillValidAddress(form: ProviderForm, index = 0): void { const a = addressAt(form, index) a.siteIris = [SITE_86] - a.categoryIris = [CAT_MAINT] a.postalCode = '86100' a.city = 'Châtellerault' a.street = '1 rue du Test' } - it('RG-3.05 : « + Nouvelle adresse » desactive tant que site + categorie manquent', () => { + it('RG-3.05 : « + Nouvelle adresse » desactive tant que le site manque', () => { const form = createdForm() expect(form.canAddAddress.value).toBe(false) @@ -349,8 +348,6 @@ describe('useProviderForm — onglet Adresse (ERP-143)', () => { expect(form.addresses.value).toHaveLength(1) addressAt(form).siteIris = [SITE_86] - expect(form.canAddAddress.value).toBe(false) // categorie manquante - addressAt(form).categoryIris = [CAT_MAINT] expect(form.canAddAddress.value).toBe(true) form.addAddress() expect(form.addresses.value).toHaveLength(2) @@ -377,7 +374,7 @@ describe('useProviderForm — onglet Adresse (ERP-143)', () => { expect(ok).toBe(true) const [url, body, opts] = mockPost.mock.calls[0] ?? [] expect(url).toBe('/providers/7/addresses') - expect(body).toMatchObject({ sites: [SITE_86], categories: [CAT_MAINT], city: 'Châtellerault' }) + expect(body).toMatchObject({ sites: [SITE_86], city: 'Châtellerault' }) expect(opts).toMatchObject({ toast: false, headers: { Accept: 'application/ld+json' } }) expect(addressAt(form).id).toBe(88) expect(form.isValidated('address')).toBe(true) diff --git a/frontend/modules/technique/pages/providers/[id]/edit.vue b/frontend/modules/technique/pages/providers/[id]/edit.vue index d4ce808..9eb564a 100644 --- a/frontend/modules/technique/pages/providers/[id]/edit.vue +++ b/frontend/modules/technique/pages/providers/[id]/edit.vue @@ -104,7 +104,6 @@ v-for="(address, index) in addresses" :key="index" :model-value="address" - :category-options="referentials.categories.value" :site-options="referentials.sites.value" :contact-options="contactOptions" :country-options="countryOptions" diff --git a/frontend/modules/technique/pages/providers/[id]/index.vue b/frontend/modules/technique/pages/providers/[id]/index.vue index be904d0..ea7137f 100644 --- a/frontend/modules/technique/pages/providers/[id]/index.vue +++ b/frontend/modules/technique/pages/providers/[id]/index.vue @@ -90,7 +90,6 @@ v-for="(view, index) in addressViews" :key="index" :model-value="view.draft" - :category-options="view.categoryOptions" :site-options="view.siteOptions" :contact-options="contactOptions" :country-options="countryOptionsFor(view.draft.country)" @@ -242,16 +241,15 @@ const contacts = computed(() => { // Contacts rattachables (pour resoudre les libelles des contacts lies aux adresses). const contactOptions = computed(() => contactOptionsOf(provider.value?.contacts)) -// Vue par adresse : brouillon + options propres a l'adresse (sites/categories embarques). +// Vue par adresse : brouillon + options propres a l'adresse (sites embarques). const addressViews = computed(() => { const views = (provider.value?.addresses ?? []).map(address => ({ draft: mapAddressToDraft(address), siteOptions: siteOptionsOf(address.sites), - categoryOptions: categoryOptionsOf(address.categories), })) return views.length > 0 ? views - : [{ draft: emptyProviderAddress(), siteOptions: [], categoryOptions: [] }] + : [{ draft: emptyProviderAddress(), siteOptions: [] }] }) /** Pays : une seule option (la valeur courante), suffisant pour l'affichage readonly. */ diff --git a/frontend/modules/technique/pages/providers/new.vue b/frontend/modules/technique/pages/providers/new.vue index 8a318b5..7e66ed4 100644 --- a/frontend/modules/technique/pages/providers/new.vue +++ b/frontend/modules/technique/pages/providers/new.vue @@ -108,7 +108,6 @@ v-for="(address, index) in addresses" :key="index" :model-value="address" - :category-options="referentials.categories.value" :site-options="referentials.sites.value" :contact-options="contactOptions" :country-options="countryOptions" diff --git a/frontend/modules/technique/types/providerForm.ts b/frontend/modules/technique/types/providerForm.ts index 2a4f1b7..0c2b288 100644 --- a/frontend/modules/technique/types/providerForm.ts +++ b/frontend/modules/technique/types/providerForm.ts @@ -97,8 +97,6 @@ export interface ProviderAddressFormDraft { city: string | null street: string | null streetComplement: string | null - /** IRI des categories rattachees (type PRESTATAIRE, RG-3.09 ; >= 1). */ - categoryIris: string[] /** IRI des sites rattaches a l'adresse (M2M `provider_address_site`, RG-3.05 ; >= 1). */ siteIris: string[] /** IRI des contacts rattaches (= blocs Contact deja persistes de l'onglet Contact). */ @@ -114,7 +112,6 @@ export function emptyProviderAddress(): ProviderAddressFormDraft { city: null, street: null, streetComplement: null, - categoryIris: [], siteIris: [], contactIris: [], } diff --git a/frontend/modules/technique/utils/forms/__tests__/providerAddress.spec.ts b/frontend/modules/technique/utils/forms/__tests__/providerAddress.spec.ts index a453a41..f9a9335 100644 --- a/frontend/modules/technique/utils/forms/__tests__/providerAddress.spec.ts +++ b/frontend/modules/technique/utils/forms/__tests__/providerAddress.spec.ts @@ -12,21 +12,15 @@ import { emptyProviderAddress } from '~/modules/technique/types/providerForm' */ describe('providerAddress helpers', () => { const SITE = '/api/sites/1' - const CAT = '/api/categories/7' - describe('isProviderAddressValid (RG-3.05 / RG-3.09)', () => { + describe('isProviderAddressValid (RG-3.05)', () => { it('false sans site', () => { - const address = { ...emptyProviderAddress(), categoryIris: [CAT] } + const address = { ...emptyProviderAddress() } expect(isProviderAddressValid(address)).toBe(false) }) - it('false sans categorie', () => { + it('true avec au moins un site', () => { const address = { ...emptyProviderAddress(), siteIris: [SITE] } - expect(isProviderAddressValid(address)).toBe(false) - }) - - it('true avec au moins un site ET une categorie', () => { - const address = { ...emptyProviderAddress(), siteIris: [SITE], categoryIris: [CAT] } expect(isProviderAddressValid(address)).toBe(true) }) }) @@ -39,7 +33,6 @@ describe('providerAddress helpers', () => { city: 'Châtellerault', street: '1 rue du Test', siteIris: [SITE], - categoryIris: [CAT], contactIris: ['/api/provider_contacts/9'], }) expect(payload).toEqual({ @@ -48,7 +41,6 @@ describe('providerAddress helpers', () => { city: 'Châtellerault', street: '1 rue du Test', streetComplement: null, - categories: [CAT], sites: [SITE], contacts: ['/api/provider_contacts/9'], }) @@ -61,7 +53,6 @@ describe('providerAddress helpers', () => { const payload = buildProviderAddressPayload({ ...emptyProviderAddress(), siteIris: [SITE], - categoryIris: [CAT], }) expect(payload).not.toHaveProperty('postalCode') expect(payload).not.toHaveProperty('city') diff --git a/frontend/modules/technique/utils/forms/__tests__/providerDetail.spec.ts b/frontend/modules/technique/utils/forms/__tests__/providerDetail.spec.ts index 879f94b..1b953e9 100644 --- a/frontend/modules/technique/utils/forms/__tests__/providerDetail.spec.ts +++ b/frontend/modules/technique/utils/forms/__tests__/providerDetail.spec.ts @@ -76,7 +76,7 @@ describe('providerDetail helpers', () => { }) describe('mapAddressToDraft', () => { - it('extrait les IRI des sites / categories / contacts embarques', () => { + it('extrait les IRI des sites / contacts embarques', () => { const draft = mapAddressToDraft({ '@id': '/api/provider_addresses/3', id: 3, @@ -85,11 +85,9 @@ describe('providerDetail helpers', () => { city: 'Châtellerault', street: '1 rue du Test', sites: [{ '@id': '/api/sites/1' }], - categories: [{ '@id': '/api/categories/7' }], contacts: [{ '@id': '/api/provider_contacts/5' }, '/api/provider_contacts/6'], }) expect(draft.siteIris).toEqual(['/api/sites/1']) - expect(draft.categoryIris).toEqual(['/api/categories/7']) expect(draft.contactIris).toEqual(['/api/provider_contacts/5', '/api/provider_contacts/6']) expect(draft.id).toBe(3) }) diff --git a/frontend/modules/technique/utils/forms/providerAddress.ts b/frontend/modules/technique/utils/forms/providerAddress.ts index ff1f5ed..aa3cd6f 100644 --- a/frontend/modules/technique/utils/forms/providerAddress.ts +++ b/frontend/modules/technique/utils/forms/providerAddress.ts @@ -14,12 +14,12 @@ import type { ProviderAddressFormDraft } from '~/modules/technique/types/provide const REQUIRED_NON_NULLABLE_KEYS = ['postalCode', 'city', 'street'] as const /** - * RG-3.05 (+ RG-3.09) : une adresse est « valide » pour autoriser l'ajout d'un - * nouveau bloc des qu'elle porte au moins un site ET au moins une categorie. Les - * scalaires (CP/ville/rue) restent valides par le back (422 inline). + * RG-3.05 : une adresse est « valide » pour autoriser l'ajout d'un nouveau bloc + * des qu'elle porte au moins un site. Les scalaires (CP/ville/rue) restent valides + * par le back (422 inline). */ export function isProviderAddressValid(address: ProviderAddressFormDraft): boolean { - return address.siteIris.length >= 1 && address.categoryIris.length >= 1 + return address.siteIris.length >= 1 } /** @@ -34,7 +34,6 @@ export function buildProviderAddressPayload(address: ProviderAddressFormDraft): city: address.city || null, street: address.street || null, streetComplement: address.streetComplement || null, - categories: [...address.categoryIris], sites: [...address.siteIris], contacts: [...address.contactIris], } diff --git a/frontend/modules/technique/utils/forms/providerDetail.ts b/frontend/modules/technique/utils/forms/providerDetail.ts index f5a82d3..8904a8f 100644 --- a/frontend/modules/technique/utils/forms/providerDetail.ts +++ b/frontend/modules/technique/utils/forms/providerDetail.ts @@ -68,7 +68,6 @@ export interface AddressRead extends HydraRef { street?: string | null streetComplement?: string | null sites?: SiteRead[] - categories?: CategoryRead[] // L'embed M2M des contacts d'adresse peut etre un objet (partiel) ou un IRI nu. contacts?: Array } @@ -146,7 +145,6 @@ export function mapAddressToDraft(address: AddressRead): ProviderAddressFormDraf city: address.city ?? null, street: address.street ?? null, streetComplement: address.streetComplement ?? null, - categoryIris: (address.categories ?? []).map(c => c['@id']), siteIris: (address.sites ?? []).map(s => s['@id']), contactIris: (address.contacts ?? []).map(c => (typeof c === 'string' ? c : c['@id'])), } diff --git a/migrations/Version20260622100000.php b/migrations/Version20260622100000.php new file mode 100644 index 0000000..c21e7c6 --- /dev/null +++ b/migrations/Version20260622100000.php @@ -0,0 +1,74 @@ +addSql('DROP TABLE IF EXISTS provider_address_category'); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE provider_address_category ( + provider_address_id INT NOT NULL, + category_id INT NOT NULL, + PRIMARY KEY (provider_address_id, category_id), + CONSTRAINT fk_provider_address_category_address + FOREIGN KEY (provider_address_id) REFERENCES provider_address (id) ON DELETE CASCADE, + CONSTRAINT fk_provider_address_category_category + FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE RESTRICT + ) + SQL); + $this->comment('provider_address_category', '_table', 'Jointure M2M provider_address <-> category — categories d adresse de type PRESTATAIRE (RG-3.09).'); + $this->comment('provider_address_category', 'provider_address_id', 'FK -> provider_address.id, ON DELETE CASCADE — adresse concernee.'); + $this->comment('provider_address_category', 'category_id', 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type PRESTATAIRE (RG-3.09).'); + } + + /** + * Pose un COMMENT ON TABLE/COLUMN en dollar-quoting Postgres ($_$...$_$) + * pour eviter tout echappement d'apostrophes dans les descriptions. + */ + private function comment(string $table, string $column, string $description): void + { + $quotedTable = '"'.str_replace('"', '""', $table).'"'; + + if ('_table' === $column) { + $this->addSql(sprintf('COMMENT ON TABLE %s IS $_$%s$_$', $quotedTable, $description)); + + return; + } + + $this->addSql(sprintf( + 'COMMENT ON COLUMN %s.%s IS $_$%s$_$', + $quotedTable, + '"'.str_replace('"', '""', $column).'"', + $description, + )); + } +} diff --git a/src/Module/Technique/Domain/Entity/Provider.php b/src/Module/Technique/Domain/Entity/Provider.php index 1f0290c..13b80d6 100644 --- a/src/Module/Technique/Domain/Entity/Provider.php +++ b/src/Module/Technique/Domain/Entity/Provider.php @@ -278,8 +278,7 @@ class Provider implements TimestampableInterface, BlamableInterface /** * RG-3.09 : toute categorie posee sur le prestataire doit etre de type * PRESTATAIRE -> sinon 422 avec violation sur le champ `categories` - * (propertyPath aligne ERP-101, message FR ERP-107). Miroir de - * ProviderAddress::validateCategoryType. S'appuie sur + * (propertyPath aligne ERP-101, message FR ERP-107). S'appuie sur * CategoryInterface::getCategoryTypeCodes() (multi-type — la categorie est * acceptee des qu'elle PORTE le type PRESTATAIRE ; pas d'import du module * Catalog, regle ABSOLUE n°1). Joue avant la base via la validation API diff --git a/src/Module/Technique/Domain/Entity/ProviderAddress.php b/src/Module/Technique/Domain/Entity/ProviderAddress.php index dc89acb..b95f703 100644 --- a/src/Module/Technique/Domain/Entity/ProviderAddress.php +++ b/src/Module/Technique/Domain/Entity/ProviderAddress.php @@ -14,7 +14,6 @@ use App\Module\Technique\Infrastructure\ApiPlatform\State\Processor\ProviderAddr use App\Module\Technique\Infrastructure\ApiPlatform\State\Provider\ProviderSubResourceItemProvider; use App\Shared\Domain\Attribute\Auditable; use App\Shared\Domain\Contract\BlamableInterface; -use App\Shared\Domain\Contract\CategoryInterface; use App\Shared\Domain\Contract\SiteInterface; use App\Shared\Domain\Contract\TimestampableInterface; use App\Shared\Domain\Trait\TimestampableBlamableTrait; @@ -24,21 +23,17 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * Adresse d'un prestataire (1:n) — onglet Adresse. Version SIMPLIFIEE de * SupplierAddress : PAS de address_type (PROSPECT/DEPART/RENDU), PAS de bennes, * PAS de triage_provider (champs specifiques fournisseur). Champs : country / - * postal_code / city / street / street_complement + M2M sites / contacts / - * categories. + * postal_code / city / street / street_complement + M2M sites / contacts. * * Relations M2M : * - sites : SiteInterface (module Sites) via resolve_target_entities — au moins * un site obligatoire (RG-3.05, Assert\Count). Site n'a pas de `code`. * - contacts : ProviderContact (meme module). - * - categories : CategoryInterface (module Catalog) via resolve_target_entities — - * type PRESTATAIRE attendu (RG-3.09, Assert\Callback validateCategoryType). * * Embarquee sous `provider.addresses` au detail (groupe provider:item:read, * maillon (a)). @@ -50,7 +45,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; * - GET /api/provider_addresses/{id} : lecture unitaire (security view) — la lecture * courante reste via le parent. Pas de GET collection autonome. * Tout passe par le ProviderAddressProcessor (rattachement parent + cloisonnement - * d'ecriture des sites, § 2.13). Les regles RG-3.05/3.06/3.09 sont portees par les + * d'ecriture des sites, § 2.13). Les regles RG-3.05/3.06 sont portees par les * contraintes de l'entite (jouees avant le processor). * * Audite (#[Auditable]) + Timestampable / Blamable. @@ -59,9 +54,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; operations: [ new Get( security: "is_granted('technique.providers.view')", - // site:read + category:read : embarquent les Site / Category lies - // (maillon (c)) plutot que des IRI nus dans le retour. - normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'category:read', 'default:read']], + // site:read : embarque les Site lies (maillon (c)) plutot que des IRI + // nus dans le retour. + normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'default:read']], // Cloisonnement par site du prestataire parent (§ 2.13) : 404 hors perimetre. provider: ProviderSubResourceItemProvider::class, ), @@ -76,13 +71,13 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; // manuellement par ProviderAddressProcessor::linkParent (404 si absent). read: false, security: "is_granted('technique.providers.manage')", - normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'category:read', 'default:read']], + normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'default:read']], denormalizationContext: ['groups' => ['provider:write:addresses']], processor: ProviderAddressProcessor::class, ), new Patch( security: "is_granted('technique.providers.manage')", - normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'category:read', 'default:read']], + normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'default:read']], denormalizationContext: ['groups' => ['provider:write:addresses']], provider: ProviderSubResourceItemProvider::class, processor: ProviderAddressProcessor::class, @@ -102,13 +97,6 @@ class ProviderAddress implements TimestampableInterface, BlamableInterface, Prov { use TimestampableBlamableTrait; - /** - * RG-3.09 : seules les categories PORTANT ce type sont autorisees sur une - * adresse prestataire. S'appuie sur CategoryInterface::getCategoryTypeCodes() - * (pas d'import du module Catalog — regle ABSOLUE n°1). - */ - private const string REQUIRED_CATEGORY_TYPE_CODE = 'PRESTATAIRE'; - #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] @@ -175,46 +163,10 @@ class ProviderAddress implements TimestampableInterface, BlamableInterface, Prov #[Groups(['provider:item:read', 'provider:write:addresses'])] private Collection $contacts; - // RG-3.09 : au moins une categorie de type PRESTATAIRE par adresse (le type est - // controle par validateCategoryType ; le minimum par Assert\Count, miroir sites). - /** @var Collection */ - #[ORM\ManyToMany(targetEntity: CategoryInterface::class)] - #[ORM\JoinTable(name: 'provider_address_category')] - #[ORM\JoinColumn(name: 'provider_address_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - #[ORM\InverseJoinColumn(name: 'category_id', referencedColumnName: 'id', onDelete: 'RESTRICT')] - #[Assert\Count(min: 1, minMessage: 'Au moins une catégorie est obligatoire.')] - #[Groups(['provider:item:read', 'provider:write:addresses'])] - private Collection $categories; - public function __construct() { - $this->sites = new ArrayCollection(); - $this->contacts = new ArrayCollection(); - $this->categories = new ArrayCollection(); - } - - /** - * RG-3.09 : toute categorie posee sur une adresse prestataire doit etre de - * type PRESTATAIRE -> sinon 422 avec violation sur le champ `categories` - * (propertyPath aligne ERP-101, message FR ERP-107). S'appuie sur - * CategoryInterface::getCategoryTypeCodes() (multi-type — la categorie est - * acceptee des qu'elle PORTE le type PRESTATAIRE ; pas d'import du module - * Catalog, regle ABSOLUE n°1). Joue avant la base via la validation API Platform. - */ - #[Assert\Callback] - public function validateCategoryType(ExecutionContextInterface $context): void - { - foreach ($this->categories as $category) { - if ($category instanceof CategoryInterface - && !in_array(self::REQUIRED_CATEGORY_TYPE_CODE, $category->getCategoryTypeCodes(), true)) { - $context->buildViolation('Type de catégorie non autorisé (PRESTATAIRE attendu).') - ->atPath('categories') - ->addViolation() - ; - - return; - } - } + $this->sites = new ArrayCollection(); + $this->contacts = new ArrayCollection(); } public function getId(): ?int @@ -349,26 +301,4 @@ class ProviderAddress implements TimestampableInterface, BlamableInterface, Prov return $this; } - - /** @return Collection */ - public function getCategories(): Collection - { - return $this->categories; - } - - public function addCategory(CategoryInterface $category): static - { - if (!$this->categories->contains($category)) { - $this->categories->add($category); - } - - return $this; - } - - public function removeCategory(CategoryInterface $category): static - { - $this->categories->removeElement($category); - - return $this; - } } diff --git a/src/Module/Technique/Infrastructure/ApiPlatform/State/Processor/ProviderAddressProcessor.php b/src/Module/Technique/Infrastructure/ApiPlatform/State/Processor/ProviderAddressProcessor.php index 653cf91..50c6d8e 100644 --- a/src/Module/Technique/Infrastructure/ApiPlatform/State/Processor/ProviderAddressProcessor.php +++ b/src/Module/Technique/Infrastructure/ApiPlatform/State/Processor/ProviderAddressProcessor.php @@ -33,8 +33,7 @@ use Symfony\Component\Validator\ConstraintViolationList; * sites de l'adresse (RG-3.05 / § 2.13). Les regles de l'onglet Adresse sont * garanties en amont par des contraintes sur l'entite, jouees par API Platform * avant ce processor : RG-3.06 (code postal, Assert\Regex), RG-3.05 (>= 1 site, - * Assert\Count), RG-3.09 (categorie de type PRESTATAIRE, Assert\Callback - * ProviderAddress::validateCategoryType). + * Assert\Count). * - DELETE : aucune regle metier specifique (suppression physique directe). * * La security de l'operation (technique.providers.manage) est appliquee par API diff --git a/src/Module/Technique/Infrastructure/DataFixtures/ProviderFixtures.php b/src/Module/Technique/Infrastructure/DataFixtures/ProviderFixtures.php index 998ec15..505d51d 100644 --- a/src/Module/Technique/Infrastructure/DataFixtures/ProviderFixtures.php +++ b/src/Module/Technique/Infrastructure/DataFixtures/ProviderFixtures.php @@ -63,7 +63,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; class ProviderFixtures extends Fixture implements DependentFixtureInterface { /** - * Type de categorie exige pour un prestataire et ses adresses (RG-3.09). + * Type de categorie exige pour un prestataire (RG-3.09). * Miroir de Provider::REQUIRED_CATEGORY_TYPE_CODE (non importable — regle n°1). */ private const string PROVIDER_CATEGORY_TYPE_CODE = 'PRESTATAIRE'; @@ -117,7 +117,7 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface $maintenance->setPaymentType($this->paymentType($manager, 'VIREMENT')); $maintenance->setBank($this->bank($manager, 'SG')); $this->addContact($maintenance, 'Marie', 'Martin', 'Responsable', '05 49 00 00 01', null, 'marie.martin@maintenance-pro.fr'); - $this->addAddress($maintenance, ['Chatellerault', 'Saint-Jean'], '86000', 'Poitiers', '12 rue des Acacias', categoryNames: ['Maintenance industrielle']); + $this->addAddress($maintenance, ['Chatellerault', 'Saint-Jean'], '86000', 'Poitiers', '12 rue des Acacias'); $this->addRib($maintenance, 'Compte principal', 'BNPAFRPPXXX', 'FR1420041010050500013M02606', 0); } @@ -140,7 +140,7 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface $transport->setPaymentDelay($this->paymentDelay($manager, 'A_RECEPTION')); $transport->setPaymentType($this->paymentType($manager, 'CHEQUE')); $this->addContact($transport, 'Thomas', 'Petit', 'Responsable logistique', '05 56 31 32 33', null, 'thomas.petit@transport-express.fr'); - $this->addAddress($transport, ['Saint-Jean'], '17400', 'Fontenet', '4 zone des Transporteurs', categoryNames: ['Transport']); + $this->addAddress($transport, ['Saint-Jean'], '17400', 'Fontenet', '4 zone des Transporteurs'); } // === Prestataire minimal — contact par le seul nom (RG-3.04), site 86 === @@ -237,8 +237,7 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface * moins un site est rattache (RG-3.05) ; categories d'adresse de type * PRESTATAIRE (RG-3.09). * - * @param list $siteNames au moins un site (RG-3.05) - * @param list $categoryNames categories de type PRESTATAIRE (RG-3.09) + * @param list $siteNames au moins un site (RG-3.05) */ private function addAddress( Provider $provider, @@ -247,7 +246,6 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface string $city, string $street, ?string $streetComplement = null, - array $categoryNames = [], int $position = 0, ): void { $address = new ProviderAddress(); @@ -262,9 +260,6 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface foreach ($siteNames as $siteName) { $address->addSite($this->site($siteName)); } - foreach ($categoryNames as $categoryName) { - $address->addCategory($this->category($this->manager, $categoryName)); - } $provider->addAddress($address); } diff --git a/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php b/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php index 11068ed..eec268c 100644 --- a/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php +++ b/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php @@ -444,12 +444,6 @@ final class ColumnCommentsCatalog 'provider_contact_id' => 'FK -> provider_contact.id, ON DELETE CASCADE — contact associe a l adresse.', ], - 'provider_address_category' => [ - '_table' => 'Jointure M2M provider_address <-> category — categories d adresse de type PRESTATAIRE (RG-3.09).', - 'provider_address_id' => 'FK -> provider_address.id, ON DELETE CASCADE — adresse concernee.', - 'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type PRESTATAIRE (RG-3.09).', - ], - 'provider_rib' => [ '_table' => 'Coordonnees bancaires d un prestataire (1:n) — >= 1 RIB attendu selon le type de reglement (RG-3.08). Tous les champs audites (pas d AuditIgnore).', 'id' => 'Identifiant interne auto-incremente.', diff --git a/tests/Module/Technique/Api/AbstractProviderApiTestCase.php b/tests/Module/Technique/Api/AbstractProviderApiTestCase.php index 2992694..3cead5e 100644 --- a/tests/Module/Technique/Api/AbstractProviderApiTestCase.php +++ b/tests/Module/Technique/Api/AbstractProviderApiTestCase.php @@ -388,7 +388,6 @@ abstract class AbstractProviderApiTestCase extends AbstractApiTestCase foreach ($sites as $site) { $address->addSite($site); } - $address->addCategory($this->providerCategory('NETTOYAGE')); $address->addContact($contact); $provider->addAddress($address); $em->persist($address); diff --git a/tests/Module/Technique/Api/ProviderSerializationContractTest.php b/tests/Module/Technique/Api/ProviderSerializationContractTest.php index ab0fefb..46d7ed4 100644 --- a/tests/Module/Technique/Api/ProviderSerializationContractTest.php +++ b/tests/Module/Technique/Api/ProviderSerializationContractTest.php @@ -162,11 +162,6 @@ final class ProviderSerializationContractTest extends AbstractProviderApiTestCas self::assertArrayHasKey('code', $category); self::assertArrayHasKey('name', $category); self::assertSame('NETTOYAGE', $category['code']); - - // Categories d'adresse aussi (category:read dans le contexte du detail). - self::assertArrayHasKey('categories', $data['addresses'][0]); - self::assertNotEmpty($data['addresses'][0]['categories']); - self::assertArrayHasKey('code', $data['addresses'][0]['categories'][0]); } public function testCategoriesEmbedCodeAndNameInList(): void diff --git a/tests/Module/Technique/Api/ProviderSubResourceApiTest.php b/tests/Module/Technique/Api/ProviderSubResourceApiTest.php index 1063fa9..13459cd 100644 --- a/tests/Module/Technique/Api/ProviderSubResourceApiTest.php +++ b/tests/Module/Technique/Api/ProviderSubResourceApiTest.php @@ -10,7 +10,7 @@ use App\Module\Technique\Domain\Entity\Provider; * Tests fonctionnels des sous-ressources Contacts / Adresses / RIB du prestataire * (M3, spec § 4.5 — ERP-135). Couvrent : normalisation contact (RG-3.11), RG-3.04 * (au moins le prenom OU le nom — aligne M1/M2), RG-3.05 (>= 1 site sur - * l'adresse), RG-3.06 (code postal), RG-3.09 (categorie PRESTATAIRE sur adresse), + * l'adresse), RG-3.06 (code postal), * le cloisonnement d'ecriture des sites de l'adresse (§ 2.13 -> 422 sur `sites`), * RG-3.08 (DELETE dernier RIB sous LCR -> 409), DELETE contact libre au M3 (pas de * garde « dernier contact ») et le gating selon permission (Contacts/Adresses = @@ -166,9 +166,8 @@ final class ProviderSubResourceApiTest extends AbstractProviderApiTestCase public function testPostAddressWithValidPayloadReturns201(): void { - $client = $this->createAdminClient(); - $seed = $this->seedProvider('Address Host'); - $category = $this->providerCategory('NETTOYAGE'); + $client = $this->createAdminClient(); + $seed = $this->seedProvider('Address Host'); $data = $client->request('POST', '/api/providers/'.$seed->getId().'/addresses', [ 'headers' => ['Content-Type' => self::LD], @@ -177,7 +176,6 @@ final class ProviderSubResourceApiTest extends AbstractProviderApiTestCase 'city' => 'Châtellerault', 'street' => '1 rue du Test', 'sites' => ['/api/sites/'.$this->site(self::SITE_86)->getId()], - 'categories' => ['/api/categories/'.$category->getId()], ], ])->toArray(); @@ -197,7 +195,6 @@ final class ProviderSubResourceApiTest extends AbstractProviderApiTestCase 'city' => 'Châtellerault', 'street' => '1 rue du Test', 'sites' => [], - 'categories' => ['/api/categories/'.$this->providerCategory()->getId()], ], ]); @@ -217,7 +214,6 @@ final class ProviderSubResourceApiTest extends AbstractProviderApiTestCase 'city' => 'Châtellerault', 'street' => '1 rue du Test', 'sites' => ['/api/sites/'.$this->site(self::SITE_86)->getId()], - 'categories' => ['/api/categories/'.$this->providerCategory()->getId()], ], ]); @@ -225,33 +221,10 @@ final class ProviderSubResourceApiTest extends AbstractProviderApiTestCase self::assertResponseStatusCodeSame(422); } - public function testPostAddressWithNonPrestataireCategoryReturns422(): void - { - $client = $this->createAdminClient(); - $seed = $this->seedProvider('Address Bad Cat'); - $foreign = $this->foreignCategory(); // type CLIENT -> interdite (RG-3.09). - - $response = $client->request('POST', '/api/providers/'.$seed->getId().'/addresses', [ - 'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD], - 'json' => [ - 'postalCode' => '86100', - 'city' => 'Châtellerault', - 'street' => '1 rue du Test', - 'sites' => ['/api/sites/'.$this->site(self::SITE_86)->getId()], - 'categories' => ['/api/categories/'.$foreign->getId()], - ], - ]); - - // RG-3.09 -> 422 rattachee a categories. - self::assertResponseStatusCodeSame(422); - self::assertArrayHasKey('categories', $this->violationsByPath($response->toArray(false))); - } - public function testDeleteAddressReturns204(): void { - $client = $this->createAdminClient(); - $seed = $this->seedProvider('Address Delete'); - $category = $this->providerCategory('NETTOYAGE'); + $client = $this->createAdminClient(); + $seed = $this->seedProvider('Address Delete'); $created = $client->request('POST', '/api/providers/'.$seed->getId().'/addresses', [ 'headers' => ['Content-Type' => self::LD], @@ -260,7 +233,6 @@ final class ProviderSubResourceApiTest extends AbstractProviderApiTestCase 'city' => 'Châtellerault', 'street' => '1 rue du Test', 'sites' => ['/api/sites/'.$this->site(self::SITE_86)->getId()], - 'categories' => ['/api/categories/'.$category->getId()], ], ])->toArray(); @@ -294,8 +266,7 @@ final class ProviderSubResourceApiTest extends AbstractProviderApiTestCase */ public function testPostAddressWithOutOfScopeSiteReturns422OnSitesPath(): void { - $seed = $this->seedProvider('Address Scope', [self::SITE_86]); - $category = $this->providerCategory('NETTOYAGE'); + $seed = $this->seedProvider('Address Scope', [self::SITE_86]); $creds = $this->createScopedUser( ['technique.providers.view', 'technique.providers.manage', 'sites.read_ref'], @@ -311,7 +282,6 @@ final class ProviderSubResourceApiTest extends AbstractProviderApiTestCase 'city' => 'Saint-Jean-d\'Angély', 'street' => '1 rue du Test', 'sites' => ['/api/sites/'.$this->site(self::SITE_17)->getId()], // hors user_site - 'categories' => ['/api/categories/'.$category->getId()], ], ]);