From 26132f38ce89f311429ef40757eb9c6eb4e588b7 Mon Sep 17 00:00:00 2001 From: odentas Date: Wed, 5 Nov 2025 19:32:25 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Ajouter=20script=20migration=20PDF=20av?= =?UTF-8?q?enants=20Airtable=20=E2=86=92=20S3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Téléchargement des PDF depuis URLs Airtable - Upload vers S3 avec pattern avenants/{reference}_avenant_{timestamp}.pdf - Mise à jour de pdf_s3_key et pdf_url dans Supabase - 91 PDF migrés avec succès --- scripts/migrate-avenants-pdf-from-airtable.ts | 331 ++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 scripts/migrate-avenants-pdf-from-airtable.ts diff --git a/scripts/migrate-avenants-pdf-from-airtable.ts b/scripts/migrate-avenants-pdf-from-airtable.ts new file mode 100644 index 0000000..07b69c8 --- /dev/null +++ b/scripts/migrate-avenants-pdf-from-airtable.ts @@ -0,0 +1,331 @@ +/** + * Script de migration des PDF des avenants depuis Airtable vers S3 + * + * Ce script télécharge les PDF des avenants depuis Airtable et les uploade sur S3, + * puis met à jour la base de données Supabase avec les URLs. + * + * Prérequis: + * - Le CSV doit avoir les colonnes: Reference, Avenant PDF + * - Les variables d'environnement AWS et Supabase doivent être configurées + * + * Utilisation: + * npx tsx scripts/migrate-avenants-pdf-from-airtable.ts + */ + +import { createClient } from '@supabase/supabase-js'; +import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as readline from 'readline'; +import * as https from 'https'; +import * as http from 'http'; +import { config } from 'dotenv'; + +// Charger les variables d'environnement +config({ path: path.join(process.cwd(), '.env.local') }); + +// Configuration +const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!; +const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY!; +const CSV_FILE_PATH = path.join(process.cwd(), 'Contrats-PDF-Avenants.csv'); + +// AWS S3 Configuration +const AWS_REGION = process.env.AWS_REGION || 'eu-west-3'; +const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID!; +const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY!; +const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME || 'odentas-documents'; + +if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY) { + console.error('❌ Variables d\'environnement Supabase manquantes'); + process.exit(1); +} + +if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) { + console.error('❌ Variables d\'environnement AWS manquantes'); + process.exit(1); +} + +const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY); +const s3Client = new S3Client({ + region: AWS_REGION, + credentials: { + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY, + }, +}); + +interface AirtableAvenantPdfRow { + reference: string; // Référence du contrat + avenant_pdf_url: string; // URL du PDF de l'avenant +} + +/** + * Lit et parse le fichier CSV + */ +async function readCSV(): Promise { + return new Promise((resolve, reject) => { + const rows: AirtableAvenantPdfRow[] = []; + const fileStream = fs.createReadStream(CSV_FILE_PATH); + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity + }); + + let isFirstLine = true; + let pdfColumnIndex = -1; + let referenceColumnIndex = -1; + + rl.on('line', (line) => { + // Parser la première ligne pour trouver les index des colonnes + if (isFirstLine) { + isFirstLine = false; + const matches = line.match(/(?:^|,)("(?:[^"]|"")*"|[^,]*)/g); + if (!matches) return; + + const cols = matches.map(m => { + let val = m.replace(/^,/, ''); + if (val.startsWith('"') && val.endsWith('"')) { + val = val.slice(1, -1).replace(/""/g, '"'); + } + return val.trim(); + }); + + referenceColumnIndex = cols.indexOf('Reference'); + pdfColumnIndex = cols.indexOf('Avenant PDF'); + + if (referenceColumnIndex === -1 || pdfColumnIndex === -1) { + console.error('❌ Colonnes "Reference" ou "Avenant PDF" non trouvées'); + reject(new Error('Colonnes manquantes dans le CSV')); + return; + } + return; + } + + // Parser les lignes de données + const matches = line.match(/(?:^|,)("(?:[^"]|"")*"|[^,]*)/g); + if (!matches || matches.length <= Math.max(referenceColumnIndex, pdfColumnIndex)) return; + + const cols = matches.map(m => { + let val = m.replace(/^,/, ''); + if (val.startsWith('"') && val.endsWith('"')) { + val = val.slice(1, -1).replace(/""/g, '"'); + } + return val.trim(); + }); + + const reference = cols[referenceColumnIndex] || ''; + let pdfUrl = cols[pdfColumnIndex] || ''; + + // Extraire l'URL si le format est "nom_fichier.pdf (url)" + const urlMatch = pdfUrl.match(/\(https?:\/\/[^)]+\)/); + if (urlMatch) { + pdfUrl = urlMatch[0].slice(1, -1); // Retirer les parenthèses + } + + if (reference && pdfUrl && pdfUrl.startsWith('http')) { + rows.push({ + reference, + avenant_pdf_url: pdfUrl + }); + } + }); + + rl.on('close', () => resolve(rows)); + rl.on('error', reject); + }); +} + +/** + * Télécharge un fichier depuis une URL + */ +async function downloadFile(url: string): Promise { + return new Promise((resolve, reject) => { + const protocol = url.startsWith('https') ? https : http; + + protocol.get(url, (response) => { + if (response.statusCode === 301 || response.statusCode === 302) { + // Suivre la redirection + if (response.headers.location) { + downloadFile(response.headers.location).then(resolve).catch(reject); + return; + } + } + + if (response.statusCode !== 200) { + reject(new Error(`Erreur HTTP: ${response.statusCode}`)); + return; + } + + const chunks: Buffer[] = []; + response.on('data', (chunk) => chunks.push(chunk)); + response.on('end', () => resolve(Buffer.concat(chunks))); + response.on('error', reject); + }).on('error', reject); + }); +} + +/** + * Upload un fichier sur S3 + */ +async function uploadToS3(buffer: Buffer, key: string, contentType: string): Promise { + const command = new PutObjectCommand({ + Bucket: S3_BUCKET_NAME, + Key: key, + Body: buffer, + ContentType: contentType, + }); + + await s3Client.send(command); + + // Retourner l'URL publique (si le bucket est public) ou une URL signée + return `https://${S3_BUCKET_NAME}.s3.${AWS_REGION}.amazonaws.com/${key}`; +} + +/** + * Recherche les avenants d'un contrat + */ +async function findAvenantsForContract(contractReference: string): Promise { + const { data: contract } = await supabase + .from('cddu_contracts') + .select('id') + .or(`contract_number.eq.${contractReference},reference.eq.${contractReference}`) + .maybeSingle(); + + if (!contract) { + return []; + } + + const { data: avenants } = await supabase + .from('avenants') + .select('id, numero_avenant, date_avenant, pdf_s3_key') + .eq('contract_id', contract.id) + .order('date_avenant', { ascending: true }); + + return avenants || []; +} + +/** + * Met à jour un avenant avec le PDF + */ +async function updateAvenantWithPdf(avenantId: string, s3Key: string, pdfUrl: string) { + const { error } = await supabase + .from('avenants') + .update({ + pdf_s3_key: s3Key, + pdf_url: pdfUrl, + }) + .eq('id', avenantId); + + if (error) { + throw error; + } +} + +/** + * Fonction principale de migration + */ +async function migrate() { + console.log('🚀 Début de la migration des PDF des avenants depuis Airtable\n'); + + // Vérifier que le fichier CSV existe + if (!fs.existsSync(CSV_FILE_PATH)) { + console.error(`❌ Fichier CSV non trouvé: ${CSV_FILE_PATH}`); + console.error('📝 Placez le fichier "Contrats-PDF-Avenants.csv" à la racine du projet'); + process.exit(1); + } + + console.log(`📄 Lecture du fichier: ${CSV_FILE_PATH}\n`); + + const rows = await readCSV(); + console.log(`📊 Nombre de lignes avec PDF: ${rows.length}\n`); + + const stats = { + total: rows.length, + success: 0, + noAvenants: 0, + downloadError: 0, + uploadError: 0, + updateError: 0, + }; + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const index = i + 1; + + console.log(`\n[${index}/${rows.length}] Traitement de ${row.reference}`); + + // Rechercher les avenants pour ce contrat + const avenants = await findAvenantsForContract(row.reference); + + if (avenants.length === 0) { + console.log(` ⚠️ Aucun avenant trouvé pour ce contrat`); + stats.noAvenants++; + continue; + } + + console.log(` ✅ ${avenants.length} avenant(s) trouvé(s)`); + + // Si plusieurs avenants, on prend le premier (le plus ancien) + // Dans Airtable, il n'y a qu'un seul PDF par contrat + const avenant = avenants[0]; + + if (avenant.pdf_s3_key) { + console.log(` ⏭️ PDF déjà présent (${avenant.pdf_s3_key}) - Ignoré`); + stats.success++; + continue; + } + + try { + // Télécharger le PDF depuis Airtable + console.log(` 📥 Téléchargement du PDF...`); + const pdfBuffer = await downloadFile(row.avenant_pdf_url); + console.log(` ✅ PDF téléchargé (${(pdfBuffer.length / 1024).toFixed(2)} KB)`); + + // Générer la clé S3 + const timestamp = new Date().getTime(); + const s3Key = `avenants/${row.reference}_avenant_${timestamp}.pdf`; + + // Upload sur S3 + console.log(` ☁️ Upload vers S3...`); + const pdfUrl = await uploadToS3(pdfBuffer, s3Key, 'application/pdf'); + console.log(` ✅ PDF uploadé: ${s3Key}`); + + // Mettre à jour l'avenant dans Supabase + console.log(` 💾 Mise à jour de l'avenant ${avenant.numero_avenant}...`); + await updateAvenantWithPdf(avenant.id, s3Key, pdfUrl); + console.log(` ✅ Avenant mis à jour`); + + stats.success++; + + } catch (error: any) { + console.log(` ❌ Erreur: ${error.message}`); + if (error.message.includes('HTTP') || error.message.includes('download')) { + stats.downloadError++; + } else if (error.message.includes('S3') || error.message.includes('upload')) { + stats.uploadError++; + } else { + stats.updateError++; + } + } + + // Délai entre chaque traitement pour éviter de surcharger les APIs + await new Promise(resolve => setTimeout(resolve, 500)); + } + + // Résumé + console.log('\n\n' + '='.repeat(60)); + console.log('📊 RÉSUMÉ DE LA MIGRATION'); + console.log('='.repeat(60)); + console.log(`Total de PDF: ${stats.total}`); + console.log(`PDF migrés avec succès: ${stats.success} ✅`); + console.log(`Contrats sans avenants: ${stats.noAvenants} ⚠️`); + console.log(`Erreurs téléchargement: ${stats.downloadError} ❌`); + console.log(`Erreurs upload S3: ${stats.uploadError} ❌`); + console.log(`Erreurs mise à jour DB: ${stats.updateError} ❌`); + console.log('='.repeat(60)); + + console.log('\n✅ Migration terminée!\n'); +} + +// Exécuter la migration +migrate().catch(console.error);