/** * Connecteur SFR Pro - Script cron pour import automatique de factures * * Prérequis sur le serveur LWS : * npm install playwright @playwright/test * npx playwright install chromium * * Configuration : * Copier .env.example en .env et remplir les variables * * Utilisation : * node sfr-connector.mjs * * Cron (mensuel le 5 du mois à 8h) : * 0 8 5 * * /usr/bin/node /opt/web-import/sfr-connector.mjs >> /var/log/sfr-import.log 2>&1 */ import { chromium } from 'playwright'; import fs from 'fs'; import path from 'path'; import https from 'https'; import http from 'http'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // ============ CONFIGURATION ============ // Ces variables peuvent être définies dans un fichier .env ou directement ici const CONFIG = { // URL de l'espace client SFR Pro portalUrl: process.env.SFR_PORTAL_URL || 'https://www.sfr-business.fr/espace-client/', // Identifiants SFR login: process.env.SFR_LOGIN || '', password: process.env.SFR_PASSWORD || '', // URL de l'application de dématérialisation appUrl: process.env.APP_URL || 'https://demat-facturation.santinova-soft.org', // Token API de la source web (récupéré depuis l'interface Connecteurs web) apiToken: process.env.API_TOKEN || '', // Dossier temporaire pour les PDFs téléchargés downloadDir: process.env.DOWNLOAD_DIR || '/tmp/sfr-invoices', // Ne pas réimporter les factures déjà traitées (fichier de suivi) processedFile: process.env.PROCESSED_FILE || '/tmp/sfr-processed.json', }; // ============ HELPERS ============ function log(msg) { console.log(`[${new Date().toISOString()}] [SFR] ${msg}`); } function loadProcessed() { try { if (fs.existsSync(CONFIG.processedFile)) { return JSON.parse(fs.readFileSync(CONFIG.processedFile, 'utf8')); } } catch {} return []; } function saveProcessed(list) { fs.writeFileSync(CONFIG.processedFile, JSON.stringify(list, null, 2)); } async function pushInvoiceToApp(filePath, fileName) { const fileBuffer = fs.readFileSync(filePath); const fileBase64 = fileBuffer.toString('base64'); const body = JSON.stringify({ apiToken: CONFIG.apiToken, fileName, fileBase64, mimeType: 'application/pdf', }); return new Promise((resolve, reject) => { const url = new URL(`${CONFIG.appUrl}/api/web-import/push-invoice`); const options = { hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), }, }; const lib = url.protocol === 'https:' ? https : http; const req = lib.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { resolve({ status: res.statusCode, body: JSON.parse(data) }); } catch { resolve({ status: res.statusCode, body: data }); } }); }); req.on('error', reject); req.write(body); req.end(); }); } // ============ CONNECTEUR SFR ============ async function runSfrConnector() { log('Démarrage du connecteur SFR Pro'); if (!CONFIG.login || !CONFIG.password || !CONFIG.apiToken) { log('ERREUR : SFR_LOGIN, SFR_PASSWORD et API_TOKEN sont requis'); process.exit(1); } // Créer le dossier de téléchargement if (!fs.existsSync(CONFIG.downloadDir)) { fs.mkdirSync(CONFIG.downloadDir, { recursive: true }); } const processed = loadProcessed(); let newInvoices = 0; const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ acceptDownloads: true, }); const page = await context.newPage(); try { // 1. Naviguer vers l'espace client SFR log(`Navigation vers ${CONFIG.portalUrl}`); await page.goto(CONFIG.portalUrl, { waitUntil: 'networkidle', timeout: 30000 }); // 2. Accepter les cookies si présent try { await page.click('[id*="accept"], [class*="accept-cookie"], #didomi-notice-agree-button', { timeout: 3000 }); log('Cookies acceptés'); } catch {} // 3. Remplir le formulaire de connexion log('Connexion en cours...'); await page.fill('input[type="email"], input[name="login"], input[id*="login"], input[id*="email"]', CONFIG.login); await page.fill('input[type="password"], input[name="password"], input[id*="password"]', CONFIG.password); await page.click('button[type="submit"], input[type="submit"], button:has-text("Connexion"), button:has-text("Se connecter")'); await page.waitForNavigation({ waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); log('Connecté'); // 4. Naviguer vers la section factures // Adapter selon la structure réelle du site SFR Pro await page.goto(`${CONFIG.portalUrl}factures`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); // Chercher les liens de factures PDF const invoiceLinks = await page.$$eval( 'a[href*=".pdf"], a[href*="facture"], a[href*="invoice"], a[download]', links => links.map(a => ({ href: a.href, text: a.textContent?.trim() || '', download: a.getAttribute('download') || '', })) ); log(`${invoiceLinks.length} lien(s) de facture trouvé(s)`); // 5. Télécharger et envoyer chaque facture for (const link of invoiceLinks) { const invoiceId = link.href || link.text; if (processed.includes(invoiceId)) { log(`Déjà traité : ${link.text}`); continue; } try { // Télécharger le PDF const [download] = await Promise.all([ context.waitForEvent('download', { timeout: 15000 }), page.click(`a[href="${link.href}"]`).catch(() => page.goto(link.href)), ]); const fileName = download?.suggestedFilename() || `sfr-facture-${Date.now()}.pdf`; const filePath = path.join(CONFIG.downloadDir, fileName); await download?.saveAs(filePath); log(`Téléchargé : ${fileName}`); // Envoyer à l'application const result = await pushInvoiceToApp(filePath, fileName); log(`Envoyé : ${fileName} → ${JSON.stringify(result.body)}`); // Marquer comme traité processed.push(invoiceId); saveProcessed(processed); newInvoices++; // Nettoyer le fichier temporaire fs.unlinkSync(filePath); } catch (err) { log(`ERREUR sur ${link.text} : ${err.message}`); } } } catch (err) { log(`ERREUR FATALE : ${err.message}`); await page.screenshot({ path: path.join(CONFIG.downloadDir, 'error-screenshot.png') }).catch(() => {}); throw err; } finally { await browser.close(); } log(`Terminé : ${newInvoices} nouvelle(s) facture(s) importée(s)`); return newInvoices; } // ============ POINT D'ENTRÉE ============ runSfrConnector().catch(err => { console.error(`[FATAL] ${err.message}`); process.exit(1); });