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,636 @@
1
+ import { fetchSiradigJson, isRecord, parseInlineTable } from "./http.js";
2
+ import { resolveReceiptTypeId } from "./receipt-types.js";
3
+ const DEDUCTIONS_MENU_URL = "verMenuDeducciones.do#header_deducciones";
4
+ const MEDICAL_EXPENSES_URL = "https://serviciosjava2.afip.gob.ar/radig/jsp/verGastosMedicos.do";
5
+ export async function readDeductions(page, request = page.request) {
6
+ const data = await fetchSiradigJson(request, "verMenuDeducciones.do", "datosDeduccionesAgrupadasJSON");
7
+ const deductions = parseDeductionsJson(data);
8
+ if (deductions !== null)
9
+ return deductions;
10
+ await openDeductionsMenu(page);
11
+ return listDeductions(page);
12
+ }
13
+ export function parseDeductionsJson(data) {
14
+ if (!Array.isArray(data))
15
+ return null;
16
+ const deductions = [];
17
+ for (const group of data) {
18
+ if (!isRecord(group) || typeof group.linkAltaEdicion !== "string")
19
+ return null;
20
+ const rows = parseInlineTable(group.datosTabla);
21
+ if (rows === null)
22
+ return null;
23
+ const type = deriveDeductionType(group.linkAltaEdicion);
24
+ for (const row of rows) {
25
+ const [cuit, name, period, amount] = row.cells;
26
+ if (!row.id ||
27
+ !cuit ||
28
+ !name ||
29
+ row.cells.length !== 4 ||
30
+ !type ||
31
+ period === undefined ||
32
+ amount === undefined)
33
+ return null;
34
+ deductions.push({ type, id: row.id, cuit, name, period, amount });
35
+ }
36
+ }
37
+ return deductions;
38
+ }
39
+ export async function openDeductionsMenu(page) {
40
+ await page.goto(new URL(DEDUCTIONS_MENU_URL, page.url()).href, { waitUntil: "domcontentloaded" });
41
+ }
42
+ export async function listDeductions(page) {
43
+ return (await listDeductionEntries(page)).map(({ editUrl: _editUrl, ...deduction }) => deduction);
44
+ }
45
+ export async function findDeductionById(page, deductionId) {
46
+ const deductions = await listDeductionEntries(page);
47
+ return deductions.find((deduction) => deduction.id === deductionId) ?? null;
48
+ }
49
+ export async function findDeductionByTypeAndCuit(page, type, cuit) {
50
+ const deductions = await listDeductionsByTypeAndCuit(page, type, cuit);
51
+ if (deductions.length > 1) {
52
+ throw new Error(`Se encontró más de una deducción ${type} con CUIT ${cuit}. No puedo elegir una automáticamente.`);
53
+ }
54
+ return deductions[0] ?? null;
55
+ }
56
+ export async function findDeductionByTypeCuitAndMonth(page, type, cuit, month) {
57
+ const deductions = await listDeductionsByTypeAndCuit(page, type, cuit);
58
+ return deductions.find((deduction) => parseMonthNumber(deduction.period) === month) ?? null;
59
+ }
60
+ async function findRequiredDeductionByTypeAndCuit(page, type, cuit) {
61
+ const deduction = await findDeductionByTypeAndCuit(page, type, cuit);
62
+ if (!deduction) {
63
+ throw new Error(`No se encontró una deducción ${type} con CUIT ${cuit}. Revisá que el tipo y CUIT coincidan con arca-siradig form deductions list.`);
64
+ }
65
+ return deduction;
66
+ }
67
+ async function listDeductionsByTypeAndCuit(page, type, cuit) {
68
+ const deductions = await listDeductionEntries(page);
69
+ return deductions.filter((deduction) => deduction.type === type && normalizeCuit(deduction.cuit) === normalizeCuit(cuit));
70
+ }
71
+ export async function openDeductionDetail(page, editUrl, deductionId) {
72
+ const baseUrl = page.url().startsWith("http")
73
+ ? page.url()
74
+ : "https://serviciosjava2.afip.gob.ar/radig/jsp/";
75
+ const detailUrl = new URL(editUrl, baseUrl);
76
+ detailUrl.searchParams.set("id", deductionId);
77
+ await page.goto(detailUrl.href, { waitUntil: "domcontentloaded" });
78
+ }
79
+ export async function openDeductionDetailById(page, deductionId) {
80
+ await openDeductionDetail(page, "verCuotasMedicoAsistenciales.do", deductionId);
81
+ }
82
+ export async function extractDeductionDetail(page) {
83
+ const heading = normalizeText(await readLocatorText(page.locator("fieldset legend").first())) || "Entidad";
84
+ const cuit = normalizeText(await readLocatorInput(page, "#numeroDoc"));
85
+ const name = normalizeText(await readLocatorInput(page, "#razonSocial"));
86
+ const fromMonthValue = normalizeText(await readLocatorInput(page, "#mesDesde"));
87
+ const toMonthValue = normalizeText(await readLocatorInput(page, "#mesHasta"));
88
+ const fromMonthText = normalizeText(await readLocatorInput(page, "#mesDesdeTexto"));
89
+ const toMonthText = normalizeText(await readLocatorInput(page, "#mesHastaTexto"));
90
+ const amount = normalizeText(await readLocatorInput(page, "#montoTotal"));
91
+ const monthlyRows = await readIndexedRows(page, "#tabla_meses");
92
+ const receiptRows = await readIndexedRows(page, "#tabla_comprobantes");
93
+ const hasMonthlyDetail = monthlyRows.length > 0;
94
+ const receiptMonth = parseMonthNumber(fromMonthValue) || parseMonthNumber(fromMonthText);
95
+ return {
96
+ heading,
97
+ cuit,
98
+ name,
99
+ fromMonth: parseMonthNumber(fromMonthValue) || parseMonthNumber(fromMonthText),
100
+ fromMonthLabel: parseMonthLabel(fromMonthValue, fromMonthText),
101
+ toMonth: parseMonthNumber(toMonthValue) || parseMonthNumber(toMonthText),
102
+ toMonthLabel: parseMonthLabel(toMonthValue, toMonthText),
103
+ amount: normalizeAmount(amount),
104
+ monthlyDetail: monthlyRows.map((row) => ({
105
+ month: parseMonthNumber(row.cells[0]),
106
+ monthlyAmount: normalizeAmount(row.cells[1]),
107
+ receiptsTotalAmount: normalizeAmount(row.cells[2]),
108
+ })),
109
+ receipts: receiptRows.map((row) => hasMonthlyDetail
110
+ ? {
111
+ month: parseMonthNumber(row.cells[0]),
112
+ date: normalizeDate(row.cells[1]),
113
+ typeId: resolveReceiptTypeId(row.cells[2]) ?? row.cells[2],
114
+ type: row.cells[2],
115
+ id: row.rowId,
116
+ amount: normalizeAmount(row.cells[4]),
117
+ }
118
+ : {
119
+ month: receiptMonth,
120
+ date: normalizeDate(row.cells[0]),
121
+ typeId: resolveReceiptTypeId(row.cells[1]) ?? row.cells[1],
122
+ type: row.cells[1],
123
+ id: row.rowId,
124
+ amount: normalizeAmount(row.cells[3]),
125
+ }),
126
+ };
127
+ }
128
+ export async function addDeductionMonthlyItem(page, options) {
129
+ await openDeductionsMenu(page);
130
+ const deduction = await findRequiredDeductionByTypeAndCuit(page, options.deductionType, options.cuit);
131
+ await openDeductionDetail(page, deduction.editUrl, deduction.id);
132
+ await addDeductionMonthlyItemOnOpenPage(page, options);
133
+ }
134
+ export async function addDeductionMonthlyItemOnOpenPage(page, options) {
135
+ const monthSelect = page.locator("#detalleIndividualMes");
136
+ await page.locator("#btn_alta_mes").click();
137
+ await expectVisible(page, "#dialog_alta_mes");
138
+ const option = monthSelect.locator(`option[value="${options.month}"]`);
139
+ if ((await option.count()) === 0) {
140
+ throw new Error(`No se puede agregar el mes ${options.month}: el formulario no lo admite.`);
141
+ }
142
+ if (await option.isDisabled()) {
143
+ throw new Error(`No se puede agregar el mes ${options.month} porque ya está agregado o es mayor al mes en curso.`);
144
+ }
145
+ await monthSelect.selectOption(String(options.month));
146
+ await page.locator("#detalleIndividualMontoMensual").fill(options.amount);
147
+ await page.getByRole("button", { name: "Agregar" }).last().click();
148
+ await page.locator("#btn_guardar").click();
149
+ }
150
+ export async function addDeductionReceiptItem(page, options) {
151
+ if (options.deductionType === "gastos-medicos") {
152
+ await addMedicalExpenseReceiptItem(page, {
153
+ cuit: options.cuit,
154
+ month: options.month,
155
+ date: options.date,
156
+ receiptType: options.receiptType,
157
+ number: options.number,
158
+ amount: options.amount,
159
+ reimbursedAmount: readRequiredOption(options.reimbursedAmount, "--reimbursed-amount"),
160
+ });
161
+ return;
162
+ }
163
+ await openDeductionsMenu(page);
164
+ const deduction = await findRequiredDeductionByTypeAndCuit(page, options.deductionType, options.cuit);
165
+ await openDeductionDetail(page, deduction.editUrl, deduction.id);
166
+ await addDeductionReceiptItemOnOpenPage(page, {
167
+ deductionType: options.deductionType,
168
+ cuit: options.cuit,
169
+ month: options.month,
170
+ date: options.date,
171
+ receiptType: options.receiptType,
172
+ number: options.number,
173
+ amount: options.amount,
174
+ });
175
+ }
176
+ export async function addDeductionReceiptItemOnOpenPage(page, options) {
177
+ const monthRow = page
178
+ .locator(`#tabla_meses tbody tr[data-id-reg]`)
179
+ .filter({
180
+ hasText: formatMonthLabel(options.month),
181
+ })
182
+ .first();
183
+ if ((await monthRow.count()) === 0) {
184
+ throw new Error(`No se encontró el detalle mensual para el mes ${options.month}. Agregalo primero con arca-siradig form deductions add-item --period <YYYY> usando el mismo tipo, CUIT y mes.`);
185
+ }
186
+ await monthRow.getByRole("link", { name: "Agregar Comprobante" }).click();
187
+ await expectVisible(page, "#dialog_alta_comprobante");
188
+ await fillReceiptDialog(page, options);
189
+ await page.getByRole("button", { name: "Agregar" }).last().click();
190
+ await page.locator("#btn_guardar").click();
191
+ }
192
+ export async function addMedicalExpenseReceiptItem(page, options) {
193
+ await openDeductionsMenu(page);
194
+ const existingDeduction = await findDeductionByTypeCuitAndMonth(page, "gastos-medicos", options.cuit, options.month);
195
+ if (existingDeduction) {
196
+ await openDeductionDetail(page, existingDeduction.editUrl, existingDeduction.id);
197
+ }
198
+ else {
199
+ await page.goto(MEDICAL_EXPENSES_URL, { waitUntil: "domcontentloaded" });
200
+ await page.locator("#numeroDoc").fill(normalizeCuit(options.cuit));
201
+ await page.locator("#numeroDoc").dispatchEvent("change");
202
+ await waitForMedicalProviderLookup(page);
203
+ await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
204
+ }
205
+ await addMedicalExpenseReceiptItemOnOpenPage(page, options);
206
+ await expectMedicalExpenseSaved(page, options);
207
+ }
208
+ export async function removeMedicalExpenseReceiptItem(page, options) {
209
+ await openDeductionsMenu(page);
210
+ const deductions = await listDeductionsByTypeAndCuit(page, "gastos-medicos", options.cuit);
211
+ if (deductions.length === 0) {
212
+ throw new Error(`No se encontró una deducción gastos-medicos con CUIT ${options.cuit}.`);
213
+ }
214
+ for (const deduction of deductions) {
215
+ await openDeductionDetail(page, deduction.editUrl, deduction.id);
216
+ if (await removeReceiptByNumberOnOpenPage(page, options.number)) {
217
+ await page.locator("#btn_guardar").click();
218
+ await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
219
+ return;
220
+ }
221
+ }
222
+ throw new Error(`No se encontró un comprobante con número ${options.number}.`);
223
+ }
224
+ export async function addMedicalExpenseReceiptItemOnOpenPage(page, options) {
225
+ await page.locator("#mesDesde").selectOption(String(options.month));
226
+ await page.locator("#btn_alta_comprobante").click();
227
+ await expectVisible(page, "#dialog_alta_comprobante");
228
+ await fillReceiptDialog(page, options);
229
+ await page.locator("#cmpMontoReintegrado").fill(options.reimbursedAmount);
230
+ const addButton = page
231
+ .locator(".ui-dialog")
232
+ .filter({ has: page.locator("#dialog_alta_comprobante") })
233
+ .getByRole("button", { name: "Agregar", exact: true });
234
+ if ((await addButton.count()) > 0) {
235
+ await addButton.click();
236
+ }
237
+ else {
238
+ await page.getByRole("button", { name: "Agregar", exact: true }).click();
239
+ }
240
+ await expectReceiptInTable(page, options.number);
241
+ await ensureMedicalExpenseDeductibleAmount(page, options);
242
+ await saveMedicalExpenseForm(page);
243
+ }
244
+ export async function removeReceiptByNumberOnOpenPage(page, receiptNumber) {
245
+ const targetNumber = normalizeReceiptNumberForComparison(receiptNumber);
246
+ const rows = page.locator("#tabla_comprobantes tbody tr[data-id-reg]");
247
+ for (let index = 0; index < (await rows.count()); index += 1) {
248
+ const row = rows.nth(index);
249
+ const cells = (await row.locator("td").allTextContents()).map(normalizeText);
250
+ const rowNumber = normalizeReceiptNumberForComparison(cells[2] ?? "");
251
+ if (rowNumber !== targetNumber) {
252
+ continue;
253
+ }
254
+ const receiptId = normalizeText(await row.getAttribute("data-id-reg"));
255
+ page.once("dialog", async (dialog) => {
256
+ await dialog.accept();
257
+ });
258
+ await row.locator(".eliminar span").click();
259
+ await expectReceiptMarkedForDeletion(page, receiptId);
260
+ return true;
261
+ }
262
+ return false;
263
+ }
264
+ async function expectReceiptMarkedForDeletion(page, receiptId) {
265
+ await waitForCondition(async () => {
266
+ const value = await page
267
+ .locator("#comprobantesEliminados")
268
+ .inputValue()
269
+ .catch(() => "");
270
+ return value.split(";").includes(receiptId);
271
+ });
272
+ }
273
+ async function fillReceiptDialog(page, options) {
274
+ const formattedDate = formatIsoDate(options.date);
275
+ const receiptTypeId = resolveReceiptTypeId(options.receiptType) ?? options.receiptType;
276
+ await page.locator("#cmpFechaEmision").fill(formattedDate);
277
+ await page.locator("#cmpTipo").selectOption(receiptTypeId);
278
+ if (receiptTypeId === "88") {
279
+ await page
280
+ .locator("#cmpNumeroAlternativo")
281
+ .fill(normalizeReceiptAlternativeNumber(options.number));
282
+ }
283
+ else {
284
+ const { pointOfSale, number } = normalizeReceiptNumber(options.number);
285
+ await page.locator("#cmpPuntoVenta").fill(pointOfSale);
286
+ await page.locator("#cmpNumero").fill(number);
287
+ }
288
+ await page.locator("#cmpMontoFacturado").fill(options.amount);
289
+ }
290
+ async function waitForMedicalProviderLookup(page) {
291
+ if ((await page.locator("#razonSocial").count()) === 0) {
292
+ return;
293
+ }
294
+ await waitForCondition(async () => {
295
+ const value = await page
296
+ .locator("#razonSocial")
297
+ .inputValue()
298
+ .catch(() => "");
299
+ return value.trim().length > 0;
300
+ }).catch(() => undefined);
301
+ }
302
+ async function expectMedicalExpenseSaved(page, options) {
303
+ const savedDeduction = await waitForMedicalExpenseDeduction(page, options);
304
+ if (!savedDeduction) {
305
+ throw new Error(`ARCA no confirmó el alta de gastos-medicos para CUIT ${options.cuit} en mes ${options.month}. Revisá si el portal mostró una validación en pantalla.`);
306
+ }
307
+ await openDeductionDetail(page, savedDeduction.editUrl, savedDeduction.id);
308
+ if (!(await hasReceiptNumber(page, options.number))) {
309
+ throw new Error(`ARCA guardó la deducción de gastos-medicos para CUIT ${options.cuit} en mes ${options.month}, pero no encontré el comprobante ${options.number}.`);
310
+ }
311
+ }
312
+ async function saveMedicalExpenseForm(page) {
313
+ const saveResponse = page
314
+ .waitForResponse((response) => response.url().includes("guardarGastosMedicos.do"), {
315
+ timeout: 2_000,
316
+ })
317
+ .catch(() => null);
318
+ await page.locator("#btn_guardar").click();
319
+ await saveResponse;
320
+ await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
321
+ }
322
+ async function waitForMedicalExpenseDeduction(page, options) {
323
+ let foundDeduction = null;
324
+ await waitForCondition(async () => {
325
+ await openDeductionsMenu(page);
326
+ foundDeduction = await findDeductionByTypeCuitAndMonth(page, "gastos-medicos", options.cuit, options.month);
327
+ return foundDeduction !== null;
328
+ }, { timeoutMs: 15_000, intervalMs: 750 }).catch(() => undefined);
329
+ return foundDeduction;
330
+ }
331
+ async function expectReceiptInTable(page, receiptNumber) {
332
+ await waitForCondition(() => hasReceiptNumber(page, receiptNumber));
333
+ }
334
+ async function hasReceiptNumber(page, receiptNumber) {
335
+ const targetNumber = normalizeReceiptNumberForComparison(receiptNumber);
336
+ const rows = page.locator("#tabla_comprobantes tbody tr");
337
+ for (let index = 0; index < (await rows.count()); index += 1) {
338
+ const cells = (await rows.nth(index).locator("td").allTextContents()).map(normalizeText);
339
+ if (normalizeReceiptNumberForComparison(cells[2] ?? "") === targetNumber) {
340
+ return true;
341
+ }
342
+ }
343
+ return false;
344
+ }
345
+ async function ensureMedicalExpenseDeductibleAmount(page, options) {
346
+ const amountInput = page.locator("#montoTotal");
347
+ if ((await amountInput.count()) === 0) {
348
+ return;
349
+ }
350
+ const currentValue = normalizeText(await amountInput.inputValue().catch(() => ""));
351
+ if (parseLocalizedAmount(currentValue) > 0) {
352
+ return;
353
+ }
354
+ const deductibleAmount = await calculateMedicalExpenseDeductibleAmount(page, options);
355
+ await amountInput.fill(formatLocalizedAmount(deductibleAmount), { force: true });
356
+ await amountInput.dispatchEvent("change");
357
+ }
358
+ async function calculateMedicalExpenseDeductibleAmount(page, options) {
359
+ const rows = page.locator("#tabla_comprobantes tbody tr");
360
+ let netAmount = 0;
361
+ for (let index = 0; index < (await rows.count()); index += 1) {
362
+ const cells = (await rows.nth(index).locator("td").allTextContents()).map(normalizeText);
363
+ const billedAmount = parseLocalizedAmount(cells[3] ?? "");
364
+ const reimbursedAmount = parseLocalizedAmount(cells[4] ?? "");
365
+ netAmount += Math.max(billedAmount - reimbursedAmount, 0);
366
+ }
367
+ if (netAmount <= 0) {
368
+ netAmount =
369
+ parseLocalizedAmount(options.amount) - parseLocalizedAmount(options.reimbursedAmount);
370
+ }
371
+ return Math.max(netAmount * 0.4, 0);
372
+ }
373
+ async function listDeductionEntries(page) {
374
+ const tables = page.locator("#div_tabla_deducciones_agrupadas table.tablesorter");
375
+ const deductions = [];
376
+ for (let tableIndex = 0; tableIndex < (await tables.count()); tableIndex += 1) {
377
+ const table = tables.nth(tableIndex);
378
+ const editUrl = normalizeText(await table.getAttribute("data-url-editar"));
379
+ const type = deriveDeductionType(editUrl);
380
+ const rows = table.locator("tbody tr[data-id-reg]");
381
+ for (let rowIndex = 0; rowIndex < (await rows.count()); rowIndex += 1) {
382
+ const row = rows.nth(rowIndex);
383
+ if (!(await row.isVisible())) {
384
+ continue;
385
+ }
386
+ const rowId = normalizeText(await row.getAttribute("data-id-reg"));
387
+ const cells = await row.locator("td").allTextContents();
388
+ const [cuit = "", name = "", period = "", amount = ""] = cells.map(normalizeText);
389
+ if (!rowId || !type || !cuit || !name) {
390
+ continue;
391
+ }
392
+ deductions.push({
393
+ type,
394
+ id: rowId,
395
+ cuit,
396
+ name,
397
+ period,
398
+ amount,
399
+ editUrl,
400
+ });
401
+ }
402
+ }
403
+ return deductions;
404
+ }
405
+ function deriveDeductionType(editUrl) {
406
+ if (!editUrl) {
407
+ return "unknown";
408
+ }
409
+ const normalizedUrl = editUrl.replace(/^.*\//, "");
410
+ const urlToType = {
411
+ "verCuotasMedicoAsistenciales.do": "cuotas-medico-asistenciales",
412
+ "verPrimasAhorroSegMix.do": "primas-ahorro-seguro",
413
+ "verPrimasSeguro.do": "primas-seguro",
414
+ "verAportesSegRetiroPrivado.do": "aportes-seguro-retiro-privado",
415
+ "verDonaciones.do": "donaciones",
416
+ "verInteresesPrestamoHipotecario.do": "intereses-prestamo-hipotecario",
417
+ "verGastosSepelio.do": "gastos-sepelio",
418
+ "verGastosMedicos.do": "gastos-medicos",
419
+ "verGastosInduEquip.do": "gastos-indumentaria-equipamiento",
420
+ "verAlquilerInmuebleInquilinoN.do": "beneficio-alquiler-10",
421
+ "verAlquilerInmuebleInquilinoO.do": "beneficio-alquiler-40",
422
+ "verAlquilerInmueblePropietario.do": "beneficios-locadores-propietarios",
423
+ "verPersonalDomestico.do": "deduccion-personal-domestico",
424
+ "verAporteSociedad.do": "aporte-sociedad-garantia-reciproca",
425
+ "verVehiculoCorredorViajanteComercio.do": "vehiculos-corredores-viajantes",
426
+ "verGastosMVRICorredorViajanteComercio.do": "intereses-corredores-viajantes",
427
+ "verGastosEducacion.do": "gastos-educacion",
428
+ "verOtraDeduccion.do": "otras-deducciones",
429
+ "verImpuestoCreditoDebito.do": "impuesto-creditos-debitos",
430
+ "verImpuestoCreditoDebitoSinCBU.do": "impuesto-movimientos-fondos",
431
+ "verPercepcionRetencionAduanera.do": "percepciones-retenciones-aduaneras",
432
+ "verPagoCuentaB2.do": "pagos-cuenta-exterior-agencias",
433
+ "verPagoCuentaC2.do": "pagos-cuenta-exterior-transporte",
434
+ "verPagoCuentaRG4815A.do": "compra-moneda-extranjera",
435
+ "verPagoCuentaRG4815B.do": "adquisicion-bienes-servicios-exterior",
436
+ "verPagoCuentaRG4815C.do": "servicios-no-residentes",
437
+ "verPagoCuentaRG4815D.do": "agencias-viajes-turismo",
438
+ "verPagoCuentaRG4815E.do": "transporte-exterior",
439
+ "verAutorretenciones.do": "autorretenciones",
440
+ "verBeneficiosJubilados.do": "jubilados-pensionados-retirados",
441
+ "verAjusteMontoRetroactivo.do": "ajuste-montos-retroactivos",
442
+ "verAjusteAporteSociedad.do": "ajuste-aporte-sociedad",
443
+ };
444
+ if (urlToType[normalizedUrl]) {
445
+ return urlToType[normalizedUrl];
446
+ }
447
+ return normalizeDeductionTypeFromUrl(normalizedUrl);
448
+ }
449
+ function normalizeDeductionTypeFromUrl(url) {
450
+ return url
451
+ .replace(/^ver/, "")
452
+ .replace(/\.do$/, "")
453
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
454
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
455
+ .replace(/([a-z])([A-Z0-9])/g, "$1-$2")
456
+ .toLowerCase()
457
+ .replace(/[^a-z0-9]+/g, "-")
458
+ .replace(/^-+|-+$/g, "");
459
+ }
460
+ function normalizeText(value) {
461
+ return (value ?? "").replace(/\s+/g, " ").trim();
462
+ }
463
+ function normalizeCuit(value) {
464
+ return normalizeText(value).replace(/\D/g, "");
465
+ }
466
+ function normalizeAmount(value) {
467
+ const normalized = normalizeText(value);
468
+ if (!normalized.includes(",")) {
469
+ return normalized;
470
+ }
471
+ return normalized.replace(/\./g, "").replace(",", ".");
472
+ }
473
+ function parseLocalizedAmount(value) {
474
+ const normalized = normalizeAmount(value).replace(/[^\d.-]/g, "");
475
+ return Number.parseFloat(normalized) || 0;
476
+ }
477
+ function formatLocalizedAmount(value) {
478
+ return value.toFixed(2).replace(".", ",");
479
+ }
480
+ function normalizeDate(value) {
481
+ const normalized = normalizeText(value);
482
+ const match = normalized.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
483
+ if (!match) {
484
+ return normalized;
485
+ }
486
+ const [, day, month, year] = match;
487
+ return `${year}-${month}-${day}`;
488
+ }
489
+ async function readLocatorText(locator) {
490
+ return (await locator.textContent()) ?? "";
491
+ }
492
+ async function readLocatorInput(page, selector) {
493
+ const locator = page.locator(selector).first();
494
+ if ((await locator.count()) === 0) {
495
+ return "";
496
+ }
497
+ return await locator.inputValue().catch(() => "");
498
+ }
499
+ async function readIndexedRows(page, selector) {
500
+ const rows = page.locator(`${selector} tbody tr[data-id-reg]`);
501
+ const items = [];
502
+ for (let index = 0; index < (await rows.count()); index += 1) {
503
+ const row = rows.nth(index);
504
+ const rowId = normalizeText(await row.getAttribute("data-id-reg"));
505
+ const cells = (await row.locator("td").allTextContents()).map(normalizeText);
506
+ if (!rowId || cells.length < 5) {
507
+ continue;
508
+ }
509
+ items.push({ rowId, cells });
510
+ }
511
+ return items;
512
+ }
513
+ function parseMonthNumber(value) {
514
+ const normalizedValue = normalizeText(value);
515
+ const numericMatch = normalizedValue.match(/^(\d{1,2})(?:\s*-\s*.*)?$/);
516
+ if (numericMatch) {
517
+ return Number.parseInt(numericMatch[0], 10);
518
+ }
519
+ const monthNames = [
520
+ "Enero",
521
+ "Febrero",
522
+ "Marzo",
523
+ "Abril",
524
+ "Mayo",
525
+ "Junio",
526
+ "Julio",
527
+ "Agosto",
528
+ "Septiembre",
529
+ "Octubre",
530
+ "Noviembre",
531
+ "Diciembre",
532
+ ];
533
+ const monthIndex = monthNames.findIndex((monthName) => monthName.toLowerCase() === normalizedValue.toLowerCase() ||
534
+ normalizedValue.toLowerCase().includes(`- ${monthName.toLowerCase()}`));
535
+ return monthIndex >= 0 ? monthIndex + 1 : 0;
536
+ }
537
+ function parseMonthLabel(numberValue, textValue) {
538
+ const normalizedText = normalizeText(textValue);
539
+ if (normalizedText) {
540
+ return normalizedText;
541
+ }
542
+ const monthNumber = parseMonthNumber(numberValue);
543
+ const monthNames = [
544
+ "Enero",
545
+ "Febrero",
546
+ "Marzo",
547
+ "Abril",
548
+ "Mayo",
549
+ "Junio",
550
+ "Julio",
551
+ "Agosto",
552
+ "Septiembre",
553
+ "Octubre",
554
+ "Noviembre",
555
+ "Diciembre",
556
+ ];
557
+ return monthNames[monthNumber - 1] ?? "";
558
+ }
559
+ function formatMonthLabel(month) {
560
+ const monthNames = [
561
+ "Enero",
562
+ "Febrero",
563
+ "Marzo",
564
+ "Abril",
565
+ "Mayo",
566
+ "Junio",
567
+ "Julio",
568
+ "Agosto",
569
+ "Septiembre",
570
+ "Octubre",
571
+ "Noviembre",
572
+ "Diciembre",
573
+ ];
574
+ return `${String(month).padStart(2, "0")} - ${monthNames[month - 1] ?? String(month)}`;
575
+ }
576
+ function formatIsoDate(value) {
577
+ const match = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
578
+ if (!match) {
579
+ throw new Error(`La fecha "${value}" debe tener formato YYYY-MM-DD.`);
580
+ }
581
+ const [, year, month, day] = match;
582
+ return `${day}/${month}/${year}`;
583
+ }
584
+ function normalizeReceiptNumber(value) {
585
+ const trimmedValue = value.trim();
586
+ const compactMatch = trimmedValue.match(/^(\d{5})(\d{8})$/);
587
+ if (compactMatch) {
588
+ const [, pointOfSale, number] = compactMatch;
589
+ return { pointOfSale, number };
590
+ }
591
+ const match = trimmedValue.match(/^(\d{1,5})-(\d{1,8})$/);
592
+ if (!match) {
593
+ throw new Error('El número de comprobante debe tener formato "12345-12345678" o 13 dígitos.');
594
+ }
595
+ const [, pointOfSale, number] = match;
596
+ return {
597
+ pointOfSale: pointOfSale.padStart(5, "0"),
598
+ number: number.padStart(8, "0"),
599
+ };
600
+ }
601
+ function normalizeReceiptAlternativeNumber(value) {
602
+ const normalized = value.trim();
603
+ if (!/^\d{1,20}$/.test(normalized)) {
604
+ throw new Error("El número alternativo del comprobante debe contener hasta 20 dígitos.");
605
+ }
606
+ return normalized;
607
+ }
608
+ function normalizeReceiptNumberForComparison(value) {
609
+ const normalized = normalizeText(value);
610
+ if (/^\d{1,5}-\d{1,8}$/.test(normalized)) {
611
+ const { pointOfSale, number } = normalizeReceiptNumber(normalized);
612
+ return `${pointOfSale}${number}`;
613
+ }
614
+ return normalized.replace(/\D/g, "");
615
+ }
616
+ function readRequiredOption(value, optionName) {
617
+ if (!value || value.trim().length === 0) {
618
+ throw new Error(`${optionName} is required.`);
619
+ }
620
+ return value.trim();
621
+ }
622
+ async function waitForCondition(predicate, options = {}) {
623
+ const timeoutMs = options.timeoutMs ?? 5_000;
624
+ const intervalMs = options.intervalMs ?? 100;
625
+ const deadline = Date.now() + timeoutMs;
626
+ while (Date.now() < deadline) {
627
+ if (await predicate()) {
628
+ return;
629
+ }
630
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
631
+ }
632
+ throw new Error("Timed out waiting for ARCA page state.");
633
+ }
634
+ async function expectVisible(page, selector) {
635
+ await page.locator(selector).waitFor({ state: "visible" });
636
+ }
@@ -0,0 +1,10 @@
1
+ import type { Page } from "playwright";
2
+ import type { DataGroup, EmployeeSummary } from "../types.js";
3
+ import { type SiradigRequestClient } from "./http.js";
4
+ export declare function readEmployees(page: Page, request?: SiradigRequestClient): Promise<EmployeeSummary[]>;
5
+ export declare function parseEmployeesJson(data: unknown): EmployeeSummary[] | null;
6
+ export declare function openEmployeesMenu(page: Page): Promise<void>;
7
+ export declare function listEmployees(page: Page): Promise<EmployeeSummary[]>;
8
+ export declare function openEmployeeDetails(page: Page, employeeId: string): Promise<void>;
9
+ export declare function extractEmployeeData(page: Page): Promise<DataGroup[]>;
10
+ export declare function findEmployeeByCuit(employees: EmployeeSummary[], employeeCuit: string): EmployeeSummary | null;