Checkpoint: Ajout du système de connecteurs web : table webImportSources, CRUD tRPC, page WebImportSources.tsx, endpoint /api/web-import/push-invoice, script cron SFR (scripts/web-import/sfr-connector.mjs)
This commit is contained in:
216
scripts/web-import/sfr-connector.mjs
Normal file
216
scripts/web-import/sfr-connector.mjs
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user