Files
demat-facturation/server/manualWebImport.ts
2026-09-30 16:42:27 +02:00

130 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { chromium } from "playwright-core";
import type { WebImportSource } from "../drizzle/schema";
export type ManualWebImportResult = {
imported: number;
duplicates: number;
total: number;
};
type InvoicePushResult = ManualWebImportResult;
type InvoicePusher = (fileName: string, pdfBuffer: Buffer) => Promise<InvoicePushResult>;
const SFR_INVOICE_LINK_SELECTOR =
'a[href*=".pdf"], a[href*="facture"], a[href*="invoice"], a[download]';
/** Types de connecteurs dont l’automate est réellement livré dans l’image applicative. */
export function getManualWebImportAvailability(connectorType: string):
| { available: true }
| { available: false; reason: string } {
if (connectorType === "sfr") return { available: true };
return {
available: false,
reason: `L’import manuel n’est pas encore disponible pour le connecteur « ${connectorType} ».`,
};
}
export function summarizeManualWebImport(results: InvoicePushResult[]): ManualWebImportResult {
return results.reduce(
(summary, result) => ({
imported: summary.imported + result.imported,
duplicates: summary.duplicates + result.duplicates,
total: summary.total + result.total,
}),
{ imported: 0, duplicates: 0, total: 0 },
);
}
function getApplicationBaseUrl(): string {
return process.env.APP_BASE_URL || `http://127.0.0.1:${process.env.PORT || 3000}`;
}
async function pushInvoiceToApplication(source: WebImportSource, fileName: string, pdfBuffer: Buffer) {
const response = await fetch(`${getApplicationBaseUrl()}/api/web-import/push-invoice`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
apiToken: source.apiToken,
fileName,
fileBase64: pdfBuffer.toString("base64"),
}),
});
if (!response.ok) {
throw new Error(`Le dépôt sécurisé du PDF a échoué (HTTP ${response.status}).`);
}
const result = (await response.json()) as Partial<InvoicePushResult>;
return {
imported: Number(result.imported || 0),
duplicates: Number(result.duplicates || 0),
total: Number(result.total || 0),
};
}
/**
* Télécharge les nouvelles factures SFR puis les soumet au même endpoint
* de contrôle, dédoublonnage, stockage et analyse IA que l’import automatique.
* Les identifiants restent confinés au processus serveur et ne sont jamais loggés.
*/
export async function runManualWebImport(
source: WebImportSource,
pushInvoice: InvoicePusher = (fileName, pdfBuffer) => pushInvoiceToApplication(source, fileName, pdfBuffer),
): Promise<ManualWebImportResult> {
const availability = getManualWebImportAvailability(source.connectorType);
if (!availability.available) throw new Error(availability.reason);
const browser = await chromium.launch({
headless: true,
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || "/usr/bin/chromium",
args: ["--no-sandbox", "--disable-dev-shm-usage"],
});
try {
const context = await browser.newContext({ acceptDownloads: true });
const page = await context.newPage();
await page.goto(source.portalUrl, { waitUntil: "networkidle", timeout: 30_000 });
await page
.click('[id*="accept"], [class*="accept-cookie"], #didomi-notice-agree-button', { timeout: 3_000 })
.catch(() => undefined);
await page.fill('input[type="email"], input[name="login"], input[id*="login"], input[id*="email"]', source.loginEmail);
await page.fill('input[type="password"], input[name="password"], input[id*="password"]', source.loginPassword);
await page.click('button[type="submit"], input[type="submit"], button:has-text("Connexion"), button:has-text("Se connecter")');
await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => undefined);
const invoicePage = new URL("factures", source.portalUrl).toString();
await page.goto(invoicePage, { waitUntil: "networkidle", timeout: 20_000 }).catch(() => undefined);
const invoiceLinks = await page.$$eval(SFR_INVOICE_LINK_SELECTOR, (links) =>
links.map((link) => ({
href: (link as HTMLAnchorElement).href,
label: link.textContent?.trim() || "facture.pdf",
})),
);
const uniqueLinks = Array.from(
new Map(invoiceLinks.filter((link) => link.href).map((link) => [link.href, link])).values(),
);
const results: InvoicePushResult[] = [];
for (const link of uniqueLinks) {
// BrowserContext.request partage la session authentifiée du navigateur. Cela évite
// de dépendre du comportement du visualiseur PDF ou de l’événement download.
const response = await context.request.get(link.href, { timeout: 20_000 });
if (!response.ok()) {
throw new Error(`Téléchargement de facture indisponible (HTTP ${response.status()}).`);
}
const fileName = link.label || `sfr-facture-${Date.now()}.pdf`;
const pdfBuffer = Buffer.from(await response.body());
results.push(await pushInvoice(fileName, pdfBuffer));
}
return summarizeManualWebImport(results);
} finally {
await browser.close();
}
}