Compare commits
5 Commits
develop
...
dd87ea9216
| Author | SHA1 | Date | |
|---|---|---|---|
| dd87ea9216 | |||
| e239f6fe58 | |||
| 46f99a4243 | |||
| 579c1df495 | |||
| 818d930f24 |
1978
docs/superpowers/plans/2026-04-20-bovin-reads.md
Normal file
1978
docs/superpowers/plans/2026-04-20-bovin-reads.md
Normal file
File diff suppressed because it is too large
Load Diff
188
docs/ws-catalog.md
Normal file
188
docs/ws-catalog.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Catalogue des WebServices EDNOTIF
|
||||||
|
|
||||||
|
Inventaire des opérations exposées par les WSDL embarqués dans
|
||||||
|
`resources/ednotif-ws/`, avec le statut d'implémentation et une
|
||||||
|
recommandation de priorisation.
|
||||||
|
|
||||||
|
## Légende statut
|
||||||
|
|
||||||
|
- **Implémenté** — opération couverte par le bundle
|
||||||
|
- **À faire** — opération pertinente non encore implémentée
|
||||||
|
- **Optionnel** — opération hors périmètre probable, à confirmer selon le consommateur
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. wsIpBNotif — Notifications IPG Bovin
|
||||||
|
|
||||||
|
WS métier principal : déclarations réglementaires d'un cheptel bovin auprès de l'IPG.
|
||||||
|
|
||||||
|
### Lecture
|
||||||
|
|
||||||
|
| Opération | Statut | Description probable |
|
||||||
|
|---|---|---|
|
||||||
|
| `IpBGetDossierAnimal` | Implémenté | Dossier complet d'un bovin (identifications, mouvements, parents…) |
|
||||||
|
| `IpBGetInventaire` | À faire | Inventaire des animaux présents sur l'exploitation |
|
||||||
|
| `IpBGetRetourDossiers` | À faire | Retours de traitement des notifications envoyées |
|
||||||
|
| `IpBGetSortiesPresumees` | À faire | Animaux sortis selon l'IPG mais non déclarés par l'éleveur |
|
||||||
|
|
||||||
|
### Écriture
|
||||||
|
|
||||||
|
| Opération | Statut | Description probable |
|
||||||
|
|---|---|---|
|
||||||
|
| `IpBCreateEntree` | À faire | Déclaration d'entrée d'un bovin sur l'exploitation |
|
||||||
|
| `IpBCreateSortie` | À faire | Déclaration de sortie (vente, mort, abattage…) |
|
||||||
|
| `IpBCreateNaissance` | À faire | Déclaration de naissance |
|
||||||
|
| `IpBCreateMortNe` | À faire | Déclaration de mort-né |
|
||||||
|
| `IpBCreateAnimalEchange` | À faire | Échange intra-UE |
|
||||||
|
| `IpBCreateAnimalImporte` | À faire | Import pays tiers |
|
||||||
|
| `IpBCreateAvisAnimalImporte` | À faire | Avis d'import |
|
||||||
|
| `IpBCreateRebouclage` | À faire | Rebouclage / remplacement de boucle |
|
||||||
|
| `IpBCreateCommandeBoucles` | À faire | Commande de boucles |
|
||||||
|
| `IpBCreateInsemination` | À faire | Déclaration d'insémination |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. wsDmB\* — Déclarations de Mouvement Bovin
|
||||||
|
|
||||||
|
Famille orientée gestion des mouvements / transporteurs.
|
||||||
|
|
||||||
|
### wsDmBConsultation
|
||||||
|
- `DmBConsultationGetListeDonneesIT` — consultation de données (IT = identifiants traces ?)
|
||||||
|
- `DmBConsultationGetListeStatutDeplacement` — statuts de déplacement
|
||||||
|
|
||||||
|
### wsDmBGestion
|
||||||
|
- `DmBGestionCreateDroitAccesListeAnimalActeur` — gestion droits d'accès
|
||||||
|
- `DmBGestionCreateListeICA` — création listes ICA (information chaîne alimentaire)
|
||||||
|
|
||||||
|
### wsDmBListe
|
||||||
|
- `DmBListeCreateListeBovins` / `DmBListeGetListeBovins` — listes de bovins (lots)
|
||||||
|
|
||||||
|
### wsDmBTransport
|
||||||
|
- `DmBTransportCreateChargement` — chargement camion
|
||||||
|
- `DmBTransportCreateDechargement` — déchargement camion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. wsMdBEdel — Maîtrise des Données Bovin (Edel)
|
||||||
|
|
||||||
|
Consultation en lecture seule : génétique, lactation, IA, races.
|
||||||
|
|
||||||
|
| Opération | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `MdBGetDonneesGenetiquesAnimales` | Données génétiques d'animaux |
|
||||||
|
| `MdBGetDonneesMalesPublics` | Catalogue mâles reproducteurs publics |
|
||||||
|
| `MdBGetDonneesOrganismeHabilite` | Référentiel organismes habilités |
|
||||||
|
| `MdBGetDonneesOrganismeTiers` | Référentiel organismes tiers |
|
||||||
|
| `ClBGetDonneesCL` | Contrôle laitier |
|
||||||
|
| `CpBGetDonneesCPB` | Contrôle de performances bouchères |
|
||||||
|
| `IaBGetDonneesIA` | Données d'insémination |
|
||||||
|
| `OsBGetDonneesRAC` | Données race (RAC = race certifiée ?) |
|
||||||
|
| `TkBGetDonneesTE` | Transfert embryonnaire |
|
||||||
|
| `VaBGetDonneesCPV` | Contrôle performances veaux (?) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. wsMdCEdel — Maîtrise des Données Caprin
|
||||||
|
|
||||||
|
Équivalent caprin : CRUD + reproduction + lactation. 15 opérations (`MdCCreate*` pour l'écriture, `MdCGet*` avec variantes `MAJ` pour les deltas).
|
||||||
|
|
||||||
|
| Groupe | Opérations |
|
||||||
|
|---|---|
|
||||||
|
| Caprin | `MdCCreateCaprin`, `MdCGetDonneesCaprin`, `MdCGetDonneesCaprinMAJ` |
|
||||||
|
| Reproduction | `MdCCreateSaillie`, `MdCCreateFinGestation`, `MdCGetFinGestation[/MAJ]`, `MdCGetEvenementReproduction[/MAJ]` |
|
||||||
|
| Mouvement | `MdCCreateMouvement` |
|
||||||
|
| Contrôle laitier | `MdCGetCLDonneesBrutes[/MAJ]`, `MdCGetCLDonneesElaborees[/MAJ]` |
|
||||||
|
| Contrats | `MdCGetContratsExploitation` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. wsIpEdel — Identification Pérenne Edel
|
||||||
|
|
||||||
|
- `IpGetDonneesExploitation` — données descriptives de l'exploitation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. wsMrAde — Échanges ICAR
|
||||||
|
|
||||||
|
Conforme aux standards ICAR (flux laitiers internationaux).
|
||||||
|
|
||||||
|
- `GetHerdList`
|
||||||
|
- `UpdateAnimal`
|
||||||
|
- `UpdateDevice`
|
||||||
|
- `UpdateLivestockLocation`
|
||||||
|
- `UpdateMilkingResults`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. WsAnnuaire — Annuaire Guichet
|
||||||
|
|
||||||
|
Métadonnées techniques du guichet (pas du métier).
|
||||||
|
|
||||||
|
- `tkGetServices` — liste des WS disponibles
|
||||||
|
- `tkGetVersionsService` — versions d'un WS
|
||||||
|
- `tkGetOperationsServiceVersion` — opérations d'une version
|
||||||
|
- `tkGetUrl` — URL d'un service
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommandation de priorisation
|
||||||
|
|
||||||
|
Proposition d'ordre, à valider selon le périmètre réel du consommateur.
|
||||||
|
|
||||||
|
### Phase 1 — Compléter le bovin (priorité haute)
|
||||||
|
Continuer sur **wsIpBNotif**, en commençant par la **lecture** :
|
||||||
|
|
||||||
|
1. `IpBGetInventaire` — donne immédiatement la liste du cheptel, utile pour toute UI
|
||||||
|
2. `IpBGetRetourDossiers` — indispensable pour savoir si les notifs passent côté IPG
|
||||||
|
3. `IpBGetSortiesPresumees` — flux de rapprochement éleveur ↔ IPG
|
||||||
|
|
||||||
|
**Raison** : le dossier animal seul est peu utile sans l'inventaire qui permet de savoir *pour quels animaux* appeler `getAnimalFile`. Et sans `RetourDossiers`, toute écriture future est aveugle.
|
||||||
|
|
||||||
|
### Phase 2 — Écriture bovin (notifications obligatoires)
|
||||||
|
Implémenter les déclarations **dans l'ordre des cycles de vie d'un animal** :
|
||||||
|
|
||||||
|
4. `IpBCreateNaissance`
|
||||||
|
5. `IpBCreateEntree` / `IpBCreateSortie`
|
||||||
|
6. `IpBCreateMortNe`
|
||||||
|
7. `IpBCreateRebouclage` / `IpBCreateCommandeBoucles`
|
||||||
|
8. `IpBCreateAnimalEchange` / `IpBCreateAnimalImporte` / `IpBCreateAvisAnimalImporte` (si imports/échanges dans le périmètre)
|
||||||
|
9. `IpBCreateInsemination` (si non couvert par un autre outil)
|
||||||
|
|
||||||
|
### Phase 3 — Mouvements / transport
|
||||||
|
Si le consommateur gère du transport ou des lots :
|
||||||
|
|
||||||
|
10. `wsDmBListe` (lots bovins)
|
||||||
|
11. `wsDmBTransport` (chargement/déchargement)
|
||||||
|
12. `wsDmBConsultation` et `wsDmBGestion` selon besoin
|
||||||
|
|
||||||
|
### Phase 4 — Référentiels génétiques (optionnel)
|
||||||
|
Si le consommateur fait de la sélection / génétique :
|
||||||
|
|
||||||
|
13. `wsMdBEdel` — lectures ponctuelles, ne justifient une implémentation que s'il y a un usage métier concret
|
||||||
|
|
||||||
|
### Phase 5 — Caprin / ICAR (optionnel)
|
||||||
|
À activer uniquement si multi-espèces ou conformité ICAR requise.
|
||||||
|
|
||||||
|
### Hors priorité
|
||||||
|
- **wsIpEdel** : 1 op, à implémenter *en passant* si besoin ponctuel
|
||||||
|
- **WsAnnuaire** : utile pour du diagnostic / supervision, pas pour le métier
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Découpage structurel proposé
|
||||||
|
|
||||||
|
Pour garder un code cohérent, reproduire le pattern existant (`src/Bovin/`) par domaine :
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── Auth/ (existant)
|
||||||
|
├── Bovin/ (IpBNotif + IpEdel exploitation)
|
||||||
|
├── Mouvement/ (DmB*)
|
||||||
|
├── Genetique/ (MdBEdel, optionnel)
|
||||||
|
├── Caprin/ (MdCEdel, optionnel)
|
||||||
|
├── Icar/ (MrAde, optionnel)
|
||||||
|
├── Annuaire/ (WsAnnuaire, optionnel)
|
||||||
|
└── Shared/ (existant)
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque domaine expose une `*ApiInterface` publique + une implémentation `readonly`, avec ses DTOs et mappers dédiés. Le `TokenProvider` et `SoapClientFactory` restent partagés via `Shared/`.
|
||||||
7
makefile
7
makefile
@@ -86,4 +86,9 @@ php-cs-fixer-allow-risky:
|
|||||||
$(EXEC_PHP_CS_FIXER) fix --config=.php-cs-fixer.dist.php --allow-risky=yes $(FILES)
|
$(EXEC_PHP_CS_FIXER) fix --config=.php-cs-fixer.dist.php --allow-risky=yes $(FILES)
|
||||||
|
|
||||||
wait:
|
wait:
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
|
# Lance la suite PHPUnit. Usage : make test (tout)
|
||||||
|
# make test FILES=<path> (un fichier/dossier)
|
||||||
|
test:
|
||||||
|
$(EXEC_PHP) php vendor/bin/phpunit $(FILES)
|
||||||
19
phpunit.xml.dist
Normal file
19
phpunit.xml.dist
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
colors="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
cacheDirectory=".phpunit.cache">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
87
src/Shared/Soap/ZipMessageDecoder.php
Normal file
87
src/Shared/Soap/ZipMessageDecoder.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Malio\EdnotifBundle\Shared\Soap;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use ZipArchive;
|
||||||
|
|
||||||
|
final class ZipMessageDecoder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Décode le binaire `MessageZip` retourné par les opérations EDNOTIF de type
|
||||||
|
* `Get*` (Inventaire / RetourDossiers / SortiesPresumees) : le contenu est déjà
|
||||||
|
* décodé par ext-soap depuis base64, il reste à dézipper et parser le XML.
|
||||||
|
*/
|
||||||
|
public function decode(string $zipBinary): object
|
||||||
|
{
|
||||||
|
if ('' === $zipBinary) {
|
||||||
|
throw new RuntimeException('ZipMessageDecoder: binaire ZIP vide.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'ednotif_zip_');
|
||||||
|
if (false === $tempFile) {
|
||||||
|
throw new RuntimeException('ZipMessageDecoder: impossible de créer un fichier temporaire.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (false === file_put_contents($tempFile, $zipBinary)) {
|
||||||
|
throw new RuntimeException('ZipMessageDecoder: impossible d\'écrire le fichier temporaire.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = $this->readFirstEntry($tempFile);
|
||||||
|
} finally {
|
||||||
|
@unlink($tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$previous = libxml_use_internal_errors(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$simpleXml = simplexml_load_string($xml);
|
||||||
|
} finally {
|
||||||
|
libxml_clear_errors();
|
||||||
|
libxml_use_internal_errors($previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $simpleXml) {
|
||||||
|
throw new RuntimeException('ZipMessageDecoder: XML invalide dans l\'archive ZIP.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode($simpleXml);
|
||||||
|
if (false === $json) {
|
||||||
|
throw new RuntimeException('ZipMessageDecoder: échec de l\'encodage JSON intermédiaire.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($json, false);
|
||||||
|
if (!is_object($decoded)) {
|
||||||
|
throw new RuntimeException('ZipMessageDecoder: décodage JSON non objet.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readFirstEntry(string $filePath): string
|
||||||
|
{
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
$openResult = $zip->open($filePath, ZipArchive::RDONLY);
|
||||||
|
if (true !== $openResult) {
|
||||||
|
throw new RuntimeException(sprintf('ZipMessageDecoder: ouverture ZIP impossible (code %s).', (string) $openResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (0 === $zip->numFiles) {
|
||||||
|
throw new RuntimeException('ZipMessageDecoder: archive ZIP vide.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = $zip->getFromIndex(0);
|
||||||
|
if (false === $xml) {
|
||||||
|
throw new RuntimeException('ZipMessageDecoder: lecture de l\'entrée ZIP impossible.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$zip->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
tests/Unit/.gitkeep
Normal file
0
tests/Unit/.gitkeep
Normal file
79
tests/Unit/Shared/Soap/ZipMessageDecoderTest.php
Normal file
79
tests/Unit/Shared/Soap/ZipMessageDecoderTest.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Malio\EdnotifBundle\Tests\Unit\Shared\Soap;
|
||||||
|
|
||||||
|
use Malio\EdnotifBundle\Shared\Soap\ZipMessageDecoder;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RuntimeException;
|
||||||
|
use ZipArchive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
#[CoversClass(ZipMessageDecoder::class)]
|
||||||
|
final class ZipMessageDecoderTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDecodeReturnsObjectFromZippedXml(): void
|
||||||
|
{
|
||||||
|
$xml = '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
.'<MessageIpBNotifGetInventaire>'
|
||||||
|
.'<InformationsMessage><DateDebut>2026-01-01</DateDebut></InformationsMessage>'
|
||||||
|
.'<Bovins><Bovin><IdentiteBovin><Sexe>F</Sexe></IdentiteBovin></Bovin></Bovins>'
|
||||||
|
.'</MessageIpBNotifGetInventaire>';
|
||||||
|
|
||||||
|
$zipBinary = $this->makeZipBinary('message.xml', $xml);
|
||||||
|
$decoded = new ZipMessageDecoder()->decode($zipBinary);
|
||||||
|
|
||||||
|
self::assertIsObject($decoded);
|
||||||
|
self::assertSame('2026-01-01', (string) $decoded->InformationsMessage->DateDebut);
|
||||||
|
self::assertSame('F', (string) $decoded->Bovins->Bovin->IdentiteBovin->Sexe);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDecodeProducesArrayForMultiChildNodes(): void
|
||||||
|
{
|
||||||
|
$xml = '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
.'<MessageIpBNotifGetInventaire>'
|
||||||
|
.'<Bovins>'
|
||||||
|
.'<Bovin><IdentiteBovin><Sexe>F</Sexe></IdentiteBovin></Bovin>'
|
||||||
|
.'<Bovin><IdentiteBovin><Sexe>M</Sexe></IdentiteBovin></Bovin>'
|
||||||
|
.'</Bovins>'
|
||||||
|
.'</MessageIpBNotifGetInventaire>';
|
||||||
|
|
||||||
|
$decoded = new ZipMessageDecoder()->decode($this->makeZipBinary('message.xml', $xml));
|
||||||
|
|
||||||
|
self::assertIsArray($decoded->Bovins->Bovin);
|
||||||
|
self::assertCount(2, $decoded->Bovins->Bovin);
|
||||||
|
self::assertSame('F', (string) $decoded->Bovins->Bovin[0]->IdentiteBovin->Sexe);
|
||||||
|
self::assertSame('M', (string) $decoded->Bovins->Bovin[1]->IdentiteBovin->Sexe);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDecodeThrowsOnEmptyBinary(): void
|
||||||
|
{
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
new ZipMessageDecoder()->decode('');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDecodeThrowsOnInvalidZip(): void
|
||||||
|
{
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
new ZipMessageDecoder()->decode('not a zip');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeZipBinary(string $innerFile, string $content): string
|
||||||
|
{
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_zip_');
|
||||||
|
self::assertIsString($tempFile);
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
self::assertTrue(true === $zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE));
|
||||||
|
self::assertTrue($zip->addFromString($innerFile, $content));
|
||||||
|
self::assertTrue($zip->close());
|
||||||
|
$binary = file_get_contents($tempFile);
|
||||||
|
@unlink($tempFile);
|
||||||
|
self::assertIsString($binary);
|
||||||
|
|
||||||
|
return $binary;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
tests/bootstrap.php
Normal file
5
tests/bootstrap.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
Reference in New Issue
Block a user