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,39 @@
1
+ export const RECEIPT_TYPES = [
2
+ { id: "6", name: "Factura B" },
3
+ { id: "7", name: "Nota de Débito B" },
4
+ { id: "8", name: "Nota de Crédito B" },
5
+ { id: "9", name: "Recibo B" },
6
+ { id: "10", name: "Nota de Venta al contado B" },
7
+ { id: "11", name: "Factura C" },
8
+ { id: "12", name: "Nota de Débito C" },
9
+ { id: "13", name: "Nota de Crédito C" },
10
+ { id: "14", name: "Documento Aduanero" },
11
+ { id: "15", name: "Recibo C" },
12
+ { id: "16", name: "Nota de Venta al contado C" },
13
+ { id: "40", name: "Otro comprobante B (RG 1415)" },
14
+ { id: "41", name: "Otro comprobante C (RG 1415)" },
15
+ { id: "82", name: "Tique-factura B" },
16
+ { id: "88", name: "Otros comp. doc. exceptuados" },
17
+ ];
18
+ export function listReceiptTypes() {
19
+ return RECEIPT_TYPES.map((receiptType) => ({ ...receiptType }));
20
+ }
21
+ export function resolveReceiptTypeId(label) {
22
+ const normalizedLabel = normalizeReceiptTypeLabel(label);
23
+ for (const receiptType of RECEIPT_TYPES) {
24
+ if (normalizeReceiptTypeLabel(receiptType.name) === normalizedLabel) {
25
+ return receiptType.id;
26
+ }
27
+ }
28
+ return RECEIPT_TYPE_LABEL_ALIASES[normalizedLabel] ?? null;
29
+ }
30
+ const RECEIPT_TYPE_LABEL_ALIASES = {
31
+ [normalizeReceiptTypeLabel("Otros comprobantes documentos exceptuados")]: "88",
32
+ };
33
+ function normalizeReceiptTypeLabel(value) {
34
+ return value
35
+ .normalize("NFD")
36
+ .replace(/[\u0300-\u036f]/g, "")
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, "");
39
+ }
@@ -0,0 +1,27 @@
1
+ import { type Browser, type BrowserContext, type BrowserContextOptions, type Page } from "playwright";
2
+ import type { ArcaCredentials } from "../types.js";
3
+ export declare const ARCA_LOGIN_URL = "https://auth.afip.gob.ar/contribuyente_/login.xhtml";
4
+ export declare const SIRADIG_LOGIN_URL = "https://auth.afip.gob.ar/contribuyente_/login.xhtml?action=SYSTEM&system=radig";
5
+ export declare const SIRADIG_BASE_URL = "https://serviciosjava2.afip.gob.ar/radig/";
6
+ export declare const ARCA_PORTAL_URL = "https://portalcf.cloud.afip.gob.ar/portal/app/";
7
+ export declare const ARCA_PORTAL_EXPIRED_SESSION_URL = "https://portalcf.cloud.afip.gob.ar/portal/app/expiredSession";
8
+ export declare const SIRADIG_SERVICE_NAME = "SiRADIG - Trabajador";
9
+ export type BrowserSession = {
10
+ browser: Browser;
11
+ context: BrowserContext;
12
+ page: Page;
13
+ };
14
+ export type BrowserSessionOptions = {
15
+ headless?: boolean;
16
+ storageState?: BrowserContextOptions["storageState"];
17
+ };
18
+ export declare class ArcaLoginError extends Error {
19
+ constructor(message: string);
20
+ }
21
+ export declare function createBrowserSession(options?: BrowserSessionOptions): Promise<BrowserSession>;
22
+ export declare function closeBrowserSession(session: BrowserSession): Promise<void>;
23
+ export declare function loginToArca(page: Page, credentials: ArcaCredentials): Promise<void>;
24
+ export declare function openSiradigFromPortal(page: Page): Promise<Page>;
25
+ export declare function recoverWorkerSiradigPage(page: Page, period?: number): Promise<void>;
26
+ export declare function assertWorkerSiradigUrl(url: string): void;
27
+ export declare function isExpiredPortalSessionUrl(url: string): boolean;
@@ -0,0 +1,165 @@
1
+ import { chromium, } from "playwright";
2
+ import { safeErrorMessage } from "../security/errors.js";
3
+ export const ARCA_LOGIN_URL = "https://auth.afip.gob.ar/contribuyente_/login.xhtml";
4
+ export const SIRADIG_LOGIN_URL = `${ARCA_LOGIN_URL}?action=SYSTEM&system=radig`;
5
+ export const SIRADIG_BASE_URL = "https://serviciosjava2.afip.gob.ar/radig/";
6
+ const SIRADIG_EMPLOYER_BASE_URL = "https://serviciosjava2.afip.gob.ar/radig_emp/";
7
+ export const ARCA_PORTAL_URL = "https://portalcf.cloud.afip.gob.ar/portal/app/";
8
+ export const ARCA_PORTAL_EXPIRED_SESSION_URL = `${ARCA_PORTAL_URL}expiredSession`;
9
+ export const SIRADIG_SERVICE_NAME = "SiRADIG - Trabajador";
10
+ export class ArcaLoginError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "ArcaLoginError";
14
+ }
15
+ }
16
+ export async function createBrowserSession(options = {}) {
17
+ const browser = await chromium.launch({ headless: options.headless ?? false });
18
+ const context = await browser.newContext({ storageState: options.storageState });
19
+ const page = await context.newPage();
20
+ return { browser, context, page };
21
+ }
22
+ export async function closeBrowserSession(session) {
23
+ await session.context.close();
24
+ await session.browser.close();
25
+ }
26
+ export async function loginToArca(page, credentials) {
27
+ try {
28
+ await performLogin(page, credentials);
29
+ }
30
+ catch (error) {
31
+ const message = safeErrorMessage(error, [credentials.password, credentials.cuit]);
32
+ if (error instanceof ArcaLoginError)
33
+ throw new ArcaLoginError(message);
34
+ // Do not retain the original error or cause: Playwright may include fill values.
35
+ throw new Error(message);
36
+ }
37
+ }
38
+ async function performLogin(page, credentials) {
39
+ await page.goto(SIRADIG_LOGIN_URL, { waitUntil: "domcontentloaded" });
40
+ await page
41
+ .locator('[id="F1:username"], input[type="number"], input[role="spinbutton"]')
42
+ .first()
43
+ .fill(credentials.cuit);
44
+ await page.getByRole("button", { name: "Siguiente" }).click();
45
+ await waitForLoginOutcome(page, "password");
46
+ const cuitErrorMessage = await getLoginErrorMessage(page);
47
+ if (cuitErrorMessage) {
48
+ throw new ArcaLoginError(cuitErrorMessage);
49
+ }
50
+ const passwordInput = page.locator('input[type="password"]').first();
51
+ await passwordInput.waitFor({ state: "visible", timeout: 15_000 });
52
+ await passwordInput.fill(credentials.password);
53
+ await page.getByRole("button", { name: "Ingresar" }).click();
54
+ await waitForLoginOutcome(page, "authenticated");
55
+ const loginErrorMessage = await getLoginErrorMessage(page);
56
+ if (loginErrorMessage) {
57
+ throw new ArcaLoginError(loginErrorMessage);
58
+ }
59
+ }
60
+ export async function openSiradigFromPortal(page) {
61
+ await page.waitForLoadState("domcontentloaded").catch(() => undefined);
62
+ if (page.url().startsWith(SIRADIG_BASE_URL) || page.url().startsWith(SIRADIG_EMPLOYER_BASE_URL)) {
63
+ await recoverWorkerSiradigPage(page);
64
+ return page;
65
+ }
66
+ const portalLink = page
67
+ .getByText(/^\s*siradig\s*[-–—]?\s*trabajador\s*$/i)
68
+ .filter({ visible: true })
69
+ .first();
70
+ const viewAll = page
71
+ .getByText(/^ver todos$/i)
72
+ .filter({ visible: true })
73
+ .first();
74
+ try {
75
+ await portalLink.or(viewAll).first().waitFor({ state: "visible", timeout: 30_000 });
76
+ if (!(await portalLink.isVisible())) {
77
+ await viewAll.click();
78
+ }
79
+ await portalLink.waitFor({ state: "visible", timeout: 30_000 });
80
+ }
81
+ catch (error) {
82
+ if (error instanceof Error && error.name === "TimeoutError") {
83
+ throw new Error('Ingresaste a ARCA, pero no se encontró el servicio SiRADIG - Trabajador visible en el portal. Ejecutá login con --headless false y revisá si aparece en "Mis servicios" o si hay un aviso pendiente.');
84
+ }
85
+ throw error;
86
+ }
87
+ const currentUrl = page.url();
88
+ const popupPromise = page.waitForEvent("popup", { timeout: 10_000 }).catch(() => null);
89
+ const samePageNavigationPromise = page
90
+ .waitForURL((url) => url.href !== currentUrl, { timeout: 10_000 })
91
+ .then(() => page)
92
+ .catch(() => null);
93
+ await portalLink.click();
94
+ const siradigPage = (await Promise.race([popupPromise, samePageNavigationPromise])) ?? page;
95
+ await siradigPage.waitForLoadState("domcontentloaded").catch(() => undefined);
96
+ await recoverWorkerSiradigPage(siradigPage);
97
+ return siradigPage;
98
+ }
99
+ export async function recoverWorkerSiradigPage(page, period = new Date().getFullYear()) {
100
+ if (!new URL(page.url()).pathname.startsWith("/radig_emp/"))
101
+ return;
102
+ const workerUrl = new URL("https://serviciosjava2.afip.gob.ar/radig/jsp/determinarContribuyente.do");
103
+ workerUrl.searchParams.set("codigo", String(period));
104
+ await page.goto(workerUrl.href, { waitUntil: "domcontentloaded" });
105
+ assertWorkerSiradigUrl(page.url());
106
+ try {
107
+ await page
108
+ .locator('#btn_datos_personales, form[name="seleccionaEmpresaForm"], input[type="button"].btn_empresa')
109
+ .first()
110
+ .waitFor({ state: "attached", timeout: 30_000 });
111
+ }
112
+ catch (error) {
113
+ if (error instanceof Error && error.name === "TimeoutError") {
114
+ throw new Error("No se pudo abrir SiRADIG - Trabajador desde la sesión de Empleador. Revisá el acceso al servicio con --headless false.");
115
+ }
116
+ throw error;
117
+ }
118
+ }
119
+ export function assertWorkerSiradigUrl(url) {
120
+ if (new URL(url).pathname.startsWith("/radig_emp/")) {
121
+ throw new Error('Se abrió SiRADIG - Empleador (/radig_emp/). Este CLI requiere SiRADIG - Trabajador (/radig/). Volvé al portal y verificá que el servicio "SiRADIG - Trabajador" esté disponible en "Ver Todos".');
122
+ }
123
+ }
124
+ export function isExpiredPortalSessionUrl(url) {
125
+ return url.startsWith(ARCA_PORTAL_EXPIRED_SESSION_URL);
126
+ }
127
+ async function waitForLoginOutcome(page, stage) {
128
+ try {
129
+ await page.waitForFunction(({ stage, portalUrl, workerUrl, employerUrl }) => {
130
+ const hasError = Array.from(document.querySelectorAll('[id="F1:msg"], .text-danger')).some((element) => element.getClientRects().length > 0 && element.textContent?.trim());
131
+ if (hasError)
132
+ return true;
133
+ if (stage === "password") {
134
+ return Array.from(document.querySelectorAll('input[type="password"]')).some((element) => element.getClientRects().length > 0);
135
+ }
136
+ return (window.location.href.startsWith(portalUrl) ||
137
+ window.location.href.startsWith(workerUrl) ||
138
+ window.location.href.startsWith(employerUrl));
139
+ }, {
140
+ stage,
141
+ portalUrl: ARCA_PORTAL_URL,
142
+ workerUrl: SIRADIG_BASE_URL,
143
+ employerUrl: SIRADIG_EMPLOYER_BASE_URL,
144
+ }, { timeout: 30_000 });
145
+ }
146
+ catch (error) {
147
+ if (error instanceof Error && error.name === "TimeoutError") {
148
+ throw new ArcaLoginError(stage === "password"
149
+ ? "ARCA no mostró el campo de clave ni un mensaje de error después de enviar el CUIT/CUIL."
150
+ : "ARCA no confirmó el ingreso a SiRADIG o al portal. Revisá si requiere una verificación adicional con --headless false.");
151
+ }
152
+ throw error;
153
+ }
154
+ }
155
+ async function getLoginErrorMessage(page) {
156
+ const messages = page.locator('[id="F1:msg"], .text-danger');
157
+ for (const message of await messages.all()) {
158
+ if (!(await message.isVisible()))
159
+ continue;
160
+ const text = (await message.textContent())?.replace(/\s+/g, " ").trim();
161
+ if (text)
162
+ return text;
163
+ }
164
+ return null;
165
+ }
@@ -0,0 +1,22 @@
1
+ import type { Page } from "playwright";
2
+ import { type FormContext } from "./form.js";
3
+ export type FormSubmissionResult = FormContext & {
4
+ status: "submitted" | "cancelled" | "rejected" | "unconfirmed";
5
+ message: string;
6
+ reference?: string;
7
+ confirmationSource?: "acknowledgement" | "sent-forms";
8
+ submissionType?: string;
9
+ submittedAt?: string;
10
+ resultUrl?: string;
11
+ };
12
+ export declare function submitForm(page: Page, context: FormContext, options: {
13
+ accept: boolean;
14
+ confirm?: (message: string) => Promise<boolean>;
15
+ timeoutMs?: number;
16
+ }): Promise<FormSubmissionResult>;
17
+ export declare function openSubmissionConfirmation(page: Page, context: FormContext): Promise<string>;
18
+ export declare function cancelSubmission(page: Page, context: FormContext): Promise<FormSubmissionResult>;
19
+ /** The final click is attempted once. A timeout must never trigger another presentation. */
20
+ export declare function generatePresentation(page: Page, context: FormContext, options?: {
21
+ timeoutMs?: number;
22
+ }): Promise<FormSubmissionResult>;
@@ -0,0 +1,140 @@
1
+ import { verifyFormContext } from "./form.js";
2
+ import { findNewSubmittedForm, readSubmittedForms } from "./submitted-forms.js";
3
+ const SUCCESS_PATTERN = /(?:presentaci[oó]n|formulario)[^\n.]{0,80}(?:generad[oa]|enviad[oa]|presentad[oa])\s+(?:correctamente|con [eé]xito|exitosamente)/i;
4
+ const ERROR_SELECTOR = ".text-danger, .formError, .ui-state-error";
5
+ const NEGATIVE_PATTERN = /\bno(?:\s+(?:se|ha|sido|fue|pudo)){0,4}\s+(?:gener(?:ar|ad[oa])|envi(?:ar|ad[oa])|present(?:ar|ad[oa])|procesar|realizar)/i;
6
+ export async function submitForm(page, context, options) {
7
+ if (!options.accept && !options.confirm)
8
+ throw new Error("Falta confirmar el envío del formulario.");
9
+ const message = await openSubmissionConfirmation(page, context);
10
+ const accepted = options.accept || (await options.confirm?.(message));
11
+ if (!accepted)
12
+ return cancelSubmission(page, context);
13
+ return generatePresentation(page, context, options);
14
+ }
15
+ export async function openSubmissionConfirmation(page, context) {
16
+ await verifyFormContext(page, { period: context.period, person: context.person.id });
17
+ await page.locator("#btn_enviar_empleador").click();
18
+ const dialog = submissionDialog(page);
19
+ try {
20
+ await dialog.waitFor({ state: "visible", timeout: 15_000 });
21
+ }
22
+ catch (error) {
23
+ const portalError = await readPortalError(page);
24
+ if (portalError)
25
+ throw new Error(`ARCA no permite enviar el formulario: ${portalError}`);
26
+ throw error;
27
+ }
28
+ return page.locator("#confirmacion_generacion_presentacion").innerText();
29
+ }
30
+ export async function cancelSubmission(page, context) {
31
+ await submissionDialog(page).getByRole("button", { name: "Cancelar", exact: true }).click();
32
+ return {
33
+ ...context,
34
+ status: "cancelled",
35
+ message: "Envío cancelado. No se generó una presentación.",
36
+ };
37
+ }
38
+ /** The final click is attempted once. A timeout must never trigger another presentation. */
39
+ export async function generatePresentation(page, context, options = {}) {
40
+ await verifyFormContext(page, { period: context.period, person: context.person.id });
41
+ const button = submissionDialog(page).getByRole("button", {
42
+ name: "Generar Presentación",
43
+ exact: true,
44
+ });
45
+ await button.waitFor({ state: "visible", timeout: 15_000 });
46
+ const previousForms = await readSubmittedForms(page, context, options.timeoutMs).catch(() => null);
47
+ // Reading history can take time; bind the final action to the current preview again.
48
+ await verifyFormContext(page, { period: context.period, person: context.person.id });
49
+ const previewUrl = page.url();
50
+ try {
51
+ await button.click({ timeout: options.timeoutMs ?? 30_000 });
52
+ await page.waitForFunction(({ successPattern, negativePattern, errorSelector, previewUrl }) => {
53
+ const text = document.querySelector("#contenido")?.innerText ?? "";
54
+ const visibleError = Array.from(document.querySelectorAll(errorSelector)).some((element) => element.getClientRects().length > 0 && element.textContent?.trim());
55
+ return (new RegExp(successPattern, "i").test(text) ||
56
+ new RegExp(negativePattern, "i").test(text) ||
57
+ visibleError ||
58
+ /^\s*Error\b/m.test(text) ||
59
+ location.pathname.includes("expiredSession") ||
60
+ location.pathname.endsWith("/login.xhtml") ||
61
+ (location.href !== previewUrl &&
62
+ !location.pathname.endsWith("/generacion_presentacion.jsp") &&
63
+ document.readyState === "complete"));
64
+ }, {
65
+ successPattern: SUCCESS_PATTERN.source,
66
+ negativePattern: NEGATIVE_PATTERN.source,
67
+ errorSelector: ERROR_SELECTOR,
68
+ previewUrl,
69
+ }, { timeout: options.timeoutMs ?? 30_000 });
70
+ const error = await readPortalError(page);
71
+ if (error)
72
+ return { ...context, status: "rejected", message: `ARCA rechazó la presentación: ${error}` };
73
+ const text = await page.locator("#contenido").innerText({ timeout: 2_000 });
74
+ const acknowledgement = text.match(SUCCESS_PATTERN)?.[0];
75
+ if (acknowledgement) {
76
+ const reference = text.match(/(?:n[uú]mero|nro\.?|n[°º])\s*(?:de\s+)?presentaci[oó]n\s*:?\s*([\w-]+)/i)?.[1];
77
+ return {
78
+ ...context,
79
+ status: "submitted",
80
+ message: acknowledgement,
81
+ confirmationSource: "acknowledgement",
82
+ ...(reference ? { reference } : {}),
83
+ };
84
+ }
85
+ }
86
+ catch {
87
+ // The server may have processed the final click even if navigation or observation failed.
88
+ }
89
+ const resultUrl = new URL(page.url());
90
+ // Strip query parameters: an unknown result should not print private URL values.
91
+ const finalUrl = `${resultUrl.origin}${resultUrl.pathname}`;
92
+ let historyMessage = "No se pudo consultar el historial antes del envío para comparar el resultado.";
93
+ if (previousForms !== null) {
94
+ const currentForms = await readSubmittedForms(page, context, options.timeoutMs).catch(() => null);
95
+ if (currentForms === null) {
96
+ historyMessage = "No se pudo verificar el historial después del envío.";
97
+ }
98
+ else {
99
+ const presentation = findNewSubmittedForm(previousForms, currentForms);
100
+ if (presentation) {
101
+ return {
102
+ ...context,
103
+ status: "submitted",
104
+ message: `Presentación n.º ${presentation.number} (${presentation.description}) enviada el ${presentation.submittedAt}, confirmada en «Consulta de Formularios Enviados».`,
105
+ reference: String(presentation.number),
106
+ submissionType: presentation.description,
107
+ submittedAt: presentation.submittedAt,
108
+ confirmationSource: "sent-forms",
109
+ };
110
+ }
111
+ historyMessage = currentForms.every((form) => previousForms.some((old) => old.id === form.id))
112
+ ? "El historial no muestra una presentación nueva respecto del estado previo al envío."
113
+ : "El historial cambió, pero no se pudo identificar una única presentación nueva de este intento.";
114
+ }
115
+ }
116
+ return {
117
+ ...context,
118
+ status: "unconfirmed",
119
+ resultUrl: finalUrl,
120
+ message: "Se intentó generar la presentación, pero no se pudo confirmar el resultado.\n" +
121
+ `${historyMessage}\nPantalla final: ${finalUrl}\n` +
122
+ "Revisá «Consulta de Formularios Enviados» en ARCA antes de volver a enviar. El CLI no reintentó la presentación.",
123
+ };
124
+ }
125
+ function submissionDialog(page) {
126
+ return page
127
+ .locator(".ui-dialog")
128
+ .filter({ has: page.locator("#confirmacion_generacion_presentacion") });
129
+ }
130
+ async function readPortalError(page) {
131
+ return page.evaluate(({ selector, negativePattern }) => {
132
+ const error = Array.from(document.querySelectorAll(selector)).find((element) => element.getClientRects().length > 0 && element.textContent?.trim());
133
+ if (error)
134
+ return error.textContent?.replace(/\s+/g, " ").trim() ?? null;
135
+ const content = document.querySelector("#contenido")?.innerText ?? "";
136
+ return /^\s*Error\b/m.test(content) || new RegExp(negativePattern, "i").test(content)
137
+ ? content.replace(/\s+/g, " ").trim()
138
+ : null;
139
+ }, { selector: ERROR_SELECTOR, negativePattern: NEGATIVE_PATTERN.source });
140
+ }
@@ -0,0 +1,12 @@
1
+ import type { Page } from "playwright";
2
+ import { type FormContext } from "./form.js";
3
+ export type SubmittedForm = {
4
+ id: string;
5
+ number: number;
6
+ description: string;
7
+ submittedAt: string;
8
+ };
9
+ /** A separate page preserves the draft and its confirmation dialog. */
10
+ export declare function readSubmittedForms(page: Page, context: FormContext, timeoutMs?: number): Promise<SubmittedForm[]>;
11
+ /** Only a single next version with a new record ID can confirm this attempt. */
12
+ export declare function findNewSubmittedForm(before: SubmittedForm[], after: SubmittedForm[]): SubmittedForm | null;
@@ -0,0 +1,54 @@
1
+ import { verifyFormContext } from "./form.js";
2
+ import { extractInlineJson } from "./http.js";
3
+ import { SIRADIG_BASE_URL } from "./session.js";
4
+ /** A separate page preserves the draft and its confirmation dialog. */
5
+ export async function readSubmittedForms(page, context, timeoutMs = 15_000) {
6
+ const historyPage = await page.context().newPage();
7
+ historyPage.setDefaultTimeout(timeoutMs);
8
+ try {
9
+ await historyPage.goto(new URL("jsp/verFormulariosEnviados.do", SIRADIG_BASE_URL).href, {
10
+ waitUntil: "load",
11
+ timeout: timeoutMs,
12
+ });
13
+ await verifyFormContext(historyPage, { period: context.period, person: context.person.id });
14
+ const table = historyPage.locator("#tabla_formularios");
15
+ await table.waitFor({ state: "attached", timeout: timeoutMs });
16
+ const inlineForms = extractInlineJson(await historyPage.content(), "datosFormulariosJSON");
17
+ const rows = await table.locator("tbody tr").evaluateAll((elements) => elements.map((element) => {
18
+ const cells = Array.from(element.querySelectorAll("td")).map((cell) => cell.textContent?.replace(/\s+/g, " ").trim() ?? "");
19
+ return {
20
+ id: element.getAttribute("data-id-reg") ?? "",
21
+ number: Number(cells[0]),
22
+ description: cells[1] ?? "",
23
+ submittedAt: cells[2] ?? "",
24
+ };
25
+ }));
26
+ if ((Array.isArray(inlineForms) && inlineForms.length !== rows.length) ||
27
+ (!Array.isArray(inlineForms) && rows.length === 0) ||
28
+ rows.some((row) => !/^\d+$/.test(row.id) ||
29
+ !Number.isSafeInteger(row.number) ||
30
+ row.number < 1 ||
31
+ !row.description ||
32
+ !/^\d{2}\/\d{2}\/\d{4}$/.test(row.submittedAt)) ||
33
+ new Set(rows.map((row) => row.id)).size !== rows.length ||
34
+ new Set(rows.map((row) => row.number)).size !== rows.length) {
35
+ throw new Error("El historial de formularios enviados de ARCA tiene un formato desconocido.");
36
+ }
37
+ return rows;
38
+ }
39
+ finally {
40
+ await historyPage.close();
41
+ }
42
+ }
43
+ /** Only a single next version with a new record ID can confirm this attempt. */
44
+ export function findNewSubmittedForm(before, after) {
45
+ const previousIds = new Set(before.map((form) => form.id));
46
+ const currentIds = new Set(after.map((form) => form.id));
47
+ const added = after.filter((form) => !previousIds.has(form.id));
48
+ const previousNumber = before.reduce((maximum, form) => Math.max(maximum, form.number), 0);
49
+ if (added.length !== 1 ||
50
+ before.some((form) => !currentIds.has(form.id)) ||
51
+ added[0].number !== previousNumber + 1)
52
+ return null;
53
+ return added[0];
54
+ }
@@ -0,0 +1 @@
1
+ export declare function formatCliError(error: unknown, args?: readonly string[], defaultPassword?: string | undefined): string;
@@ -0,0 +1,11 @@
1
+ import { safeErrorMessage } from "../security/errors.js";
2
+ export function formatCliError(error, args = process.argv.slice(2), defaultPassword = process.env.ARCA_PASSWORD) {
3
+ const passwords = [defaultPassword];
4
+ for (const [index, arg] of args.entries()) {
5
+ if (arg === "--password")
6
+ passwords.push(args[index + 1]);
7
+ else if (arg.startsWith("--password="))
8
+ passwords.push(arg.slice("--password=".length));
9
+ }
10
+ return safeErrorMessage(error, passwords);
11
+ }
@@ -0,0 +1,6 @@
1
+ import type { HelpCommand } from "./options.js";
2
+ export declare function renderHelp(topic: HelpCommand["topic"], options?: {
3
+ width?: number;
4
+ color?: boolean;
5
+ version?: string;
6
+ }): string;