feat(commercial) : referentiel pays (country) en base + branchement front (ERP-116) (#79)
Auto Tag Develop / tag (push) Successful in 9s
Auto Tag Develop / tag (push) Successful in 9s
## Objectif (ERP-116, 1re iteration minimale) Sortir la liste des pays du **code en dur** cote front et la poser en base comme **referentiel `country`**, source unique du select pays. **Perimetre volontairement minimal** : code ISO + libelle + ordre uniquement — **aucune longueur bancaire/fiscale** (numero de compte, IBAN, TVA, BIC, SIREN) a ce stade. ## Backend - Entite `Country` (`code` ISO 3166-1 alpha-2 unique, `name`, `position`), calquee sur `Bank` : referentiel statique **lecture seule** (`GetCollection` + `Get`), gating `commercial.clients.view OR commercial.suppliers.view`. - Migration `Version20260609100000` : table `country` + `COMMENT ON COLUMN` + seed des **6 pays** (France, Allemagne, Belgique, Espagne, Italie, Royaume-Uni), `ON CONFLICT DO NOTHING`. - `CommercialReferentialFixtures` : re-seed des pays en dev/test. - Garde-fous : ajout au `ColumnCommentsCatalog` + whitelist `EntitiesAreTimestampableBlamableTest`. ## Frontend - `useClientReferentials` charge `/countries` (value = **nom** du pays : l'adresse stocke `country` en chaine libre, **pas de FK ni migration de donnees**). - Les 3 listes `countryOptions` en dur (clients new / edit / consultation) sont supprimees ; la consultation derive ses options de l'embed. ## Hors-scope (iterations suivantes du ticket) - Longueurs bancaires/fiscales par pays + validation associee. - FK `country_id` sur les adresses + migration de donnees. ## Tests - Back : suite complete verte (583), tests API dedies countries (200/seed/405/403/401). - Front : Vitest vert (256), spec `useClientReferentials` mise a jour. - Migration appliquee en dev + test. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #79
This commit was merged in pull request #79.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Commercial\Domain\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use App\Module\Commercial\Infrastructure\Doctrine\DoctrineCountryRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
/**
|
||||
* Pays selectionnable dans les adresses (clients / fournisseurs) : referentiel
|
||||
* statique seede par la migration (France, Allemagne, Belgique, Espagne, Italie,
|
||||
* Royaume-Uni). Remplace la liste de pays jusqu'ici codee en dur cote front.
|
||||
*
|
||||
* Perimetre minimal (ticket ERP-116, 1re iteration) : code ISO + libelle + ordre
|
||||
* d'affichage uniquement. AUCUNE longueur bancaire/fiscale (numero de compte,
|
||||
* IBAN, TVA, BIC, SIREN) a ce stade — ces colonnes feront l'objet d'une iteration
|
||||
* ulterieure du meme ticket.
|
||||
*
|
||||
* Lecture seule : GetCollection + Get uniquement ; POST/PATCH/DELETE -> 405.
|
||||
* Permission alignee sur Bank (referentiel d'adresse partage clients/fournisseurs).
|
||||
* Pas de Timestampable/Blamable (referentiel statique whiteliste dans
|
||||
* EntitiesAreTimestampableBlamableTest::EXCLUDED, comme Bank).
|
||||
*/
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
security: "is_granted('commercial.clients.view') or is_granted('commercial.suppliers.view')",
|
||||
normalizationContext: ['groups' => ['country:read']],
|
||||
// Tri par defaut : position ASC (France en tete) puis name ASC.
|
||||
order: ['position' => 'ASC', 'name' => 'ASC'],
|
||||
// Toggle ?pagination=false pour alimenter le select (cf. Bank).
|
||||
paginationClientEnabled: true,
|
||||
),
|
||||
new Get(
|
||||
security: "is_granted('commercial.clients.view') or is_granted('commercial.suppliers.view')",
|
||||
normalizationContext: ['groups' => ['country:read']],
|
||||
),
|
||||
],
|
||||
security: "is_granted('commercial.clients.view') or is_granted('commercial.suppliers.view')",
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: DoctrineCountryRepository::class)]
|
||||
#[ORM\Table(name: 'country')]
|
||||
#[ORM\UniqueConstraint(name: 'uq_country_code', columns: ['code'])]
|
||||
class Country
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['country:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 2)]
|
||||
#[Groups(['country:read'])]
|
||||
private ?string $code = null;
|
||||
|
||||
#[ORM\Column(length: 80)]
|
||||
#[Groups(['country:read'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(options: ['default' => 0])]
|
||||
#[Groups(['country:read'])]
|
||||
private int $position = 0;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCode(): ?string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function setCode(string $code): static
|
||||
{
|
||||
$this->code = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(int $position): static
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Commercial\Domain\Repository;
|
||||
|
||||
use App\Module\Commercial\Domain\Entity\Country;
|
||||
|
||||
interface CountryRepositoryInterface
|
||||
{
|
||||
public function findById(int $id): ?Country;
|
||||
|
||||
/**
|
||||
* Retourne tous les pays tries position ASC puis name ASC.
|
||||
*
|
||||
* @return list<Country>
|
||||
*/
|
||||
public function findAllOrdered(): array;
|
||||
}
|
||||
+44
-3
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Module\Commercial\Infrastructure\DataFixtures;
|
||||
|
||||
use App\Module\Commercial\Domain\Entity\Bank;
|
||||
use App\Module\Commercial\Domain\Entity\Country;
|
||||
use App\Module\Commercial\Domain\Entity\PaymentDelay;
|
||||
use App\Module\Commercial\Domain\Entity\PaymentType;
|
||||
use App\Module\Commercial\Domain\Entity\TvaMode;
|
||||
@@ -14,10 +15,11 @@ use Doctrine\Persistence\ObjectManager;
|
||||
/**
|
||||
* Fixtures du module Commercial : re-seed des 4 referentiels comptables
|
||||
* (tva_mode, payment_delay, payment_type, bank) seedes par la migration M1
|
||||
* (Version20260601000000).
|
||||
* (Version20260601000000) + du referentiel pays (country) seede par la
|
||||
* migration ERP-116 (Version20260609100000).
|
||||
*
|
||||
* Pourquoi cette fixture EN PLUS du seed de la migration : depuis ERP-54 ces
|
||||
* 4 tables sont des entites managees par l'ORM, donc le purger Doctrine les
|
||||
* Pourquoi cette fixture EN PLUS du seed de la migration : ces tables sont des
|
||||
* entites managees par l'ORM, donc le purger Doctrine les
|
||||
* vide avant chaque `doctrine:fixtures:load`. Sans cette fixture, les
|
||||
* referentiels seedes par la migration disparaitraient apres `make db-reset`
|
||||
* (0 ligne en dev/test) — cassant les FK Client -> referentiels et les tests
|
||||
@@ -59,15 +61,54 @@ class CommercialReferentialFixtures extends Fixture
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Referentiel pays (ERP-116) : code ISO alpha-2 => [name, position].
|
||||
* Doit rester aligne sur le seed de la migration Version20260609100000.
|
||||
* Traite a part car Country porte `name` (et non `label`).
|
||||
*
|
||||
* @var array<string, array{string, int}>
|
||||
*/
|
||||
private const COUNTRIES = [
|
||||
'FR' => ['France', 10],
|
||||
'DE' => ['Allemagne', 20],
|
||||
'BE' => ['Belgique', 30],
|
||||
'ES' => ['Espagne', 40],
|
||||
'IT' => ['Italie', 50],
|
||||
'GB' => ['Royaume-Uni', 60],
|
||||
'CH' => ['Suisse', 70],
|
||||
];
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach (self::REFERENTIALS as $entityClass => $rows) {
|
||||
$this->seedReferential($manager, $entityClass, $rows);
|
||||
}
|
||||
|
||||
$this->seedCountries($manager);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert idempotent du referentiel pays (lookup par code). Distinct de
|
||||
* seedReferential car Country utilise setName au lieu de setLabel.
|
||||
*/
|
||||
private function seedCountries(ObjectManager $manager): void
|
||||
{
|
||||
$existingByCode = [];
|
||||
foreach ($manager->getRepository(Country::class)->findAll() as $country) {
|
||||
$existingByCode[$country->getCode()] = $country;
|
||||
}
|
||||
|
||||
foreach (self::COUNTRIES as $code => [$name, $position]) {
|
||||
$country = $existingByCode[$code] ?? new Country();
|
||||
$country->setCode($code);
|
||||
$country->setName($name);
|
||||
$country->setPosition($position);
|
||||
$manager->persist($country);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert idempotent d'un referentiel : indexe l'existant par code puis
|
||||
* cree/met a jour chaque entree. Les 4 entites partagent le meme contrat
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Commercial\Infrastructure\Doctrine;
|
||||
|
||||
use App\Module\Commercial\Domain\Entity\Country;
|
||||
use App\Module\Commercial\Domain\Repository\CountryRepositoryInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Country>
|
||||
*/
|
||||
class DoctrineCountryRepository extends ServiceEntityRepository implements CountryRepositoryInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Country::class);
|
||||
}
|
||||
|
||||
public function findById(int $id): ?Country
|
||||
{
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
public function findAllOrdered(): array
|
||||
{
|
||||
return $this->createQueryBuilder('c')
|
||||
->orderBy('c.position', 'ASC')
|
||||
->addOrderBy('c.name', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user