feat(transport) : synchronisation du référentiel codes IDTF (ERP-149)
Commande console app:idtf:sync : récupère l'export Excel des codes IDTF (régimes de nettoyage transport) depuis icrt-idtf.com, le parse et synchronise une table référentielle (upsert sur (schema, idtf_number) + soft-delete + journal). Scope road ; discriminant schema road/water conservé. - migration : tables idtf_product + idtf_sync_log (COMMENT ON COLUMN sur chaque colonne, unique (schema, idtf_number), cas_numbers JSONB) - IdtfSheetParser : parsing pur d'une matrice (détection dynamique de l'en-tête, mapping par libellé, CAS split, date dd-mm-yyyy -> ISO) + tests unitaires - SyncIdtfCommand : options --schema / --file / --dry-run, POST avec fields[] explicites (export 11 colonnes), upsert DBAL transactionnel - cible make idtf-sync - tests fonctionnels via .xlsx généré (parsing/upsert/journal/soft-delete) Réutilise framework.http_client (activé pour QUALIMAT, ERP-39). phpoffice/phpspreadsheet déjà présent.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* ERP-149 (Module Transport) : referentiel des codes IDTF (regimes de nettoyage
|
||||
* transport).
|
||||
*
|
||||
* Tables alimentees par la commande `app:idtf:sync` (parsing de l'export Excel
|
||||
* icrt-idtf.com, upsert sur (schema, idtf_number) + soft-delete + journal).
|
||||
* Aucune FK cross-module : migration au namespace racine `DoctrineMigrations`.
|
||||
*/
|
||||
final class Version20260612160000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'ERP-149 : tables idtf_product + idtf_sync_log (referentiel codes IDTF, synchro console depuis l\'export Excel).';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE idtf_product (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||
idtf_number INTEGER NOT NULL,
|
||||
schema VARCHAR(8) NOT NULL,
|
||||
product_group VARCHAR(255) DEFAULT NULL,
|
||||
name TEXT NOT NULL,
|
||||
cleaning_regime VARCHAR(64) NOT NULL,
|
||||
important_requirements TEXT DEFAULT NULL,
|
||||
mandatory_date DATE DEFAULT NULL,
|
||||
related_products TEXT DEFAULT NULL,
|
||||
formula VARCHAR(255) DEFAULT NULL,
|
||||
eural_code VARCHAR(64) DEFAULT NULL,
|
||||
cas_numbers JSONB DEFAULT '[]' NOT NULL,
|
||||
footnotes TEXT DEFAULT NULL,
|
||||
source_export_date DATE NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
last_synced_at TIMESTAMP(6) WITHOUT TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT uq_idtf_product_schema_number UNIQUE (schema, idtf_number),
|
||||
CONSTRAINT chk_idtf_product_schema CHECK (schema IN ('road', 'water'))
|
||||
)
|
||||
SQL);
|
||||
$this->addSql('CREATE INDEX idx_idtf_product_active ON idtf_product (schema, is_active)');
|
||||
|
||||
$this->comment('idtf_product', '_table', "Referentiel des codes IDTF (marchandise + regime de nettoyage transport), synchronise depuis l'export Excel icrt-idtf.com.");
|
||||
$this->comment('idtf_product', 'id', 'Cle technique auto-incrementee.');
|
||||
$this->comment('idtf_product', 'idtf_number', 'Numero IDTF de la marchandise (identifiant metier source). Unique par schema.');
|
||||
$this->comment('idtf_product', 'schema', "Mode de transport / schema IDTF : 'road' (routier) ou 'water' (fluvial). Discriminant d'unicite avec idtf_number.");
|
||||
$this->comment('idtf_product', 'product_group', "Groupe de produit (colonne Product Group de l'export). Nullable.");
|
||||
$this->comment('idtf_product', 'name', "Nom de la marchandise (libelle FR de l'export).");
|
||||
$this->comment('idtf_product', 'cleaning_regime', 'Regime de nettoyage minimal exige (A, B, C, Interdit, ...).');
|
||||
$this->comment('idtf_product', 'important_requirements', 'Exigences importantes associees. Nullable.');
|
||||
$this->comment('idtf_product', 'mandatory_date', "Date d'application obligatoire du regime (convertie depuis dd-mm-yyyy). Nullable.");
|
||||
$this->comment('idtf_product', 'related_products', 'Produits apparentes (texte libre). Nullable.');
|
||||
$this->comment('idtf_product', 'formula', 'Formule chimique de la marchandise. Nullable.');
|
||||
$this->comment('idtf_product', 'eural_code', 'Code EURAL (dechet) associe. Nullable.');
|
||||
$this->comment('idtf_product', 'cas_numbers', 'Liste des numeros CAS (JSONB), eclatee depuis la cellule "Numero CAS" separee par ";". Tableau vide si absent.');
|
||||
$this->comment('idtf_product', 'footnotes', "Annotations / notes de bas de page de l'export. Nullable.");
|
||||
$this->comment('idtf_product', 'source_export_date', 'Date d\'export du fichier source (preambule "Export date:").');
|
||||
$this->comment('idtf_product', 'is_active', 'Faux = ligne absente du dernier export (soft-delete). Toute ligne non revue par le dernier run passe a FALSE.');
|
||||
$this->comment('idtf_product', 'last_synced_at', 'Horodatage du run de synchro ayant vu cette ligne en dernier (soft-delete : last_synced_at < run courant).');
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE idtf_sync_log (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||
schema VARCHAR(8) NOT NULL,
|
||||
export_date DATE NOT NULL,
|
||||
rows_total INT NOT NULL,
|
||||
rows_upserted INT NOT NULL,
|
||||
rows_deactivated INT NOT NULL,
|
||||
created_at TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->comment('idtf_sync_log', '_table', 'Journal des synchronisations IDTF (une ligne par run de la commande app:idtf:sync).');
|
||||
$this->comment('idtf_sync_log', 'id', 'Cle technique auto-incrementee.');
|
||||
$this->comment('idtf_sync_log', 'schema', "Mode de transport synchronise : 'road' ou 'water'.");
|
||||
$this->comment('idtf_sync_log', 'export_date', "Date d'export du fichier source traite par ce run.");
|
||||
$this->comment('idtf_sync_log', 'rows_total', 'Nombre de lignes exploitables lues dans le fichier.');
|
||||
$this->comment('idtf_sync_log', 'rows_upserted', 'Nombre de lignes inserees ou mises a jour.');
|
||||
$this->comment('idtf_sync_log', 'rows_deactivated', 'Nombre de lignes passees a is_active=false (absentes de cet export).');
|
||||
$this->comment('idtf_sync_log', 'created_at', 'Horodatage de fin du run (insertion du journal).');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE IF EXISTS idtf_sync_log');
|
||||
$this->addSql('DROP TABLE IF EXISTS idtf_product');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pose un COMMENT ON TABLE/COLUMN en dollar-quoting Postgres ($_$...$_$)
|
||||
* pour eviter tout echappement d'apostrophes dans les descriptions.
|
||||
*/
|
||||
private function comment(string $table, string $column, string $description): void
|
||||
{
|
||||
$quotedTable = '"'.str_replace('"', '""', $table).'"';
|
||||
|
||||
if ('_table' === $column) {
|
||||
$this->addSql(sprintf('COMMENT ON TABLE %s IS $_$%s$_$', $quotedTable, $description));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addSql(sprintf(
|
||||
'COMMENT ON COLUMN %s.%s IS $_$%s$_$',
|
||||
$quotedTable,
|
||||
'"'.str_replace('"', '""', $column).'"',
|
||||
$description,
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user