Checkpoint: Récupération automatique FreePro opérationnelle : utilisation de l'API REST interne Angular /api/api_red_october/v1/invoices pour lister les factures, puis téléchargement CSV via /account/invoice/{ref}/primary_csv. 6 factures importées automatiquement (01/2026 à 06/2026).

This commit is contained in:
Manus
2026-06-08 10:16:52 -04:00
parent f3e985af5f
commit 9de1a767fa

View File

@@ -269,67 +269,76 @@ async function loginToFreePro(
// ── Récupération de la liste des factures ──────────────────────────────────
/** Endpoint API REST interne du portail FreePro (Angular) qui retourne la liste des factures en JSON. */
const INVOICES_API_URL = `${FREEPRO_BASE_URL}/api/api_red_october/v1/invoices`;
/**
* Récupère la liste des factures disponibles sur le portail FreePro
* Récupère la liste des factures disponibles sur le portail FreePro.
*
* Le portail FreePro est une SPA Angular. L'API interne utilisée par Angular
* pour charger les données est : GET /api/api_red_october/v1/invoices
* Elle retourne un tableau JSON avec les champs : ref, billing_date, total_ht, total_ttc, invoice_status
*
* Cette API nécessite les cookies de session obtenus après connexion via /account/security/do_login.
*/
async function fetchInvoiceList(cookies: string): Promise<FreeproInvoice[]> {
const endpoints = [
`${FREEPRO_BASE_URL}/account/api/billing/invoices`,
`${FREEPRO_BASE_URL}/account/billing/api/invoices`,
`${FREEPRO_BASE_URL}/api/billing/invoices`,
];
try {
const resp = await fetchWithTimeout(INVOICES_API_URL, {
headers: {
"Cookie": cookies,
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
"Referer": BILLING_URL,
"Accept-Language": "fr-FR,fr;q=0.9",
},
redirect: "follow",
}, 15_000);
for (const endpoint of endpoints) {
try {
const resp = await fetchWithTimeout(endpoint, {
headers: {
"Cookie": cookies,
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
"Referer": BILLING_URL,
"X-Requested-With": "XMLHttpRequest",
},
}, 10_000);
if (!resp.ok) return [];
if (resp.ok) {
const data = await resp.json();
const invoices = parseInvoiceList(data);
if (invoices.length > 0) return invoices;
}
} catch {
continue;
}
const data = await resp.json();
return parseInvoiceListFromJson(data);
} catch {
return [];
}
return [];
}
/**
* Parse la réponse JSON de l'API de facturation FreePro
* Parse la réponse JSON de l'API interne FreePro /api/api_red_october/v1/invoices.
*
* Structure d'un élément :
* { ref: "F202606003800", billing_date: "2026-06-01T00:00:00", total_ht: 1287.37,
* total_ttc: 1544.84, invoice_status: "waiting"|"paid", ... }
*/
function parseInvoiceList(data: any): FreeproInvoice[] {
function parseInvoiceListFromJson(data: any): FreeproInvoice[] {
if (!Array.isArray(data)) return [];
const invoices: FreeproInvoice[] = [];
const items = Array.isArray(data) ? data : (data?.invoices ?? data?.data ?? []);
for (const item of items) {
const invoiceNumber = item.ref_piece ?? item.invoiceNumber ?? item.id ?? "";
const date = item.date ?? item.invoiceDate ?? "";
const amount = parseFloat(item.total ?? item.amount ?? item.ttc ?? "0");
for (const item of data) {
const invoiceNumber: string = item.ref ?? "";
const billingDate: string = item.billing_date ?? "";
const amount: number = typeof item.total_ttc === "number" ? item.total_ttc : 0;
if (!invoiceNumber || !date) continue;
if (!invoiceNumber || !billingDate) continue;
// billing_date est au format ISO : "2026-06-01T00:00:00"
// On extrait YYYY et MM pour construire le label du mois
let month = "";
if (date.includes("-")) {
const parts = date.split("-");
if (parts.length >= 2) month = `${parts[1].padStart(2, "0")}/${parts[0]}`;
} else if (date.includes("/")) {
const parts = date.split("/");
if (parts.length >= 3) month = `${parts[1].padStart(2, "0")}/${parts[2]}`;
const dateMatch = billingDate.match(/^(\d{4})-(\d{2})/);
if (dateMatch) {
month = `${dateMatch[2]}/${dateMatch[1]}`; // "06/2026"
}
if (month) invoices.push({ invoiceNumber, date, amount, month });
if (month) {
invoices.push({ invoiceNumber, date: billingDate, amount, month });
}
}
// Trier par numéro de facture décroissant (plus récente en premier)
invoices.sort((a, b) => b.invoiceNumber.localeCompare(a.invoiceNumber));
return invoices;
}
@@ -453,7 +462,7 @@ export async function runFreeproAutoImport(userId: number): Promise<AutoImportRe
const availableInvoices = await fetchInvoiceList(cookies);
if (availableInvoices.length === 0) {
result.message = "Aucune facture disponible via l'API du portail FreePro. La récupération automatique nécessite que les factures soient accessibles via l'API interne du portail.";
result.message = "Connexion réussie mais aucune facture trouvée dans la page /account/billing. Vérifiez que des factures sont disponibles dans votre espace client FreePro.";
await updateFreeproLastRun(userId, result.message, 0, false);
return result;
}