217 lines
7.0 KiB
JavaScript
217 lines
7.0 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|