[ERP-55] ClientProvider + ClientProcessor + RG métier (M1) — stackée sur ERP-54 #31

Merged
malio merged 7 commits from feature/ERP-55-impl-provider-processor-client into develop 2026-06-01 19:28:05 +00:00
Owner

MR stackée sur ERP-54 — cible = feature/ERP-54-creer-entites-client-m1 (PAS develop). Tristan validera le stack en fin de chaîne.

Branche l'API REST du répertoire clients (M1) sur l'entité Client d'ERP-54.

Périmètre

  • ClientProvider : liste paginée (Paginator ORM aligné ERP-72, ?pagination=false), exclusion archives+soft-delete par défaut (RG-1.24), ?includeArchived=true (RG-1.25), tri companyName ASC (RG-1.26), filtres ?search (fuzzy) + ?categoryType, détail 404 si soft-deleted + embarque contacts/adresses/ribs.
  • ClientProcessor : normalisation (RG-1.18→1.21), 409 doublon nom (RG-1.16) + 409 restauration (RG-1.23), gating par onglet accounting.manage/archive + mode strict 403 (RG-1.28), archivage exclusif + archivedAt (RG-1.22), RG-1.01 / RG-1.03 (mutex + type catégorie) / RG-1.12 / RG-1.13 / RG-1.04.
  • ClientReadGroupContextBuilder : ajout conditionnel du groupe client:read:accounting selon commercial.clients.accounting.view.
  • CategoryReferenceDenormalizer : résout les IRI catégorie vers Category (dénormalisation impossible sur l'interface sinon).
  • Contrats Shared : CategoryInterface::getCategoryTypeCode(), BusinessRoleAwareInterface + BusinessRoles::COMMERCIALE.

Coordination stack

  • Permissions commercial.clients.* référencées ici, déclarées en ERP-59 (tests RBAC en ERP-60).
  • Rôle métier commerciale seedé par ERP-74 (RG-1.04 dormante d'ici là).
  • Config globale pagination (itemsPerPage client / max 50) portée par ERP-72.
  • Référentiels comptables (PaymentType/Bank/...) exposés en ERP-56 → RG-1.12/1.13 testées en unitaire ici (pas d'IRI référentiel disponible avant ERP-56).

Tests

31 tests Commercial (intégration admin sur les RG métier + unitaires sur le gating / RG-1.04 / RG-1.12 / RG-1.13 / context builder). Suite complète verte (343 tests). Règle n°1 respectée (aucun import inter-modules dans Commercial).

**MR stackée sur ERP-54** — cible = `feature/ERP-54-creer-entites-client-m1` (PAS `develop`). Tristan validera le stack en fin de chaîne. Branche l'API REST du répertoire clients (M1) sur l'entité `Client` d'ERP-54. ## Périmètre - **ClientProvider** : liste paginée (Paginator ORM aligné ERP-72, `?pagination=false`), exclusion archives+soft-delete par défaut (RG-1.24), `?includeArchived=true` (RG-1.25), tri `companyName ASC` (RG-1.26), filtres `?search` (fuzzy) + `?categoryType`, détail 404 si soft-deleted + embarque contacts/adresses/ribs. - **ClientProcessor** : normalisation (RG-1.18→1.21), 409 doublon nom (RG-1.16) + 409 restauration (RG-1.23), gating par onglet `accounting.manage`/`archive` + mode strict 403 (RG-1.28), archivage exclusif + `archivedAt` (RG-1.22), RG-1.01 / RG-1.03 (mutex + type catégorie) / RG-1.12 / RG-1.13 / RG-1.04. - **ClientReadGroupContextBuilder** : ajout conditionnel du groupe `client:read:accounting` selon `commercial.clients.accounting.view`. - **CategoryReferenceDenormalizer** : résout les IRI catégorie vers `Category` (dénormalisation impossible sur l'interface sinon). - **Contrats Shared** : `CategoryInterface::getCategoryTypeCode()`, `BusinessRoleAwareInterface` + `BusinessRoles::COMMERCIALE`. ## Coordination stack - Permissions `commercial.clients.*` **référencées** ici, déclarées en **ERP-59** (tests RBAC en **ERP-60**). - Rôle métier `commerciale` seedé par **ERP-74** (RG-1.04 dormante d'ici là). - Config globale pagination (itemsPerPage client / max 50) portée par **ERP-72**. - Référentiels comptables (PaymentType/Bank/...) exposés en **ERP-56** → RG-1.12/1.13 testées en unitaire ici (pas d'IRI référentiel disponible avant ERP-56). ## Tests 31 tests Commercial (intégration admin sur les RG métier + unitaires sur le gating / RG-1.04 / RG-1.12 / RG-1.13 / context builder). Suite complète verte (343 tests). Règle n°1 respectée (aucun import inter-modules dans Commercial).
tristan reviewed 2026-06-01 08:59:34 +00:00
tristan left a comment
Owner

Revue de code ERP-55 (Provider + Processor + RG métier Client), stackée sur ERP-54.

Gros morceau, très bien construit et très bien testé (unitaires Processor/Normalizer/ContextBuilder + fonctionnels API). Points vérifiés OK :

  • hasBusinessRole() itère rbacRoles (EAGER) en contexte requête → le garde-fou « ne jamais itérer pendant le refresh JWT » ne concerne que getRoles(), donc pas de souci. Reste dormant tant qu'aucun user ne porte commerciale.
  • isArchived : groupe write sur la propriété + SerializedName('isArchived') + groupe read sur le getter → même pattern que User::isAdmin, lecture/écriture correctes.
  • Conflit d'unicité traduit en 409, avec la branche restauration RG-1.23 distincte. Gating accounting/archive strict (pas de filtrage silencieux). Recherche LIKE échappée (%/_/\) et bindée → safe.
  • ClientReadGroupContextBuilder : décoration idiomatique du context_builder, conditionne client:read:accounting sur la permission. Bien.
  • Sous-requête categoryType via IN (pas de JOIN) pour préserver DISTINCT/ORDER BY de la pagination → bon réflexe.

4 remarques ci-dessous (1 à regarder de près sur le gating POST, 3 mineures). Aucun crash.

Revue de code ERP-55 (Provider + Processor + RG métier Client), stackée sur ERP-54. Gros morceau, très bien construit et très bien testé (unitaires Processor/Normalizer/ContextBuilder + fonctionnels API). Points vérifiés OK : - `hasBusinessRole()` itère `rbacRoles` (EAGER) en contexte requête → le garde-fou « ne jamais itérer pendant le refresh JWT » ne concerne que `getRoles()`, donc pas de souci. Reste dormant tant qu'aucun user ne porte `commerciale`. - `isArchived` : groupe write sur la propriété + `SerializedName('isArchived')` + groupe read sur le getter → même pattern que `User::isAdmin`, lecture/écriture correctes. - Conflit d'unicité traduit en 409, avec la branche restauration RG-1.23 distincte. Gating accounting/archive strict (pas de filtrage silencieux). Recherche LIKE échappée (`%`/`_`/`\`) et bindée → safe. - `ClientReadGroupContextBuilder` : décoration idiomatique du `context_builder`, conditionne `client:read:accounting` sur la permission. Bien. - Sous-requête `categoryType` via IN (pas de JOIN) pour préserver DISTINCT/ORDER BY de la pagination → bon réflexe. 4 remarques ci-dessous (1 à regarder de près sur le gating POST, 3 mineures). Aucun crash.
@@ -0,0 +40,4 @@
// est le comportement attendu pour une reference cassee.
$resource = $this->iriConverter->getResourceFromIri($data);
return $resource instanceof CategoryInterface ? $resource : null;
Owner

Un IRI valide mais de mauvais type est silencieusement ignoré.

Contexte : denormalize() retourne null quand la ressource résolue n'est pas une CategoryInterface.

Cause : un payload categories: ['/api/clients/5'] résout un Client (IRI valide), instanceof CategoryInterface est faux → null → l'élément est silencieusement retiré de la collection, sans 400. Si tous les éléments tombent, Assert\Count(min:1) rattrape ; mais un mélange (1 bon IRI + 1 mauvais) passe en perdant silencieusement la mauvaise référence — erreur client masquée.

Recommandation : lever une erreur (400 / UnexpectedValueException) sur incompatibilité de type plutôt que retourner null, pour un retour explicite au client. Mineur.

**Un IRI valide mais de mauvais type est silencieusement ignoré.** **Contexte** : `denormalize()` retourne `null` quand la ressource résolue n'est pas une `CategoryInterface`. **Cause** : un payload `categories: ['/api/clients/5']` résout un `Client` (IRI valide), `instanceof CategoryInterface` est faux → `null` → l'élément est silencieusement retiré de la collection, sans 400. Si tous les éléments tombent, `Assert\Count(min:1)` rattrape ; mais un mélange (1 bon IRI + 1 mauvais) passe en perdant silencieusement la mauvaise référence — erreur client masquée. **Recommandation** : lever une erreur (400 / `UnexpectedValueException`) sur incompatibilité de type plutôt que retourner `null`, pour un retour explicite au client. Mineur.
@@ -0,0 +132,4 @@
*
* @param list<string> $payloadKeys
*/
private function guardArchive(Client $data, array $payloadKeys): bool
Owner

Le gating se base sur les clés brutes du payload → 403/422 parasites sur POST ou PATCH « objet complet ».

Contexte : guardArchive (et guardAccounting) lisent payloadKeys() = les clés JSON top-level brutes, indépendamment de l'opération et des groupes de dénormalisation.

Cause : sur un POST, isArchived n'est pas dans le groupe client:write:main (donc ignoré à la dénormalisation) mais reste visible par payloadKeys(). Si le front envoie isArchived: false avec les autres champs (réutilisation du shape de lecture), guardArchive se déclenche : array_diff non vide → 422, ou 403 si l'user n'a pas archive. Un POST légitime échoue alors qu'archiver à la création n'a aucun sens. Même problème pour un PATCH qui renvoie toute la représentation (GET → modifier → PATCH complet) : isArchived inchangé + autres champs → 422 ; champs accounting ré-émis → 403 sans accounting.manage même sans changement ; et les clés d'enveloppe JSON-LD (@id/@context) comptent aussi comme « autres champs ».

Recommandation : restreindre guardArchive au PATCH, et/ou ne considérer une requête d'archivage que si isArchived diffère de l'état persisté ; ignorer les clés non-écrivables / @*. À défaut, documenter explicitement que le front doit envoyer des merge-patch minimaux. Le jeu de tests actuel ne couvre pas ce cas (les POST de test n'incluent jamais isArchived).

**Le gating se base sur les clés brutes du payload → 403/422 parasites sur POST ou PATCH « objet complet ».** **Contexte** : `guardArchive` (et `guardAccounting`) lisent `payloadKeys()` = les clés JSON top-level brutes, indépendamment de l'opération et des groupes de dénormalisation. **Cause** : sur un **POST**, `isArchived` n'est pas dans le groupe `client:write:main` (donc ignoré à la dénormalisation) mais reste visible par `payloadKeys()`. Si le front envoie `isArchived: false` avec les autres champs (réutilisation du shape de lecture), `guardArchive` se déclenche : `array_diff` non vide → **422**, ou **403** si l'user n'a pas `archive`. Un POST légitime échoue alors qu'archiver à la création n'a aucun sens. Même problème pour un PATCH qui renvoie toute la représentation (GET → modifier → PATCH complet) : `isArchived` inchangé + autres champs → 422 ; champs accounting ré-émis → 403 sans `accounting.manage` même sans changement ; et les clés d'enveloppe JSON-LD (`@id`/`@context`) comptent aussi comme « autres champs ». **Recommandation** : restreindre `guardArchive` au PATCH, et/ou ne considérer une requête d'archivage que si `isArchived` **diffère** de l'état persisté ; ignorer les clés non-écrivables / `@*`. À défaut, documenter explicitement que le front doit envoyer des merge-patch minimaux. Le jeu de tests actuel ne couvre pas ce cas (les POST de test n'incluent jamais `isArchived`).
@@ -0,0 +285,4 @@
*
* @param list<string> $payloadKeys
*/
private function validateInformationCompleteness(Client $data, array $payloadKeys): void
Owner

RG-1.04 : un Commerciale qui crée un client SANS aucun champ Information échappe au contrôle de complétude.

Contexte : validateInformationCompleteness ne se déclenche que si payloadKeys intersecte INFORMATION_FIELDS.

Cause : un POST (ou PATCH) d'un user Commerciale qui n'envoie aucun champ de l'onglet Information ne « touche » pas l'onglet → la validation est sautée → un client est créé avec un onglet Information vide alors que la règle veut qu'il soit complet pour ce rôle. Si l'intention de RG-1.04 est « un Commerciale ne peut jamais laisser l'onglet Information incomplet » (et pas seulement « quand il l'édite »), il y a un trou.

Recommandation : confirmer l'intention exacte de RG-1.04 dans la spec § 2.9. Si la complétude est requise inconditionnellement pour le rôle Commerciale, déclencher la validation aussi sur POST indépendamment des champs envoyés. Règle dormante (aucun user commerciale avant ERP-74) → pas urgent, mais à trancher avant le seed du rôle.

**RG-1.04 : un Commerciale qui crée un client SANS aucun champ Information échappe au contrôle de complétude.** **Contexte** : `validateInformationCompleteness` ne se déclenche que si `payloadKeys` intersecte `INFORMATION_FIELDS`. **Cause** : un POST (ou PATCH) d'un user Commerciale qui n'envoie aucun champ de l'onglet Information ne « touche » pas l'onglet → la validation est sautée → un client est créé avec un onglet Information vide alors que la règle veut qu'il soit complet pour ce rôle. Si l'intention de RG-1.04 est « un Commerciale ne peut jamais laisser l'onglet Information incomplet » (et pas seulement « quand il l'édite »), il y a un trou. **Recommandation** : confirmer l'intention exacte de RG-1.04 dans la spec § 2.9. Si la complétude est requise inconditionnellement pour le rôle Commerciale, déclencher la validation aussi sur POST indépendamment des champs envoyés. Règle dormante (aucun user `commerciale` avant ERP-74) → pas urgent, mais à trancher avant le seed du rôle.
@@ -0,0 +144,4 @@
return;
}
$sub = $this->repository->createQueryBuilder('c2')
Owner

Fuite d'abstraction : createQueryBuilder() n'est pas sur l'interface du repository.

Contexte : applyCategoryType appelle $this->repository->createQueryBuilder('c2'), mais $this->repository est typé ClientRepositoryInterface, qui ne déclare que findById / save / createListQueryBuilder.

Cause : ça marche au runtime (l'implémentation concrète DoctrineClientRepository hérite de ServiceEntityRepository), mais l'appel sort du contrat de l'interface → make test passe, en revanche une analyse statique (PHPStan/Psalm) signalerait un appel de méthode indéfinie, et ça couple le Provider à l'implémentation Doctrine.

Recommandation : exposer la construction de cette sous-requête derrière une méthode du ClientRepositoryInterface (ex. createCategoryTypeFilterDQL() ou un existsCategoryType côté repo), ou récupérer le QueryBuilder via l'EntityManager. Mineur, cohérence DDD.

**Fuite d'abstraction : `createQueryBuilder()` n'est pas sur l'interface du repository.** **Contexte** : `applyCategoryType` appelle `$this->repository->createQueryBuilder('c2')`, mais `$this->repository` est typé `ClientRepositoryInterface`, qui ne déclare que `findById` / `save` / `createListQueryBuilder`. **Cause** : ça marche au runtime (l'implémentation concrète `DoctrineClientRepository` hérite de `ServiceEntityRepository`), mais l'appel sort du contrat de l'interface → `make test` passe, en revanche une analyse statique (PHPStan/Psalm) signalerait un appel de méthode indéfinie, et ça couple le Provider à l'implémentation Doctrine. **Recommandation** : exposer la construction de cette sous-requête derrière une méthode du `ClientRepositoryInterface` (ex. `createCategoryTypeFilterDQL()` ou un `existsCategoryType` côté repo), ou récupérer le QueryBuilder via l'EntityManager. Mineur, cohérence DDD.
tristan added the back label 2026-06-01 10:02:47 +00:00
matthieu added the type/feat label 2026-06-01 11:08:12 +00:00
Author
Owner

Suite review (merci) — corrections poussées en 13eb072 (fix-forward, branche rebasée sur ERP-55 corrigé) :

  • Gating sur clés brutes → 403/422 parasites : corrigé en 13eb072. ClientProcessor injecte maintenant l'EntityManager : guardArchive ne gate que sur une mise à jour d'entité gérée (plus sur POST) ET seulement si isArchived change réellement vs l'état persisté (getOriginalEntityData). guardAccounting ne gate que si un champ comptable change réellement. Les clés @* et non-écrivables sont filtrées (writablePayloadKeys). 3 tests ajoutés (POST isArchived:false, PATCH représentation complète avec @id, champ comptable inchangé).
  • Denormalizer : IRI de mauvais type silencieusement ignoré : corrigé en 13eb072. CategoryReferenceDenormalizer lève désormais UnexpectedValueException (→ 400) au lieu de retourner null. Nouveau fichier de test (CategoryReferenceDenormalizerTest, 3 cas).
  • Fuite d'abstraction createQueryBuilder() : corrigé en 13eb072. La sous-requête applyCategoryType est construite via $this->em->createQueryBuilder()->from(Client::class, …). L'interface ClientRepositoryInterface n'est pas touchée (le fix reste dans ERP-55, ne déborde pas sur ERP-54).
  • RG-1.04 contournable sur POST sans champ Information : 🟡 différé — comportement conforme à la spec, pas de changement à l'aveugle. §2.8 (« client créé via POST avec le seul formulaire principal, pas de state machine back ; s'il quitte avant de compléter, le client existe avec ses données minimales ») + RG-1.04 (scopée « lors d'un PATCH sur le groupe client:write:information ») + RG-1.14 (« complétude purement front au M1 ») → l'implémentation actuelle est intentionnelle. Règle dormante (aucun user commerciale avant ERP-74) ; décision à trancher avant ERP-74 si on veut en faire un invariant « Commerciale » plus fort.
Suite review (merci) — corrections poussées en `13eb072` (fix-forward, branche rebasée sur ERP-55 corrigé) : - **Gating sur clés brutes → 403/422 parasites** : ✅ corrigé en `13eb072`. `ClientProcessor` injecte maintenant l'`EntityManager` : `guardArchive` ne gate que sur une **mise à jour d'entité gérée** (plus sur POST) ET seulement si `isArchived` **change réellement** vs l'état persisté (`getOriginalEntityData`). `guardAccounting` ne gate que si un champ comptable **change réellement**. Les clés `@*` et non-écrivables sont filtrées (`writablePayloadKeys`). 3 tests ajoutés (POST `isArchived:false`, PATCH représentation complète avec `@id`, champ comptable inchangé). - **Denormalizer : IRI de mauvais type silencieusement ignoré** : ✅ corrigé en `13eb072`. `CategoryReferenceDenormalizer` lève désormais `UnexpectedValueException` (→ 400) au lieu de retourner `null`. Nouveau fichier de test (`CategoryReferenceDenormalizerTest`, 3 cas). - **Fuite d'abstraction `createQueryBuilder()`** : ✅ corrigé en `13eb072`. La sous-requête `applyCategoryType` est construite via `$this->em->createQueryBuilder()->from(Client::class, …)`. L'interface `ClientRepositoryInterface` n'est **pas** touchée (le fix reste dans ERP-55, ne déborde pas sur ERP-54). - **RG-1.04 contournable sur POST sans champ Information** : 🟡 différé — comportement **conforme à la spec**, pas de changement à l'aveugle. §2.8 (« client créé via POST avec le seul formulaire principal, pas de state machine back ; s'il quitte avant de compléter, le client existe avec ses données minimales ») + RG-1.04 (scopée « lors d'un PATCH sur le groupe `client:write:information` ») + RG-1.14 (« complétude purement front au M1 ») → l'implémentation actuelle est intentionnelle. Règle dormante (aucun user `commerciale` avant ERP-74) ; décision à trancher avant ERP-74 si on veut en faire un invariant « Commerciale » plus fort.
malio changed target branch from feature/ERP-54-creer-entites-client-m1 to develop 2026-06-01 15:20:26 +00:00
malio added 6 commits 2026-06-01 15:20:27 +00:00
fix(commercial) : down() orphan-only + index FK referentiels (review ERP-53)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m29s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m15s
034301ceaf
Entites metier (Client, ClientContact, ClientAddress, ClientRib) avec
#[Auditable] + Timestampable/Blamable, et 4 referentiels comptables statiques
(TvaMode, PaymentDelay, PaymentType, Bank). 8 repositories interfaces + impl
Doctrine. Aucun ApiResource (Provider/Processor = ERP-55).

- Client : 2 FK auto-referentes distributor/broker (mutuellement exclusives,
  CHECK en base), M2M categories, FK referentiels comptables, groupes de
  serialisation par onglet. Pas de #[ORM\UniqueConstraint] : unicite du nom de
  societe portee par l'index partiel Postgres (decision Q4).
- ClientRib : tous les champs audites, aucun #[AuditIgnore] sur iban/bic
  (decision 29/05, audit admin-only).
- M2M Category via le contrat Shared CategoryInterface + resolve_target_entities
  (regle n°1, pas d'import inter-modules) ; sites via SiteInterface.
- CommercialReferentialFixtures : re-seed idempotent des 4 referentiels (sinon
  vides apres db-reset car desormais tables mappees, purgees par les fixtures).
- Referentiels whitelistes dans EntitiesAreTimestampableBlamableTest::EXCLUDED.
- doctrine.yaml : mapping ORM du module Commercial + resolve CategoryInterface.
- ColumnCommentsCatalog : ajout des colonnes M1 (chemin schema:update/test) ;
  migration retrofit Version20260528120000 filtree sur les tables existantes
  pour ne pas casser sur les tables des modules crees plus tard.
- makefile test-db-setup : recreation de l'index partiel uq_client_company_name_active.

Refs ERP-54.
Branche l'API REST du repertoire clients (M1) sur l'entite Client preparee en
ERP-54. Operations GetCollection / Get / Post / Patch (pas de Delete au M1 :
l'archivage passe par PATCH isArchived).

ClientProvider :
- liste paginee (Paginator ORM, aligne sur la convention ERP-72) + echappatoire
  ?pagination=false
- exclut archives + soft-deletes par defaut (RG-1.24), ?includeArchived=true
  reintegre les archives (RG-1.25)
- tri companyName ASC (RG-1.26), filtres ?search (fuzzy companyName/lastName/
  email) et ?categoryType=<code>
- detail : 404 sur soft-delete, embarque contacts/adresses/ribs

ClientProcessor :
- normalisation serveur via ClientFieldNormalizer (RG-1.18 a 1.21)
- 409 sur doublon de nom de societe (RG-1.16) ; 409 dedie sur conflit de
  restauration (RG-1.23)
- gating par onglet : champ comptable -> accounting.manage, isArchived ->
  archive, mode strict 403 sur tout le payload (RG-1.28) ; archivage exclusif
  (RG-1.22) + pose/retrait archivedAt
- regles metier RG-1.01 (prenom/nom), RG-1.03 (distributor/broker exclusifs +
  controle du type de categorie), RG-1.12 (Virement -> banque), RG-1.13 (LCR ->
  >= 1 RIB), RG-1.04 (completude Information pour le role Commerciale)

Lecture comptable conditionnelle : ClientReadGroupContextBuilder ajoute le
groupe client:read:accounting selon commercial.clients.accounting.view.

Resolution des references categorie : CategoryReferenceDenormalizer resout les
IRI vers Category quand la propriete est type-hintee par le contrat
CategoryInterface (denormalisation impossible sur une interface sinon).

Contrats Shared :
- CategoryInterface::getCategoryTypeCode() (implemente par Category) pour la
  verification de type sans import inter-modules
- BusinessRoleAwareInterface (implemente par User) + BusinessRoles::COMMERCIALE
  pour detecter le role metier ; le code de role sera seede par ERP-74 et
  reutilise par ERP-59/60. RG-1.04 reste dormante tant qu'aucun user ne porte
  ce role.

Coordination stack :
- chaines de permission commercial.clients.* referencees ici, declarees en
  ERP-59 (tests RBAC complets en ERP-60)
- config globale de pagination (itemsPerPage client, max 50) portee par ERP-72
- referentiels comptables (PaymentType/Bank/...) exposes en ERP-56

Tests : 31 tests Commercial (integration admin sur les regles metier + unitaires
sur le gating, RG-1.04/1.12/1.13 et le context builder). Suite complete verte
(339 tests).
tristan added 1 commit 2026-06-01 15:52:47 +00:00
Merge remote-tracking branch 'origin/develop' into feature/ERP-55-impl-provider-processor-client
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m25s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m15s
8222c63315
# Conflicts:
#	src/Module/Commercial/Domain/Entity/Client.php
#	src/Shared/Domain/Contract/CategoryInterface.php
malio merged commit 0c9b563cae into develop 2026-06-01 19:28:05 +00:00
malio deleted branch feature/ERP-55-impl-provider-processor-client 2026-06-01 19:28:05 +00:00
matthieu added the M1-Client label 2026-06-01 21:14:59 +00:00
Sign in to join this conversation.