/* Correct the dates of an existing social charge through the Dolibarr UI. !! BLOQUÉ PAR UN BUG AMONT — CE SCRIPT NE PEUT PAS ABOUTIR AUJOURD'HUI. !! Sur ce déploiement (Dolibarr 22.0.4 + PostgreSQL), AUCUNE charge sociale n'est modifiable : toute soumission du formulaire d'édition, même en ne touchant que le montant, échoue sur ERROR 42601: multiple assignments to same column "fk_user_modif" ChargeSociales::update() construit un UPDATE qui affecte deux fois la même colonne. MySQL l'accepte, PostgreSQL le rejette. Vérifié le 2026-08-13 en sandbox sur la date ET sur le montant ; la mise à jour d'un tiers via REST fonctionne, donc le défaut est propre à cet objet, pas systémique. CONSÉQUENCE OPÉRATOIRE : une charge sociale est de fait IMMUABLE. Sa date doit être juste à la création — voir RUNBOOK_charges_sociales.md, « échéance ≠ prélèvement ». Ce script est conservé comme cas de reproduction : il diagnostique l'erreur au lieu de la subir, et redeviendra utile dès que le bug amont sera corrigé. WHY THIS EXISTS: recordSocialCharge.ts only creates. When a charge was booked on the wrong date the record must be corrected in place — deleting and recreating is not an option, since no agent scope grants DELETE and a ledger is append-only by doctrine. THE CORRECTION THIS WAS WRITTEN FOR: the URSSAF échéancier gives DUE dates on the 5th of the month. The bank debit lands later (22/05 for the 05/05 due date). The first échéance had been recorded at its debit date, which makes a 17-day late payment look punctual and puts the charge in the wrong month. Same double-bottom date trap as the create form: the visible `ech` / `period` inputs are decorative, the backend reads ONLY the hidden day/month/year triplets fed by the jQuery datepicker. See RUNBOOK_charges_sociales.md. Usage: deno run -A test/updateSocialCharge.ts --id 1 --due 2026-05-05 \ --period 2026-05-05 [--dry-run] */ 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 due = pick("--due"); // yyyy-mm-dd const period = pick("--period"); // yyyy-mm-dd const dryRun = argv.includes("--dry-run"); if (!id || !due) { console.error("--id and --due (yyyy-mm-dd) are required"); Deno.exit(2); } const dolibarrAddress = assertSandbox(); const fr = (iso: string) => { const [y, m, d] = iso.split("-"); return `${d}/${m}/${y}`; }; console.log(`target : ${dolibarrAddress}`); console.log(`charge ${id} : échéance → ${fr(due)}${period ? `, période → ${fr(period)}` : ""}`); if (dryRun) { console.log("\n--dry-run : rien n'est soumis."); Deno.exit(0); } const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ locale: "fr-FR" }); const page = await context.newPage(); async function setDolibarrDate(name: string, iso: string): Promise { const [y, m, d] = iso.split("-"); await page.fill(`input[name="${name}"]`, fr(iso)).catch(() => {}); await page.evaluate( ({ n, dd, mm, yy }: { n: string; dd: string; mm: string; yy: string }) => { const doc = (globalThis as unknown as { document: { querySelector(s: string): { value: string } | null }; }).document; const set = (suffix: string, v: string) => { const el = doc.querySelector(`input[name="${n}${suffix}"]`); if (el) el.value = v; }; set("day", String(Number(dd))); set("month", String(Number(mm))); set("year", yy); }, { n: name, dd: d, mm: m, yy: y }, ); } /** Read the dates back off the card — the only trustworthy confirmation. */ async function readDates(): Promise { await page.goto(`${dolibarrAddress}/compta/sociales/card.php?id=${id}`); return (await page.locator("div.fichecenter, table").first().innerText()) .replace(/\s+/g, " ").trim().slice(0, 300); } try { await login.doAdminLogin({ page, dolibarrAddress, adminCredentials: { username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined", password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined", }, }); console.log(`avant : ${await readDates()}`); await page.goto(`${dolibarrAddress}/compta/sociales/card.php?id=${id}&action=edit`); await setDolibarrDate("ech", due); if (period) await setDolibarrDate("period", period); await page.locator('input[name="save"], input[type="submit"][value="Enregistrer"]').first().click(); await page.waitForLoadState("networkidle"); // Diagnose the known upstream failure rather than reporting a bare mismatch: // "la date n'a pas changé" sends the reader hunting through the date fields, // which are not the problem. const submitted = (await page.locator("body").innerText()).replace(/\s+/g, " "); const dup = submitted.match(/multiple assignments to same column '?"?(\w+)/); if (dup) { console.error( `\nBLOQUÉ PAR LE BUG AMONT — UPDATE invalide sur la colonne « ${dup[1]} ».\n` + `ChargeSociales::update() l'affecte deux fois ; PostgreSQL rejette (42601).\n` + `Une charge sociale est immuable sur ce déploiement : la recréer avec la\n` + `bonne date, ou corriger en base, sont les seules voies.`, ); Deno.exit(1); } const after = await readDates(); console.log(`après : ${after}`); if (!after.includes(fr(due))) { console.error(`\nÉCHEC — la carte ne porte pas ${fr(due)}.`); Deno.exit(1); } console.log(`\ncorrigée et vérifiée : échéance ${fr(due)}`); } finally { await context.close(); await browser.close(); }