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:
Manus
2026-07-30 13:53:07 +00:00
parent bcb307bde1
commit 711ce6b83a
13 changed files with 3314 additions and 2 deletions

View 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);
});