Files
Starseed/tests/Module/Transport/Infrastructure/Console/SyncQualimatCommandTest.php
T
tristan 0b9aaef38e fix(transport) : securiser la synchro QUALIMAT (revue ERP-39)
- garde-fou anti-desactivation de masse : fetchRemote leve sur un payload
  non-list (2xx inattendu) et la commande abandonne sans ecriture si aucune
  ligne exploitable, au lieu de soft-delete tout le referentiel
- verrou consultatif pg_try_advisory_lock pour serialiser les runs (anti-overlap)
- deduplication par SIRET dans le mapper (rows_upserted = transporteurs distincts)
- upsert par paquets (INSERT groupe) au lieu d'un aller-retour par ligne
- migration des tables qualimat deplacee vers le namespace modulaire Transport
  (+ enregistrement du path dans doctrine_migrations.yaml)
- tests : deduplication + abandon sur source vide
2026-06-15 10:20:53 +02:00

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');
}
}