f057866e75
## ERP-39 — Intégration QUALIMAT (transporteurs) > ⚠️ MR **empilée** sur `feat/erp-150-module-transport` (PR #97). À merger après #97 (la base se recible automatiquement sur `develop`). Commande console `app:qualimat:sync` : récupère les opérateurs de transport agréés depuis l'API publique qualimat.org, normalise et synchronise une table référentielle. Idempotente (refresh complet), prévue pour un **cron quotidien**. ### Contenu - **Migration** `Version20260612150000` (namespace racine) : tables `qualimat_carrier` + `qualimat_sync_log`, `COMMENT ON COLUMN` sur chaque colonne, unique sur `siret`, index `is_active`. - **`QualimatRowMapper`** : normalisation pure — SIRET sans espaces (clé naturelle, source "sale" non contrainte à 14), `dd/mm/yyyy` → ISO avec `checkdate`, skip des items sans SIRET, `Nom`=`Societe` → une colonne. - **`SyncQualimatCommand`** : options `--file` / `--ppp` / `--dry-run`, fetch via http-client, upsert DBAL transactionnel (`ON CONFLICT (siret)`) + soft-delete des absents + journal, garde-fou troncature (`count == ppp`). - Activation de `framework.http_client` (l'alias `HttpClientInterface` n'était pas enregistré). ### Tests - Unitaires (`QualimatRowMapper`) + fonctionnels de la commande via `--file` (upsert, normalisation, journal, soft-delete). - Suite complète **598/598** verte. `ColumnsHaveSqlCommentTest` ✅. - Bout-en-bout réel : sync de **2332 transporteurs** (1 ignoré sans SIRET, 0 désactivé, 1 journal). ### Décisions - Migration au **namespace racine** `migrations/` (convention réelle M2/M3 ; pas de FK cross-module ; évite le tri FQCN) — écart assumé vs le mot "modulaire" du ticket. - `status` sans CHECK contraignant (feed externe), `siret` non contraint à 14 (source incomplète). --------- Co-authored-by: Matthieu <contact@malio.fr> Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Reviewed-on: #99 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
164 lines
6.8 KiB
PHP
164 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Transport\Infrastructure\Console;
|
|
|
|
use Doctrine\DBAL\Connection;
|
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Tester\CommandTester;
|
|
|
|
use const JSON_THROW_ON_ERROR;
|
|
|
|
/**
|
|
* Test fonctionnel de `app:qualimat:sync` via l'option --file (pas d'appel
|
|
* reseau) : verifie l'upsert normalise, le journal et le soft-delete.
|
|
*
|
|
* @internal
|
|
*/
|
|
final class SyncQualimatCommandTest extends KernelTestCase
|
|
{
|
|
private Connection $connection;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
self::bootKernel();
|
|
|
|
/** @var Connection $connection */
|
|
$connection = self::getContainer()->get('doctrine.dbal.default_connection');
|
|
$this->connection = $connection;
|
|
$this->purge();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->purge();
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testFirstSyncInsertsNormalizesAndLogs(): void
|
|
{
|
|
$tester = $this->runSync([
|
|
[
|
|
'Nom' => '2C TRANS',
|
|
'Societe' => '2C TRANS',
|
|
'Adresse' => '66 Impasse Mendi',
|
|
'CodePostal' => '65500',
|
|
'Ville' => 'VIC EN BIGORRE',
|
|
'Telephone_1' => '+33|0608890316',
|
|
'Siret' => '444 156 285 000 25',
|
|
'Validite' => '14/05/2027',
|
|
'Statut' => 'Audité',
|
|
'Departement' => '65 - Hautes-Pyrénées',
|
|
],
|
|
// Item sans SIRET : doit etre ignore (compte dans rows_skipped).
|
|
['Nom' => 'SANS SIRET', 'Siret' => null, 'Validite' => '01/01/2030', 'Statut' => 'Valide'],
|
|
]);
|
|
|
|
$tester->assertCommandIsSuccessful();
|
|
|
|
self::assertSame(1, $this->countRows('SELECT COUNT(*) FROM qualimat_carrier'));
|
|
|
|
$row = $this->connection->fetchAssociative('SELECT * FROM qualimat_carrier');
|
|
self::assertNotFalse($row);
|
|
self::assertSame('44415628500025', $row['siret']);
|
|
self::assertSame('2C TRANS', $row['name']);
|
|
self::assertSame('2027-05-14', $row['validity_date']);
|
|
self::assertSame('+33|0608890316', $row['phone']);
|
|
self::assertSame(1, $this->countRows('SELECT COUNT(*) FROM qualimat_carrier WHERE is_active = TRUE'));
|
|
|
|
$log = $this->connection->fetchAssociative('SELECT * FROM qualimat_sync_log ORDER BY id DESC LIMIT 1');
|
|
self::assertNotFalse($log);
|
|
self::assertSame(2, (int) $log['rows_total']);
|
|
self::assertSame(1, (int) $log['rows_upserted']);
|
|
self::assertSame(1, (int) $log['rows_skipped']);
|
|
self::assertSame(0, (int) $log['rows_deactivated']);
|
|
}
|
|
|
|
public function testSecondSyncUpdatesAndSoftDeletesMissing(): void
|
|
{
|
|
$a = ['Nom' => 'A', 'Siret' => '111 111 111 00011', 'Validite' => '01/01/2030', 'Statut' => 'Audité'];
|
|
$b = ['Nom' => 'B', 'Siret' => '222 222 222 00022', 'Validite' => '01/01/2030', 'Statut' => 'Audité'];
|
|
|
|
$this->runSync([$a, $b])->assertCommandIsSuccessful();
|
|
self::assertSame(2, $this->countRows('SELECT COUNT(*) FROM qualimat_carrier WHERE is_active = TRUE'));
|
|
|
|
// 2e run sans B et avec A renomme : A est mis a jour, B est soft-delete.
|
|
$aRenamed = ['Nom' => 'A BIS', 'Siret' => '111 111 111 00011', 'Validite' => '02/02/2031', 'Statut' => 'Valide'];
|
|
$tester = $this->runSync([$aRenamed]);
|
|
$tester->assertCommandIsSuccessful();
|
|
|
|
// Toujours 2 lignes en base, mais une seule active.
|
|
self::assertSame(2, $this->countRows('SELECT COUNT(*) FROM qualimat_carrier'));
|
|
self::assertSame(1, $this->countRows('SELECT COUNT(*) FROM qualimat_carrier WHERE is_active = TRUE'));
|
|
self::assertSame(1, $this->countRows("SELECT COUNT(*) FROM qualimat_carrier WHERE siret = '22222222200022' AND is_active = FALSE"));
|
|
|
|
// A a bien ete mis a jour (nom + statut + date).
|
|
$a = $this->connection->fetchAssociative("SELECT * FROM qualimat_carrier WHERE siret = '11111111100011'");
|
|
self::assertNotFalse($a);
|
|
self::assertSame('A BIS', $a['name']);
|
|
self::assertSame('Valide', $a['status']);
|
|
self::assertSame('2031-02-02', $a['validity_date']);
|
|
|
|
$log = $this->connection->fetchAssociative('SELECT * FROM qualimat_sync_log ORDER BY id DESC LIMIT 1');
|
|
self::assertNotFalse($log);
|
|
self::assertSame(1, (int) $log['rows_upserted']);
|
|
self::assertSame(1, (int) $log['rows_deactivated']);
|
|
self::assertSame(0, (int) $log['rows_skipped']);
|
|
}
|
|
|
|
public function testEmptySourceAbortsWithoutMassDeactivation(): void
|
|
{
|
|
// Premier run : 2 transporteurs actifs.
|
|
$a = ['Nom' => 'A', 'Siret' => '111 111 111 00011', 'Validite' => '01/01/2030', 'Statut' => 'Audité'];
|
|
$b = ['Nom' => 'B', 'Siret' => '222 222 222 00022', 'Validite' => '01/01/2030', 'Statut' => 'Audité'];
|
|
$this->runSync([$a, $b])->assertCommandIsSuccessful();
|
|
self::assertSame(2, $this->countRows('SELECT COUNT(*) FROM qualimat_carrier WHERE is_active = TRUE'));
|
|
|
|
// Source ne contenant que des items inexploitables (zero ligne mappee) :
|
|
// la commande doit ECHOUER sans toucher le referentiel (pas de soft-delete
|
|
// de masse) et sans journaliser de run.
|
|
$logsBefore = $this->countRows('SELECT COUNT(*) FROM qualimat_sync_log');
|
|
$tester = $this->runSync([
|
|
['Nom' => 'SANS SIRET 1', 'Siret' => null],
|
|
['Nom' => 'SANS SIRET 2', 'Siret' => ' '],
|
|
]);
|
|
|
|
self::assertSame(Command::FAILURE, $tester->getStatusCode());
|
|
// Les 2 transporteurs restent ACTIFS (aucune desactivation de masse).
|
|
self::assertSame(2, $this->countRows('SELECT COUNT(*) FROM qualimat_carrier WHERE is_active = TRUE'));
|
|
// Aucun journal supplementaire (abandon avant la transaction).
|
|
self::assertSame($logsBefore, $this->countRows('SELECT COUNT(*) FROM qualimat_sync_log'));
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $items
|
|
*/
|
|
private function runSync(array $items): CommandTester
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'qualimat_').'.json';
|
|
file_put_contents($path, json_encode($items, JSON_THROW_ON_ERROR));
|
|
|
|
$application = new Application(self::$kernel);
|
|
$tester = new CommandTester($application->find('app:qualimat:sync'));
|
|
$tester->execute(['--file' => $path]);
|
|
|
|
@unlink($path);
|
|
|
|
return $tester;
|
|
}
|
|
|
|
private function countRows(string $sql): int
|
|
{
|
|
return (int) $this->connection->fetchOne($sql);
|
|
}
|
|
|
|
private function purge(): void
|
|
{
|
|
$this->connection->executeStatement('DELETE FROM qualimat_carrier');
|
|
$this->connection->executeStatement('DELETE FROM qualimat_sync_log');
|
|
}
|
|
}
|