feat(field_sales) : fondations du module Tournées + VisitableInterface + RBAC (ERP-123)
- Module FieldSales (ID field_sales, REQUIRED false) avec 2 permissions field_sales.tours.view / .manage (scope V0.2, pas de reports.*), active dans config/modules.php. - Contrat partage VisitableInterface (getId/getDisplayName/getVisitableType) implemente par Client (client) et Supplier (supplier) sans import inter-module. Note doctrine.yaml : contrat polymorphe (2 implementations) donc resolu par service via (tier_type, tier_id), pas via resolve_target_entities. - 3 miroirs RBAC alignes : sidebar.php (section Tournées, item /tours, i18n sidebar.field_sales.*), personas.ts et SeedE2ECommand.php (user-full) ; matrice metier RbacSeeder (Commerciale = view+manage, Bureau = view, Compta exclue, Admin bypass).
This commit is contained in:
@@ -17,6 +17,7 @@ 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\Contract\VisitableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
@@ -135,7 +136,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
#[ORM\Index(name: 'idx_client_created_by', columns: ['created_by'])]
|
||||
#[ORM\Index(name: 'idx_client_updated_by', columns: ['updated_by'])]
|
||||
#[Auditable]
|
||||
class Client implements TimestampableInterface, BlamableInterface
|
||||
class Client implements TimestampableInterface, BlamableInterface, VisitableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
@@ -321,6 +322,24 @@ class Client implements TimestampableInterface, BlamableInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Libelle affichable du Tiers pour le module FieldSales (carte/etapes).
|
||||
* La raison sociale est NotBlank (RG M1), le fallback chaine vide ne sert
|
||||
* qu'a honorer le type non-nullable du contrat VisitableInterface.
|
||||
*/
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return $this->companyName ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type stable porte par tour_stop.tier_type pour un Client (cf. M6 § 3.1).
|
||||
*/
|
||||
public function getVisitableType(): string
|
||||
{
|
||||
return 'client';
|
||||
}
|
||||
|
||||
public function getDistributor(): ?Client
|
||||
{
|
||||
return $this->distributor;
|
||||
|
||||
@@ -17,6 +17,7 @@ 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\Contract\VisitableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
@@ -130,7 +131,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
#[ORM\Index(name: 'idx_supplier_created_by', columns: ['created_by'])]
|
||||
#[ORM\Index(name: 'idx_supplier_updated_by', columns: ['updated_by'])]
|
||||
#[Auditable]
|
||||
class Supplier implements TimestampableInterface, BlamableInterface
|
||||
class Supplier implements TimestampableInterface, BlamableInterface, VisitableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
@@ -375,6 +376,24 @@ class Supplier implements TimestampableInterface, BlamableInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Libelle affichable du Tiers pour le module FieldSales (carte/etapes).
|
||||
* La raison sociale est NotBlank (RG M2), le fallback chaine vide ne sert
|
||||
* qu'a honorer le type non-nullable du contrat VisitableInterface.
|
||||
*/
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return $this->companyName ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type stable porte par tour_stop.tier_type pour un Fournisseur (M6 § 3.1).
|
||||
*/
|
||||
public function getVisitableType(): string
|
||||
{
|
||||
return 'supplier';
|
||||
}
|
||||
|
||||
/** @return Collection<int, CategoryInterface> */
|
||||
public function getCategories(): Collection
|
||||
{
|
||||
|
||||
@@ -66,6 +66,8 @@ final class RbacSeeder
|
||||
// Fournisseurs (M2 § 2.9, ERP-90) : view + manage (hors Comptabilite).
|
||||
'commercial.suppliers.view',
|
||||
'commercial.suppliers.manage',
|
||||
// Tournees (M6 § 8, ERP-123) : Bureau = consultation seule.
|
||||
'field_sales.tours.view',
|
||||
// Lecture des referentiels transverses pour les selects client (ERP-102).
|
||||
'catalog.categories.read_ref',
|
||||
'sites.read_ref',
|
||||
@@ -96,6 +98,9 @@ final class RbacSeeder
|
||||
// (onglet Comptabilite masque/filtre pour la Commerciale).
|
||||
'commercial.suppliers.view',
|
||||
'commercial.suppliers.manage',
|
||||
// Tournees (M6 § 8, ERP-123) : Commerciale = view + manage.
|
||||
'field_sales.tours.view',
|
||||
'field_sales.tours.manage',
|
||||
// Lecture des referentiels transverses pour les selects client (ERP-102).
|
||||
'catalog.categories.read_ref',
|
||||
'sites.read_ref',
|
||||
|
||||
@@ -203,6 +203,10 @@ final class SeedE2ECommand extends Command
|
||||
'commercial.suppliers.accounting.view',
|
||||
'commercial.suppliers.accounting.manage',
|
||||
'commercial.suppliers.archive',
|
||||
// FieldSales — Tournees (M6, ERP-123). Mappe sur le persona
|
||||
// "tout". Miroir de frontend/tests/e2e/_fixtures/personas.ts.
|
||||
'field_sales.tours.view',
|
||||
'field_sales.tours.manage',
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\FieldSales;
|
||||
|
||||
final class FieldSalesModule
|
||||
{
|
||||
public const string ID = 'field_sales';
|
||||
public const string LABEL = 'Tournées';
|
||||
public const bool REQUIRED = false;
|
||||
|
||||
/**
|
||||
* Liste declarative des permissions RBAC exposees par le module FieldSales.
|
||||
*
|
||||
* Consommee par la commande `app:sync-permissions` (SyncPermissionsCommand)
|
||||
* qui upserte ces entrees dans la table `permission`, reactive les codes
|
||||
* precedemment orphelins et marque comme orphelins ceux disparus du code.
|
||||
*
|
||||
* La cle `module` est auto-injectee par le sync command a partir de
|
||||
* `self::ID`, inutile de la repeter dans chaque entree.
|
||||
*
|
||||
* Convention de nommage : `module.resource[.sub].action` en snake_case, le
|
||||
* prefixe devant correspondre exactement a `self::ID`.
|
||||
*
|
||||
* Scope V0.2 (spec M6 § 8) : UNIQUEMENT les tournees (le volet « rapport de
|
||||
* visite » a ete retire, plus aucune permission `reports.*`). Granularite
|
||||
* view/manage, alignee sur Core/Commercial. Attribution (matrice § 8) :
|
||||
* Commerciale + Admin = manage ; Bureau = view ; Compta exclue.
|
||||
*
|
||||
* @return array<int, array{code: string, label: string}>
|
||||
*/
|
||||
public static function permissions(): array
|
||||
{
|
||||
return [
|
||||
['code' => 'field_sales.tours.view', 'label' => 'Voir les tournées et l\'onglet Carte'],
|
||||
['code' => 'field_sales.tours.manage', 'label' => 'Créer / modifier / optimiser / dupliquer / supprimer une tournée'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat partage rendant un Tiers « visitable » par le module FieldSales (M6),
|
||||
* sans creer de couplage direct FieldSales -> Commercial (regle ABSOLUE n°1).
|
||||
*
|
||||
* Implemente par les Tiers du referentiel : Client (M1) et Supplier (M2), et
|
||||
* extensible aux futurs types (Prestataire...) sans toucher au module FieldSales.
|
||||
*
|
||||
* Resolution polymorphe : une etape de tournee (TourStop, ERP-124) cible un Tiers
|
||||
* via le couple (`tier_type`, `tier_id`) plutot que via une association Doctrine.
|
||||
* `getVisitableType()` fournit la valeur stable de `tier_type` ('client',
|
||||
* 'supplier', ...) qui permet, cote service, de retrouver l'implementation
|
||||
* concrete a partir de l'id. Cette interface n'est donc PAS une cible
|
||||
* `resolve_target_entities` (qui ne mappe qu'un contrat -> une seule classe,
|
||||
* alors qu'ici plusieurs entites l'implementent) : cf. note dans doctrine.yaml.
|
||||
*/
|
||||
interface VisitableInterface
|
||||
{
|
||||
/**
|
||||
* Identifiant du Tiers (null tant que non persiste).
|
||||
*/
|
||||
public function getId(): ?int;
|
||||
|
||||
/**
|
||||
* Libelle affichable du Tiers (ex: raison sociale), pour les pins de la carte
|
||||
* et la liste d'etapes d'une tournee. Jamais null (chaine vide a defaut).
|
||||
*/
|
||||
public function getDisplayName(): string;
|
||||
|
||||
/**
|
||||
* Type stable du Tiers, valeur portee par `tour_stop.tier_type`
|
||||
* ('client' | 'supplier' | ... ). Volontairement une string (et non un enum
|
||||
* ferme) pour rester ouvert aux futurs types Visitable + au point `custom`.
|
||||
*/
|
||||
public function getVisitableType(): string;
|
||||
}
|
||||
Reference in New Issue
Block a user