fix(back,front) : adresse client — au moins une categorie obligatoire
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m54s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m15s

Spec-front § Adresse : la categorie est obligatoire sur une adresse, mais
n'etait enforced ni au back ni au front.

- Back : ClientAddress::$categories porte desormais Assert\Count(min: 1)
  (POST/PATCH sans categorie -> 422). Test testAddressRequiresAtLeastOneCategory ;
  deux tests existants qui creaient une adresse sans categorie recoivent une
  categorie SECTEUR.
- Front : canValidateAddresses (creation + modification) exige >= 1 categorie
  par adresse -> bouton Enregistrer desactive tant qu'aucune categorie n'est
  choisie (meme gating que les sites).
This commit is contained in:
2026-06-03 15:57:26 +02:00
parent 9c301371fb
commit 7a45d17724
5 changed files with 40 additions and 7 deletions
@@ -743,7 +743,9 @@ const canValidateAddresses = computed(() =>
addresses.value.length > 0 addresses.value.length > 0
&& addresses.value.every((a) => { && addresses.value.every((a) => {
const filledBillingEmail = a.billingEmail !== null && a.billingEmail.trim() !== '' const filledBillingEmail = a.billingEmail !== null && a.billingEmail.trim() !== ''
return a.siteIris.length >= 1 && (!isBillingEmailRequired(a) || filledBillingEmail) return a.siteIris.length >= 1
&& a.categoryIris.length >= 1
&& (!isBillingEmailRequired(a) || filledBillingEmail)
}), }),
) )
@@ -755,7 +755,9 @@ const canValidateAddresses = computed(() =>
addresses.value.length > 0 addresses.value.length > 0
&& addresses.value.every((a) => { && addresses.value.every((a) => {
const filledBillingEmail = a.billingEmail !== null && a.billingEmail.trim() !== '' const filledBillingEmail = a.billingEmail !== null && a.billingEmail.trim() !== ''
return a.siteIris.length >= 1 && (!isBillingEmailRequired(a) || filledBillingEmail) return a.siteIris.length >= 1
&& a.categoryIris.length >= 1
&& (!isBillingEmailRequired(a) || filledBillingEmail)
}), }),
) )
@@ -177,12 +177,14 @@ class ClientAddress implements TimestampableInterface, BlamableInterface
#[Groups(['client_address:read', 'client_address:write'])] #[Groups(['client_address:read', 'client_address:write'])]
private Collection $contacts; private Collection $contacts;
// Au moins une categorie est obligatoire sur une adresse (spec-front § Adresse).
// RG-1.29 : categories de code DISTRIBUTEUR/COURTIER interdites (validateCategoryCodes). // RG-1.29 : categories de code DISTRIBUTEUR/COURTIER interdites (validateCategoryCodes).
/** @var Collection<int, CategoryInterface> */ /** @var Collection<int, CategoryInterface> */
#[ORM\ManyToMany(targetEntity: CategoryInterface::class)] #[ORM\ManyToMany(targetEntity: CategoryInterface::class)]
#[ORM\JoinTable(name: 'client_address_category')] #[ORM\JoinTable(name: 'client_address_category')]
#[ORM\JoinColumn(name: 'client_address_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[ORM\JoinColumn(name: 'client_address_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'category_id', referencedColumnName: 'id', onDelete: 'RESTRICT')] #[ORM\InverseJoinColumn(name: 'category_id', referencedColumnName: 'id', onDelete: 'RESTRICT')]
#[Assert\Count(min: 1, minMessage: 'Au moins une catégorie est obligatoire.')]
#[Groups(['client_address:read', 'client_address:write'])] #[Groups(['client_address:read', 'client_address:write'])]
private Collection $categories; private Collection $categories;
@@ -167,8 +167,9 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
public function testNonBillingAddressAcceptsEmptyBillingEmail(): void public function testNonBillingAddressAcceptsEmptyBillingEmail(): void
{ {
$this->skipIfSitesModuleDisabled(); $this->skipIfSitesModuleDisabled();
$client = $this->createAdminClient(); $client = $this->createAdminClient();
$seed = $this->seedClient('Non Billing Empty Email'); $seed = $this->seedClient('Non Billing Empty Email');
$category = $this->createCategory('SECTEUR');
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [ $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
'headers' => ['Content-Type' => self::LD], 'headers' => ['Content-Type' => self::LD],
@@ -179,6 +180,7 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
'city' => 'Châtellerault', 'city' => 'Châtellerault',
'street' => '1 rue du Test', 'street' => '1 rue du Test',
'sites' => [$this->firstSiteIri()], 'sites' => [$this->firstSiteIri()],
'categories' => ['/api/categories/'.$category->getId()],
], ],
]); ]);
@@ -286,6 +288,29 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
self::assertResponseStatusCodeSame(201); self::assertResponseStatusCodeSame(201);
} }
/**
* Spec-front § Adresse : au moins une categorie est obligatoire sur une
* adresse. POST sans categorie (mais avec site) -> 422.
*/
public function testAddressRequiresAtLeastOneCategory(): void
{
$this->skipIfSitesModuleDisabled();
$client = $this->createAdminClient();
$seed = $this->seedClient('Address No Cat');
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
'headers' => ['Content-Type' => self::LD],
'json' => [
'postalCode' => '86100',
'city' => 'Châtellerault',
'street' => '1 rue du Test',
'sites' => [$this->firstSiteIri()],
],
]);
self::assertResponseStatusCodeSame(422);
}
/** /**
* Retourne l'IRI du premier site seede (fixtures Sites). * Retourne l'IRI du premier site seede (fixtures Sites).
*/ */
@@ -110,9 +110,10 @@ final class ClientSubResourceApiTest extends AbstractCommercialApiTestCase
public function testPostAddressNormalizesBillingEmail(): void public function testPostAddressNormalizesBillingEmail(): void
{ {
$this->skipIfSitesModuleDisabled(); $this->skipIfSitesModuleDisabled();
$client = $this->createAdminClient(); $client = $this->createAdminClient();
$seed = $this->seedClient('Address Host'); $seed = $this->seedClient('Address Host');
$siteIri = $this->firstSiteIri(); $siteIri = $this->firstSiteIri();
$category = $this->createCategory('SECTEUR');
$data = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [ $data = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
'headers' => ['Content-Type' => self::LD], 'headers' => ['Content-Type' => self::LD],
@@ -123,6 +124,7 @@ final class ClientSubResourceApiTest extends AbstractCommercialApiTestCase
'city' => 'Châtellerault', 'city' => 'Châtellerault',
'street' => '1 rue du Test', 'street' => '1 rue du Test',
'sites' => [$siteIri], 'sites' => [$siteIri],
'categories' => ['/api/categories/'.$category->getId()],
], ],
])->toArray(); ])->toArray();