arca-siradig 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +311 -0
  3. package/dist/src/arca/deductions.d.ts +71 -0
  4. package/dist/src/arca/deductions.js +636 -0
  5. package/dist/src/arca/employees.d.ts +10 -0
  6. package/dist/src/arca/employees.js +80 -0
  7. package/dist/src/arca/form.d.ts +39 -0
  8. package/dist/src/arca/form.js +143 -0
  9. package/dist/src/arca/http.d.ts +14 -0
  10. package/dist/src/arca/http.js +108 -0
  11. package/dist/src/arca/menu.d.ts +6 -0
  12. package/dist/src/arca/menu.js +36 -0
  13. package/dist/src/arca/personal-data.d.ts +4 -0
  14. package/dist/src/arca/personal-data.js +62 -0
  15. package/dist/src/arca/persons.d.ts +11 -0
  16. package/dist/src/arca/persons.js +115 -0
  17. package/dist/src/arca/receipt-types.d.ts +4 -0
  18. package/dist/src/arca/receipt-types.js +39 -0
  19. package/dist/src/arca/session.d.ts +27 -0
  20. package/dist/src/arca/session.js +165 -0
  21. package/dist/src/arca/submission.d.ts +22 -0
  22. package/dist/src/arca/submission.js +140 -0
  23. package/dist/src/arca/submitted-forms.d.ts +12 -0
  24. package/dist/src/arca/submitted-forms.js +54 -0
  25. package/dist/src/cli/errors.d.ts +1 -0
  26. package/dist/src/cli/errors.js +11 -0
  27. package/dist/src/cli/help.d.ts +6 -0
  28. package/dist/src/cli/help.js +301 -0
  29. package/dist/src/cli/index.d.ts +2 -0
  30. package/dist/src/cli/index.js +810 -0
  31. package/dist/src/cli/options.d.ts +120 -0
  32. package/dist/src/cli/options.js +419 -0
  33. package/dist/src/cli/prompts.d.ts +9 -0
  34. package/dist/src/cli/prompts.js +99 -0
  35. package/dist/src/cli/render.d.ts +13 -0
  36. package/dist/src/cli/render.js +262 -0
  37. package/dist/src/cli/structured-output.d.ts +4 -0
  38. package/dist/src/cli/structured-output.js +92 -0
  39. package/dist/src/files/pdf.d.ts +3 -0
  40. package/dist/src/files/pdf.js +43 -0
  41. package/dist/src/files/private.d.ts +4 -0
  42. package/dist/src/files/private.js +21 -0
  43. package/dist/src/security/errors.d.ts +2 -0
  44. package/dist/src/security/errors.js +22 -0
  45. package/dist/src/session/store.d.ts +15 -0
  46. package/dist/src/session/store.js +35 -0
  47. package/dist/src/types.d.ts +106 -0
  48. package/dist/src/types.js +1 -0
  49. package/package.json +59 -0
@@ -0,0 +1,80 @@
1
+ import { fetchSiradigJson, parseInlineTable } from "./http.js";
2
+ import { extractVisibleFieldsetData } from "./personal-data.js";
3
+ const EMPLOYEES_MENU_URL = "verMenuEmpleadores.do";
4
+ export async function readEmployees(page, request = page.request) {
5
+ const data = await fetchSiradigJson(request, EMPLOYEES_MENU_URL, "datosEmpleadoresJSON");
6
+ const employees = parseEmployeesJson(data);
7
+ if (employees !== null)
8
+ return employees;
9
+ await openEmployeesMenu(page);
10
+ return listEmployees(page);
11
+ }
12
+ export function parseEmployeesJson(data) {
13
+ const rows = parseInlineTable(data);
14
+ if (rows === null)
15
+ return null;
16
+ const employees = [];
17
+ for (const row of rows) {
18
+ const [cuit, name, startDate, endDate, withholdingAgent] = row.cells;
19
+ if (!row.id || !cuit || !name || row.cells.length !== 5)
20
+ return null;
21
+ employees.push({
22
+ id: row.id,
23
+ cuit,
24
+ name,
25
+ startDate: startDate || null,
26
+ endDate: endDate || null,
27
+ withholdingAgent: withholdingAgent || null,
28
+ });
29
+ }
30
+ return employees;
31
+ }
32
+ export async function openEmployeesMenu(page) {
33
+ await page.goto(new URL(EMPLOYEES_MENU_URL, page.url()).href, { waitUntil: "domcontentloaded" });
34
+ }
35
+ export async function listEmployees(page) {
36
+ const rows = page.locator("#nueva_tabla_empleadores tbody tr");
37
+ const employees = [];
38
+ for (let index = 0; index < (await rows.count()); index += 1) {
39
+ const row = rows.nth(index);
40
+ if (!(await row.isVisible())) {
41
+ continue;
42
+ }
43
+ const cells = await row.locator("td").allTextContents();
44
+ const [cuit = "", name = "", startDate = "", endDate = "", withholdingAgent = ""] = cells.map(normalizeText);
45
+ const id = normalizeText(await row.getAttribute("data-id-reg"));
46
+ if (!id || !cuit || !name) {
47
+ continue;
48
+ }
49
+ employees.push({
50
+ id,
51
+ cuit,
52
+ name,
53
+ startDate: startDate || null,
54
+ endDate: endDate || null,
55
+ withholdingAgent: withholdingAgent || null,
56
+ });
57
+ }
58
+ return employees;
59
+ }
60
+ export async function openEmployeeDetails(page, employeeId) {
61
+ await page.goto(new URL(`verEmpleador.do?id=${encodeURIComponent(employeeId)}`, page.url()).href, {
62
+ waitUntil: "domcontentloaded",
63
+ });
64
+ await page
65
+ .waitForFunction(() => {
66
+ const cuit = document.querySelector("#cuit")?.value?.trim();
67
+ const razonSocial = document.querySelector("#razonSocial")?.value?.trim();
68
+ return Boolean(cuit || razonSocial);
69
+ })
70
+ .catch(() => undefined);
71
+ }
72
+ export async function extractEmployeeData(page) {
73
+ return extractVisibleFieldsetData(page);
74
+ }
75
+ export function findEmployeeByCuit(employees, employeeCuit) {
76
+ return employees.find((employee) => employee.cuit === employeeCuit) ?? null;
77
+ }
78
+ function normalizeText(value) {
79
+ return (value ?? "").replace(/\s+/g, " ").trim();
80
+ }
@@ -0,0 +1,39 @@
1
+ import type { Page } from "playwright";
2
+ export type FormContext = {
3
+ period: number;
4
+ person: {
5
+ id: string;
6
+ name: string;
7
+ };
8
+ };
9
+ export type FormSummary = FormContext & {
10
+ source: "draft-preview";
11
+ submissionType: string | null;
12
+ withholdingAgent: {
13
+ id: string;
14
+ name: string;
15
+ } | null;
16
+ totals: Array<{
17
+ label: string;
18
+ amount: string;
19
+ }>;
20
+ sections: Array<{
21
+ title: string;
22
+ tables: string[][][];
23
+ hasData: boolean;
24
+ }>;
25
+ };
26
+ export declare function readFormContext(page: Page): Promise<FormContext>;
27
+ export declare function verifyFormContext(page: Page, expected: {
28
+ period?: number;
29
+ person: string;
30
+ }): Promise<FormContext>;
31
+ export declare function openFormPeriod(page: Page, period: number): Promise<void>;
32
+ export declare function selectFormPeriod(page: Page, expected: {
33
+ period?: number;
34
+ person: string;
35
+ selector?: string;
36
+ }): Promise<FormContext>;
37
+ export declare function openFormPreview(page: Page): Promise<void>;
38
+ export declare function readFormSummary(page: Page): Promise<FormSummary>;
39
+ export declare function printFormDraft(page: Page, path: string, period?: number): Promise<string>;
@@ -0,0 +1,143 @@
1
+ import { resolvePdfDestination, savePdfWithUniqueName } from "../files/pdf.js";
2
+ import { acceptReminderModalIfPresent, selectPerson } from "./persons.js";
3
+ import { assertWorkerSiradigUrl, SIRADIG_BASE_URL } from "./session.js";
4
+ export async function readFormContext(page) {
5
+ assertWorkerSiradigUrl(page.url());
6
+ const breadcrumb = await page.locator("#breadCrumb0").innerText();
7
+ const period = breadcrumb.match(/Per[ií]odo\s+(\d{4})/i)?.[1];
8
+ const person = await page.locator("#encabezado_usuario").evaluate((table) => {
9
+ const row = Array.from(table.querySelectorAll("tr")).find((row) => /Representando\s+a\s*:/i.test(row.cells[0]?.textContent ?? ""));
10
+ return row?.cells[1]?.textContent?.replace(/\s+/g, " ").trim() ?? "";
11
+ });
12
+ const match = person.match(/^(\d{11})\s*-\s*(.+)$/);
13
+ if (!period || !match) {
14
+ throw new Error(`No se pudo confirmar la persona y el período del formulario en ARCA.\n` +
15
+ `Encabezado: ${person || "no disponible"}. Período: ${period || "no disponible"}.\n` +
16
+ "Revisá la pantalla con --headless false y la selección con `arca-siradig persons list`.");
17
+ }
18
+ return { period: Number(period), person: { id: match[1], name: match[2] } };
19
+ }
20
+ export async function verifyFormContext(page, expected) {
21
+ const context = await readFormContext(page);
22
+ if (expected.period !== undefined && context.period !== expected.period) {
23
+ throw new Error(`ARCA abrió el período ${context.period}; se solicitó ${expected.period}.`);
24
+ }
25
+ if (context.person.id !== expected.person &&
26
+ normalizePersonName(context.person.name) !== normalizePersonName(expected.person)) {
27
+ throw new Error("ARCA abrió el formulario de una persona diferente de la solicitada.\n" +
28
+ `Persona esperada (CUIT o nombre): ${expected.person}.\n` +
29
+ `Persona en ARCA: CUIT ${context.person.id} - ${context.person.name}. Período: ${context.period}.\n` +
30
+ "Revisá `arca-siradig persons list` y elegí la persona con `--person <id|nombre>` o `arca-siradig persons select <id|nombre>`.\n" +
31
+ "El ID de selección de ARCA puede ser un índice interno; no necesariamente es el CUIT.");
32
+ }
33
+ return context;
34
+ }
35
+ export async function openFormPeriod(page, period) {
36
+ const url = new URL("jsp/determinarContribuyente.do", SIRADIG_BASE_URL);
37
+ url.searchParams.set("codigo", String(period));
38
+ await page.goto(url.href, { waitUntil: "domcontentloaded" });
39
+ const error = await page
40
+ .getByText("El período seleccionado es inválido o inexistente", {
41
+ exact: true,
42
+ })
43
+ .isVisible();
44
+ if (error)
45
+ throw new Error(`ARCA no permite acceder al período ${period}.`);
46
+ }
47
+ export async function selectFormPeriod(page, expected) {
48
+ const current = await verifyFormContext(page, { person: expected.person });
49
+ if (expected.period !== undefined && current.period !== expected.period) {
50
+ await openFormPeriod(page, expected.period);
51
+ if (await page.locator('input[type="button"].btn_empresa').count()) {
52
+ await selectPerson(page, expected.selector ?? current.person.name);
53
+ }
54
+ await acceptReminderModalIfPresent(page);
55
+ }
56
+ return verifyFormContext(page, { period: expected.period, person: current.person.id });
57
+ }
58
+ function normalizePersonName(name) {
59
+ return name.replace(/,/g, " ").replace(/\s+/g, " ").trim().toUpperCase();
60
+ }
61
+ export async function openFormPreview(page) {
62
+ await page.goto(new URL("jsp/vistaPreviaFormulario.do", SIRADIG_BASE_URL).href, {
63
+ waitUntil: "domcontentloaded",
64
+ });
65
+ await page.locator("#tabla_datos_personales").waitFor({ state: "attached", timeout: 15_000 });
66
+ }
67
+ export async function readFormSummary(page) {
68
+ const context = await readFormContext(page);
69
+ const preview = await page.locator("#contenido").evaluate((content) => {
70
+ const totals = [];
71
+ for (const table of Array.from(content.querySelectorAll("table.tabla_vista_previa"))) {
72
+ const cells = Array.from(table.rows[0]?.cells ?? []).map((cell) => cell.textContent?.replace(/\s+/g, " ").trim() ?? "");
73
+ if (cells.length === 3 && /Importe Total/i.test(cells[1])) {
74
+ totals.push({ label: cells[0], amount: cells[2] });
75
+ }
76
+ }
77
+ const sections = Array.from(content.querySelectorAll("h3")).map((heading) => {
78
+ const tables = [];
79
+ let hasData = false;
80
+ let row = heading.closest("tr")?.nextElementSibling;
81
+ while (row && !row.querySelector("h3")) {
82
+ for (const table of Array.from(row.querySelectorAll("table.tabla_vista_previa"))) {
83
+ const rows = Array.from(table.rows)
84
+ .filter((item) => item.closest("table") === table)
85
+ .filter((item) => !Array.from(item.cells).some((cell) => cell.querySelector("table")))
86
+ .map((item) => {
87
+ if (Array.from(item.cells).some((cell) => cell.tagName === "TD" && cell.textContent?.trim()) ||
88
+ /Subtotal\s*:/i.test(item.textContent ?? ""))
89
+ hasData = true;
90
+ return Array.from(item.cells).map((cell) => cell.textContent?.replace(/\s+/g, " ").trim() ?? "");
91
+ });
92
+ if (rows.length)
93
+ tables.push(rows);
94
+ }
95
+ row = row.nextElementSibling;
96
+ }
97
+ return { title: heading.textContent?.replace(/\s+/g, " ").trim() ?? "", tables, hasData };
98
+ });
99
+ const ownCells = Array.from(content.querySelectorAll("td")).filter((cell) => !cell.querySelector("table"));
100
+ const submissionType = ownCells
101
+ .map((cell) => cell.textContent?.replace(/\s+/g, " ").trim() ?? "")
102
+ .find((text) => /Car[aá]cter de la presentaci[oó]n\s*:/i.test(text))
103
+ ?.split(":")
104
+ .slice(1)
105
+ .join(":")
106
+ .trim() ?? null;
107
+ const agentText = Array.from(content.querySelectorAll("th, td, div"))
108
+ .filter((element) => !element.querySelector("table"))
109
+ .map((element) => element.textContent?.replace(/\s+/g, " ").trim() ?? "")
110
+ .find((text) => /^Agente de retenci[oó]n:/i.test(text));
111
+ const agent = agentText?.match(/^Agente de retenci[oó]n:\s*CUIT\s*(\d{11})\s*-\s*(.+)$/i);
112
+ const withholdingAgent = agent
113
+ ? { id: agent[1], name: agent[2].replace(/\s+/g, " ").trim() }
114
+ : null;
115
+ return { totals, sections, submissionType, withholdingAgent };
116
+ });
117
+ if (!preview.sections.length || !preview.totals.length) {
118
+ throw new Error("La vista previa de ARCA no tiene el formato esperado; no se pudo leer el resumen.");
119
+ }
120
+ return { ...context, source: "draft-preview", ...preview };
121
+ }
122
+ export async function printFormDraft(page, path, period) {
123
+ const fiscalYear = period ?? (await readFormContext(page)).period;
124
+ const destination = await resolvePdfDestination(path, `f572-${fiscalYear}-borrador.pdf`);
125
+ const [download] = await Promise.all([
126
+ page.waitForEvent("download", { timeout: 30_000 }),
127
+ page.locator("#btn_imprimir_borrador").click(),
128
+ ]);
129
+ try {
130
+ const stream = await download.createReadStream();
131
+ const chunks = [];
132
+ for await (const chunk of stream)
133
+ chunks.push(Buffer.from(chunk));
134
+ const body = Buffer.concat(chunks);
135
+ if (body.subarray(0, 5).toString("ascii") !== "%PDF-") {
136
+ throw new Error("ARCA no devolvió el PDF del borrador. No se guardó ningún archivo.");
137
+ }
138
+ return await savePdfWithUniqueName(body, destination);
139
+ }
140
+ finally {
141
+ await download.delete();
142
+ }
143
+ }
@@ -0,0 +1,14 @@
1
+ import type { APIRequestContext, APIResponse } from "playwright";
2
+ export type SiradigRequestClient = {
3
+ get: (url: string, options?: Parameters<APIRequestContext["get"]>[1]) => Promise<Pick<APIResponse, "status" | "headers" | "body" | "dispose">>;
4
+ };
5
+ /** Reads only known consultation endpoints, sharing Playwright's authenticated context. */
6
+ export declare function fetchSiradigJson(request: SiradigRequestClient, endpoint: string, variable: string): Promise<unknown | undefined>;
7
+ export declare function decodeSiradigHtml(body: Uint8Array, contentType: string): string;
8
+ /** Parses JSON literals without evaluating any of the portal's JavaScript. */
9
+ export declare function extractInlineJson(html: string, variable: string): unknown | undefined;
10
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
11
+ export declare function parseInlineTable(value: unknown): Array<{
12
+ id: string;
13
+ cells: string[];
14
+ }> | null;
@@ -0,0 +1,108 @@
1
+ const WORKER_BASE_URL = "https://serviciosjava2.afip.gob.ar/radig/jsp/";
2
+ const READ_ENDPOINTS = new Set(["verMenuEmpleadores.do", "verMenuDeducciones.do"]);
3
+ /** Reads only known consultation endpoints, sharing Playwright's authenticated context. */
4
+ export async function fetchSiradigJson(request, endpoint, variable) {
5
+ if (!READ_ENDPOINTS.has(endpoint)) {
6
+ throw new Error("The SiRADIG HTTP endpoint is not an allowed consultation.");
7
+ }
8
+ try {
9
+ const response = await request.get(new URL(endpoint, WORKER_BASE_URL).href, {
10
+ maxRedirects: 0,
11
+ maxRetries: 0,
12
+ timeout: 15_000,
13
+ });
14
+ try {
15
+ // Redirects, expired sessions, and changed formats fall back to browser navigation.
16
+ const contentType = response.headers()["content-type"] ?? "";
17
+ if (response.status() !== 200 || !contentType.toLowerCase().includes("text/html")) {
18
+ return undefined;
19
+ }
20
+ return extractInlineJson(decodeSiradigHtml(await response.body(), contentType), variable);
21
+ }
22
+ finally {
23
+ await response.dispose();
24
+ }
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ export function decodeSiradigHtml(body, contentType) {
31
+ const charsetPattern = /charset\s*=\s*["']?([\w-]+)/i;
32
+ const headerCharset = contentType.match(charsetPattern)?.[1];
33
+ const prefix = new TextDecoder().decode(body.subarray(0, 8192));
34
+ const metaCharset = prefix.match(/<meta\b[^>]*charset\s*=\s*["']?([\w-]+)/i)?.[1];
35
+ return new TextDecoder(headerCharset ?? metaCharset ?? "utf-8").decode(body);
36
+ }
37
+ /** Parses JSON literals without evaluating any of the portal's JavaScript. */
38
+ export function extractInlineJson(html, variable) {
39
+ if (!/^[A-Za-z_$][\w$]*$/.test(variable))
40
+ return undefined;
41
+ const escapedVariable = variable.replace(/\$/g, "\\$");
42
+ const assignment = new RegExp(`\\b(?:var|let|const)\\s+${escapedVariable}\\s*=\\s*`, "g");
43
+ for (const script of html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi)) {
44
+ const source = script[1] ?? "";
45
+ assignment.lastIndex = 0;
46
+ const match = assignment.exec(source);
47
+ if (!match)
48
+ continue;
49
+ const start = assignment.lastIndex;
50
+ if (source[start] !== "{" && source[start] !== "[")
51
+ continue;
52
+ let depth = 0;
53
+ let inString = false;
54
+ let escaped = false;
55
+ for (let index = start; index < source.length; index += 1) {
56
+ const character = source[index];
57
+ if (inString) {
58
+ if (escaped)
59
+ escaped = false;
60
+ else if (character === "\\")
61
+ escaped = true;
62
+ else if (character === '"')
63
+ inString = false;
64
+ continue;
65
+ }
66
+ if (character === '"')
67
+ inString = true;
68
+ else if (character === "{" || character === "[")
69
+ depth += 1;
70
+ else if (character === "}" || character === "]") {
71
+ depth -= 1;
72
+ if (depth === 0) {
73
+ if (!/^\s*(?:;|$)/.test(source.slice(index + 1)))
74
+ return undefined;
75
+ try {
76
+ return JSON.parse(source.slice(start, index + 1));
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+ export function isRecord(value) {
88
+ return value !== null && typeof value === "object" && !Array.isArray(value);
89
+ }
90
+ export function parseInlineTable(value) {
91
+ if (!isRecord(value) || !Array.isArray(value.filas))
92
+ return null;
93
+ const rows = [];
94
+ for (const row of value.filas) {
95
+ if (!isRecord(row) ||
96
+ !["string", "number"].includes(typeof row.idReg) ||
97
+ !Array.isArray(row.celdas) ||
98
+ !row.celdas.every((cell) => cell === null || ["string", "number"].includes(typeof cell)))
99
+ return null;
100
+ rows.push({
101
+ id: String(row.idReg).trim(),
102
+ cells: row.celdas.map((cell) => String(cell ?? "")
103
+ .replace(/\s+/g, " ")
104
+ .trim()),
105
+ });
106
+ }
107
+ return rows;
108
+ }
@@ -0,0 +1,6 @@
1
+ import type { Page } from "playwright";
2
+ import type { MenuItem } from "../types.js";
3
+ export declare const ENABLED_MENU_ITEM = "Datos Personales";
4
+ export declare function listMenuItems(page: Page): Promise<MenuItem[]>;
5
+ export declare function selectMenuItem(page: Page, label: string): Promise<void>;
6
+ export declare function returnToMainMenu(page: Page): Promise<void>;
@@ -0,0 +1,36 @@
1
+ export const ENABLED_MENU_ITEM = "Datos Personales";
2
+ const MENU_BUTTON_SELECTOR = 'a[id^="btn_"].ui-button';
3
+ export async function listMenuItems(page) {
4
+ await page.locator("#btn_datos_personales").waitFor({ state: "attached", timeout: 15_000 });
5
+ const labels = await page.locator(MENU_BUTTON_SELECTOR).evaluateAll((buttons) => buttons
6
+ .filter((button) => !["btn_nuevo_borrador", "btn_guardar"].includes(button.id))
7
+ .map((button) => button.textContent?.replace(/\s+/g, " ").trim() ?? "")
8
+ .filter((label) => label.length > 0));
9
+ return [...new Set(labels)].map((label) => ({
10
+ label,
11
+ enabled: label === ENABLED_MENU_ITEM,
12
+ }));
13
+ }
14
+ export async function selectMenuItem(page, label) {
15
+ if (label !== ENABLED_MENU_ITEM) {
16
+ throw new Error(`The action "${label}" is not implemented yet.`);
17
+ }
18
+ await navigateByHref(page, page.locator("#btn_datos_personales"));
19
+ await page.waitForLoadState("domcontentloaded").catch(() => undefined);
20
+ }
21
+ export async function returnToMainMenu(page) {
22
+ const mainMenuLink = page
23
+ .locator("#btn_volver, #btn_volver_menu")
24
+ .filter({ hasText: "Menú Principal" })
25
+ .first();
26
+ await mainMenuLink.waitFor({ state: "visible", timeout: 15_000 });
27
+ await mainMenuLink.click();
28
+ await page.waitForLoadState("domcontentloaded").catch(() => undefined);
29
+ }
30
+ async function navigateByHref(page, link) {
31
+ const href = await link.getAttribute("href");
32
+ if (!href || href === "#") {
33
+ throw new Error("The selected action does not have a navigable href.");
34
+ }
35
+ await page.goto(new URL(href, page.url()).href, { waitUntil: "domcontentloaded" });
36
+ }
@@ -0,0 +1,4 @@
1
+ import type { Page } from "playwright";
2
+ import type { DataGroup, PersonalDataGroup } from "../types.js";
3
+ export declare function extractPersonalData(page: Page): Promise<PersonalDataGroup[]>;
4
+ export declare function extractVisibleFieldsetData(page: Page): Promise<DataGroup[]>;
@@ -0,0 +1,62 @@
1
+ export async function extractPersonalData(page) {
2
+ return extractVisibleFieldsetData(page);
3
+ }
4
+ export async function extractVisibleFieldsetData(page) {
5
+ const groups = [];
6
+ const fieldsets = page.locator("#contenido fieldset");
7
+ for (let fieldsetIndex = 0; fieldsetIndex < (await fieldsets.count()); fieldsetIndex += 1) {
8
+ const fieldset = fieldsets.nth(fieldsetIndex);
9
+ if (!(await fieldset.isVisible())) {
10
+ continue;
11
+ }
12
+ const title = normalizeText(await fieldset.locator("legend").first().textContent());
13
+ const fields = [];
14
+ const rows = fieldset.locator("tr");
15
+ for (let rowIndex = 0; rowIndex < (await rows.count()); rowIndex += 1) {
16
+ const row = rows.nth(rowIndex);
17
+ if (!(await row.isVisible())) {
18
+ continue;
19
+ }
20
+ const label = normalizeText(await row.locator("th, [scope='row']").first().textContent());
21
+ if (!label) {
22
+ continue;
23
+ }
24
+ const value = await readRowValue(row);
25
+ if (value) {
26
+ fields.push({ label, value });
27
+ }
28
+ }
29
+ if (title && fields.length > 0) {
30
+ groups.push({ title, fields });
31
+ }
32
+ }
33
+ return groups;
34
+ }
35
+ async function readRowValue(row) {
36
+ const controls = row.locator("td input:not([type='hidden']), td textarea, td select");
37
+ const values = [];
38
+ for (let controlIndex = 0; controlIndex < (await controls.count()); controlIndex += 1) {
39
+ const control = controls.nth(controlIndex);
40
+ if (!(await control.isVisible())) {
41
+ continue;
42
+ }
43
+ const tagName = await control.evaluate((element) => element.tagName.toLowerCase());
44
+ const value = tagName === "select"
45
+ ? await control.locator("option:checked").textContent()
46
+ : await control.inputValue();
47
+ const normalized = normalizeText(value);
48
+ if (normalized) {
49
+ values.push(normalized);
50
+ }
51
+ }
52
+ if (values.length > 0) {
53
+ return values.join(" ");
54
+ }
55
+ return normalizeText(await row
56
+ .locator("td")
57
+ .allTextContents()
58
+ .then((items) => items.join(" ")));
59
+ }
60
+ function normalizeText(value) {
61
+ return (value ?? "").replace(/\s+/g, " ").trim();
62
+ }
@@ -0,0 +1,11 @@
1
+ import type { Page } from "playwright";
2
+ import type { Person, ReminderModalInfo } from "../types.js";
3
+ export declare function acceptReminderModalIfPresent(page: Page): Promise<ReminderModalInfo | null>;
4
+ export declare function acceptReminderModal(page: Page, options?: {
5
+ timeoutMs?: number;
6
+ }): Promise<ReminderModalInfo | null>;
7
+ export declare function waitForPersonSelection(page: Page, options?: {
8
+ timeoutMs?: number;
9
+ }): Promise<void>;
10
+ export declare function listPersons(page: Page): Promise<Person[]>;
11
+ export declare function selectPerson(page: Page, personIdentifier: string): Promise<void>;
@@ -0,0 +1,115 @@
1
+ import { assertWorkerSiradigUrl, recoverWorkerSiradigPage } from "./session.js";
2
+ export async function acceptReminderModalIfPresent(page) {
3
+ return acceptReminderModal(page);
4
+ }
5
+ export async function acceptReminderModal(page, options = {}) {
6
+ const dialog = page
7
+ .locator(".ui-dialog")
8
+ .filter({ hasText: "Recordatorio - Formulario" })
9
+ .first();
10
+ const visible = await dialog
11
+ .isVisible({ timeout: options.timeoutMs ?? 1_500 })
12
+ .catch(() => false);
13
+ if (!visible) {
14
+ return null;
15
+ }
16
+ const title = normalize(await dialog
17
+ .locator(".ui-dialog-title")
18
+ .first()
19
+ .textContent()
20
+ .catch(() => ""));
21
+ const description = normalize(await dialog
22
+ .locator("p")
23
+ .first()
24
+ .textContent()
25
+ .catch(() => ""));
26
+ await dialog.getByRole("button", { name: "Aceptar" }).click();
27
+ await dialog.waitFor({ state: "hidden", timeout: 5_000 }).catch(() => undefined);
28
+ return { title, description };
29
+ }
30
+ export async function waitForPersonSelection(page, options = {}) {
31
+ assertWorkerSiradigUrl(page.url());
32
+ try {
33
+ await page
34
+ .locator('form[name="seleccionaEmpresaForm"], input[type="button"].btn_empresa')
35
+ .first()
36
+ .waitFor({ state: "attached", timeout: options.timeoutMs ?? 30_000 });
37
+ }
38
+ catch (error) {
39
+ if (error instanceof Error && error.name === "TimeoutError") {
40
+ throw new Error("SiRADIG no mostró la pantalla de selección de personas. Revisá la pantalla con --headless false; no se pudo confirmar el listado de personas.");
41
+ }
42
+ throw error;
43
+ }
44
+ }
45
+ export async function listPersons(page) {
46
+ const persons = await page.locator('input[type="button"].btn_empresa').evaluateAll((buttons) => buttons
47
+ .map((button) => {
48
+ const name = button.getAttribute("value")?.trim() ?? "";
49
+ const onclick = button.getAttribute("onclick") ?? "";
50
+ const match = onclick.match(/idcontribuyente'\)\.value='([^']+)'/i);
51
+ const id = match?.[1] ?? null;
52
+ return {
53
+ id,
54
+ name,
55
+ };
56
+ })
57
+ .filter((person) => person.name.length > 0));
58
+ return dedupePersons(persons);
59
+ }
60
+ export async function selectPerson(page, personIdentifier) {
61
+ return selectPersonOnWorkerPage(page, personIdentifier, true);
62
+ }
63
+ async function selectPersonOnWorkerPage(page, personIdentifier, allowRecovery) {
64
+ assertWorkerSiradigUrl(page.url());
65
+ const personButtons = page.locator('input[type="button"].btn_empresa');
66
+ for (let index = 0; index < (await personButtons.count()); index += 1) {
67
+ const button = personButtons.nth(index);
68
+ const value = (await button.getAttribute("value"))?.trim();
69
+ const onclick = await button.getAttribute("onclick");
70
+ const id = onclick ? extractContribuyenteId(onclick) : null;
71
+ if (value === personIdentifier || id === personIdentifier) {
72
+ await button.click();
73
+ await page.waitForLoadState("domcontentloaded").catch(() => undefined);
74
+ try {
75
+ await page.waitForFunction(() => document.querySelector("#btn_datos_personales") !== null ||
76
+ window.location.pathname.startsWith("/radig_emp/"), undefined, { timeout: 15_000 });
77
+ if (allowRecovery && new URL(page.url()).pathname.startsWith("/radig_emp/")) {
78
+ await recoverWorkerSiradigPage(page);
79
+ if (!(await page.locator("#btn_datos_personales").count())) {
80
+ await selectPersonOnWorkerPage(page, personIdentifier, false);
81
+ }
82
+ }
83
+ assertWorkerSiradigUrl(page.url());
84
+ }
85
+ catch (error) {
86
+ if (error instanceof Error && error.name === "TimeoutError") {
87
+ throw new Error('Después de seleccionar la persona, SiRADIG no mostró el menú con "Datos Personales". Verificá que se haya abierto SiRADIG - Trabajador y revisá si hay un aviso pendiente con --headless false.');
88
+ }
89
+ throw error;
90
+ }
91
+ return;
92
+ }
93
+ }
94
+ throw new Error(`Person "${personIdentifier}" was not found.`);
95
+ }
96
+ function normalize(value) {
97
+ return (value ?? "").replace(/\s+/g, " ").trim();
98
+ }
99
+ function extractContribuyenteId(onclick) {
100
+ const match = onclick.match(/idcontribuyente'\)\.value='([^']+)'/i);
101
+ return match?.[1] ?? null;
102
+ }
103
+ function dedupePersons(persons) {
104
+ const seen = new Set();
105
+ const deduped = [];
106
+ for (const person of persons) {
107
+ const key = `${person.id ?? ""}:${person.name}`;
108
+ if (seen.has(key)) {
109
+ continue;
110
+ }
111
+ seen.add(key);
112
+ deduped.push(person);
113
+ }
114
+ return deduped;
115
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReceiptType } from "../types.js";
2
+ export declare const RECEIPT_TYPES: ReceiptType[];
3
+ export declare function listReceiptTypes(): ReceiptType[];
4
+ export declare function resolveReceiptTypeId(label: string): string | null;