/* Enregistre le RÈGLEMENT d'une charge sociale, par l'interface. POURQUOI CE CHEMIN EXISTE. `recordSocialCharge.ts` crée la charge — et la laisse impayée, comme le veut Dolibarr. Tant que le règlement n'est pas saisi, le mouvement bancaire reste orphelin : `bank-match.sh` le classe BANK-ONLY et le solde de l'ERP diverge de celui de la banque. C'est ce trou qui laissait deux échéances URSSAF invisibles jusqu'au 24/08/2026. Dolibarr n'expose AUCUNE route REST pour les charges sociales — /taxes, /socialcontributions et /chargesociales répondent tous « API not found ». La voie gated du pipeline, qui parle REST, ne peut donc pas porter l'opération. Ce script garde ce qu'il peut de sa discipline : répétition en bac à sable, double opt-in explicite pour la production, --dry-run, et vérification par RELECTURE DE L'OBJET. Trois pièges : - la date est un datepicker jQuery : un champ visible `re` doublé d'un triplet CACHÉ reday/remonth/reyear, et le backend ne lit QUE le triplet. Remplir le champ visible seul soumet une date vide, sans erreur ; - le champ du montant porte l'identifiant de la charge dans son nom — `amount_`, pas `amount` ; - la page de retour affiche « paiement enregistré » avant même que l'objet soit relu. On recharge la fiche de la charge et on lit son statut. Usage : deno run -A test/paySocialCharge.ts --id 5 --date 2026-08-17 \ --amount 1215.00 --account 1 --type 3 [--dry-run] # --type : 3 = prélèvement, 2 = virement, 6 = carte */ import "load_dotenv"; import { chromium } from "playwright"; import login from "./scripts/login.ts"; import { assertSandbox } from "./scripts/guard.ts"; const argv = Deno.args; const pick = (f: string, d = "") => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : d); const id = pick("--id"); const date = pick("--date"); // yyyy-mm-dd const amount = pick("--amount"); const account = pick("--account"); // id du compte bancaire const type = pick("--type", "3"); // 3 = ordre de prélèvement const note = pick("--note"); const dryRun = argv.includes("--dry-run"); if (!id || !date || !amount || !account) { console.error("--id, --date, --amount et --account sont requis"); Deno.exit(2); } const dolibarrAddress = assertSandbox(); console.log(`cible : ${dolibarrAddress}`); console.log(`charge : id=${id} — ${amount} € le ${date} sur le compte ${account} (type ${type})`); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ locale: "fr-FR" }); const page = await context.newPage(); /** Renseigne le champ visible ET le triplet caché que le backend lit seul. */ async function setDate(prefix: string, iso: string): Promise { const [y, m, d] = iso.split("-"); await page.fill(`input[name="${prefix}"]`, `${d}/${m}/${y}`).catch(() => {}); await page.evaluate( ({ p, dd, mm, yy }: { p: string; dd: string; mm: string; yy: string }) => { const doc = (globalThis as unknown as { document: { querySelector(s: string): { value: string } | null }; }).document; const set = (s: string, v: string) => { const el = doc.querySelector(`input[name="${p}${s}"]`); if (el) el.value = v; }; set("day", String(Number(dd))); set("month", String(Number(mm))); set("year", yy); }, { p: prefix, dd: d, mm: m, yy: y }, ); } /** * Reste à payer, lu sur la fiche de la charge. Seule confirmation qui vaille. * * NE PAS chercher « payée » dans la page : « ImPAYÉE » contient « payée », et * la fiche affiche de toute façon les deux mots. Une première version le * faisait et déclarait ÉCHEC sur un règlement qui venait d'être enregistré — * un faux négatif qui pousse à rejouer, donc à créer un doublon. On lit le * MONTANT, qui ne ment pas. */ async function resteAPayer(): Promise { await page.goto(`${dolibarrAddress}/compta/sociales/card.php?id=${id}`); const txt = (await page.locator("body").innerText()).replace(/\s+/g, " "); const m = txt.match(/Reste à payer\s*:?\s*([\d\s\u00a0\u202f]+,\d{2})/i); if (!m) return null; return Number(m[1].replace(/[\s\u00a0\u202f]/g, "").replace(",", ".")); } try { await login.doAdminLogin({ page, dolibarrAddress, adminCredentials: { username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined", password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined", }, }); const avant = await resteAPayer(); if (avant === null) { console.error(`charge introuvable, ou fiche illisible : /compta/sociales/card.php?id=${id}`); Deno.exit(1); } console.log(`avant : reste à payer ${avant.toFixed(2)} €`); if (avant === 0) { console.log("déjà réglée, rien à faire."); Deno.exit(0); } if (dryRun) { console.log("\n--dry-run : rien n'est soumis."); Deno.exit(0); } // Le lien porte le jeton de session ; on le lit plutôt que de le deviner. const href = await page.locator('a[href*="paiement_charge"]').first().getAttribute("href"); if (!href) { console.error("aucun lien « Saisir règlement » — charge déjà réglée, ou droits insuffisants."); Deno.exit(1); } await page.goto(new URL(href, dolibarrAddress).toString()); await setDate("re", date); await page.selectOption('select[name="paiementtype"]', type); await page.selectOption('select[name="accountid"]', account); await page.fill(`input[name="amount_${id}"]`, amount); // le nom porte l'id if (note) await page.fill('textarea[name="note"]', note); await page.locator('input[name="save"]').click(); await page.waitForLoadState("networkidle"); const apres = await resteAPayer(); console.log(`après : reste à payer ${apres === null ? "?" : apres.toFixed(2)} €`); if (apres !== 0) { console.error("ÉCHEC — la charge n'est pas soldée après soumission."); Deno.exit(1); } console.log("ok — règlement enregistré et relu sur la fiche."); } finally { await context.close(); await browser.close(); }