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,70 @@
# Connecteurs Web - Scripts d'import automatique
## Prérequis sur le serveur LWS
```bash
cd /opt/web-import
npm install playwright
npx playwright install chromium --with-deps
```
## Configuration
1. Depuis l'application : **Configuration > Connecteurs web** → créer une source SFR → copier le token API
2. Créer un fichier de configuration :
```bash
cp config.example.json config.json
# Éditer config.json avec vos valeurs
```
Contenu de `config.json` :
```json
{
"sfrLogin": "votre-login@sfr.fr",
"sfrPassword": "votre-mot-de-passe",
"appUrl": "https://demat-facturation.santinova-soft.org",
"apiToken": "votre-token-api-copié-depuis-lappli",
"downloadDir": "/tmp/sfr-invoices",
"processedFile": "/tmp/sfr-processed.json"
}
```
Ou utiliser des variables d'environnement :
```bash
export SFR_LOGIN=votre-login@sfr.fr
export SFR_PASSWORD=votre-mot-de-passe
export APP_URL=https://demat-facturation.santinova-soft.org
export API_TOKEN=votre-token-api
```
## Exécution manuelle
```bash
node sfr-connector.mjs
```
## Planification (cron)
Ajouter dans le crontab (`crontab -e`) :
```
# Import SFR le 5 de chaque mois à 8h00
0 8 5 * * /usr/bin/node /opt/web-import/sfr-connector.mjs >> /var/log/sfr-import.log 2>&1
```
## Ajouter un nouveau connecteur
Dupliquer `sfr-connector.mjs` et adapter :
1. L'URL du portail (`portalUrl`)
2. Les sélecteurs CSS pour le login et les liens de factures
3. Le nom du fichier de suivi (`processedFile`)
## Fonctionnement
1. Le script se connecte au site SFR avec les identifiants fournis
2. Il navigue vers la section factures
3. Il télécharge les PDFs non encore traités
4. Il les envoie à l'application via l'endpoint `/api/web-import/push-invoice`
5. L'application extrait les données avec l'IA et crée les factures
6. Le script marque les factures comme traitées pour éviter les doublons

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