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.
- package/LICENSE +15 -0
- package/README.md +311 -0
- package/dist/src/arca/deductions.d.ts +71 -0
- package/dist/src/arca/deductions.js +636 -0
- package/dist/src/arca/employees.d.ts +10 -0
- package/dist/src/arca/employees.js +80 -0
- package/dist/src/arca/form.d.ts +39 -0
- package/dist/src/arca/form.js +143 -0
- package/dist/src/arca/http.d.ts +14 -0
- package/dist/src/arca/http.js +108 -0
- package/dist/src/arca/menu.d.ts +6 -0
- package/dist/src/arca/menu.js +36 -0
- package/dist/src/arca/personal-data.d.ts +4 -0
- package/dist/src/arca/personal-data.js +62 -0
- package/dist/src/arca/persons.d.ts +11 -0
- package/dist/src/arca/persons.js +115 -0
- package/dist/src/arca/receipt-types.d.ts +4 -0
- package/dist/src/arca/receipt-types.js +39 -0
- package/dist/src/arca/session.d.ts +27 -0
- package/dist/src/arca/session.js +165 -0
- package/dist/src/arca/submission.d.ts +22 -0
- package/dist/src/arca/submission.js +140 -0
- package/dist/src/arca/submitted-forms.d.ts +12 -0
- package/dist/src/arca/submitted-forms.js +54 -0
- package/dist/src/cli/errors.d.ts +1 -0
- package/dist/src/cli/errors.js +11 -0
- package/dist/src/cli/help.d.ts +6 -0
- package/dist/src/cli/help.js +301 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +810 -0
- package/dist/src/cli/options.d.ts +120 -0
- package/dist/src/cli/options.js +419 -0
- package/dist/src/cli/prompts.d.ts +9 -0
- package/dist/src/cli/prompts.js +99 -0
- package/dist/src/cli/render.d.ts +13 -0
- package/dist/src/cli/render.js +262 -0
- package/dist/src/cli/structured-output.d.ts +4 -0
- package/dist/src/cli/structured-output.js +92 -0
- package/dist/src/files/pdf.d.ts +3 -0
- package/dist/src/files/pdf.js +43 -0
- package/dist/src/files/private.d.ts +4 -0
- package/dist/src/files/private.js +21 -0
- package/dist/src/security/errors.d.ts +2 -0
- package/dist/src/security/errors.js +22 -0
- package/dist/src/session/store.d.ts +15 -0
- package/dist/src/session/store.js +35 -0
- package/dist/src/types.d.ts +106 -0
- package/dist/src/types.js +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
export function printFormSummary(summary) {
|
|
2
|
+
console.log(renderFormSummary(summary, {
|
|
3
|
+
color: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
|
|
4
|
+
width: process.stdout.columns,
|
|
5
|
+
}));
|
|
6
|
+
}
|
|
7
|
+
export function renderFormSummary(summary, options = {}) {
|
|
8
|
+
const width = Math.max(36, Math.min(options.width ?? 90, 110));
|
|
9
|
+
const lines = [];
|
|
10
|
+
const styled = (text, code) => options.color ? `\u001b[${code}m${text}\u001b[0m` : text;
|
|
11
|
+
const append = (text, indent = 0, style) => {
|
|
12
|
+
for (const line of wrapText(text, width - indent)) {
|
|
13
|
+
lines.push(`${" ".repeat(indent)}${style ? styled(line, style) : line}`);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const amountRow = (label, amount, indent = 0, style) => {
|
|
17
|
+
const money = `$ ${amount}`;
|
|
18
|
+
const labelWidth = Math.max(10, width - indent - money.length - 2);
|
|
19
|
+
const wrapped = wrapText(label, labelWidth);
|
|
20
|
+
for (const line of wrapped.slice(0, -1))
|
|
21
|
+
append(line, indent, style);
|
|
22
|
+
const last = wrapped.at(-1) ?? "";
|
|
23
|
+
lines.push(`${" ".repeat(indent)}${style ? styled(last.padEnd(labelWidth), style) : last.padEnd(labelWidth)} ${styled(money, style ?? "38;5;151")}`);
|
|
24
|
+
};
|
|
25
|
+
lines.push("");
|
|
26
|
+
append(`F572 WEB · ${summary.period}`, 0, "1");
|
|
27
|
+
append(`Vista previa · Borrador${summary.submissionType ? ` · ${summary.submissionType}` : ""}`, 0, "90");
|
|
28
|
+
lines.push("─".repeat(width));
|
|
29
|
+
append(summary.person.name, 0, "1");
|
|
30
|
+
append(`CUIT ${summary.person.id}`, 0, "90");
|
|
31
|
+
if (summary.withholdingAgent) {
|
|
32
|
+
lines.push("");
|
|
33
|
+
append("Agente de retención", 0, "90");
|
|
34
|
+
append(summary.withholdingAgent.name);
|
|
35
|
+
append(`CUIT ${summary.withholdingAgent.id}`, 0, "90");
|
|
36
|
+
}
|
|
37
|
+
lines.push("");
|
|
38
|
+
append("TOTALES INFORMADOS POR ARCA", 0, "1");
|
|
39
|
+
for (const total of summary.totals)
|
|
40
|
+
amountRow(total.label, total.amount);
|
|
41
|
+
const emptySections = [];
|
|
42
|
+
for (const section of summary.sections) {
|
|
43
|
+
const title = shortFormSectionTitle(section.title);
|
|
44
|
+
if (!section.hasData) {
|
|
45
|
+
emptySections.push(title);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
lines.push("");
|
|
49
|
+
append(title.toUpperCase(), 0, "1");
|
|
50
|
+
lines.push(styled("─".repeat(width), "90"));
|
|
51
|
+
for (const table of section.tables) {
|
|
52
|
+
const isMoneyTable = table.some((row) => /Subtotal\s*:|Importe Total\s*:|\$/i.test(row[1] ?? ""));
|
|
53
|
+
if (!isMoneyTable) {
|
|
54
|
+
const headers = table[0] ?? [];
|
|
55
|
+
for (const row of table.slice(1)) {
|
|
56
|
+
for (const [index, value] of row.entries()) {
|
|
57
|
+
if (value)
|
|
58
|
+
append(headers[index] ? `${headers[index]}: ${value}` : value, 2);
|
|
59
|
+
}
|
|
60
|
+
lines.push("");
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (const row of table) {
|
|
65
|
+
if (/Importe Total\s*:/i.test(row[1] ?? ""))
|
|
66
|
+
continue;
|
|
67
|
+
if (/Subtotal\s*:/i.test(row[1] ?? "")) {
|
|
68
|
+
const provider = row[0].match(/^(\d{11})\s*-\s*(.+)$/);
|
|
69
|
+
lines.push("");
|
|
70
|
+
if (provider) {
|
|
71
|
+
append(provider[2], 2);
|
|
72
|
+
amountRow(`Subtotal · CUIT ${provider[1]}`, row[2], 2, "90");
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
amountRow(row[0], row[2], 0, "1");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (row[1]?.includes("$") && row.length >= 3) {
|
|
79
|
+
const label = row.length >= 5 ? `${row[0]} (${row[1].replace(/x/i, "×")} ${row[2]})` : row[0];
|
|
80
|
+
amountRow(label, row.at(-1) ?? "", 4);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
append(row.filter(Boolean).join(" · "), 2);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (emptySections.length) {
|
|
89
|
+
lines.push("");
|
|
90
|
+
append("Sin datos informados", 0, "90");
|
|
91
|
+
append(emptySections.join(" · "), 2, "90");
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
function shortFormSectionTitle(title) {
|
|
96
|
+
if (/^1\s*-/.test(title))
|
|
97
|
+
return "Cargas de familia";
|
|
98
|
+
if (/^2\s*-/.test(title))
|
|
99
|
+
return "Otros empleadores";
|
|
100
|
+
if (/^3\s*-/.test(title))
|
|
101
|
+
return "Deducciones y desgravaciones";
|
|
102
|
+
if (/^4\s*-/.test(title))
|
|
103
|
+
return "Retenciones, percepciones y pagos a cuenta";
|
|
104
|
+
if (/^5\s*-/.test(title))
|
|
105
|
+
return "Beneficios";
|
|
106
|
+
return title;
|
|
107
|
+
}
|
|
108
|
+
function wrapText(text, width) {
|
|
109
|
+
const lines = [];
|
|
110
|
+
let line = "";
|
|
111
|
+
for (const word of text.split(/\s+/).filter(Boolean)) {
|
|
112
|
+
if (line && line.length + word.length + 1 > width) {
|
|
113
|
+
lines.push(line);
|
|
114
|
+
line = "";
|
|
115
|
+
}
|
|
116
|
+
if (word.length > width) {
|
|
117
|
+
if (line)
|
|
118
|
+
lines.push(line);
|
|
119
|
+
for (let index = 0; index < word.length - width; index += width)
|
|
120
|
+
lines.push(word.slice(index, index + width));
|
|
121
|
+
line = word.slice(Math.floor((word.length - 1) / width) * width);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
line += `${line ? " " : ""}${word}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (line || !lines.length)
|
|
128
|
+
lines.push(line);
|
|
129
|
+
return lines;
|
|
130
|
+
}
|
|
131
|
+
export function printPersonalData(groups) {
|
|
132
|
+
printDataGroups("Datos Personales", groups, "No se encontraron datos personales para mostrar.");
|
|
133
|
+
}
|
|
134
|
+
export function printEmployees(employees) {
|
|
135
|
+
console.log("\nEmpleadores");
|
|
136
|
+
if (employees.length === 0) {
|
|
137
|
+
console.log("No se encontraron empleadores para mostrar.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
employees.forEach((employee, index) => {
|
|
141
|
+
if (index > 0) {
|
|
142
|
+
console.log("");
|
|
143
|
+
}
|
|
144
|
+
console.log(`CUIT: ${employee.cuit}`);
|
|
145
|
+
console.log(`Razón Social / Apellido y Nombre: ${employee.name}`);
|
|
146
|
+
console.log(`Fecha Inicio: ${employee.startDate ?? ""}`);
|
|
147
|
+
console.log(`Fecha Fin: ${employee.endDate ?? ""}`);
|
|
148
|
+
console.log(`Agente de Retención: ${employee.withholdingAgent ?? ""}`);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
export function printEmployeeData(groups) {
|
|
152
|
+
printDataGroups("Empleador", groups, "No se encontraron datos del empleador para mostrar.", {
|
|
153
|
+
skipFirstGroupTitleIfMatchesHeading: true,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
export function printDeductions(deductions) {
|
|
157
|
+
console.log("\nDeducciones y desgravaciones");
|
|
158
|
+
if (deductions.length === 0) {
|
|
159
|
+
console.log("No se encontraron deducciones para mostrar.");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
deductions.forEach((deduction, index) => {
|
|
163
|
+
if (index > 0) {
|
|
164
|
+
console.log("");
|
|
165
|
+
}
|
|
166
|
+
console.log(`Type: ${deduction.type}`);
|
|
167
|
+
console.log(`ID: ${deduction.id}`);
|
|
168
|
+
console.log(`CUIT: ${deduction.cuit}`);
|
|
169
|
+
console.log(`Denominación: ${deduction.name}`);
|
|
170
|
+
console.log(`Período: ${deduction.period}`);
|
|
171
|
+
console.log(`Importe: ${deduction.amount}`);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export function printReceiptTypes(receiptTypes) {
|
|
175
|
+
console.log("ID Tipo");
|
|
176
|
+
for (const receiptType of receiptTypes) {
|
|
177
|
+
console.log(`${receiptType.id.padEnd(7)} ${receiptType.name}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export function printDeductionDetail(detail) {
|
|
181
|
+
console.log(`\n${detail.heading}`);
|
|
182
|
+
console.log(`CUIT: ${detail.cuit}`);
|
|
183
|
+
console.log(`Razón Social: ${detail.name}`);
|
|
184
|
+
console.log("");
|
|
185
|
+
if (isSinglePeriodDeduction(detail)) {
|
|
186
|
+
printSectionTitle("Período y Monto");
|
|
187
|
+
console.log(`Período: ${detail.fromMonthLabel}`);
|
|
188
|
+
console.log(`Monto Deducible: ${detail.amount}`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
printSectionTitle("Período");
|
|
192
|
+
console.log(`Mes Desde: ${detail.fromMonthLabel}`);
|
|
193
|
+
console.log(`Mes Hasta: ${detail.toMonthLabel}`);
|
|
194
|
+
console.log("");
|
|
195
|
+
printSectionTitle("Monto");
|
|
196
|
+
console.log(`Monto Total: ${detail.amount}`);
|
|
197
|
+
}
|
|
198
|
+
if (detail.monthlyDetail.length > 0) {
|
|
199
|
+
console.log("");
|
|
200
|
+
printSectionTitle("Detalle Mensual");
|
|
201
|
+
for (const monthlyDetail of detail.monthlyDetail) {
|
|
202
|
+
console.log(`Mes: ${formatMonthName(monthlyDetail.month)}`);
|
|
203
|
+
console.log(`Monto Mensual: ${monthlyDetail.monthlyAmount}`);
|
|
204
|
+
console.log(`Monto Comprobantes Ingresados: ${monthlyDetail.receiptsTotalAmount}`);
|
|
205
|
+
console.log("");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (detail.receipts.length > 0) {
|
|
209
|
+
if (detail.monthlyDetail.length === 0) {
|
|
210
|
+
console.log("");
|
|
211
|
+
}
|
|
212
|
+
printSectionTitle("Comprobantes");
|
|
213
|
+
for (const receipt of detail.receipts) {
|
|
214
|
+
console.log(`Mes: ${formatMonthName(receipt.month)}`);
|
|
215
|
+
console.log(`Fecha: ${receipt.date}`);
|
|
216
|
+
console.log(`Tipo: ${receipt.type}`);
|
|
217
|
+
console.log(`ID: ${receipt.id}`);
|
|
218
|
+
console.log(`Monto: ${receipt.amount}`);
|
|
219
|
+
console.log("");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function printSectionTitle(title) {
|
|
224
|
+
console.log(title);
|
|
225
|
+
console.log("-".repeat(title.length));
|
|
226
|
+
}
|
|
227
|
+
function isSinglePeriodDeduction(detail) {
|
|
228
|
+
return detail.monthlyDetail.length === 0 && detail.fromMonth > 0 && detail.toMonth === 0;
|
|
229
|
+
}
|
|
230
|
+
function printDataGroups(title, groups, emptyMessage, options = {}) {
|
|
231
|
+
console.log(`\n${title}`);
|
|
232
|
+
if (groups.length === 0) {
|
|
233
|
+
console.log(emptyMessage);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
groups.forEach((group, index) => {
|
|
237
|
+
const shouldSkipGroupTitle = options.skipFirstGroupTitleIfMatchesHeading && index === 0 && group.title === title;
|
|
238
|
+
if (!shouldSkipGroupTitle) {
|
|
239
|
+
console.log(`\n${group.title}`);
|
|
240
|
+
}
|
|
241
|
+
for (const field of group.fields) {
|
|
242
|
+
console.log(`- ${field.label}: ${field.value}`);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function formatMonthName(month) {
|
|
247
|
+
const monthNames = [
|
|
248
|
+
"Enero",
|
|
249
|
+
"Febrero",
|
|
250
|
+
"Marzo",
|
|
251
|
+
"Abril",
|
|
252
|
+
"Mayo",
|
|
253
|
+
"Junio",
|
|
254
|
+
"Julio",
|
|
255
|
+
"Agosto",
|
|
256
|
+
"Septiembre",
|
|
257
|
+
"Octubre",
|
|
258
|
+
"Noviembre",
|
|
259
|
+
"Diciembre",
|
|
260
|
+
];
|
|
261
|
+
return monthNames[month - 1] ?? String(month);
|
|
262
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DataGroup, DeductionDetailView, StructuredDeductionDetail, StructuredEmployeeData, StructuredPersonalData } from "../types.js";
|
|
2
|
+
export declare function toStructuredPersonalData(groups: DataGroup[]): StructuredPersonalData[];
|
|
3
|
+
export declare function toStructuredEmployeeData(groups: DataGroup[]): StructuredEmployeeData[];
|
|
4
|
+
export declare function toStructuredDeductionDetail(detail: DeductionDetailView): StructuredDeductionDetail;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const personalDataFieldMap = {
|
|
2
|
+
Apellido: "lastName",
|
|
3
|
+
Nombre: "firstName",
|
|
4
|
+
Calle: "street",
|
|
5
|
+
"Nro.": "streetNumber",
|
|
6
|
+
Provincia: "province",
|
|
7
|
+
"Código Postal": "zipCode",
|
|
8
|
+
};
|
|
9
|
+
const employeeDataFieldMap = {
|
|
10
|
+
"CUIT del Empleador": "cuit",
|
|
11
|
+
"Razón Social o Apellido y Nombre": "name",
|
|
12
|
+
"Fecha de Inicio de Relación": "startDate",
|
|
13
|
+
"Fecha de Fin de Relación": "endDate",
|
|
14
|
+
"¿Es su agente de retención?": "retentionAgent",
|
|
15
|
+
};
|
|
16
|
+
export function toStructuredPersonalData(groups) {
|
|
17
|
+
const record = {};
|
|
18
|
+
for (const group of groups) {
|
|
19
|
+
for (const field of group.fields) {
|
|
20
|
+
const key = personalDataFieldMap[field.label];
|
|
21
|
+
if (!key || !field.value) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
record[key] = field.value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return Object.keys(record).length > 0 ? [record] : [];
|
|
28
|
+
}
|
|
29
|
+
export function toStructuredEmployeeData(groups) {
|
|
30
|
+
const record = {};
|
|
31
|
+
for (const group of groups) {
|
|
32
|
+
for (const field of group.fields) {
|
|
33
|
+
const key = employeeDataFieldMap[field.label];
|
|
34
|
+
if (!key || !field.value) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (key === "startDate" || key === "endDate") {
|
|
38
|
+
const normalizedDate = normalizeArcaDate(field.value);
|
|
39
|
+
if (normalizedDate) {
|
|
40
|
+
record[key] = normalizedDate;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (key === "retentionAgent") {
|
|
45
|
+
const retentionAgent = normalizeRetentionAgent(field.value);
|
|
46
|
+
if (retentionAgent !== null) {
|
|
47
|
+
record.retentionAgent = retentionAgent;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
record[key] = field.value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Object.keys(record).length > 0 ? [record] : [];
|
|
55
|
+
}
|
|
56
|
+
export function toStructuredDeductionDetail(detail) {
|
|
57
|
+
return {
|
|
58
|
+
cuit: detail.cuit,
|
|
59
|
+
name: detail.name,
|
|
60
|
+
fromMonth: detail.fromMonth,
|
|
61
|
+
toMonth: detail.toMonth,
|
|
62
|
+
amount: detail.amount,
|
|
63
|
+
monthlyDetail: detail.monthlyDetail,
|
|
64
|
+
receipts: detail.receipts.map(toStructuredDeductionReceipt),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function toStructuredDeductionReceipt(receipt) {
|
|
68
|
+
return {
|
|
69
|
+
month: receipt.month,
|
|
70
|
+
date: receipt.date,
|
|
71
|
+
type: receipt.typeId,
|
|
72
|
+
id: receipt.id,
|
|
73
|
+
amount: receipt.amount,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function normalizeArcaDate(value) {
|
|
77
|
+
const match = value.trim().match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
78
|
+
if (!match) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const [, day, month, year] = match;
|
|
82
|
+
return `${year}-${month}-${day}`;
|
|
83
|
+
}
|
|
84
|
+
function normalizeRetentionAgent(value) {
|
|
85
|
+
if (value === "Sí" || value === "Si") {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (value === "No") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function resolvePdfDestination(path: string, defaultFilename: string): Promise<string>;
|
|
2
|
+
/** Uses exclusive creation so simultaneous downloads never overwrite one another. */
|
|
3
|
+
export declare function savePdfWithUniqueName(body: Buffer, destination: string): Promise<string>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
|
+
export async function resolvePdfDestination(path, defaultFilename) {
|
|
4
|
+
if (!path.trim() || path.includes("\0")) {
|
|
5
|
+
throw new Error("--path debe indicar una carpeta o un nombre de archivo .pdf.");
|
|
6
|
+
}
|
|
7
|
+
const absolutePath = resolve(path);
|
|
8
|
+
const info = await stat(absolutePath).catch((error) => {
|
|
9
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
10
|
+
return null;
|
|
11
|
+
throw error;
|
|
12
|
+
});
|
|
13
|
+
if (info && !info.isDirectory() && /[\\/]$/.test(path)) {
|
|
14
|
+
throw new Error(`La ruta ${absolutePath} es un archivo. Indicá una carpeta o quitá la barra final.`);
|
|
15
|
+
}
|
|
16
|
+
const isDirectory = info?.isDirectory() || /[\\/]$/.test(path) || (!info && !extname(path));
|
|
17
|
+
const destination = isDirectory ? join(absolutePath, defaultFilename) : absolutePath;
|
|
18
|
+
if (extname(destination).toLowerCase() !== ".pdf") {
|
|
19
|
+
throw new Error("--path debe ser una carpeta o un archivo con extensión .pdf, por ejemplo ./f572.pdf.");
|
|
20
|
+
}
|
|
21
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
22
|
+
return destination;
|
|
23
|
+
}
|
|
24
|
+
/** Uses exclusive creation so simultaneous downloads never overwrite one another. */
|
|
25
|
+
export async function savePdfWithUniqueName(body, destination) {
|
|
26
|
+
const extension = extname(destination);
|
|
27
|
+
const stem = basename(destination, extension);
|
|
28
|
+
for (let index = 0;; index += 1) {
|
|
29
|
+
const filename = index === 0 ? `${stem}${extension}` : `${stem} (${index})${extension}`;
|
|
30
|
+
const path = join(dirname(destination), filename);
|
|
31
|
+
try {
|
|
32
|
+
await writeFile(path, body, { flag: "wx", mode: 0o600 });
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (!hasErrorCode(error, "EEXIST"))
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function hasErrorCode(error, code) {
|
|
42
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
43
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function ensurePrivateDirectory(path: string): Promise<void>;
|
|
2
|
+
export declare function writePrivateFile(path: string, body: string | Uint8Array): Promise<void>;
|
|
3
|
+
/** Replaces existing files atomically, including their previous permissions. */
|
|
4
|
+
export declare function atomicWritePrivateFile(path: string, body: string | Uint8Array): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
export async function ensurePrivateDirectory(path) {
|
|
5
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
6
|
+
await chmod(path, 0o700);
|
|
7
|
+
}
|
|
8
|
+
export async function writePrivateFile(path, body) {
|
|
9
|
+
await writeFile(path, body, { flag: "wx", mode: 0o600 });
|
|
10
|
+
}
|
|
11
|
+
/** Replaces existing files atomically, including their previous permissions. */
|
|
12
|
+
export async function atomicWritePrivateFile(path, body) {
|
|
13
|
+
const temporaryPath = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
14
|
+
try {
|
|
15
|
+
await writePrivateFile(temporaryPath, body);
|
|
16
|
+
await rename(temporaryPath, path);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await rm(temporaryPath, { force: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Redacts literal, JSON-escaped, and URL-encoded secrets from error diagnostics. */
|
|
2
|
+
export function safeErrorMessage(error, secrets = []) {
|
|
3
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
4
|
+
const variants = new Set();
|
|
5
|
+
for (const secret of secrets) {
|
|
6
|
+
if (!secret)
|
|
7
|
+
continue;
|
|
8
|
+
variants.add(secret);
|
|
9
|
+
variants.add(JSON.stringify(secret).slice(1, -1));
|
|
10
|
+
// A malformed surrogate must not break error reporting.
|
|
11
|
+
try {
|
|
12
|
+
variants.add(encodeURIComponent(secret));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// The literal and JSON forms still cover browser call logs.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const secret of [...variants].sort((a, b) => b.length - a.length)) {
|
|
19
|
+
message = message.replaceAll(secret, "[REDACTED]");
|
|
20
|
+
}
|
|
21
|
+
return message;
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BrowserContextOptions } from "playwright";
|
|
2
|
+
import type { SelectedPerson } from "../types.js";
|
|
3
|
+
export type StoredSession = {
|
|
4
|
+
cuit: string;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
selectedPerson?: SelectedPerson | null;
|
|
7
|
+
storageState: NonNullable<BrowserContextOptions["storageState"]>;
|
|
8
|
+
};
|
|
9
|
+
export type SessionStore = {
|
|
10
|
+
read: () => Promise<StoredSession | null>;
|
|
11
|
+
write: (session: StoredSession) => Promise<void>;
|
|
12
|
+
clear: () => Promise<void>;
|
|
13
|
+
path: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function createSessionStore(baseDir?: string): SessionStore;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { chmod, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { atomicWritePrivateFile, ensurePrivateDirectory } from "../files/private.js";
|
|
5
|
+
export function createSessionStore(baseDir = join(homedir(), ".arca-siradig")) {
|
|
6
|
+
const path = join(baseDir, "session.json");
|
|
7
|
+
return {
|
|
8
|
+
path,
|
|
9
|
+
read: async () => {
|
|
10
|
+
try {
|
|
11
|
+
await chmod(baseDir, 0o700);
|
|
12
|
+
await chmod(path, 0o600);
|
|
13
|
+
const content = await readFile(path, "utf8");
|
|
14
|
+
return JSON.parse(content);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (isMissingFileError(error)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
write: async (session) => {
|
|
24
|
+
await ensurePrivateDirectory(baseDir);
|
|
25
|
+
const { cuit, createdAt, selectedPerson, storageState } = session;
|
|
26
|
+
await atomicWritePrivateFile(path, `${JSON.stringify({ cuit, createdAt, selectedPerson, storageState }, null, 2)}\n`);
|
|
27
|
+
},
|
|
28
|
+
clear: async () => {
|
|
29
|
+
await rm(path, { force: true });
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function isMissingFileError(error) {
|
|
34
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
35
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export type ArcaCredentials = {
|
|
2
|
+
cuit: string;
|
|
3
|
+
password: string;
|
|
4
|
+
};
|
|
5
|
+
export type ArcaSession = {
|
|
6
|
+
close: () => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export type Person = {
|
|
9
|
+
id: string | null;
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
export type SelectedPerson = Person;
|
|
13
|
+
export type MenuItem = {
|
|
14
|
+
label: string;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type ReminderModalInfo = {
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
export type DataField = {
|
|
22
|
+
label: string;
|
|
23
|
+
value: string;
|
|
24
|
+
};
|
|
25
|
+
export type DataGroup = {
|
|
26
|
+
title: string;
|
|
27
|
+
fields: DataField[];
|
|
28
|
+
};
|
|
29
|
+
export type PersonalDataField = DataField;
|
|
30
|
+
export type PersonalDataGroup = DataGroup;
|
|
31
|
+
export type EmployeeSummary = {
|
|
32
|
+
id: string;
|
|
33
|
+
cuit: string;
|
|
34
|
+
name: string;
|
|
35
|
+
startDate: string | null;
|
|
36
|
+
endDate: string | null;
|
|
37
|
+
withholdingAgent: string | null;
|
|
38
|
+
};
|
|
39
|
+
export type StructuredPersonalData = {
|
|
40
|
+
lastName?: string;
|
|
41
|
+
firstName?: string;
|
|
42
|
+
street?: string;
|
|
43
|
+
streetNumber?: string;
|
|
44
|
+
province?: string;
|
|
45
|
+
zipCode?: string;
|
|
46
|
+
};
|
|
47
|
+
export type StructuredEmployeeData = {
|
|
48
|
+
cuit?: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
startDate?: string;
|
|
51
|
+
endDate?: string;
|
|
52
|
+
retentionAgent?: boolean;
|
|
53
|
+
};
|
|
54
|
+
export type ReceiptType = {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
};
|
|
58
|
+
export type DeductionSummary = {
|
|
59
|
+
type: string;
|
|
60
|
+
id: string;
|
|
61
|
+
cuit: string;
|
|
62
|
+
name: string;
|
|
63
|
+
period: string;
|
|
64
|
+
amount: string;
|
|
65
|
+
};
|
|
66
|
+
export type DeductionMonthlyDetail = {
|
|
67
|
+
month: number;
|
|
68
|
+
monthlyAmount: string;
|
|
69
|
+
receiptsTotalAmount: string;
|
|
70
|
+
};
|
|
71
|
+
export type DeductionReceipt = {
|
|
72
|
+
month: number;
|
|
73
|
+
date: string;
|
|
74
|
+
typeId: string;
|
|
75
|
+
type: string;
|
|
76
|
+
id: string;
|
|
77
|
+
amount: string;
|
|
78
|
+
};
|
|
79
|
+
export type StructuredDeductionReceipt = {
|
|
80
|
+
month: number;
|
|
81
|
+
date: string;
|
|
82
|
+
type: string;
|
|
83
|
+
id: string;
|
|
84
|
+
amount: string;
|
|
85
|
+
};
|
|
86
|
+
export type DeductionDetailView = {
|
|
87
|
+
heading: string;
|
|
88
|
+
cuit: string;
|
|
89
|
+
name: string;
|
|
90
|
+
fromMonth: number;
|
|
91
|
+
fromMonthLabel: string;
|
|
92
|
+
toMonth: number;
|
|
93
|
+
toMonthLabel: string;
|
|
94
|
+
amount: string;
|
|
95
|
+
monthlyDetail: DeductionMonthlyDetail[];
|
|
96
|
+
receipts: DeductionReceipt[];
|
|
97
|
+
};
|
|
98
|
+
export type StructuredDeductionDetail = {
|
|
99
|
+
cuit: string;
|
|
100
|
+
name: string;
|
|
101
|
+
fromMonth: number;
|
|
102
|
+
toMonth: number;
|
|
103
|
+
amount: string;
|
|
104
|
+
monthlyDetail: DeductionMonthlyDetail[];
|
|
105
|
+
receipts: StructuredDeductionReceipt[];
|
|
106
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "arca-siradig",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to inspect, update, print and submit ARCA SiRADIG Form 572 drafts.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/jpgarcia/arca-siradig.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/jpgarcia/arca-siradig#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/jpgarcia/arca-siradig/issues"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"arca-siradig": "./dist/src/cli/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/src/**/*.js",
|
|
23
|
+
"dist/src/**/*.d.ts",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": "^20.17.0 || ^22.13.0 || >=23.5.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"arca-siradig": "tsx src/cli/index.ts",
|
|
32
|
+
"build": "node scripts/build.mjs",
|
|
33
|
+
"prepack": "npm run build",
|
|
34
|
+
"check": "biome check .",
|
|
35
|
+
"format": "biome format --write .",
|
|
36
|
+
"test": "playwright test"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"arca",
|
|
40
|
+
"afip",
|
|
41
|
+
"siradig",
|
|
42
|
+
"cli",
|
|
43
|
+
"playwright"
|
|
44
|
+
],
|
|
45
|
+
"author": "",
|
|
46
|
+
"license": "ISC",
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@biomejs/biome": "^2.5.0",
|
|
49
|
+
"@playwright/test": "^1.61.0",
|
|
50
|
+
"@types/node": "^25.9.3",
|
|
51
|
+
"tsx": "^4.22.4",
|
|
52
|
+
"typescript": "^6.0.3"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@inquirer/prompts": "^8.5.2",
|
|
56
|
+
"dotenv": "^17.4.2",
|
|
57
|
+
"playwright": "^1.61.0"
|
|
58
|
+
}
|
|
59
|
+
}
|