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:
@@ -269,66 +269,75 @@ 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`,
|
||||
];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
const resp = await fetchWithTimeout(endpoint, {
|
||||
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,
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Accept-Language": "fr-FR,fr;q=0.9",
|
||||
},
|
||||
}, 10_000);
|
||||
redirect: "follow",
|
||||
}, 15_000);
|
||||
|
||||
if (!resp.ok) return [];
|
||||
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
const invoices = parseInvoiceList(data);
|
||||
if (invoices.length > 0) return invoices;
|
||||
}
|
||||
return parseInvoiceListFromJson(data);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user