704 lines
22 KiB
JavaScript
704 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
|
/* eslint-disable no-console */
|
|
require('dotenv').config();
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { parse } = require('csv-parse/sync');
|
|
const XLSX = require('xlsx');
|
|
const { connect, db } = require('../src/db/db-connect');
|
|
|
|
const VALID_AUTH_GROUPS = new Set(['SOUSCRIPTEUR', 'MANAGER', 'ADMIN', 'REVOQUE']);
|
|
// Sources embarquées dans le repo (prod) ; repli sur le repo v1 advalorem (dev local).
|
|
const BUNDLED_SEED_ROOT = path.resolve(__dirname, 'seed-data');
|
|
const LEGACY_V1_ROOT = path.resolve(__dirname, '..', '..', 'advalorem', 'test');
|
|
const DEFAULT_V1_ROOT = fs.existsSync(path.join(BUNDLED_SEED_ROOT, 'bdd', 'bordereau.csv'))
|
|
? BUNDLED_SEED_ROOT
|
|
: LEGACY_V1_ROOT;
|
|
const DEFAULT_REPORT_DIR = path.resolve(__dirname, '..', 'reports');
|
|
const PAGE_SIZE = 500;
|
|
|
|
function textField(name, { required = false, unique = false } = {}) {
|
|
return {
|
|
name,
|
|
type: 'text',
|
|
required,
|
|
unique,
|
|
options: { min: null, max: null, pattern: '' }
|
|
};
|
|
}
|
|
|
|
function numberField(name, { required = false, unique = false } = {}) {
|
|
return {
|
|
name,
|
|
type: 'number',
|
|
required,
|
|
unique,
|
|
options: { min: null, max: null }
|
|
};
|
|
}
|
|
|
|
function boolField(name, { required = false, unique = false } = {}) {
|
|
return {
|
|
name,
|
|
type: 'bool',
|
|
required,
|
|
unique,
|
|
options: {}
|
|
};
|
|
}
|
|
|
|
function jsonField(name, { required = false, unique = false } = {}) {
|
|
return {
|
|
name,
|
|
type: 'json',
|
|
required,
|
|
unique,
|
|
options: {}
|
|
};
|
|
}
|
|
|
|
function selectField(name, values, { required = false, unique = false } = {}) {
|
|
return {
|
|
name,
|
|
type: 'select',
|
|
required,
|
|
unique,
|
|
options: {
|
|
maxSelect: 1,
|
|
values
|
|
}
|
|
};
|
|
}
|
|
|
|
const REQUIRED_ADVALO_COLLECTIONS = [
|
|
{
|
|
name: 'advalo_deleguee',
|
|
schema: [
|
|
textField('numDemande', { required: true, unique: true }),
|
|
textField('numClient'),
|
|
textField('nomClient'),
|
|
textField('numContrat'),
|
|
textField('dateDemande'),
|
|
textField('marchandise'),
|
|
textField('mode'),
|
|
textField('capital'),
|
|
textField('depart'),
|
|
textField('arrivee'),
|
|
textField('dateDebut'),
|
|
textField('dateFin'),
|
|
textField('dateDebutIso'),
|
|
textField('dateFinIso'),
|
|
textField('nomDevis'),
|
|
textField('proprietaire'),
|
|
textField('tarif'),
|
|
textField('statutCommande'),
|
|
textField('statutFacturation'),
|
|
textField('souscripteur'),
|
|
textField('numPortefeuille')
|
|
]
|
|
},
|
|
{
|
|
name: 'advalo_demande',
|
|
schema: [
|
|
textField('sourceType'),
|
|
textField('numDemande', { required: true, unique: true }),
|
|
textField('numClient'),
|
|
textField('nomClient'),
|
|
textField('numContrat'),
|
|
textField('dateDemande'),
|
|
textField('marchandise'),
|
|
textField('mode'),
|
|
textField('capital'),
|
|
textField('depart'),
|
|
textField('arrivee'),
|
|
textField('dateDebut'),
|
|
textField('dateFin'),
|
|
textField('dateDebutIso'),
|
|
textField('dateFinIso'),
|
|
textField('nomDevis'),
|
|
textField('proprietaire'),
|
|
textField('tarif'),
|
|
textField('statutCommande'),
|
|
textField('statutFacturation'),
|
|
boolField('isDeleted'),
|
|
textField('createdBy'),
|
|
textField('region'),
|
|
textField('dpt'),
|
|
textField('souscripteur'),
|
|
textField('numPortefeuille')
|
|
]
|
|
},
|
|
{
|
|
name: 'advalo_ref_contrat',
|
|
schema: [
|
|
textField('numContrat', { required: true, unique: true }),
|
|
textField('numContratBrut'),
|
|
textField('type'),
|
|
textField('nomClient'),
|
|
textField('matricule'),
|
|
textField('region'),
|
|
textField('dpt')
|
|
]
|
|
},
|
|
{
|
|
name: 'advalo_audit',
|
|
schema: [
|
|
textField('eventType', { required: true }),
|
|
textField('createdAt'),
|
|
jsonField('data')
|
|
]
|
|
},
|
|
{
|
|
name: 'advalo_facturation_batch',
|
|
schema: [
|
|
textField('numContrat'),
|
|
textField('dateDebut'),
|
|
textField('dateFin'),
|
|
textField('sourceMode'),
|
|
jsonField('demandeIds'),
|
|
numberField('totalCapitaux'),
|
|
numberField('totalCotisation'),
|
|
textField('fingerprint', { unique: true }),
|
|
selectField('status', ['IN_PROGRESS', 'DONE', 'FAILED'], { required: true }),
|
|
textField('createdBy'),
|
|
textField('createdAt'),
|
|
textField('facturedAt'),
|
|
textField('errorMessage')
|
|
]
|
|
},
|
|
{
|
|
name: 'advalo_document',
|
|
schema: [
|
|
textField('type'),
|
|
textField('path'),
|
|
textField('sha256'),
|
|
textField('demandeId'),
|
|
textField('batchId'),
|
|
textField('createdAt')
|
|
]
|
|
}
|
|
];
|
|
|
|
function parseArgs(argv) {
|
|
const out = {
|
|
reset: false,
|
|
v1Root: DEFAULT_V1_ROOT,
|
|
reportDir: DEFAULT_REPORT_DIR
|
|
};
|
|
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
if (arg === '--reset') {
|
|
out.reset = true;
|
|
continue;
|
|
}
|
|
if (arg === '--v1-root' && argv[i + 1]) {
|
|
out.v1Root = path.resolve(argv[++i]);
|
|
continue;
|
|
}
|
|
if (arg === '--report-dir' && argv[i + 1]) {
|
|
out.reportDir = path.resolve(argv[++i]);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function parseCsvFile(filePath) {
|
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
return parse(raw, {
|
|
delimiter: ';',
|
|
columns: true,
|
|
skip_empty_lines: true,
|
|
relax_quotes: true,
|
|
bom: true
|
|
});
|
|
}
|
|
|
|
function normalizeContract(raw) {
|
|
const digits = String(raw || '').replace(/\D/g, '');
|
|
if (!digits) return '';
|
|
return digits.padStart(16, '0').slice(-16);
|
|
}
|
|
|
|
function parseFrDateToIso(dateFr) {
|
|
const m = String(dateFr || '').trim().match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
if (!m) return '';
|
|
return `${m[3]}-${m[2]}-${m[1]} 00:00:00.000Z`;
|
|
}
|
|
|
|
function parseFrDate(dateFr) {
|
|
const m = String(dateFr || '').trim().match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
if (!m) return null;
|
|
const date = new Date(`${m[3]}-${m[2]}-${m[1]}T00:00:00.000Z`);
|
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
}
|
|
|
|
function uniqueByLast(rows, keySelector) {
|
|
const map = new Map();
|
|
rows.forEach((row) => {
|
|
const key = keySelector(row);
|
|
if (!key) return;
|
|
map.set(key, row);
|
|
});
|
|
return [...map.values()];
|
|
}
|
|
|
|
async function listAllIds(collectionName) {
|
|
const ids = [];
|
|
let page = 1;
|
|
let totalPages = 1;
|
|
do {
|
|
const list = await db.records.getList(collectionName, page, PAGE_SIZE, {
|
|
fields: 'id'
|
|
});
|
|
ids.push(...list.items.map((item) => item.id));
|
|
totalPages = Number(list.totalPages || 1);
|
|
page += 1;
|
|
} while (page <= totalPages);
|
|
return ids;
|
|
}
|
|
|
|
async function deleteCollectionRecords(collectionName, report) {
|
|
const ids = await listAllIds(collectionName);
|
|
report.purged[collectionName] = ids.length;
|
|
if (!ids.length) return;
|
|
|
|
const chunkSize = 20;
|
|
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
const chunk = ids.slice(i, i + chunkSize);
|
|
await Promise.all(chunk.map((id) => db.records.delete(collectionName, id)));
|
|
if ((i + chunk.length) % 2000 === 0 || i + chunk.length === ids.length) {
|
|
console.log(`[purge:${collectionName}] ${i + chunk.length}/${ids.length}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function insertRows(collectionName, rows, report) {
|
|
const chunkSize = 20;
|
|
let inserted = 0;
|
|
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
const chunk = rows.slice(i, i + chunkSize);
|
|
try {
|
|
const created = await Promise.all(chunk.map((row) => db.records.create(collectionName, row)));
|
|
inserted += created.length;
|
|
} catch (chunkError) {
|
|
for (const row of chunk) {
|
|
try {
|
|
await db.records.create(collectionName, row);
|
|
inserted += 1;
|
|
} catch (rowError) {
|
|
const details = rowError?.data || rowError?.details || {};
|
|
const diagnostic = {
|
|
collection: collectionName,
|
|
index: i,
|
|
row,
|
|
details
|
|
};
|
|
report.anomalies.push({
|
|
type: 'insert_error',
|
|
...diagnostic
|
|
});
|
|
const err = new Error(`Insert failed for ${collectionName}: ${JSON.stringify(diagnostic)}`);
|
|
err.originalError = rowError;
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
if (inserted % 2000 === 0 || inserted === rows.length) {
|
|
console.log(`[insert:${collectionName}] ${inserted}/${rows.length}`);
|
|
}
|
|
}
|
|
report.inserted[collectionName] = inserted;
|
|
}
|
|
|
|
function splitName(fullName) {
|
|
const cleaned = String(fullName || '').trim().replace(/\s+/g, ' ');
|
|
if (!cleaned) return { nom: '', prenom: '' };
|
|
const parts = cleaned.split(' ');
|
|
const nom = parts.shift() || '';
|
|
const prenom = parts.join(' ');
|
|
return { nom, prenom };
|
|
}
|
|
|
|
function normalizeMail(value) {
|
|
const mail = String(value || '').trim();
|
|
if (!mail.includes('@')) return '';
|
|
return mail;
|
|
}
|
|
|
|
function parseClasse(value) {
|
|
const parsed = Number(String(value || '').replace(',', '.'));
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
async function loadRegionMap() {
|
|
const regions = [];
|
|
let page = 1;
|
|
let totalPages = 1;
|
|
do {
|
|
const list = await db.records.getList('region', page, PAGE_SIZE);
|
|
regions.push(...list.items);
|
|
totalPages = Number(list.totalPages || 1);
|
|
page += 1;
|
|
} while (page <= totalPages);
|
|
|
|
const map = new Map();
|
|
regions.forEach((region) => {
|
|
map.set(String(region.nom || '').trim().toUpperCase(), region.id);
|
|
});
|
|
return map;
|
|
}
|
|
|
|
function normalizeAuthGroup(value) {
|
|
const out = String(value || '').trim().toUpperCase();
|
|
if (VALID_AUTH_GROUPS.has(out)) return out;
|
|
return 'SOUSCRIPTEUR';
|
|
}
|
|
|
|
function normalizeFacturationStatus(value) {
|
|
const raw = String(value || '').trim().toLowerCase();
|
|
if (!raw) return 'unknown';
|
|
if (raw.includes('non') && raw.includes('factur')) return 'non_facture';
|
|
if (raw.includes('factur')) return 'facture';
|
|
return 'unknown';
|
|
}
|
|
|
|
function trackQualityForCommonRow(row, report, kind) {
|
|
const numDemande = String(row['N° Demande'] || '').trim();
|
|
const numContrat = normalizeContract(row['N° du contrat']);
|
|
if (numContrat.length !== 16) {
|
|
report.quality.invalidContracts += 1;
|
|
report.anomalies.push({
|
|
type: `${kind}_invalid_numContrat`,
|
|
numDemande,
|
|
numContratRaw: String(row['N° du contrat'] || '')
|
|
});
|
|
}
|
|
|
|
const dateDebut = parseFrDate(row['Date de début du transport']);
|
|
const dateFin = parseFrDate(row['Date de fin du transport']);
|
|
if (dateDebut && dateFin && dateDebut > dateFin) {
|
|
report.quality.invalidDateRanges += 1;
|
|
report.anomalies.push({
|
|
type: `${kind}_invalid_date_range`,
|
|
numDemande,
|
|
dateDebut: String(row['Date de début du transport'] || ''),
|
|
dateFin: String(row['Date de fin du transport'] || '')
|
|
});
|
|
}
|
|
|
|
const statusClass = normalizeFacturationStatus(row['Statut de la facturation']);
|
|
if (statusClass === 'unknown') {
|
|
report.quality.incoherentFacturationStatus += 1;
|
|
report.anomalies.push({
|
|
type: `${kind}_incoherent_facturation_status`,
|
|
numDemande,
|
|
statutFacturation: String(row['Statut de la facturation'] || '')
|
|
});
|
|
}
|
|
}
|
|
|
|
function buildUsersRows(usersSheetRows, regionMap, report) {
|
|
const normalized = usersSheetRows.map((row) => {
|
|
const matricule = String(row.Matricule || '').trim().toUpperCase();
|
|
const regionName = String(row.Region || '').trim().toUpperCase();
|
|
const regionId = regionMap.get(regionName) || '';
|
|
const { nom, prenom } = splitName(row.Nom_Prenom);
|
|
|
|
if (!matricule) {
|
|
report.anomalies.push({ type: 'user_missing_matricule', row });
|
|
return null;
|
|
}
|
|
if (!regionId) {
|
|
report.anomalies.push({ type: 'user_region_not_found', matricule, region: regionName });
|
|
}
|
|
|
|
return {
|
|
matricule,
|
|
matriculeIT: matricule,
|
|
nom,
|
|
prenom,
|
|
mail: normalizeMail(row.Mail),
|
|
classe: parseClasse(row.Classe),
|
|
region: regionId || '',
|
|
authGroupe: normalizeAuthGroup(row.Auth_Groupe)
|
|
};
|
|
}).filter(Boolean);
|
|
|
|
const deduped = uniqueByLast(normalized, (row) => row.matricule);
|
|
if (deduped.length !== normalized.length) {
|
|
report.anomalies.push({
|
|
type: 'user_duplicate_matricule',
|
|
sourceRows: normalized.length,
|
|
dedupedRows: deduped.length
|
|
});
|
|
}
|
|
return deduped;
|
|
}
|
|
|
|
function buildDelegueeRows(csvRows, report) {
|
|
const normalized = csvRows.map((row) => {
|
|
const numDemande = String(row['N° Demande'] || '').trim();
|
|
if (!numDemande) {
|
|
report.quality.missingNumDemande += 1;
|
|
report.anomalies.push({ type: 'deleguee_missing_numDemande', row });
|
|
return null;
|
|
}
|
|
const numContrat = normalizeContract(row['N° du contrat']);
|
|
trackQualityForCommonRow(row, report, 'deleguee');
|
|
|
|
return {
|
|
numDemande,
|
|
numClient: String(row['N° Client'] || '').trim(),
|
|
nomClient: String(row['Nom de client'] || '').trim(),
|
|
numContrat,
|
|
dateDemande: String(row['Date de la demande'] || '').trim(),
|
|
marchandise: String(row['Nature de la marchandise'] || '').trim(),
|
|
mode: String(row['Mode de transports'] || '').trim(),
|
|
capital: String(row['Valeur de marchandise'] || '').trim(),
|
|
depart: String(row['Zone de départ'] || '').trim(),
|
|
arrivee: String(row["Zone d'arrivée"] || '').trim(),
|
|
dateDebut: String(row['Date de début du transport'] || '').trim(),
|
|
dateFin: String(row['Date de fin du transport'] || '').trim(),
|
|
dateDebutIso: parseFrDateToIso(row['Date de début du transport']),
|
|
dateFinIso: parseFrDateToIso(row['Date de fin du transport']),
|
|
nomDevis: String(row['Nom du devis'] || '').trim(),
|
|
proprietaire: String(row['Propriétaire de la marchandise'] || '').trim(),
|
|
tarif: String(row.Tarif || '').trim(),
|
|
statutCommande: String(row['Statut de la commande'] || '').trim(),
|
|
statutFacturation: String(row['Statut de la facturation'] || '').trim()
|
|
};
|
|
}).filter(Boolean);
|
|
|
|
return uniqueByLast(normalized, (row) => row.numDemande);
|
|
}
|
|
|
|
function buildDemandeRows(csvRows, report) {
|
|
const normalized = csvRows.map((row) => {
|
|
const numDemande = String(row['N° Demande'] || '').trim();
|
|
if (!numDemande) {
|
|
report.quality.missingNumDemande += 1;
|
|
report.anomalies.push({ type: 'demande_missing_numDemande', row });
|
|
return null;
|
|
}
|
|
const numContrat = normalizeContract(row['N° du contrat']);
|
|
trackQualityForCommonRow(row, report, 'demande');
|
|
|
|
return {
|
|
sourceType: 'hors_grille',
|
|
numDemande,
|
|
numClient: String(row['N° Client'] || '').trim(),
|
|
nomClient: String(row['Nom de client'] || '').trim(),
|
|
numContrat,
|
|
dateDemande: String(row['Date de la demande'] || '').trim(),
|
|
marchandise: String(row['Nature de la marchandise'] || '').trim(),
|
|
mode: String(row['Mode de transports'] || '').trim(),
|
|
capital: String(row['Valeur de marchandise'] || '').trim(),
|
|
depart: String(row['Zone de départ'] || '').trim(),
|
|
arrivee: String(row["Zone d'arrivée"] || '').trim(),
|
|
dateDebut: String(row['Date de début du transport'] || '').trim(),
|
|
dateFin: String(row['Date de fin du transport'] || '').trim(),
|
|
dateDebutIso: parseFrDateToIso(row['Date de début du transport']),
|
|
dateFinIso: parseFrDateToIso(row['Date de fin du transport']),
|
|
nomDevis: String(row['Nom du devis'] || '').trim(),
|
|
proprietaire: String(row['Propriétaire de la marchandise'] || '').trim(),
|
|
tarif: String(row.Tarif || '').trim(),
|
|
statutCommande: String(row['Statut de la commande'] || '').trim(),
|
|
statutFacturation: String(row['Statut de la facturation'] || '').trim(),
|
|
isDeleted: String(row.SUPPRIME || '').toUpperCase().includes('SUPPRIME'),
|
|
createdBy: '',
|
|
region: ''
|
|
};
|
|
}).filter(Boolean);
|
|
|
|
return uniqueByLast(normalized, (row) => row.numDemande);
|
|
}
|
|
|
|
function buildRefContratRows(refWorkbookPath, report) {
|
|
const workbook = XLSX.readFile(refWorkbookPath);
|
|
const sheet = workbook.Sheets.BaseContratRegionCumulAdvalo;
|
|
if (!sheet) {
|
|
throw new Error(`Sheet BaseContratRegionCumulAdvalo missing in ${refWorkbookPath}`);
|
|
}
|
|
const rows = XLSX.utils.sheet_to_json(sheet, { defval: '' });
|
|
const normalized = rows.map((row) => {
|
|
const numContrat = normalizeContract(row.N_contrat || row.N_contrat_brut || '');
|
|
if (!numContrat) return null;
|
|
return {
|
|
numContrat,
|
|
numContratBrut: String(row.N_contrat_brut || '').trim(),
|
|
type: String(row.Type || 'Inconnu').trim() || 'Inconnu',
|
|
nomClient: String(row.Nom_client || 'Inconnu').trim() || 'Inconnu',
|
|
matricule: String(row.Matricule || 'Inconnu').trim() || 'Inconnu',
|
|
region: String(row.Region || 'Inconnu').trim() || 'Inconnu',
|
|
dpt: String(row.Dpt || 'Inconnu').trim() || 'Inconnu'
|
|
};
|
|
}).filter(Boolean);
|
|
|
|
const deduped = uniqueByLast(normalized, (row) => row.numContrat);
|
|
if (deduped.length !== 938) {
|
|
report.anomalies.push({
|
|
type: 'ref_contrat_unexpected_count',
|
|
expected: 938,
|
|
actual: deduped.length
|
|
});
|
|
}
|
|
return deduped;
|
|
}
|
|
|
|
async function getCollectionCount(collectionName) {
|
|
const list = await db.records.getList(collectionName, 1, 1);
|
|
return Number(list.totalItems || 0);
|
|
}
|
|
|
|
async function ensureAdvaloCollections(report) {
|
|
const existing = await db.collections.getFullList(200, { sort: '+name' });
|
|
const byName = new Map(existing.map((collection) => [String(collection.name || ''), collection]));
|
|
const created = [];
|
|
const alreadyPresent = [];
|
|
|
|
for (const definition of REQUIRED_ADVALO_COLLECTIONS) {
|
|
if (byName.has(definition.name)) {
|
|
alreadyPresent.push(definition.name);
|
|
continue;
|
|
}
|
|
await db.collections.create({
|
|
name: definition.name,
|
|
type: 'base',
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
schema: definition.schema
|
|
});
|
|
created.push(definition.name);
|
|
}
|
|
|
|
report.collectionSetup = {
|
|
created,
|
|
alreadyPresent
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const opts = parseArgs(process.argv.slice(2));
|
|
const report = {
|
|
generatedAt: new Date().toISOString(),
|
|
options: opts,
|
|
sourcePaths: {},
|
|
purged: {},
|
|
inserted: {},
|
|
postCheck: {},
|
|
quality: {
|
|
invalidContracts: 0,
|
|
missingNumDemande: 0,
|
|
invalidDateRanges: 0,
|
|
incoherentFacturationStatus: 0
|
|
},
|
|
anomalies: [],
|
|
stats: {}
|
|
};
|
|
|
|
const sourceFiles = {
|
|
bordereauDeleguee: path.join(opts.v1Root, 'bdd', 'bordereau.csv'),
|
|
bordereauDemande: path.join(opts.v1Root, 'bdd', 'bordereau_hors_grille.csv'),
|
|
usersExcel: path.join(opts.v1Root, 'bdd', 'xl_utilisateur.xlsx'),
|
|
refContratExcel: path.join(opts.v1Root, 'bdd', 'archives', 'xl_ref_contrat_region_cumul_advalo.xlsx')
|
|
};
|
|
report.sourcePaths = sourceFiles;
|
|
|
|
Object.entries(sourceFiles).forEach(([label, filePath]) => {
|
|
if (!fs.existsSync(filePath)) {
|
|
throw new Error(`Missing source file (${label}): ${filePath}`);
|
|
}
|
|
});
|
|
|
|
await connect();
|
|
await ensureAdvaloCollections(report);
|
|
|
|
const regionMap = await loadRegionMap();
|
|
const delegueeRaw = parseCsvFile(sourceFiles.bordereauDeleguee);
|
|
const demandeRaw = parseCsvFile(sourceFiles.bordereauDemande);
|
|
const usersWorkbook = XLSX.readFile(sourceFiles.usersExcel);
|
|
const usersSheet = usersWorkbook.Sheets.Users;
|
|
if (!usersSheet) {
|
|
throw new Error(`Sheet Users missing in ${sourceFiles.usersExcel}`);
|
|
}
|
|
const usersRaw = XLSX.utils.sheet_to_json(usersSheet, { defval: '' });
|
|
|
|
const delegueeRows = buildDelegueeRows(delegueeRaw, report);
|
|
const demandeRows = buildDemandeRows(demandeRaw, report);
|
|
const usersRows = buildUsersRows(usersRaw, regionMap, report);
|
|
const refContratRows = buildRefContratRows(sourceFiles.refContratExcel, report);
|
|
|
|
report.stats = {
|
|
sourceRows: {
|
|
utilisateur: usersRaw.length,
|
|
advalo_deleguee: delegueeRaw.length,
|
|
advalo_demande: demandeRaw.length
|
|
},
|
|
dedupRows: {
|
|
utilisateur: usersRows.length,
|
|
advalo_deleguee: delegueeRows.length,
|
|
advalo_demande: demandeRows.length
|
|
}
|
|
};
|
|
|
|
report.expected = {
|
|
utilisateur: usersRows.length,
|
|
advalo_ref_contrat: refContratRows.length,
|
|
advalo_deleguee: delegueeRows.length,
|
|
advalo_demande: demandeRows.length
|
|
};
|
|
|
|
console.log('Expected rows:', report.expected);
|
|
|
|
if (opts.reset) {
|
|
for (const collection of ['advalo_deleguee', 'advalo_demande', 'advalo_ref_contrat', 'utilisateur']) {
|
|
await deleteCollectionRecords(collection, report);
|
|
}
|
|
}
|
|
|
|
await insertRows('utilisateur', usersRows, report);
|
|
await insertRows('advalo_ref_contrat', refContratRows, report);
|
|
await insertRows('advalo_deleguee', delegueeRows, report);
|
|
await insertRows('advalo_demande', demandeRows, report);
|
|
|
|
report.postCheck = {
|
|
utilisateur: await getCollectionCount('utilisateur'),
|
|
advalo_ref_contrat: await getCollectionCount('advalo_ref_contrat'),
|
|
advalo_deleguee: await getCollectionCount('advalo_deleguee'),
|
|
advalo_demande: await getCollectionCount('advalo_demande')
|
|
};
|
|
|
|
report.acceptance = {
|
|
utilisateur_is_188: report.postCheck.utilisateur === 188,
|
|
advalo_ref_contrat_is_938: report.postCheck.advalo_ref_contrat === 938,
|
|
advalo_data_loaded: report.postCheck.advalo_deleguee > 0 && report.postCheck.advalo_demande > 0,
|
|
quality_numContrat_16_digits: report.quality.invalidContracts === 0,
|
|
quality_numDemande_non_empty: report.quality.missingNumDemande === 0,
|
|
quality_facturation_status_coherent: report.quality.incoherentFacturationStatus === 0
|
|
};
|
|
|
|
fs.mkdirSync(opts.reportDir, { recursive: true });
|
|
const reportPath = path.join(opts.reportDir, `advalo-migration-report-${Date.now()}.json`);
|
|
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
|
|
console.log(JSON.stringify({
|
|
reportPath,
|
|
expected: report.expected,
|
|
postCheck: report.postCheck,
|
|
acceptance: report.acceptance,
|
|
anomalies: report.anomalies.length
|
|
}, null, 2));
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.stack || error.message || String(error));
|
|
process.exit(1);
|
|
});
|