150 lines
6.0 KiB
TypeScript
150 lines
6.0 KiB
TypeScript
/*
|
|
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_<id>`, 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<void> {
|
|
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<number | null> {
|
|
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();
|
|
}
|