test/.env ships DOLIBARR_ADDRESS pointing at PRODUCTION and test/main.ts defaults to it, so any Playwright admin script run with the ambient environment drives the real ERP. The REST path has been structurally safe since ADR-0003 (dol-write.sh refuses non-sandbox hosts); the UI path had no equivalent. scripts/guard.ts closes that gap — assertSandbox() resolves the target and refuses anything that is not erp-sandbox.*, with the override spelled out in the error. Verified: an unqualified run now dies instead of reaching prod. Two settings the write agent cannot reach (non-admin by design, 403 on /setup/conf), rehearsed on the sandbox: - sandboxLegalSetup.ts — capital social + multi-currency module. Finding: the capital was NOT missing, it was stored as "1000€"; the symbol made the value unusable by the PDF template, which is why "Capital de 1 000 €" was absent from every invoice since January (C. com. R.123-238). Normalised to "1000" → the mention now renders. - sandboxCurrencySetup.ts — registers the USD reference rate. Enabling the module is not enough: a currency absent from the rate table makes Dolibarr silently fall back to EUR (observed on a probe invoice). With the rate registered, an invoice carries USD 3,000.00 with its EUR counter-value, i.e. the contractual obligation itself rather than a drifting equivalent. Both scripts are report-only when they cannot recognise a form, and screenshot what they did. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
/*
|
|
Sandbox legal-mentions setup — rehearsal for two settings the REST API cannot
|
|
reach (the write agent is deliberately non-admin: 403 on /setup/conf).
|
|
|
|
1. Capital social = 1 000 € — a mandatory invoice mention (C. com. R.123-238)
|
|
missing from every Arcodange invoice to date.
|
|
2. Multi-currency module — so the KM cycle can be invoiced in USD, which is
|
|
the actual contractual obligation ($2,500 / $3,000), instead of a EUR
|
|
counter-value that drifts with the rate (decision of 2026-06-30).
|
|
|
|
SAFETY: this script drives a browser as an ADMIN user, so it calls
|
|
assertSandbox() first — production is never touched by a script. Note that
|
|
test/.env ships DOLIBARR_ADDRESS pointing at PRODUCTION and test/main.ts
|
|
defaults to it too, which is exactly why the guard exists.
|
|
|
|
Run:
|
|
DOLIBARR_ADDRESS=https://erp-sandbox.arcodange.lab \
|
|
deno run -A test/sandboxLegalSetup.ts [--capital 1000] [--no-multicurrency]
|
|
*/
|
|
import "load_dotenv";
|
|
import { chromium } from "playwright";
|
|
import login from "./scripts/login.ts";
|
|
import forms from "./scripts/forms.ts";
|
|
import { assertSandbox } from "./scripts/guard.ts";
|
|
|
|
const args = new Set(Deno.args);
|
|
const capitalArg = Deno.args.includes("--capital")
|
|
? Deno.args[Deno.args.indexOf("--capital") + 1]
|
|
: "1000";
|
|
const doMulticurrency = !args.has("--no-multicurrency");
|
|
const shotDir = Deno.env.get("SHOT_DIR") ?? "/tmp";
|
|
|
|
// The guard reads the ambient env — an unqualified run dies here, by design.
|
|
const dolibarrAddress = assertSandbox();
|
|
console.log(`target: ${dolibarrAddress} (sandbox verified)`);
|
|
|
|
const adminCredentials = {
|
|
username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined",
|
|
password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined",
|
|
};
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({ locale: "fr-FR" });
|
|
const page = await context.newPage();
|
|
const globalCtx = { page, dolibarrAddress, adminCredentials };
|
|
|
|
try {
|
|
await login.doAdminLogin(globalCtx);
|
|
console.log(`logged in as: ${await login.whoAmI({ page })}`);
|
|
|
|
// --- 1. Capital social -----------------------------------------------------
|
|
// company.php renders one pre-filled form per block; filling a single field
|
|
// and submitting its block preserves every other value already in the DOM.
|
|
await page.goto(`${dolibarrAddress}/admin/company.php`);
|
|
const before = await page.inputValue('input[name="capital"]').catch(() => "");
|
|
console.log(`capital before: '${before}'`);
|
|
|
|
await forms.fillForm(
|
|
{ page, imgFolderPath: "" },
|
|
{ capital: capitalArg },
|
|
new Map([["capital", "capital"]]) as Map<"capital", string>,
|
|
2, // the identity block's submit button
|
|
);
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
await page.goto(`${dolibarrAddress}/admin/company.php`);
|
|
const after = await page.inputValue('input[name="capital"]').catch(() => "");
|
|
console.log(`capital after : '${after}'`);
|
|
await page.screenshot({ path: `${shotDir}/capital.png`, fullPage: false });
|
|
|
|
// --- 2. Multi-currency module ---------------------------------------------
|
|
if (doMulticurrency) {
|
|
await page.goto(`${dolibarrAddress}/admin/modules.php?mode=commonkanban`);
|
|
// The fr_FR card label is not pinned in this repo yet — match tolerantly on
|
|
// "devise" and report what was found rather than guessing a title.
|
|
const cards = page.locator(".info-box", {
|
|
has: page.locator(".info-box-title", { hasText: /devise/i }),
|
|
});
|
|
const n = await cards.count();
|
|
console.log(`module cards matching /devise/i: ${n}`);
|
|
for (let i = 0; i < n; i++) {
|
|
const title = (await cards.nth(i).locator(".info-box-title").textContent())
|
|
?.trim();
|
|
console.log(` [${i}] ${title}`);
|
|
}
|
|
if (n === 0) {
|
|
console.warn("multicurrency: no matching card — skipped (report only)");
|
|
} else {
|
|
await forms.toggleOnOff(cards.first(), true);
|
|
await page.waitForLoadState("networkidle");
|
|
await page.screenshot({
|
|
path: `${shotDir}/multicurrency.png`,
|
|
fullPage: false,
|
|
});
|
|
console.log("multicurrency: toggled on");
|
|
}
|
|
}
|
|
} finally {
|
|
await context.close();
|
|
await browser.close();
|
|
}
|