@openfactura/cli 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +395 -2
- package/dist/index.js +2137 -0
- package/package.json +51 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,2137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "util";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "@openfactura/cli",
|
|
9
|
+
version: "0.2.0",
|
|
10
|
+
description: "Cliente de l\xEDnea de comandos y servidor MCP (stdio) de Open Factura: facturaci\xF3n electr\xF3nica de El Salvador (DTE).",
|
|
11
|
+
license: "MIT",
|
|
12
|
+
repository: {
|
|
13
|
+
type: "git",
|
|
14
|
+
url: "git+https://github.com/xaviigna/openfactura.git",
|
|
15
|
+
directory: "packages/cli"
|
|
16
|
+
},
|
|
17
|
+
homepage: "https://github.com/xaviigna/openfactura/tree/main/packages/cli#readme",
|
|
18
|
+
bugs: "https://github.com/xaviigna/openfactura/issues",
|
|
19
|
+
type: "module",
|
|
20
|
+
bin: {
|
|
21
|
+
openfactura: "dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
files: [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
engines: {
|
|
29
|
+
node: ">=20"
|
|
30
|
+
},
|
|
31
|
+
publishConfig: {
|
|
32
|
+
access: "public"
|
|
33
|
+
},
|
|
34
|
+
keywords: [
|
|
35
|
+
"dte",
|
|
36
|
+
"el-salvador",
|
|
37
|
+
"factura-electronica",
|
|
38
|
+
"hacienda",
|
|
39
|
+
"mcp",
|
|
40
|
+
"cli"
|
|
41
|
+
],
|
|
42
|
+
scripts: {
|
|
43
|
+
build: "tsup",
|
|
44
|
+
dev: "tsx src/index.ts",
|
|
45
|
+
test: "vitest run",
|
|
46
|
+
"type-check": "tsc --noEmit"
|
|
47
|
+
},
|
|
48
|
+
dependencies: {
|
|
49
|
+
"@modelcontextprotocol/server": "^2.4.0",
|
|
50
|
+
zod: "^4.0.0"
|
|
51
|
+
},
|
|
52
|
+
devDependencies: {
|
|
53
|
+
"@types/node": "^22.0.0",
|
|
54
|
+
tsup: "^8.5.0",
|
|
55
|
+
tsx: "^4.20.0",
|
|
56
|
+
typescript: "^5.9.0",
|
|
57
|
+
vitest: "^4.1.0"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/comandos.ts
|
|
62
|
+
import { randomUUID } from "crypto";
|
|
63
|
+
import { readFile, writeFile } from "fs/promises";
|
|
64
|
+
import { basename, resolve } from "path";
|
|
65
|
+
|
|
66
|
+
// src/config.ts
|
|
67
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
68
|
+
import { homedir } from "os";
|
|
69
|
+
import { dirname, isAbsolute, join } from "path";
|
|
70
|
+
|
|
71
|
+
// src/errores.ts
|
|
72
|
+
var SALIDA = { ok: 0, error: 1, uso: 2, noAutenticado: 3, limite: 4 };
|
|
73
|
+
var ErrorUso = class extends Error {
|
|
74
|
+
};
|
|
75
|
+
var ErrorCli = class extends Error {
|
|
76
|
+
constructor(message, code = "error", salida = SALIDA.error) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.code = code;
|
|
79
|
+
this.salida = salida;
|
|
80
|
+
}
|
|
81
|
+
code;
|
|
82
|
+
salida;
|
|
83
|
+
get cuerpo() {
|
|
84
|
+
return { error: { code: this.code, message: this.message } };
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var ErrorApi = class extends Error {
|
|
88
|
+
constructor(status, cuerpo, retryAfter = null) {
|
|
89
|
+
super(cuerpo.error.message);
|
|
90
|
+
this.status = status;
|
|
91
|
+
this.cuerpo = cuerpo;
|
|
92
|
+
this.retryAfter = retryAfter;
|
|
93
|
+
}
|
|
94
|
+
status;
|
|
95
|
+
cuerpo;
|
|
96
|
+
retryAfter;
|
|
97
|
+
get code() {
|
|
98
|
+
return this.cuerpo.error.code;
|
|
99
|
+
}
|
|
100
|
+
get details() {
|
|
101
|
+
return this.cuerpo.error.details;
|
|
102
|
+
}
|
|
103
|
+
get salida() {
|
|
104
|
+
if (this.status === 401) return SALIDA.noAutenticado;
|
|
105
|
+
if (this.status === 429) return SALIDA.limite;
|
|
106
|
+
return SALIDA.error;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/config.ts
|
|
111
|
+
var URL_POR_DEFECTO = "http://localhost:8788";
|
|
112
|
+
function rutaConfig(env) {
|
|
113
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
114
|
+
const base = xdg && isAbsolute(xdg) ? xdg : join(env.HOME || homedir(), ".config");
|
|
115
|
+
return join(base, "openfactura", "config.json");
|
|
116
|
+
}
|
|
117
|
+
function leerConfig(env) {
|
|
118
|
+
const ruta2 = rutaConfig(env);
|
|
119
|
+
let texto2;
|
|
120
|
+
try {
|
|
121
|
+
texto2 = readFileSync(ruta2, "utf8");
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error.code === "ENOENT") return { perfiles: {} };
|
|
124
|
+
throw new ErrorCli(`No se pudo leer ${ruta2}: ${error.message}`, "config_ilegible");
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const datos = JSON.parse(texto2);
|
|
128
|
+
const perfiles = datos && typeof datos.perfiles === "object" && datos.perfiles ? datos.perfiles : {};
|
|
129
|
+
return { perfiles: { ...perfiles } };
|
|
130
|
+
} catch {
|
|
131
|
+
throw new ErrorCli(`${ruta2} no es un JSON v\xE1lido. Corr\xEDjalo o b\xF3rrelo.`, "config_ilegible");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function guardarConfig(env, config) {
|
|
135
|
+
const ruta2 = rutaConfig(env);
|
|
136
|
+
const carpeta = dirname(ruta2);
|
|
137
|
+
mkdirSync(carpeta, { recursive: true, mode: 448 });
|
|
138
|
+
chmodSync(carpeta, 448);
|
|
139
|
+
const temporal = `${ruta2}.${process.pid}.tmp`;
|
|
140
|
+
rmSync(temporal, { force: true });
|
|
141
|
+
writeFileSync(temporal, `${JSON.stringify(config, null, 2)}
|
|
142
|
+
`, { mode: 384, flag: "wx" });
|
|
143
|
+
chmodSync(temporal, 384);
|
|
144
|
+
renameSync(temporal, ruta2);
|
|
145
|
+
return ruta2;
|
|
146
|
+
}
|
|
147
|
+
function normalizarUrl(valor) {
|
|
148
|
+
let url;
|
|
149
|
+
try {
|
|
150
|
+
url = new URL(valor.trim());
|
|
151
|
+
} catch {
|
|
152
|
+
throw new ErrorUso(`URL del servidor inv\xE1lida: ${valor}`);
|
|
153
|
+
}
|
|
154
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
155
|
+
throw new ErrorUso(`La URL del servidor debe empezar con http:// o https://: ${valor}`);
|
|
156
|
+
}
|
|
157
|
+
if (url.username || url.password) {
|
|
158
|
+
throw new ErrorUso("La URL del servidor no debe llevar usuario ni contrase\xF1a");
|
|
159
|
+
}
|
|
160
|
+
const ruta2 = url.pathname.replace(/\/+$/, "").replace(/\/api\/v1$/, "");
|
|
161
|
+
return `${url.origin}${ruta2}`;
|
|
162
|
+
}
|
|
163
|
+
function nombrePerfil(env, opcion) {
|
|
164
|
+
const nombre = opcion || env.OPENFACTURA_PROFILE || "default";
|
|
165
|
+
if (!/^[\w.-]{1,64}$/.test(nombre) || nombre === "__proto__") {
|
|
166
|
+
throw new ErrorUso(`Nombre de perfil inv\xE1lido: ${nombre}`);
|
|
167
|
+
}
|
|
168
|
+
return nombre;
|
|
169
|
+
}
|
|
170
|
+
function resolver(env, opciones = {}) {
|
|
171
|
+
const perfil = nombrePerfil(env, opciones.perfil);
|
|
172
|
+
const guardado = leerConfig(env).perfiles[perfil] ?? {};
|
|
173
|
+
let url = URL_POR_DEFECTO;
|
|
174
|
+
let origenUrl = "defecto";
|
|
175
|
+
if (opciones.url) {
|
|
176
|
+
url = opciones.url;
|
|
177
|
+
origenUrl = "opci\xF3n";
|
|
178
|
+
} else if (env.OPENFACTURA_URL) {
|
|
179
|
+
url = env.OPENFACTURA_URL;
|
|
180
|
+
origenUrl = "entorno";
|
|
181
|
+
} else if (guardado.url) {
|
|
182
|
+
url = guardado.url;
|
|
183
|
+
origenUrl = "config";
|
|
184
|
+
}
|
|
185
|
+
const deEntorno = env.OPENFACTURA_API_KEY?.trim();
|
|
186
|
+
const apiKey = deEntorno || guardado.apiKey || null;
|
|
187
|
+
return {
|
|
188
|
+
perfil,
|
|
189
|
+
url: normalizarUrl(url),
|
|
190
|
+
origenUrl,
|
|
191
|
+
apiKey,
|
|
192
|
+
origenClave: deEntorno ? "entorno" : apiKey ? "config" : null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function enmascarar(clave) {
|
|
196
|
+
return `${clave.slice(0, 14)}\u2026`;
|
|
197
|
+
}
|
|
198
|
+
var esClaveValida = (clave) => /^of_(test|live)_\S+$/.test(clave);
|
|
199
|
+
var esClaveDeProduccion = (clave) => clave.startsWith("of_live_");
|
|
200
|
+
|
|
201
|
+
// src/esquemas.ts
|
|
202
|
+
import { z } from "zod";
|
|
203
|
+
var TIPOS_DTE = {
|
|
204
|
+
"01": "Factura",
|
|
205
|
+
"03": "Comprobante de Cr\xE9dito Fiscal",
|
|
206
|
+
"04": "Nota de Remisi\xF3n",
|
|
207
|
+
"05": "Nota de Cr\xE9dito",
|
|
208
|
+
"06": "Nota de D\xE9bito",
|
|
209
|
+
"07": "Comprobante de Retenci\xF3n",
|
|
210
|
+
"11": "Factura de Exportaci\xF3n",
|
|
211
|
+
"14": "Factura de Sujeto Excluido"
|
|
212
|
+
};
|
|
213
|
+
var ESTADOS_DTE = [
|
|
214
|
+
"pendiente",
|
|
215
|
+
"procesado",
|
|
216
|
+
"rechazado",
|
|
217
|
+
"contingencia",
|
|
218
|
+
"invalidado"
|
|
219
|
+
];
|
|
220
|
+
var NOMBRE_AMBIENTE = { "00": "Pruebas", "01": "Producci\xF3n" };
|
|
221
|
+
var codigosTipo = Object.keys(TIPOS_DTE);
|
|
222
|
+
var texto = (max = 250) => z.string().trim().min(1).max(max);
|
|
223
|
+
var opcional = (max = 250) => z.string().trim().max(max).nullish().transform((v) => v || null);
|
|
224
|
+
var tipoDte = z.enum(codigosTipo);
|
|
225
|
+
var cliente = z.object({
|
|
226
|
+
tipoDocumento: z.enum(["36", "13", "03", "02", "37"]),
|
|
227
|
+
numDocumento: texto(20),
|
|
228
|
+
nrc: opcional(8),
|
|
229
|
+
nombre: texto(),
|
|
230
|
+
nombreComercial: opcional(150),
|
|
231
|
+
codActividad: opcional(6),
|
|
232
|
+
descActividad: opcional(150),
|
|
233
|
+
departamento: opcional(2),
|
|
234
|
+
municipio: opcional(2),
|
|
235
|
+
distrito: opcional(2),
|
|
236
|
+
complemento: opcional(200),
|
|
237
|
+
telefono: opcional(30),
|
|
238
|
+
correo: z.union([z.email().max(100), z.literal("")]).nullish().transform((v) => v || null)
|
|
239
|
+
});
|
|
240
|
+
var producto = z.object({
|
|
241
|
+
codigo: texto(25),
|
|
242
|
+
descripcion: texto(1e3),
|
|
243
|
+
precioUnitario: z.number().nonnegative().max(99999999999),
|
|
244
|
+
tipoItem: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).default(1),
|
|
245
|
+
uniMedida: z.number().int().min(1).max(99).default(59),
|
|
246
|
+
tipoVenta: z.enum(["gravada", "exenta", "noSujeta"]).default("gravada")
|
|
247
|
+
});
|
|
248
|
+
var persona = z.object({
|
|
249
|
+
nombre: texto(100),
|
|
250
|
+
tipoDocumento: z.string().min(2).max(2),
|
|
251
|
+
numDocumento: texto(20)
|
|
252
|
+
});
|
|
253
|
+
var invalidacion = z.object({
|
|
254
|
+
tipoAnulacion: z.union([z.literal(1), z.literal(2), z.literal(3)]),
|
|
255
|
+
motivoAnulacion: opcional(250),
|
|
256
|
+
codigoGeneracionReemplazo: opcional(36),
|
|
257
|
+
solicitante: persona.optional(),
|
|
258
|
+
responsable: persona.optional()
|
|
259
|
+
});
|
|
260
|
+
var fecha = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use el formato AAAA-MM-DD");
|
|
261
|
+
var retorno = z.object({
|
|
262
|
+
items: z.array(
|
|
263
|
+
z.object({
|
|
264
|
+
descripcion: texto(1e3),
|
|
265
|
+
cantidad: z.number().positive(),
|
|
266
|
+
precioUni: z.number().nonnegative(),
|
|
267
|
+
tipoItem: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).optional(),
|
|
268
|
+
codigo: opcional(25),
|
|
269
|
+
uniMedida: z.number().int().min(1).max(99).optional(),
|
|
270
|
+
descuento: z.number().nonnegative().optional(),
|
|
271
|
+
tipoVenta: z.enum(["gravada", "exenta", "noSujeta"]).optional(),
|
|
272
|
+
seguro: z.number().nonnegative().optional(),
|
|
273
|
+
flete: z.number().nonnegative().optional()
|
|
274
|
+
})
|
|
275
|
+
).min(1).max(2e3).describe(
|
|
276
|
+
"Bienes que se retornan. precioUni: en Factura (01) CON IVA, como en la factura; en Exportaci\xF3n (11) y Sujeto Excluido (14) SIN IVA."
|
|
277
|
+
)
|
|
278
|
+
});
|
|
279
|
+
var operacionesEspeciales = z.object({
|
|
280
|
+
tipoDocumento: z.enum(["02", "97"]).describe("02 factura de venta simplificada (por rangos), 97 comprobante de control interno"),
|
|
281
|
+
fecha: fecha.optional().describe("Fecha del evento; define el periodo informado"),
|
|
282
|
+
items: z.array(
|
|
283
|
+
z.object({
|
|
284
|
+
descripcion: texto(1500),
|
|
285
|
+
precioUni: z.number().nonnegative().describe("SIN IVA"),
|
|
286
|
+
cantidad: z.number().int().positive().optional(),
|
|
287
|
+
numDocumento: opcional(36),
|
|
288
|
+
docDel: z.string().trim().min(1).max(36).optional(),
|
|
289
|
+
docAl: z.string().trim().min(1).max(36).optional(),
|
|
290
|
+
fechaEmision: fecha.optional(),
|
|
291
|
+
tipoVenta: z.enum(["gravada", "exenta", "noSujeta"]).optional(),
|
|
292
|
+
codigoGeneracionRef: z.string().length(36).optional()
|
|
293
|
+
})
|
|
294
|
+
).min(1).max(2e3)
|
|
295
|
+
});
|
|
296
|
+
var EVENTOS_WEBHOOK = [
|
|
297
|
+
"dte.procesado",
|
|
298
|
+
"dte.rechazado",
|
|
299
|
+
"dte.contingencia",
|
|
300
|
+
"dte.invalidado",
|
|
301
|
+
"retorno.procesado",
|
|
302
|
+
"operaciones_especiales.procesado"
|
|
303
|
+
];
|
|
304
|
+
var webhook = z.object({
|
|
305
|
+
url: z.string().max(2e3).describe("URL https:// p\xFAblica que recibir\xE1 los POST firmados"),
|
|
306
|
+
descripcion: opcional(200),
|
|
307
|
+
eventos: z.array(z.enum(EVENTOS_WEBHOOK)).min(1).describe("Eventos a los que se suscribe"),
|
|
308
|
+
activo: z.boolean().optional()
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// src/http.ts
|
|
312
|
+
var TIEMPO_LIMITE_MS = 6e4;
|
|
313
|
+
var ClienteApi = class {
|
|
314
|
+
url;
|
|
315
|
+
opciones;
|
|
316
|
+
constructor(opciones) {
|
|
317
|
+
this.opciones = opciones;
|
|
318
|
+
this.url = opciones.url;
|
|
319
|
+
}
|
|
320
|
+
async pedir(metodo, ruta2, peticion = {}) {
|
|
321
|
+
const respuesta = await this.enviar(metodo, ruta2, peticion);
|
|
322
|
+
const texto2 = await respuesta.text();
|
|
323
|
+
let datos = null;
|
|
324
|
+
if (texto2) {
|
|
325
|
+
try {
|
|
326
|
+
datos = JSON.parse(texto2);
|
|
327
|
+
} catch {
|
|
328
|
+
throw new ErrorCli(
|
|
329
|
+
`El servidor respondi\xF3 algo que no es JSON (HTTP ${respuesta.status}). Revise la URL: ${this.url}`,
|
|
330
|
+
"respuesta_invalida"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { status: respuesta.status, headers: respuesta.headers, datos };
|
|
335
|
+
}
|
|
336
|
+
/** Descarga binaria (PDF). */
|
|
337
|
+
async descargar(ruta2) {
|
|
338
|
+
const respuesta = await this.enviar("GET", ruta2, {});
|
|
339
|
+
return new Uint8Array(await respuesta.arrayBuffer());
|
|
340
|
+
}
|
|
341
|
+
async enviar(metodo, ruta2, { query, cuerpo, headers }) {
|
|
342
|
+
const destino = new URL(`${this.url}/api/v1${ruta2}`);
|
|
343
|
+
for (const [clave, valor] of Object.entries(query ?? {})) {
|
|
344
|
+
if (valor !== void 0 && valor !== null && valor !== "")
|
|
345
|
+
destino.searchParams.set(clave, String(valor));
|
|
346
|
+
}
|
|
347
|
+
const { apiKey, version, timeoutMs = TIEMPO_LIMITE_MS } = this.opciones;
|
|
348
|
+
const hacerFetch = this.opciones.fetch ?? fetch;
|
|
349
|
+
let respuesta;
|
|
350
|
+
try {
|
|
351
|
+
respuesta = await hacerFetch(destino, {
|
|
352
|
+
method: metodo,
|
|
353
|
+
headers: {
|
|
354
|
+
Authorization: `Bearer ${apiKey}`,
|
|
355
|
+
"User-Agent": `openfactura-cli/${version}`,
|
|
356
|
+
Accept: "application/json",
|
|
357
|
+
...cuerpo === void 0 ? {} : { "Content-Type": "application/json" },
|
|
358
|
+
...headers
|
|
359
|
+
},
|
|
360
|
+
body: cuerpo === void 0 ? void 0 : JSON.stringify(cuerpo),
|
|
361
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
362
|
+
});
|
|
363
|
+
} catch (error) {
|
|
364
|
+
const nombre = error?.name;
|
|
365
|
+
if (nombre === "TimeoutError" || nombre === "AbortError") {
|
|
366
|
+
throw new ErrorCli(
|
|
367
|
+
`El servidor no respondi\xF3 en ${Math.round(timeoutMs / 1e3)} s (${this.url})`,
|
|
368
|
+
"tiempo_agotado"
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const causa = error?.cause;
|
|
372
|
+
const detalle = causa?.code || causa?.message || error?.message || "error de red";
|
|
373
|
+
throw new ErrorCli(`No se pudo conectar con ${this.url} (${detalle})`, "sin_conexion");
|
|
374
|
+
}
|
|
375
|
+
if (!respuesta.ok) throw await leerError(respuesta);
|
|
376
|
+
return respuesta;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
async function leerError(respuesta) {
|
|
380
|
+
const texto2 = await respuesta.text().catch(() => "");
|
|
381
|
+
let cuerpo = null;
|
|
382
|
+
try {
|
|
383
|
+
const json = JSON.parse(texto2);
|
|
384
|
+
const e = json?.error;
|
|
385
|
+
if (e && typeof e.code === "string" && typeof e.message === "string") {
|
|
386
|
+
cuerpo = {
|
|
387
|
+
error: {
|
|
388
|
+
code: e.code,
|
|
389
|
+
message: e.message,
|
|
390
|
+
...e.details === void 0 ? {} : { details: e.details }
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
cuerpo ??= {
|
|
397
|
+
error: {
|
|
398
|
+
code: `http_${respuesta.status}`,
|
|
399
|
+
message: `El servidor respondi\xF3 HTTP ${respuesta.status}${respuesta.statusText ? ` ${respuesta.statusText}` : ""}`
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const espera = Number.parseInt(respuesta.headers.get("Retry-After") ?? "", 10);
|
|
403
|
+
return new ErrorApi(respuesta.status, cuerpo, Number.isFinite(espera) ? espera : null);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/mcp.ts
|
|
407
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
408
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
409
|
+
import { z as z2 } from "zod";
|
|
410
|
+
var listaTipos = Object.entries(TIPOS_DTE).map(([codigo, nombre]) => `${codigo} ${nombre}`).join(", ");
|
|
411
|
+
var DATOS_DTE = `Datos del documento. Campos habituales:
|
|
412
|
+
- items: [{ descripcion, cantidad, precioUni, tipoItem?: 1 bien | 2 servicio, codigo?, uniMedida?: 59 unidad, descuento?, tipoVenta?: 'gravada'|'exenta'|'noSujeta' }]. En Factura (01) precioUni INCLUYE IVA; en CCF (03), notas y remisi\xF3n va SIN IVA.
|
|
413
|
+
- receptor: { nit o tipoDocumento+numDocumento, nrc, nombre, codActividad, descActividad, direccion: { departamento, municipio, distrito, complemento }, telefono, correo }. En Factura es opcional bajo USD 25,000. Puede omitirse si env\xEDa clienteId.
|
|
414
|
+
- condicionOperacion?: 1 contado | 2 cr\xE9dito | 3 otro; pagos?; credito?: { plazo, periodo }.
|
|
415
|
+
- documentosRelacionados (notas de cr\xE9dito/d\xE9bito): [{ tipoDocumento, tipoGeneracion: 2, numeroDocumento: <c\xF3digo de generaci\xF3n>, fechaEmision }].
|
|
416
|
+
Los c\xF3digos de cat\xE1logo se consultan con catalogo_buscar.`;
|
|
417
|
+
var entradaEmision = {
|
|
418
|
+
tipoDte: tipoDte.describe(`Tipo de documento: ${listaTipos}`),
|
|
419
|
+
datos: z2.record(z2.string(), z2.unknown()).describe(DATOS_DTE),
|
|
420
|
+
clienteId: z2.string().optional().describe("Id de un cliente guardado para usarlo como receptor"),
|
|
421
|
+
puntoVentaId: z2.string().optional().describe("Punto de venta emisor; por defecto el primero")
|
|
422
|
+
};
|
|
423
|
+
async function responder(accion) {
|
|
424
|
+
try {
|
|
425
|
+
const { datos } = await accion();
|
|
426
|
+
return {
|
|
427
|
+
content: [{ type: "text", text: JSON.stringify(datos, null, 2) }],
|
|
428
|
+
structuredContent: { resultado: datos }
|
|
429
|
+
};
|
|
430
|
+
} catch (error) {
|
|
431
|
+
const cuerpo = error instanceof ErrorApi || error instanceof ErrorCli ? error.cuerpo : { error: { code: "error_interno", message: "Ocurri\xF3 un error inesperado" } };
|
|
432
|
+
return { content: [{ type: "text", text: JSON.stringify(cuerpo, null, 2) }], isError: true };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
var lectura = { readOnlyHint: true, openWorldHint: false };
|
|
436
|
+
var ruta = (segmento2) => encodeURIComponent(segmento2);
|
|
437
|
+
function crearServidorMcp(api, version) {
|
|
438
|
+
const servidor = new McpServer({ name: "openfactura", version });
|
|
439
|
+
servidor.registerTool(
|
|
440
|
+
"emisor_obtener",
|
|
441
|
+
{
|
|
442
|
+
title: "Ver emisor",
|
|
443
|
+
description: "Datos fiscales del emisor, sus establecimientos y puntos de venta, el ambiente activo (00 pruebas, 01 producci\xF3n) y si las credenciales del MH est\xE1n cargadas.",
|
|
444
|
+
inputSchema: z2.object({}),
|
|
445
|
+
annotations: lectura
|
|
446
|
+
},
|
|
447
|
+
() => responder(() => api.pedir("GET", "/emisor"))
|
|
448
|
+
);
|
|
449
|
+
servidor.registerTool(
|
|
450
|
+
"catalogo_buscar",
|
|
451
|
+
{
|
|
452
|
+
title: "Buscar en cat\xE1logos del MH",
|
|
453
|
+
description: "Consulta un cat\xE1logo oficial (CAT-001 \u2026 CAT-033): CAT-012 departamentos, CAT-013 municipios, CAT-008 distritos, CAT-019 actividades econ\xF3micas, CAT-014 unidades de medida, CAT-017 formas de pago, CAT-022 tipos de documento. Sin `id` devuelve la lista de cat\xE1logos.",
|
|
454
|
+
inputSchema: z2.object({
|
|
455
|
+
id: z2.string().optional().describe("Por ejemplo CAT-019"),
|
|
456
|
+
q: z2.string().optional().describe("Texto a buscar en el c\xF3digo o la descripci\xF3n"),
|
|
457
|
+
departamento: z2.string().optional().describe("Filtra municipios o distritos por departamento")
|
|
458
|
+
}),
|
|
459
|
+
annotations: lectura
|
|
460
|
+
},
|
|
461
|
+
({ id, q, departamento }) => responder(
|
|
462
|
+
() => id ? api.pedir("GET", `/catalogos/${ruta(id.toUpperCase())}`, {
|
|
463
|
+
query: { q, departamento, limite: 50 }
|
|
464
|
+
}) : api.pedir("GET", "/catalogos")
|
|
465
|
+
)
|
|
466
|
+
);
|
|
467
|
+
servidor.registerTool(
|
|
468
|
+
"clientes_buscar",
|
|
469
|
+
{
|
|
470
|
+
title: "Buscar clientes",
|
|
471
|
+
description: "Busca clientes guardados por nombre o n\xFAmero de documento.",
|
|
472
|
+
inputSchema: z2.object({ q: z2.string().optional() }),
|
|
473
|
+
annotations: lectura
|
|
474
|
+
},
|
|
475
|
+
({ q }) => responder(() => api.pedir("GET", "/clientes", { query: { q, limite: 25 } }))
|
|
476
|
+
);
|
|
477
|
+
servidor.registerTool(
|
|
478
|
+
"cliente_crear",
|
|
479
|
+
{
|
|
480
|
+
title: "Crear cliente",
|
|
481
|
+
description: "Guarda un cliente para reutilizarlo como receptor. tipoDocumento: 36 NIT, 13 DUI, 03 pasaporte, 02 carn\xE9 de residente, 37 otro.",
|
|
482
|
+
inputSchema: cliente,
|
|
483
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
484
|
+
},
|
|
485
|
+
(datos) => responder(() => api.pedir("POST", "/clientes", { cuerpo: datos }))
|
|
486
|
+
);
|
|
487
|
+
servidor.registerTool(
|
|
488
|
+
"productos_buscar",
|
|
489
|
+
{
|
|
490
|
+
title: "Buscar productos",
|
|
491
|
+
description: "Busca productos y servicios guardados por c\xF3digo o descripci\xF3n. El precio guardado es SIN IVA.",
|
|
492
|
+
inputSchema: z2.object({ q: z2.string().optional() }),
|
|
493
|
+
annotations: lectura
|
|
494
|
+
},
|
|
495
|
+
({ q }) => responder(() => api.pedir("GET", "/productos", { query: { q, limite: 25 } }))
|
|
496
|
+
);
|
|
497
|
+
servidor.registerTool(
|
|
498
|
+
"producto_crear",
|
|
499
|
+
{
|
|
500
|
+
title: "Crear producto",
|
|
501
|
+
description: "Guarda un producto o servicio. precioUnitario va SIN IVA.",
|
|
502
|
+
inputSchema: producto,
|
|
503
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
504
|
+
},
|
|
505
|
+
(datos) => responder(() => api.pedir("POST", "/productos", { cuerpo: datos }))
|
|
506
|
+
);
|
|
507
|
+
servidor.registerTool(
|
|
508
|
+
"dte_validar",
|
|
509
|
+
{
|
|
510
|
+
title: "Validar un DTE sin emitirlo",
|
|
511
|
+
description: "Construye el documento y lo valida contra el esquema oficial del MH. No consume n\xFAmero de control, no firma y no transmite. \xDAselo antes de dte_emitir.",
|
|
512
|
+
inputSchema: z2.object(entradaEmision),
|
|
513
|
+
annotations: lectura
|
|
514
|
+
},
|
|
515
|
+
(entrada) => responder(() => api.pedir("POST", "/dte/validar", { cuerpo: entrada }))
|
|
516
|
+
);
|
|
517
|
+
servidor.registerTool(
|
|
518
|
+
"dte_emitir",
|
|
519
|
+
{
|
|
520
|
+
title: "Emitir un DTE",
|
|
521
|
+
description: 'Emite un documento tributario electr\xF3nico: lo firma y lo transmite al Ministerio de Hacienda. Tiene efecto fiscal y consume un n\xFAmero de control. Confirme los datos con el usuario antes de llamar. Devuelve estado "procesado" con sello, o "contingencia" si el MH no respondi\xF3.',
|
|
522
|
+
inputSchema: z2.object({
|
|
523
|
+
...entradaEmision,
|
|
524
|
+
idempotencyKey: z2.string().min(8).max(200).describe(
|
|
525
|
+
"Identificador \xFAnico de este intento. Repetirlo devuelve el mismo resultado sin emitir dos veces."
|
|
526
|
+
)
|
|
527
|
+
}),
|
|
528
|
+
annotations: {
|
|
529
|
+
readOnlyHint: false,
|
|
530
|
+
destructiveHint: false,
|
|
531
|
+
idempotentHint: true,
|
|
532
|
+
openWorldHint: true
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
({ idempotencyKey, ...entrada }) => responder(
|
|
536
|
+
() => api.pedir("POST", "/dte", {
|
|
537
|
+
cuerpo: entrada,
|
|
538
|
+
headers: { "Idempotency-Key": idempotencyKey }
|
|
539
|
+
})
|
|
540
|
+
)
|
|
541
|
+
);
|
|
542
|
+
servidor.registerTool(
|
|
543
|
+
"dte_listar",
|
|
544
|
+
{
|
|
545
|
+
title: "Listar documentos",
|
|
546
|
+
description: "Lista los DTE del ambiente activo, del m\xE1s reciente al m\xE1s antiguo.",
|
|
547
|
+
inputSchema: z2.object({
|
|
548
|
+
tipo: tipoDte.optional(),
|
|
549
|
+
estado: z2.enum(ESTADOS_DTE).optional(),
|
|
550
|
+
q: z2.string().optional().describe("Receptor, n\xFAmero de control o c\xF3digo de generaci\xF3n"),
|
|
551
|
+
desde: z2.string().optional().describe("AAAA-MM-DD"),
|
|
552
|
+
hasta: z2.string().optional().describe("AAAA-MM-DD"),
|
|
553
|
+
pagina: z2.number().int().min(1).optional()
|
|
554
|
+
}),
|
|
555
|
+
annotations: lectura
|
|
556
|
+
},
|
|
557
|
+
(filtros) => responder(
|
|
558
|
+
() => api.pedir("GET", "/dte", { query: { pagina: 1, porPagina: 25, ...filtros } })
|
|
559
|
+
)
|
|
560
|
+
);
|
|
561
|
+
servidor.registerTool(
|
|
562
|
+
"dte_obtener",
|
|
563
|
+
{
|
|
564
|
+
title: "Ver un documento",
|
|
565
|
+
description: "Detalle de un DTE por c\xF3digo de generaci\xF3n: estado, sello, JSON oficial, respuesta del MH, eventos y enlace de consulta p\xFAblica.",
|
|
566
|
+
inputSchema: z2.object({ codigoGeneracion: z2.string() }),
|
|
567
|
+
annotations: lectura
|
|
568
|
+
},
|
|
569
|
+
({ codigoGeneracion }) => responder(() => api.pedir("GET", `/dte/${ruta(codigoGeneracion)}`))
|
|
570
|
+
);
|
|
571
|
+
servidor.registerTool(
|
|
572
|
+
"dte_invalidar",
|
|
573
|
+
{
|
|
574
|
+
title: "Invalidar un DTE",
|
|
575
|
+
description: "Transmite el evento de invalidaci\xF3n de un DTE procesado, o de un evento de retorno u operaciones especiales (use su c\xF3digo de generaci\xF3n). Un DTE con retorno vigente exige invalidar antes el retorno. Es irreversible y tiene efecto fiscal: confirme con el usuario. tipoAnulacion: 1 error en la informaci\xF3n (requiere codigoGeneracionReemplazo), 2 rescindir la operaci\xF3n, 3 otro (requiere motivo y reemplazo).",
|
|
576
|
+
inputSchema: z2.object({ codigoGeneracion: z2.string(), ...invalidacion.shape }),
|
|
577
|
+
annotations: {
|
|
578
|
+
readOnlyHint: false,
|
|
579
|
+
destructiveHint: true,
|
|
580
|
+
idempotentHint: false,
|
|
581
|
+
openWorldHint: true
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
({ codigoGeneracion, ...entrada }) => responder(
|
|
585
|
+
() => api.pedir("POST", `/dte/${ruta(codigoGeneracion)}/invalidar`, { cuerpo: entrada })
|
|
586
|
+
)
|
|
587
|
+
);
|
|
588
|
+
servidor.registerTool(
|
|
589
|
+
"dte_retorno",
|
|
590
|
+
{
|
|
591
|
+
title: "Registrar un retorno (evento 18)",
|
|
592
|
+
description: "Transmite un Evento de Retorno sobre una Factura (01), Factura de Exportaci\xF3n (11) o Sujeto Excluido (14) procesados: devoluci\xF3n de bienes, reembolso o envases, sin invalidar el documento. Plazo: 3 meses desde la fecha del DTE. La suma de retornos no puede superar el DTE. Tiene efecto fiscal: confirme con el usuario. Para un Cr\xE9dito Fiscal use una Nota de Cr\xE9dito (05).",
|
|
593
|
+
inputSchema: z2.object({
|
|
594
|
+
codigoGeneracion: z2.string().describe("C\xF3digo de generaci\xF3n del DTE original"),
|
|
595
|
+
...retorno.shape,
|
|
596
|
+
idempotencyKey: z2.string().min(8).max(200).describe(
|
|
597
|
+
"Identificador \xFAnico de este intento. Repetirlo devuelve el mismo resultado sin transmitir dos veces."
|
|
598
|
+
)
|
|
599
|
+
}),
|
|
600
|
+
annotations: {
|
|
601
|
+
readOnlyHint: false,
|
|
602
|
+
destructiveHint: false,
|
|
603
|
+
idempotentHint: true,
|
|
604
|
+
openWorldHint: true
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
({ codigoGeneracion, idempotencyKey, ...entrada }) => responder(
|
|
608
|
+
() => api.pedir("POST", `/dte/${ruta(codigoGeneracion)}/retorno`, {
|
|
609
|
+
cuerpo: entrada,
|
|
610
|
+
headers: { "Idempotency-Key": idempotencyKey }
|
|
611
|
+
})
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
servidor.registerTool(
|
|
615
|
+
"operaciones_especiales_informar",
|
|
616
|
+
{
|
|
617
|
+
title: "Informar operaciones especiales (evento 17)",
|
|
618
|
+
description: "Transmite un Evento de Operaciones Especiales: informa al MH las facturas simplificadas (02, siempre por rangos docDel\u2013docAl) o los comprobantes de control interno (97, uno a uno con numDocumento o por rangos, sin mezclar) de un periodo. precioUni SIN IVA. Los documentos anulados van en un evento aparte con codigoGeneracionRef. Tiene efecto fiscal: confirme con el usuario.",
|
|
619
|
+
inputSchema: z2.object({
|
|
620
|
+
...operacionesEspeciales.shape,
|
|
621
|
+
idempotencyKey: z2.string().min(8).max(200).describe(
|
|
622
|
+
"Identificador \xFAnico de este intento. Repetirlo devuelve el mismo resultado sin transmitir dos veces."
|
|
623
|
+
)
|
|
624
|
+
}),
|
|
625
|
+
annotations: {
|
|
626
|
+
readOnlyHint: false,
|
|
627
|
+
destructiveHint: false,
|
|
628
|
+
idempotentHint: true,
|
|
629
|
+
openWorldHint: true
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
({ idempotencyKey, ...entrada }) => responder(
|
|
633
|
+
() => api.pedir("POST", "/eventos/operaciones-especiales", {
|
|
634
|
+
cuerpo: entrada,
|
|
635
|
+
headers: { "Idempotency-Key": idempotencyKey }
|
|
636
|
+
})
|
|
637
|
+
)
|
|
638
|
+
);
|
|
639
|
+
servidor.registerTool(
|
|
640
|
+
"eventos_listar",
|
|
641
|
+
{
|
|
642
|
+
title: "Listar eventos",
|
|
643
|
+
description: "Eventos de retorno y de operaciones especiales del ambiente activo (los 50 m\xE1s recientes).",
|
|
644
|
+
inputSchema: z2.object({ tipo: z2.enum(["retorno", "operaciones_especiales"]).optional() }),
|
|
645
|
+
annotations: lectura
|
|
646
|
+
},
|
|
647
|
+
({ tipo }) => responder(() => api.pedir("GET", "/eventos", { query: { tipo } }))
|
|
648
|
+
);
|
|
649
|
+
servidor.registerTool(
|
|
650
|
+
"evento_obtener",
|
|
651
|
+
{
|
|
652
|
+
title: "Ver un evento",
|
|
653
|
+
description: "Detalle de un evento por c\xF3digo de generaci\xF3n: estado, sello, JSON oficial y respuesta del MH.",
|
|
654
|
+
inputSchema: z2.object({ codigoGeneracion: z2.string() }),
|
|
655
|
+
annotations: lectura
|
|
656
|
+
},
|
|
657
|
+
({ codigoGeneracion }) => responder(() => api.pedir("GET", `/eventos/${ruta(codigoGeneracion)}`))
|
|
658
|
+
);
|
|
659
|
+
servidor.registerTool(
|
|
660
|
+
"webhooks_listar",
|
|
661
|
+
{
|
|
662
|
+
title: "Listar webhooks",
|
|
663
|
+
description: "Webhooks de la organizaci\xF3n: URL, eventos suscritos y si est\xE1n activos. Nunca devuelve el secreto de firma.",
|
|
664
|
+
inputSchema: z2.object({}),
|
|
665
|
+
annotations: lectura
|
|
666
|
+
},
|
|
667
|
+
() => responder(() => api.pedir("GET", "/webhooks"))
|
|
668
|
+
);
|
|
669
|
+
servidor.registerTool(
|
|
670
|
+
"webhook_crear",
|
|
671
|
+
{
|
|
672
|
+
title: "Crear webhook",
|
|
673
|
+
description: `Registra una URL https:// que recibir\xE1 un POST firmado cuando cambie el estado de un documento. Eventos: ${EVENTOS_WEBHOOK.join(", ")}. Devuelve el secreto de firma (whsec_\u2026) UNA sola vez: entr\xE9gueselo al usuario para que verifique la firma OpenFactura-Signature.`,
|
|
674
|
+
inputSchema: webhook,
|
|
675
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
676
|
+
},
|
|
677
|
+
(datos) => responder(() => api.pedir("POST", "/webhooks", { cuerpo: datos }))
|
|
678
|
+
);
|
|
679
|
+
servidor.registerTool(
|
|
680
|
+
"webhook_eliminar",
|
|
681
|
+
{
|
|
682
|
+
title: "Eliminar webhook",
|
|
683
|
+
description: "Elimina un webhook y su historial de entregas. Deja de recibir notificaciones de inmediato. Confirme con el usuario.",
|
|
684
|
+
inputSchema: z2.object({ id: z2.string() }),
|
|
685
|
+
annotations: {
|
|
686
|
+
readOnlyHint: false,
|
|
687
|
+
destructiveHint: true,
|
|
688
|
+
idempotentHint: true,
|
|
689
|
+
openWorldHint: false
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
({ id }) => responder(() => api.pedir("DELETE", `/webhooks/${ruta(id)}`))
|
|
693
|
+
);
|
|
694
|
+
servidor.registerTool(
|
|
695
|
+
"webhook_probar",
|
|
696
|
+
{
|
|
697
|
+
title: "Probar webhook",
|
|
698
|
+
description: "Env\xEDa ya un evento webhook.prueba firmado a la URL del webhook y devuelve si respondi\xF3 2xx, su estado HTTP o el error.",
|
|
699
|
+
inputSchema: z2.object({ id: z2.string() }),
|
|
700
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
701
|
+
},
|
|
702
|
+
({ id }) => responder(() => api.pedir("POST", `/webhooks/${ruta(id)}/probar`))
|
|
703
|
+
);
|
|
704
|
+
return servidor;
|
|
705
|
+
}
|
|
706
|
+
function servirMcp(api, version) {
|
|
707
|
+
return new Promise((resolve2) => {
|
|
708
|
+
const conexion = serveStdio(() => crearServidorMcp(api, version), {
|
|
709
|
+
onerror: (error) => process.stderr.write(`openfactura mcp: ${error.message}
|
|
710
|
+
`)
|
|
711
|
+
});
|
|
712
|
+
const cerrar = () => void conexion.close().finally(resolve2);
|
|
713
|
+
process.stdin.once("end", cerrar);
|
|
714
|
+
process.stdin.once("close", cerrar);
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/salida.ts
|
|
719
|
+
var ANCHO_MAXIMO = 44;
|
|
720
|
+
var celda = (valor) => {
|
|
721
|
+
if (valor === null || valor === void 0 || valor === "") return "\u2014";
|
|
722
|
+
const texto2 = String(valor).replace(/\s+/g, " ");
|
|
723
|
+
return texto2.length > ANCHO_MAXIMO ? `${texto2.slice(0, ANCHO_MAXIMO - 1)}\u2026` : texto2;
|
|
724
|
+
};
|
|
725
|
+
function tabla(encabezados, filas, derecha = []) {
|
|
726
|
+
const celdas = filas.map((fila) => fila.map(celda));
|
|
727
|
+
const anchos = encabezados.map(
|
|
728
|
+
(titulo, i) => Math.max(titulo.length, ...celdas.map((fila) => fila[i]?.length ?? 0))
|
|
729
|
+
);
|
|
730
|
+
const linea = (fila) => fila.map(
|
|
731
|
+
(texto2, i) => derecha.includes(i) ? texto2.padStart(anchos[i] ?? 0) : texto2.padEnd(anchos[i] ?? 0)
|
|
732
|
+
).join(" ").trimEnd();
|
|
733
|
+
return [linea(encabezados.map((t) => t.toUpperCase())), ...celdas.map(linea)].join("\n");
|
|
734
|
+
}
|
|
735
|
+
function fichas(pares) {
|
|
736
|
+
const visibles = pares.filter(
|
|
737
|
+
([, valor]) => valor !== null && valor !== void 0 && valor !== ""
|
|
738
|
+
);
|
|
739
|
+
const ancho = Math.max(0, ...visibles.map(([clave]) => clave.length));
|
|
740
|
+
return visibles.map(([clave, valor]) => `${`${clave}:`.padEnd(ancho + 2)}${String(valor)}`).join("\n");
|
|
741
|
+
}
|
|
742
|
+
var dinero = (valor) => typeof valor === "number" ? `$${valor.toFixed(2)}` : "\u2014";
|
|
743
|
+
var siNo = (valor) => valor ? "s\xED" : "no";
|
|
744
|
+
|
|
745
|
+
// src/comandos.ts
|
|
746
|
+
var nombreTipo = (codigo) => `${codigo} ${TIPOS_DTE[String(codigo)] ?? ""}`.trim();
|
|
747
|
+
var nombreAmbiente = (codigo) => `${codigo} ${NOMBRE_AMBIENTE[String(codigo)] ?? ""}`.trim();
|
|
748
|
+
var fecha2 = (ms) => typeof ms === "number" ? new Date(ms).toISOString().replace("T", " ").slice(0, 19) : ms;
|
|
749
|
+
var segmento = (texto2) => encodeURIComponent(texto2);
|
|
750
|
+
function mostrar(ctx, datos, legible) {
|
|
751
|
+
ctx.ent.stdout(ctx.json ? JSON.stringify(datos, null, 2) : legible());
|
|
752
|
+
}
|
|
753
|
+
async function leerJson(ctx, origen) {
|
|
754
|
+
let texto2;
|
|
755
|
+
if (origen === "-") {
|
|
756
|
+
texto2 = await ctx.ent.leerEntrada();
|
|
757
|
+
} else {
|
|
758
|
+
try {
|
|
759
|
+
texto2 = await readFile(resolve(ctx.ent.cwd, origen), "utf8");
|
|
760
|
+
} catch (error) {
|
|
761
|
+
throw new ErrorUso(`No se pudo leer ${origen}: ${error.message}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
return JSON.parse(texto2);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
throw new ErrorUso(
|
|
768
|
+
`${origen === "-" ? "La entrada est\xE1ndar" : origen} no es un JSON v\xE1lido: ${error.message}`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function leerEmision(ctx, origen) {
|
|
773
|
+
const cuerpo = await leerJson(ctx, origen);
|
|
774
|
+
const valido = cuerpo && typeof cuerpo === "object" && !Array.isArray(cuerpo) && "tipoDte" in cuerpo && "datos" in cuerpo;
|
|
775
|
+
if (!valido) {
|
|
776
|
+
throw new ErrorUso(
|
|
777
|
+
'El archivo debe ser un objeto { "tipoDte": "01", "datos": { \u2026 }, "clienteId"?, "puntoVentaId"? }'
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
return cuerpo;
|
|
781
|
+
}
|
|
782
|
+
async function confirmar(ctx, accion, entradaOcupada = false) {
|
|
783
|
+
if (ctx.si || !ctx.cfg.apiKey || !esClaveDeProduccion(ctx.cfg.apiKey)) return;
|
|
784
|
+
if (!ctx.ent.esTTY || entradaOcupada) {
|
|
785
|
+
throw new ErrorUso(
|
|
786
|
+
`Va a ${accion} en PRODUCCI\xD3N y no hay una terminal para confirmar: agregue --yes.`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
const respuesta = await ctx.ent.preguntar(
|
|
790
|
+
`Va a ${accion} en PRODUCCI\xD3N (clave of_live_). \xBFContinuar? [s/N] `
|
|
791
|
+
);
|
|
792
|
+
if (!/^(s|si|sí|y|yes)$/i.test(respuesta.trim()))
|
|
793
|
+
throw new ErrorCli("Operaci\xF3n cancelada.", "cancelado");
|
|
794
|
+
}
|
|
795
|
+
function entero(ctx, opcion, minimo = 1) {
|
|
796
|
+
const texto2 = ctx.op(opcion);
|
|
797
|
+
if (texto2 === void 0) return void 0;
|
|
798
|
+
const numero = Number(texto2);
|
|
799
|
+
if (!Number.isInteger(numero) || numero < minimo)
|
|
800
|
+
throw new ErrorUso(`--${opcion} debe ser un n\xFAmero entero (recibido: ${texto2})`);
|
|
801
|
+
return numero;
|
|
802
|
+
}
|
|
803
|
+
function fichaDocumento(doc) {
|
|
804
|
+
return fichas([
|
|
805
|
+
["Estado", doc.estado],
|
|
806
|
+
["Tipo", nombreTipo(doc.tipoDte)],
|
|
807
|
+
["N\xFAmero de control", doc.numeroControl],
|
|
808
|
+
["C\xF3digo de generaci\xF3n", doc.codigoGeneracion],
|
|
809
|
+
["Ambiente", nombreAmbiente(doc.ambiente)],
|
|
810
|
+
["Emisi\xF3n", `${doc.fechaEmision ?? ""} ${doc.horaEmision ?? ""}`.trim()],
|
|
811
|
+
["Receptor", [doc.receptorNombre, doc.receptorDocumento].filter(Boolean).join(" \xB7 ")],
|
|
812
|
+
["Total", dinero(doc.total)],
|
|
813
|
+
["Sello recibido", doc.selloRecibido],
|
|
814
|
+
["Origen", doc.origen],
|
|
815
|
+
["Error", doc.error],
|
|
816
|
+
["Consulta p\xFAblica", doc.urlConsulta]
|
|
817
|
+
]);
|
|
818
|
+
}
|
|
819
|
+
async function cuerpoDeAlta(ctx, campos, numericos = []) {
|
|
820
|
+
const dados = Object.keys(campos).filter((opcion) => ctx.op(opcion) !== void 0);
|
|
821
|
+
const archivo = ctx.op("file");
|
|
822
|
+
if (archivo !== void 0) {
|
|
823
|
+
if (dados.length) throw new ErrorUso("Use --file o las opciones sueltas, no ambas.");
|
|
824
|
+
return leerJson(ctx, archivo);
|
|
825
|
+
}
|
|
826
|
+
if (!dados.length) {
|
|
827
|
+
throw new ErrorUso(
|
|
828
|
+
`Indique --file <archivo.json> o los datos con opciones: ${Object.keys(campos).map((o) => `--${o}`).join(", ")}`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
const cuerpo = {};
|
|
832
|
+
for (const opcion of dados) {
|
|
833
|
+
const texto2 = ctx.op(opcion);
|
|
834
|
+
const campo = campos[opcion];
|
|
835
|
+
if (numericos.includes(opcion)) {
|
|
836
|
+
const numero = Number(texto2);
|
|
837
|
+
if (texto2.trim() === "" || !Number.isFinite(numero))
|
|
838
|
+
throw new ErrorUso(`--${opcion} debe ser un n\xFAmero (recibido: ${texto2})`);
|
|
839
|
+
cuerpo[campo] = numero;
|
|
840
|
+
} else {
|
|
841
|
+
cuerpo[campo] = texto2;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return cuerpo;
|
|
845
|
+
}
|
|
846
|
+
var CAMPOS_CLIENTE = {
|
|
847
|
+
"tipo-documento": "tipoDocumento",
|
|
848
|
+
"num-documento": "numDocumento",
|
|
849
|
+
nrc: "nrc",
|
|
850
|
+
nombre: "nombre",
|
|
851
|
+
"nombre-comercial": "nombreComercial",
|
|
852
|
+
"cod-actividad": "codActividad",
|
|
853
|
+
"desc-actividad": "descActividad",
|
|
854
|
+
departamento: "departamento",
|
|
855
|
+
municipio: "municipio",
|
|
856
|
+
distrito: "distrito",
|
|
857
|
+
complemento: "complemento",
|
|
858
|
+
telefono: "telefono",
|
|
859
|
+
correo: "correo"
|
|
860
|
+
};
|
|
861
|
+
var CAMPOS_PRODUCTO = {
|
|
862
|
+
codigo: "codigo",
|
|
863
|
+
descripcion: "descripcion",
|
|
864
|
+
precio: "precioUnitario",
|
|
865
|
+
"tipo-item": "tipoItem",
|
|
866
|
+
"uni-medida": "uniMedida",
|
|
867
|
+
"tipo-venta": "tipoVenta"
|
|
868
|
+
};
|
|
869
|
+
async function login(ctx) {
|
|
870
|
+
let clave = ctx.op("key");
|
|
871
|
+
if (clave === void 0) {
|
|
872
|
+
clave = ctx.ent.esTTY ? await ctx.ent.preguntar("Clave de API (of_test_\u2026 u of_live_\u2026): ", true) : (await ctx.ent.leerEntrada()).split("\n")[0];
|
|
873
|
+
}
|
|
874
|
+
clave = (clave ?? "").trim();
|
|
875
|
+
if (!esClaveValida(clave))
|
|
876
|
+
throw new ErrorUso("La clave de API debe empezar con of_test_ o of_live_.");
|
|
877
|
+
const api = new ClienteApi({
|
|
878
|
+
url: ctx.cfg.url,
|
|
879
|
+
apiKey: clave,
|
|
880
|
+
version: ctx.version,
|
|
881
|
+
fetch: ctx.ent.fetch
|
|
882
|
+
});
|
|
883
|
+
const { datos } = await api.pedir("GET", "/yo");
|
|
884
|
+
const config = leerConfig(ctx.ent.env);
|
|
885
|
+
config.perfiles[ctx.cfg.perfil] = { url: ctx.cfg.url, apiKey: clave };
|
|
886
|
+
const ruta2 = guardarConfig(ctx.ent.env, config);
|
|
887
|
+
mostrar(
|
|
888
|
+
ctx,
|
|
889
|
+
datos,
|
|
890
|
+
() => [
|
|
891
|
+
`Sesi\xF3n iniciada en ${ctx.cfg.url}`,
|
|
892
|
+
fichas([
|
|
893
|
+
["Clave", enmascarar(clave)],
|
|
894
|
+
["Ambiente", nombreAmbiente(datos.ambiente)],
|
|
895
|
+
["Perfil", ctx.cfg.perfil],
|
|
896
|
+
["Guardado en", ruta2]
|
|
897
|
+
])
|
|
898
|
+
].join("\n")
|
|
899
|
+
);
|
|
900
|
+
if (ctx.ent.env.OPENFACTURA_API_KEY) {
|
|
901
|
+
ctx.ent.stderr(
|
|
902
|
+
"Aviso: OPENFACTURA_API_KEY est\xE1 definida y tiene prioridad sobre la clave guardada."
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
return void 0;
|
|
906
|
+
}
|
|
907
|
+
async function logout(ctx) {
|
|
908
|
+
const config = leerConfig(ctx.ent.env);
|
|
909
|
+
const habia = ctx.cfg.perfil in config.perfiles;
|
|
910
|
+
if (habia) {
|
|
911
|
+
delete config.perfiles[ctx.cfg.perfil];
|
|
912
|
+
guardarConfig(ctx.ent.env, config);
|
|
913
|
+
}
|
|
914
|
+
mostrar(
|
|
915
|
+
ctx,
|
|
916
|
+
{ perfil: ctx.cfg.perfil, eliminado: habia },
|
|
917
|
+
() => habia ? `Sesi\xF3n cerrada: se borr\xF3 la clave del perfil \xAB${ctx.cfg.perfil}\xBB (${rutaConfig(ctx.ent.env)}).` : `No hab\xEDa una sesi\xF3n guardada en el perfil \xAB${ctx.cfg.perfil}\xBB.`
|
|
918
|
+
);
|
|
919
|
+
if (ctx.ent.env.OPENFACTURA_API_KEY) {
|
|
920
|
+
ctx.ent.stderr("Aviso: OPENFACTURA_API_KEY sigue definida en el entorno.");
|
|
921
|
+
}
|
|
922
|
+
return void 0;
|
|
923
|
+
}
|
|
924
|
+
async function whoami(ctx) {
|
|
925
|
+
const { datos } = await ctx.api().pedir("GET", "/yo");
|
|
926
|
+
const usuario = datos.usuario;
|
|
927
|
+
mostrar(
|
|
928
|
+
ctx,
|
|
929
|
+
datos,
|
|
930
|
+
() => fichas([
|
|
931
|
+
["Actor", datos.actor],
|
|
932
|
+
["Usuario", usuario ? `${usuario.nombre} <${usuario.correo}>` : null],
|
|
933
|
+
["Rol", datos.rol],
|
|
934
|
+
["Organizaci\xF3n", datos.organizationId],
|
|
935
|
+
[
|
|
936
|
+
"Ambiente",
|
|
937
|
+
`${nombreAmbiente(datos.ambiente)}${datos.simulador ? " (simulador del MH)" : ""}`
|
|
938
|
+
],
|
|
939
|
+
["Servidor", `${ctx.cfg.url} (${ctx.cfg.origenUrl})`],
|
|
940
|
+
["Clave", `${enmascarar(ctx.cfg.apiKey)} (${ctx.cfg.origenClave})`],
|
|
941
|
+
["Perfil", ctx.cfg.perfil],
|
|
942
|
+
["Permisos", Array.isArray(datos.permisos) ? datos.permisos.join(", ") : null]
|
|
943
|
+
])
|
|
944
|
+
);
|
|
945
|
+
return void 0;
|
|
946
|
+
}
|
|
947
|
+
async function emisor(ctx) {
|
|
948
|
+
const { datos } = await ctx.api().pedir("GET", "/emisor");
|
|
949
|
+
mostrar(ctx, datos, () => {
|
|
950
|
+
const e = datos.emisor;
|
|
951
|
+
if (!e) return "El emisor a\xFAn no est\xE1 configurado. Complete sus datos en el panel web.";
|
|
952
|
+
const establecimientos = datos.establecimientos ?? [];
|
|
953
|
+
const credenciales = datos.credenciales ?? {};
|
|
954
|
+
return [
|
|
955
|
+
fichas([
|
|
956
|
+
["Nombre", e.nombre],
|
|
957
|
+
["Nombre comercial", e.nombreComercial],
|
|
958
|
+
["NIT", e.nit],
|
|
959
|
+
["NRC", e.nrc],
|
|
960
|
+
["Actividad", `${e.codActividad} ${e.descActividad}`],
|
|
961
|
+
[
|
|
962
|
+
"Direcci\xF3n",
|
|
963
|
+
`${e.complemento} (depto. ${e.departamento}, mun. ${e.municipio}, dist. ${e.distrito})`
|
|
964
|
+
],
|
|
965
|
+
["Tel\xE9fono", e.telefono],
|
|
966
|
+
["Correo", e.correo],
|
|
967
|
+
["Ambiente activo", nombreAmbiente(datos.ambienteActivo)]
|
|
968
|
+
]),
|
|
969
|
+
"",
|
|
970
|
+
"Establecimientos",
|
|
971
|
+
tabla(
|
|
972
|
+
["c\xF3digo", "nombre", "tipo", "puntos de venta"],
|
|
973
|
+
establecimientos.map((est) => [
|
|
974
|
+
est.codMh,
|
|
975
|
+
est.nombre,
|
|
976
|
+
est.tipo,
|
|
977
|
+
(est.puntosVenta ?? []).map((pv) => `${pv.codMh} ${pv.nombre}`).join(", ")
|
|
978
|
+
])
|
|
979
|
+
),
|
|
980
|
+
"",
|
|
981
|
+
"Credenciales del MH",
|
|
982
|
+
tabla(
|
|
983
|
+
["ambiente", "contrase\xF1a api", "certificado", "nit del certificado"],
|
|
984
|
+
Object.entries(credenciales).map(([ambiente, c]) => [
|
|
985
|
+
nombreAmbiente(ambiente),
|
|
986
|
+
siNo(c.apiPassword),
|
|
987
|
+
siNo(c.certificado),
|
|
988
|
+
c.certificadoNit
|
|
989
|
+
])
|
|
990
|
+
)
|
|
991
|
+
].join("\n");
|
|
992
|
+
});
|
|
993
|
+
return void 0;
|
|
994
|
+
}
|
|
995
|
+
async function catalogos(ctx) {
|
|
996
|
+
const id = ctx.args[0];
|
|
997
|
+
if (!id) {
|
|
998
|
+
if (ctx.op("q") !== void 0 || ctx.op("departamento") !== void 0) {
|
|
999
|
+
throw new ErrorUso(
|
|
1000
|
+
"--q y --departamento requieren el id del cat\xE1logo, por ejemplo: openfactura catalogos CAT-013 --departamento 06"
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
const { datos: datos2 } = await ctx.api().pedir("GET", "/catalogos");
|
|
1004
|
+
mostrar(
|
|
1005
|
+
ctx,
|
|
1006
|
+
datos2,
|
|
1007
|
+
() => tabla(
|
|
1008
|
+
["id", "nombre", "valores"],
|
|
1009
|
+
datos2.map((c) => [c.id, c.nombre, c.total]),
|
|
1010
|
+
[2]
|
|
1011
|
+
)
|
|
1012
|
+
);
|
|
1013
|
+
return void 0;
|
|
1014
|
+
}
|
|
1015
|
+
const { datos } = await ctx.api().pedir("GET", `/catalogos/${segmento(id.toUpperCase())}`, {
|
|
1016
|
+
query: { q: ctx.op("q"), departamento: ctx.op("departamento") }
|
|
1017
|
+
});
|
|
1018
|
+
mostrar(ctx, datos, () => {
|
|
1019
|
+
const conDepartamento = datos.valores.some((v) => v.departamento);
|
|
1020
|
+
const filas = datos.valores.map(
|
|
1021
|
+
(v) => conDepartamento ? [v.codigo, v.valor, v.departamento] : [v.codigo, v.valor]
|
|
1022
|
+
);
|
|
1023
|
+
return [
|
|
1024
|
+
`${datos.id} ${datos.nombre} \u2014 ${datos.valores.length} de ${datos.total} valores`,
|
|
1025
|
+
tabla(conDepartamento ? ["c\xF3digo", "valor", "departamento"] : ["c\xF3digo", "valor"], filas)
|
|
1026
|
+
].join("\n");
|
|
1027
|
+
});
|
|
1028
|
+
return void 0;
|
|
1029
|
+
}
|
|
1030
|
+
async function clientesList(ctx) {
|
|
1031
|
+
const { datos } = await ctx.api().pedir("GET", "/clientes", { query: { q: ctx.op("q") } });
|
|
1032
|
+
mostrar(
|
|
1033
|
+
ctx,
|
|
1034
|
+
datos,
|
|
1035
|
+
() => datos.length ? tabla(
|
|
1036
|
+
["id", "doc", "n\xFAmero", "nombre", "nrc", "correo"],
|
|
1037
|
+
datos.map((c) => [c.id, c.tipoDocumento, c.numDocumento, c.nombre, c.nrc, c.correo])
|
|
1038
|
+
) : "No hay clientes."
|
|
1039
|
+
);
|
|
1040
|
+
return void 0;
|
|
1041
|
+
}
|
|
1042
|
+
async function clientesAdd(ctx) {
|
|
1043
|
+
const cuerpo = await cuerpoDeAlta(ctx, CAMPOS_CLIENTE);
|
|
1044
|
+
const { datos } = await ctx.api().pedir("POST", "/clientes", { cuerpo });
|
|
1045
|
+
mostrar(
|
|
1046
|
+
ctx,
|
|
1047
|
+
datos,
|
|
1048
|
+
() => [
|
|
1049
|
+
"Cliente creado.",
|
|
1050
|
+
fichas([
|
|
1051
|
+
["Id", datos.id],
|
|
1052
|
+
["Documento", `${datos.tipoDocumento} ${datos.numDocumento}`],
|
|
1053
|
+
["Nombre", datos.nombre],
|
|
1054
|
+
["NRC", datos.nrc],
|
|
1055
|
+
["Correo", datos.correo]
|
|
1056
|
+
])
|
|
1057
|
+
].join("\n")
|
|
1058
|
+
);
|
|
1059
|
+
return void 0;
|
|
1060
|
+
}
|
|
1061
|
+
async function productosList(ctx) {
|
|
1062
|
+
const { datos } = await ctx.api().pedir("GET", "/productos", { query: { q: ctx.op("q") } });
|
|
1063
|
+
mostrar(
|
|
1064
|
+
ctx,
|
|
1065
|
+
datos,
|
|
1066
|
+
() => datos.length ? tabla(
|
|
1067
|
+
["id", "c\xF3digo", "descripci\xF3n", "precio sin iva", "\xEDtem", "unidad", "venta"],
|
|
1068
|
+
datos.map((p) => [
|
|
1069
|
+
p.id,
|
|
1070
|
+
p.codigo,
|
|
1071
|
+
p.descripcion,
|
|
1072
|
+
dinero(p.precioUnitario),
|
|
1073
|
+
p.tipoItem,
|
|
1074
|
+
p.uniMedida,
|
|
1075
|
+
p.tipoVenta
|
|
1076
|
+
]),
|
|
1077
|
+
[3]
|
|
1078
|
+
) : "No hay productos."
|
|
1079
|
+
);
|
|
1080
|
+
return void 0;
|
|
1081
|
+
}
|
|
1082
|
+
async function productosAdd(ctx) {
|
|
1083
|
+
const cuerpo = await cuerpoDeAlta(ctx, CAMPOS_PRODUCTO, ["precio", "tipo-item", "uni-medida"]);
|
|
1084
|
+
const { datos } = await ctx.api().pedir("POST", "/productos", { cuerpo });
|
|
1085
|
+
mostrar(
|
|
1086
|
+
ctx,
|
|
1087
|
+
datos,
|
|
1088
|
+
() => [
|
|
1089
|
+
"Producto creado.",
|
|
1090
|
+
fichas([
|
|
1091
|
+
["Id", datos.id],
|
|
1092
|
+
["C\xF3digo", datos.codigo],
|
|
1093
|
+
["Descripci\xF3n", datos.descripcion],
|
|
1094
|
+
["Precio sin IVA", dinero(datos.precioUnitario)],
|
|
1095
|
+
["Tipo de venta", datos.tipoVenta]
|
|
1096
|
+
])
|
|
1097
|
+
].join("\n")
|
|
1098
|
+
);
|
|
1099
|
+
return void 0;
|
|
1100
|
+
}
|
|
1101
|
+
async function dteValidar(ctx) {
|
|
1102
|
+
const cuerpo = await leerEmision(ctx, ctx.args[0]);
|
|
1103
|
+
const { datos } = await ctx.api().pedir("POST", "/dte/validar", { cuerpo });
|
|
1104
|
+
const problemas = datos.problemas ?? [];
|
|
1105
|
+
mostrar(ctx, datos, () => {
|
|
1106
|
+
const resumen = datos.dte?.resumen ?? {};
|
|
1107
|
+
const numero = (campo) => typeof resumen[campo] === "number" ? dinero(resumen[campo]) : null;
|
|
1108
|
+
return [
|
|
1109
|
+
fichas([
|
|
1110
|
+
["Tipo", nombreTipo(cuerpo.tipoDte)],
|
|
1111
|
+
["Receptor", datos.receptorNombre],
|
|
1112
|
+
["Gravado", numero("totalGravada")],
|
|
1113
|
+
["Exento", numero("totalExenta")],
|
|
1114
|
+
["No sujeto", numero("totalNoSuj")],
|
|
1115
|
+
["IVA", numero("totalIva")],
|
|
1116
|
+
["Total", dinero(datos.total)],
|
|
1117
|
+
["En letras", resumen.totalLetras]
|
|
1118
|
+
]),
|
|
1119
|
+
"",
|
|
1120
|
+
problemas.length ? [
|
|
1121
|
+
`${problemas.length} problema(s):`,
|
|
1122
|
+
...problemas.map((p) => ` ${p.ruta}: ${p.mensaje}`)
|
|
1123
|
+
].join("\n") : "V\xE1lido: cumple el esquema del MH. No se emiti\xF3 nada."
|
|
1124
|
+
].join("\n");
|
|
1125
|
+
});
|
|
1126
|
+
return problemas.length ? SALIDA.error : SALIDA.ok;
|
|
1127
|
+
}
|
|
1128
|
+
async function enviarUnaVez(ctx, ruta2, cuerpo, qu\u00E9, reintento, stdin = false) {
|
|
1129
|
+
const api = ctx.api();
|
|
1130
|
+
const clave = ctx.op("idempotency-key") ?? randomUUID();
|
|
1131
|
+
if (clave.length < 1 || clave.length > 200)
|
|
1132
|
+
throw new ErrorUso("--idempotency-key debe tener entre 1 y 200 caracteres.");
|
|
1133
|
+
await confirmar(ctx, qu\u00E9, stdin);
|
|
1134
|
+
ctx.ent.stderr(`Idempotency-Key: ${clave}`);
|
|
1135
|
+
try {
|
|
1136
|
+
const respuesta = await api.pedir("POST", ruta2, {
|
|
1137
|
+
cuerpo,
|
|
1138
|
+
headers: { "Idempotency-Key": clave }
|
|
1139
|
+
});
|
|
1140
|
+
if (respuesta.headers.get("Idempotent-Replayed") === "true") {
|
|
1141
|
+
ctx.ent.stderr(
|
|
1142
|
+
"Respuesta repetida: esta Idempotency-Key ya se hab\xEDa usado; no se transmiti\xF3 de nuevo."
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
return respuesta.datos;
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
if (error instanceof ErrorCli && (error.code === "sin_conexion" || error.code === "tiempo_agotado")) {
|
|
1148
|
+
ctx.ent.stderr(
|
|
1149
|
+
`No se sabe si se transmiti\xF3. Reintente con la misma clave para no duplicarlo:
|
|
1150
|
+
openfactura ${reintento} --idempotency-key ${clave}`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
throw error;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async function dteEmitir(ctx) {
|
|
1157
|
+
const origen = ctx.args[0];
|
|
1158
|
+
const cuerpo = await leerEmision(ctx, origen);
|
|
1159
|
+
const doc = await enviarUnaVez(
|
|
1160
|
+
ctx,
|
|
1161
|
+
"/dte",
|
|
1162
|
+
cuerpo,
|
|
1163
|
+
`emitir un DTE (${nombreTipo(cuerpo.tipoDte)})`,
|
|
1164
|
+
`dte emitir ${origen}`,
|
|
1165
|
+
origen === "-"
|
|
1166
|
+
);
|
|
1167
|
+
if (doc.estado === "contingencia") {
|
|
1168
|
+
ctx.ent.stderr(
|
|
1169
|
+
"El MH no respondi\xF3: el DTE qued\xF3 firmado en contingencia. Transm\xEDtalo con `openfactura contingencia transmitir`."
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
mostrar(ctx, doc, () => fichaDocumento(doc));
|
|
1173
|
+
return void 0;
|
|
1174
|
+
}
|
|
1175
|
+
async function dteList(ctx) {
|
|
1176
|
+
const estado = ctx.op("estado");
|
|
1177
|
+
if (estado !== void 0 && !ESTADOS_DTE.includes(estado)) {
|
|
1178
|
+
throw new ErrorUso(`--estado debe ser uno de: ${ESTADOS_DTE.join(", ")}`);
|
|
1179
|
+
}
|
|
1180
|
+
for (const opcion of ["desde", "hasta"]) {
|
|
1181
|
+
const valor = ctx.op(opcion);
|
|
1182
|
+
if (valor !== void 0 && !/^\d{4}-\d{2}-\d{2}$/.test(valor))
|
|
1183
|
+
throw new ErrorUso(`--${opcion} debe tener el formato AAAA-MM-DD`);
|
|
1184
|
+
}
|
|
1185
|
+
const { datos } = await ctx.api().pedir("GET", "/dte", {
|
|
1186
|
+
query: {
|
|
1187
|
+
tipo: ctx.op("tipo"),
|
|
1188
|
+
estado,
|
|
1189
|
+
q: ctx.op("q"),
|
|
1190
|
+
desde: ctx.op("desde"),
|
|
1191
|
+
hasta: ctx.op("hasta"),
|
|
1192
|
+
pagina: entero(ctx, "pagina")
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
mostrar(
|
|
1196
|
+
ctx,
|
|
1197
|
+
datos,
|
|
1198
|
+
() => [
|
|
1199
|
+
datos.items.length ? tabla(
|
|
1200
|
+
[
|
|
1201
|
+
"fecha",
|
|
1202
|
+
"tipo",
|
|
1203
|
+
"n\xFAmero de control",
|
|
1204
|
+
"estado",
|
|
1205
|
+
"receptor",
|
|
1206
|
+
"total",
|
|
1207
|
+
"c\xF3digo de generaci\xF3n"
|
|
1208
|
+
],
|
|
1209
|
+
datos.items.map((d) => [
|
|
1210
|
+
d.fechaEmision,
|
|
1211
|
+
d.tipoDte,
|
|
1212
|
+
d.numeroControl,
|
|
1213
|
+
d.estado,
|
|
1214
|
+
d.receptorNombre,
|
|
1215
|
+
dinero(d.total),
|
|
1216
|
+
d.codigoGeneracion
|
|
1217
|
+
]),
|
|
1218
|
+
[5]
|
|
1219
|
+
) : "No hay documentos.",
|
|
1220
|
+
`P\xE1gina ${datos.pagina} \xB7 ${datos.items.length} de ${datos.total} documento(s) \xB7 ambiente ${nombreAmbiente(datos.ambiente)}`
|
|
1221
|
+
].join("\n")
|
|
1222
|
+
);
|
|
1223
|
+
return void 0;
|
|
1224
|
+
}
|
|
1225
|
+
async function dteGet(ctx) {
|
|
1226
|
+
const { datos } = await ctx.api().pedir("GET", `/dte/${segmento(ctx.args[0])}`);
|
|
1227
|
+
mostrar(ctx, datos, () => {
|
|
1228
|
+
const mh = datos.respuestaMh;
|
|
1229
|
+
const eventos = datos.eventos ?? [];
|
|
1230
|
+
const observaciones = Array.isArray(mh?.observaciones) ? mh.observaciones.join("; ") : null;
|
|
1231
|
+
return [
|
|
1232
|
+
fichaDocumento(datos),
|
|
1233
|
+
...mh ? [
|
|
1234
|
+
"",
|
|
1235
|
+
"Respuesta del MH",
|
|
1236
|
+
fichas([
|
|
1237
|
+
["Estado", mh.estado],
|
|
1238
|
+
["Mensaje", `${mh.codigoMsg ?? ""} ${mh.descripcionMsg ?? ""}`.trim()],
|
|
1239
|
+
["Procesado", mh.fhProcesamiento],
|
|
1240
|
+
["Observaciones", observaciones]
|
|
1241
|
+
])
|
|
1242
|
+
] : [],
|
|
1243
|
+
...eventos.length ? [
|
|
1244
|
+
"",
|
|
1245
|
+
"Eventos",
|
|
1246
|
+
tabla(
|
|
1247
|
+
["tipo", "estado", "fecha (UTC)", "c\xF3digo de generaci\xF3n", "sello"],
|
|
1248
|
+
eventos.map((e) => [
|
|
1249
|
+
e.tipo,
|
|
1250
|
+
e.estado,
|
|
1251
|
+
fecha2(e.creado),
|
|
1252
|
+
e.codigoGeneracion,
|
|
1253
|
+
e.selloRecibido
|
|
1254
|
+
])
|
|
1255
|
+
)
|
|
1256
|
+
] : []
|
|
1257
|
+
].join("\n");
|
|
1258
|
+
});
|
|
1259
|
+
return void 0;
|
|
1260
|
+
}
|
|
1261
|
+
async function dteJson(ctx) {
|
|
1262
|
+
const { datos } = await ctx.api().pedir("GET", `/dte/${segmento(ctx.args[0])}/json`);
|
|
1263
|
+
const texto2 = JSON.stringify(datos, null, 2);
|
|
1264
|
+
const salida = ctx.op("output");
|
|
1265
|
+
if (salida === void 0) {
|
|
1266
|
+
ctx.ent.stdout(texto2);
|
|
1267
|
+
return void 0;
|
|
1268
|
+
}
|
|
1269
|
+
const destino = resolve(ctx.ent.cwd, salida);
|
|
1270
|
+
await writeFile(destino, `${texto2}
|
|
1271
|
+
`);
|
|
1272
|
+
mostrar(ctx, { archivo: destino }, () => `JSON guardado en ${destino}`);
|
|
1273
|
+
return void 0;
|
|
1274
|
+
}
|
|
1275
|
+
async function dtePdf(ctx) {
|
|
1276
|
+
const codigo = ctx.args[0];
|
|
1277
|
+
const bytes = await ctx.api().descargar(`/dte/${segmento(codigo)}/pdf`);
|
|
1278
|
+
const destino = resolve(ctx.ent.cwd, ctx.op("output") ?? `${basename(codigo)}.pdf`);
|
|
1279
|
+
await writeFile(destino, bytes);
|
|
1280
|
+
mostrar(
|
|
1281
|
+
ctx,
|
|
1282
|
+
{ archivo: destino, bytes: bytes.length },
|
|
1283
|
+
() => `PDF guardado en ${destino} (${bytes.length} bytes)`
|
|
1284
|
+
);
|
|
1285
|
+
return void 0;
|
|
1286
|
+
}
|
|
1287
|
+
async function dteInvalidar(ctx) {
|
|
1288
|
+
const codigo = ctx.args[0];
|
|
1289
|
+
const tipo = ctx.op("tipo");
|
|
1290
|
+
if (tipo !== "1" && tipo !== "2" && tipo !== "3") {
|
|
1291
|
+
throw new ErrorUso(
|
|
1292
|
+
"Indique --tipo 1|2|3 (1 error en la informaci\xF3n, 2 rescindir la operaci\xF3n, 3 otro)."
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
const cuerpo = { tipoAnulacion: Number(tipo) };
|
|
1296
|
+
if (ctx.op("motivo") !== void 0) cuerpo.motivoAnulacion = ctx.op("motivo");
|
|
1297
|
+
if (ctx.op("reemplazo") !== void 0) cuerpo.codigoGeneracionReemplazo = ctx.op("reemplazo");
|
|
1298
|
+
const api = ctx.api();
|
|
1299
|
+
await confirmar(ctx, `invalidar el DTE ${codigo} (irreversible)`);
|
|
1300
|
+
const { datos } = await api.pedir("POST", `/dte/${segmento(codigo)}/invalidar`, {
|
|
1301
|
+
cuerpo
|
|
1302
|
+
});
|
|
1303
|
+
mostrar(ctx, datos, () => `DTE invalidado.
|
|
1304
|
+
${fichaDocumento(datos)}`);
|
|
1305
|
+
return void 0;
|
|
1306
|
+
}
|
|
1307
|
+
var TIPOS_EVENTO = ["retorno", "operaciones_especiales"];
|
|
1308
|
+
function fichaEvento(e) {
|
|
1309
|
+
return fichas([
|
|
1310
|
+
[
|
|
1311
|
+
"Evento",
|
|
1312
|
+
e.tipoEvento === "18" ? "18 Retorno" : e.tipoEvento === "17" ? "17 Operaciones especiales" : e.tipo
|
|
1313
|
+
],
|
|
1314
|
+
["Estado", e.estado],
|
|
1315
|
+
["C\xF3digo de generaci\xF3n", e.codigoGeneracion],
|
|
1316
|
+
["Ambiente", nombreAmbiente(e.ambiente)],
|
|
1317
|
+
["Sello recibido", e.selloRecibido]
|
|
1318
|
+
]);
|
|
1319
|
+
}
|
|
1320
|
+
async function leerObjeto(ctx, origen, forma) {
|
|
1321
|
+
const cuerpo = await leerJson(ctx, origen);
|
|
1322
|
+
if (!cuerpo || typeof cuerpo !== "object" || Array.isArray(cuerpo) || !("items" in cuerpo)) {
|
|
1323
|
+
throw new ErrorUso(`El archivo debe ser un objeto ${forma}`);
|
|
1324
|
+
}
|
|
1325
|
+
return cuerpo;
|
|
1326
|
+
}
|
|
1327
|
+
async function dteRetorno(ctx) {
|
|
1328
|
+
const [codigo, origen] = ctx.args;
|
|
1329
|
+
const cuerpo = await leerObjeto(
|
|
1330
|
+
ctx,
|
|
1331
|
+
origen,
|
|
1332
|
+
'{ "items": [{ "descripcion", "cantidad", "precioUni" }] }'
|
|
1333
|
+
);
|
|
1334
|
+
const evento = await enviarUnaVez(
|
|
1335
|
+
ctx,
|
|
1336
|
+
`/dte/${segmento(codigo)}/retorno`,
|
|
1337
|
+
cuerpo,
|
|
1338
|
+
`registrar un retorno sobre el DTE ${codigo}`,
|
|
1339
|
+
`dte retorno ${codigo} ${origen}`,
|
|
1340
|
+
origen === "-"
|
|
1341
|
+
);
|
|
1342
|
+
mostrar(ctx, evento, () => `Retorno registrado.
|
|
1343
|
+
${fichaEvento(evento)}`);
|
|
1344
|
+
return void 0;
|
|
1345
|
+
}
|
|
1346
|
+
async function eventosOperacionesEspeciales(ctx) {
|
|
1347
|
+
const origen = ctx.args[0];
|
|
1348
|
+
const cuerpo = await leerObjeto(
|
|
1349
|
+
ctx,
|
|
1350
|
+
origen,
|
|
1351
|
+
'{ "tipoDocumento": "02" | "97", "fecha"?, "items": [{ "descripcion", "precioUni", "docDel", "docAl" }] }'
|
|
1352
|
+
);
|
|
1353
|
+
const evento = await enviarUnaVez(
|
|
1354
|
+
ctx,
|
|
1355
|
+
"/eventos/operaciones-especiales",
|
|
1356
|
+
cuerpo,
|
|
1357
|
+
"informar operaciones especiales",
|
|
1358
|
+
`eventos operaciones-especiales ${origen}`,
|
|
1359
|
+
origen === "-"
|
|
1360
|
+
);
|
|
1361
|
+
mostrar(ctx, evento, () => `Operaciones especiales informadas.
|
|
1362
|
+
${fichaEvento(evento)}`);
|
|
1363
|
+
return void 0;
|
|
1364
|
+
}
|
|
1365
|
+
async function eventosList(ctx) {
|
|
1366
|
+
const tipo = ctx.op("tipo");
|
|
1367
|
+
if (tipo !== void 0 && !TIPOS_EVENTO.includes(tipo)) {
|
|
1368
|
+
throw new ErrorUso(`--tipo debe ser uno de: ${TIPOS_EVENTO.join(", ")}`);
|
|
1369
|
+
}
|
|
1370
|
+
const { datos } = await ctx.api().pedir("GET", "/eventos", { query: { tipo } });
|
|
1371
|
+
mostrar(
|
|
1372
|
+
ctx,
|
|
1373
|
+
datos,
|
|
1374
|
+
() => datos.length ? tabla(
|
|
1375
|
+
["creado", "evento", "estado", "c\xF3digo de generaci\xF3n"],
|
|
1376
|
+
datos.map((e) => [
|
|
1377
|
+
new Date(e.creado).toISOString().slice(0, 10),
|
|
1378
|
+
e.tipoEvento,
|
|
1379
|
+
e.estado,
|
|
1380
|
+
e.codigoGeneracion
|
|
1381
|
+
])
|
|
1382
|
+
) : "No hay eventos."
|
|
1383
|
+
);
|
|
1384
|
+
return void 0;
|
|
1385
|
+
}
|
|
1386
|
+
async function eventosGet(ctx) {
|
|
1387
|
+
const { datos } = await ctx.api().pedir("GET", `/eventos/${segmento(ctx.args[0])}`);
|
|
1388
|
+
mostrar(ctx, datos, () => fichaEvento(datos));
|
|
1389
|
+
return void 0;
|
|
1390
|
+
}
|
|
1391
|
+
async function eventosJson(ctx) {
|
|
1392
|
+
const codigo = ctx.args[0];
|
|
1393
|
+
const { datos } = await ctx.api().pedir("GET", `/eventos/${segmento(codigo)}/json`);
|
|
1394
|
+
const texto2 = JSON.stringify(datos, null, 2);
|
|
1395
|
+
const salida = ctx.op("output");
|
|
1396
|
+
if (salida === void 0) {
|
|
1397
|
+
ctx.ent.stdout(texto2);
|
|
1398
|
+
return void 0;
|
|
1399
|
+
}
|
|
1400
|
+
const destino = resolve(ctx.ent.cwd, salida);
|
|
1401
|
+
await writeFile(destino, `${texto2}
|
|
1402
|
+
`);
|
|
1403
|
+
mostrar(ctx, { archivo: destino }, () => `JSON guardado en ${destino}`);
|
|
1404
|
+
return void 0;
|
|
1405
|
+
}
|
|
1406
|
+
async function eventosPdf(ctx) {
|
|
1407
|
+
const codigo = ctx.args[0];
|
|
1408
|
+
const bytes = await ctx.api().descargar(`/eventos/${segmento(codigo)}/pdf`);
|
|
1409
|
+
const destino = resolve(ctx.ent.cwd, ctx.op("output") ?? `${basename(codigo)}.pdf`);
|
|
1410
|
+
await writeFile(destino, bytes);
|
|
1411
|
+
mostrar(
|
|
1412
|
+
ctx,
|
|
1413
|
+
{ archivo: destino, bytes: bytes.length },
|
|
1414
|
+
() => `PDF guardado en ${destino} (${bytes.length} bytes)`
|
|
1415
|
+
);
|
|
1416
|
+
return void 0;
|
|
1417
|
+
}
|
|
1418
|
+
async function contingenciaTransmitir(ctx) {
|
|
1419
|
+
const api = ctx.api();
|
|
1420
|
+
await confirmar(ctx, "transmitir los DTE en contingencia");
|
|
1421
|
+
const { datos } = await api.pedir("POST", "/contingencia/transmitir");
|
|
1422
|
+
mostrar(ctx, datos, () => {
|
|
1423
|
+
const procesados = datos.procesados ?? [];
|
|
1424
|
+
const rechazados = datos.rechazados ?? [];
|
|
1425
|
+
if (!datos.evento && !procesados.length && !rechazados.length)
|
|
1426
|
+
return "No hay documentos en contingencia.";
|
|
1427
|
+
const linea = (d) => ` ${typeof d === "string" ? d : JSON.stringify(d)}`;
|
|
1428
|
+
return [
|
|
1429
|
+
fichas([
|
|
1430
|
+
["Procesados", procesados.length],
|
|
1431
|
+
["Rechazados", rechazados.length],
|
|
1432
|
+
["Restantes", datos.restantes]
|
|
1433
|
+
]),
|
|
1434
|
+
...rechazados.length ? ["", "Rechazados:", ...rechazados.map(linea)] : []
|
|
1435
|
+
].join("\n");
|
|
1436
|
+
});
|
|
1437
|
+
return void 0;
|
|
1438
|
+
}
|
|
1439
|
+
function fichaWebhook(w) {
|
|
1440
|
+
return fichas([
|
|
1441
|
+
["Id", w.id],
|
|
1442
|
+
["URL", w.url],
|
|
1443
|
+
["Descripci\xF3n", w.descripcion],
|
|
1444
|
+
["Eventos", Array.isArray(w.eventos) ? w.eventos.join(", ") : null],
|
|
1445
|
+
["Activo", siNo(w.activo)]
|
|
1446
|
+
]);
|
|
1447
|
+
}
|
|
1448
|
+
function mostrarSecreto(ctx, w, titulo) {
|
|
1449
|
+
mostrar(
|
|
1450
|
+
ctx,
|
|
1451
|
+
w,
|
|
1452
|
+
() => [
|
|
1453
|
+
titulo,
|
|
1454
|
+
fichaWebhook(w),
|
|
1455
|
+
"",
|
|
1456
|
+
`Secreto de firma: ${w.secreto}`,
|
|
1457
|
+
"Gu\xE1rdelo ahora: no se vuelve a mostrar. Verifique con \xE9l la cabecera OpenFactura-Signature."
|
|
1458
|
+
].join("\n")
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
async function webhooksList(ctx) {
|
|
1462
|
+
const { datos } = await ctx.api().pedir("GET", "/webhooks");
|
|
1463
|
+
mostrar(
|
|
1464
|
+
ctx,
|
|
1465
|
+
datos,
|
|
1466
|
+
() => datos.length ? tabla(
|
|
1467
|
+
["id", "url", "eventos", "activo", "descripci\xF3n"],
|
|
1468
|
+
datos.map((w) => [
|
|
1469
|
+
w.id,
|
|
1470
|
+
w.url,
|
|
1471
|
+
w.eventos.join(", "),
|
|
1472
|
+
siNo(w.activo),
|
|
1473
|
+
w.descripcion
|
|
1474
|
+
])
|
|
1475
|
+
) : "No hay webhooks."
|
|
1476
|
+
);
|
|
1477
|
+
return void 0;
|
|
1478
|
+
}
|
|
1479
|
+
async function webhooksAdd(ctx) {
|
|
1480
|
+
const texto2 = ctx.op("eventos");
|
|
1481
|
+
const eventos = texto2 ? texto2.split(",").map((e) => e.trim()).filter(Boolean) : [...EVENTOS_WEBHOOK];
|
|
1482
|
+
const desconocido = eventos.find((e) => !EVENTOS_WEBHOOK.includes(e));
|
|
1483
|
+
if (desconocido || !eventos.length) {
|
|
1484
|
+
throw new ErrorUso(`--eventos acepta, separados por coma: ${EVENTOS_WEBHOOK.join(", ")}`);
|
|
1485
|
+
}
|
|
1486
|
+
const cuerpo = { url: ctx.args[0], eventos };
|
|
1487
|
+
if (ctx.op("descripcion") !== void 0) cuerpo.descripcion = ctx.op("descripcion");
|
|
1488
|
+
const { datos } = await ctx.api().pedir("POST", "/webhooks", { cuerpo });
|
|
1489
|
+
mostrarSecreto(ctx, datos, "Webhook creado.");
|
|
1490
|
+
return void 0;
|
|
1491
|
+
}
|
|
1492
|
+
async function webhooksRemove(ctx) {
|
|
1493
|
+
const id = ctx.args[0];
|
|
1494
|
+
const { datos } = await ctx.api().pedir("DELETE", `/webhooks/${segmento(id)}`);
|
|
1495
|
+
mostrar(ctx, datos, () => `Webhook ${id} eliminado.`);
|
|
1496
|
+
return void 0;
|
|
1497
|
+
}
|
|
1498
|
+
async function webhooksTest(ctx) {
|
|
1499
|
+
const id = ctx.args[0];
|
|
1500
|
+
const { datos } = await ctx.api().pedir("POST", `/webhooks/${segmento(id)}/probar`);
|
|
1501
|
+
mostrar(
|
|
1502
|
+
ctx,
|
|
1503
|
+
datos,
|
|
1504
|
+
() => datos.entregado ? `Entregado: el destino respondi\xF3 HTTP ${datos.status}.` : `No se entreg\xF3: ${datos.error ?? "error desconocido"}.`
|
|
1505
|
+
);
|
|
1506
|
+
return datos.entregado ? SALIDA.ok : SALIDA.error;
|
|
1507
|
+
}
|
|
1508
|
+
async function webhooksDeliveries(ctx) {
|
|
1509
|
+
const id = ctx.args[0];
|
|
1510
|
+
const { datos } = await ctx.api().pedir("GET", `/webhooks/${segmento(id)}/entregas`);
|
|
1511
|
+
mostrar(
|
|
1512
|
+
ctx,
|
|
1513
|
+
datos,
|
|
1514
|
+
() => datos.length ? tabla(
|
|
1515
|
+
[
|
|
1516
|
+
"creado (UTC)",
|
|
1517
|
+
"evento",
|
|
1518
|
+
"estado",
|
|
1519
|
+
"intentos",
|
|
1520
|
+
"http",
|
|
1521
|
+
"error",
|
|
1522
|
+
"pr\xF3ximo intento",
|
|
1523
|
+
"id"
|
|
1524
|
+
],
|
|
1525
|
+
datos.map((e) => [
|
|
1526
|
+
fecha2(e.creado),
|
|
1527
|
+
e.evento,
|
|
1528
|
+
e.estado,
|
|
1529
|
+
e.intentos,
|
|
1530
|
+
e.ultimoStatus,
|
|
1531
|
+
e.ultimoError,
|
|
1532
|
+
fecha2(e.proximoIntento),
|
|
1533
|
+
e.id
|
|
1534
|
+
]),
|
|
1535
|
+
[3]
|
|
1536
|
+
) : "Este webhook todav\xEDa no tiene entregas."
|
|
1537
|
+
);
|
|
1538
|
+
return void 0;
|
|
1539
|
+
}
|
|
1540
|
+
async function webhooksRotate(ctx) {
|
|
1541
|
+
const id = ctx.args[0];
|
|
1542
|
+
const { datos } = await ctx.api().pedir("POST", `/webhooks/${segmento(id)}/rotar-secreto`);
|
|
1543
|
+
mostrarSecreto(ctx, datos, "Secreto rotado: el anterior ya no firma.");
|
|
1544
|
+
return void 0;
|
|
1545
|
+
}
|
|
1546
|
+
async function mcp(ctx) {
|
|
1547
|
+
const api = ctx.api();
|
|
1548
|
+
ctx.ent.stderr(
|
|
1549
|
+
`openfactura mcp: servidor MCP por stdio \u2192 ${ctx.cfg.url} (clave ${enmascarar(ctx.cfg.apiKey)})`
|
|
1550
|
+
);
|
|
1551
|
+
await servirMcp(api, ctx.version);
|
|
1552
|
+
return void 0;
|
|
1553
|
+
}
|
|
1554
|
+
var GRUPOS = {
|
|
1555
|
+
clientes: "Clientes guardados",
|
|
1556
|
+
productos: "Productos y servicios guardados",
|
|
1557
|
+
dte: "Documentos tributarios electr\xF3nicos",
|
|
1558
|
+
eventos: "Eventos de retorno (18) y de operaciones especiales (17)",
|
|
1559
|
+
contingencia: "Documentos emitidos en contingencia",
|
|
1560
|
+
webhooks: "Notificaciones firmadas a su sistema cuando cambia un documento"
|
|
1561
|
+
};
|
|
1562
|
+
var COMANDOS = [
|
|
1563
|
+
{
|
|
1564
|
+
nombre: "login",
|
|
1565
|
+
uso: "login [--url <url>] [--key <clave>]",
|
|
1566
|
+
resumen: "Verifica una clave de API y la guarda",
|
|
1567
|
+
opciones: ["key"],
|
|
1568
|
+
args: [0, 0],
|
|
1569
|
+
accion: login
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
nombre: "logout",
|
|
1573
|
+
uso: "logout",
|
|
1574
|
+
resumen: "Borra la clave guardada del perfil",
|
|
1575
|
+
opciones: [],
|
|
1576
|
+
args: [0, 0],
|
|
1577
|
+
accion: logout
|
|
1578
|
+
},
|
|
1579
|
+
{
|
|
1580
|
+
nombre: "whoami",
|
|
1581
|
+
uso: "whoami",
|
|
1582
|
+
resumen: "Muestra el actor, sus permisos, el ambiente y el servidor",
|
|
1583
|
+
opciones: [],
|
|
1584
|
+
args: [0, 0],
|
|
1585
|
+
accion: whoami
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
nombre: "emisor",
|
|
1589
|
+
uso: "emisor",
|
|
1590
|
+
resumen: "Datos del emisor, establecimientos y credenciales cargadas",
|
|
1591
|
+
opciones: [],
|
|
1592
|
+
args: [0, 0],
|
|
1593
|
+
accion: emisor
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
nombre: "clientes list",
|
|
1597
|
+
uso: "clientes list [--q <texto>]",
|
|
1598
|
+
resumen: "Lista o busca clientes",
|
|
1599
|
+
opciones: ["q"],
|
|
1600
|
+
args: [0, 0],
|
|
1601
|
+
accion: clientesList
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
nombre: "clientes add",
|
|
1605
|
+
uso: "clientes add --file <cliente.json> | --tipo-documento <36|13|03|02|37> --num-documento <n> --nombre <nombre> [\u2026]",
|
|
1606
|
+
resumen: "Crea un cliente",
|
|
1607
|
+
opciones: ["file", ...Object.keys(CAMPOS_CLIENTE)],
|
|
1608
|
+
args: [0, 0],
|
|
1609
|
+
accion: clientesAdd
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
nombre: "productos list",
|
|
1613
|
+
uso: "productos list [--q <texto>]",
|
|
1614
|
+
resumen: "Lista o busca productos",
|
|
1615
|
+
opciones: ["q"],
|
|
1616
|
+
args: [0, 0],
|
|
1617
|
+
accion: productosList
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
nombre: "productos add",
|
|
1621
|
+
uso: "productos add --file <producto.json> | --codigo <c> --descripcion <d> --precio <sin IVA> [\u2026]",
|
|
1622
|
+
resumen: "Crea un producto o servicio",
|
|
1623
|
+
opciones: ["file", ...Object.keys(CAMPOS_PRODUCTO)],
|
|
1624
|
+
args: [0, 0],
|
|
1625
|
+
accion: productosAdd
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
nombre: "catalogos",
|
|
1629
|
+
uso: "catalogos [<id>] [--q <texto>] [--departamento <06>]",
|
|
1630
|
+
resumen: "Cat\xE1logos oficiales del MH (CAT-012, CAT-019\u2026)",
|
|
1631
|
+
opciones: ["q", "departamento"],
|
|
1632
|
+
args: [0, 1],
|
|
1633
|
+
accion: catalogos
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
nombre: "dte validar",
|
|
1637
|
+
uso: "dte validar <archivo.json | ->",
|
|
1638
|
+
resumen: "Valida un DTE sin emitirlo (salida 1 si hay problemas)",
|
|
1639
|
+
opciones: [],
|
|
1640
|
+
args: [1, 1],
|
|
1641
|
+
accion: dteValidar
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
nombre: "dte emitir",
|
|
1645
|
+
uso: "dte emitir <archivo.json | -> [--idempotency-key <k>] [--yes]",
|
|
1646
|
+
resumen: "Emite un DTE: lo firma y lo transmite al MH",
|
|
1647
|
+
opciones: ["idempotency-key", "yes"],
|
|
1648
|
+
args: [1, 1],
|
|
1649
|
+
accion: dteEmitir
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
nombre: "dte list",
|
|
1653
|
+
uso: "dte list [--tipo <01>] [--estado <estado>] [--q <texto>] [--desde <AAAA-MM-DD>] [--hasta <AAAA-MM-DD>] [--pagina <n>]",
|
|
1654
|
+
resumen: "Lista documentos, del m\xE1s reciente al m\xE1s antiguo",
|
|
1655
|
+
opciones: ["tipo", "estado", "q", "desde", "hasta", "pagina"],
|
|
1656
|
+
args: [0, 0],
|
|
1657
|
+
accion: dteList
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
nombre: "dte get",
|
|
1661
|
+
uso: "dte get <codigoGeneracion>",
|
|
1662
|
+
resumen: "Detalle de un documento",
|
|
1663
|
+
opciones: [],
|
|
1664
|
+
args: [1, 1],
|
|
1665
|
+
accion: dteGet
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
nombre: "dte json",
|
|
1669
|
+
uso: "dte json <codigoGeneracion> [-o <archivo.json>]",
|
|
1670
|
+
resumen: "JSON oficial del DTE (firma y sello)",
|
|
1671
|
+
opciones: ["output"],
|
|
1672
|
+
args: [1, 1],
|
|
1673
|
+
accion: dteJson
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
nombre: "dte pdf",
|
|
1677
|
+
uso: "dte pdf <codigoGeneracion> [-o <archivo.pdf>]",
|
|
1678
|
+
resumen: "Descarga la representaci\xF3n gr\xE1fica en PDF",
|
|
1679
|
+
opciones: ["output"],
|
|
1680
|
+
args: [1, 1],
|
|
1681
|
+
accion: dtePdf
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
nombre: "dte invalidar",
|
|
1685
|
+
uso: "dte invalidar <codigoGeneracion> --tipo <1|2|3> [--motivo <texto>] [--reemplazo <codigo>] [--yes]",
|
|
1686
|
+
resumen: "Invalida un DTE procesado, o un evento de retorno u operaciones especiales (irreversible)",
|
|
1687
|
+
opciones: ["tipo", "motivo", "reemplazo", "yes"],
|
|
1688
|
+
args: [1, 1],
|
|
1689
|
+
accion: dteInvalidar
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
nombre: "dte retorno",
|
|
1693
|
+
uso: "dte retorno <codigoGeneracion> <archivo.json | -> [--idempotency-key <k>] [--yes]",
|
|
1694
|
+
resumen: "Registra un retorno (evento 18) sobre una Factura, Exportaci\xF3n o Sujeto Excluido",
|
|
1695
|
+
opciones: ["idempotency-key", "yes"],
|
|
1696
|
+
args: [2, 2],
|
|
1697
|
+
accion: dteRetorno
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
nombre: "eventos operaciones-especiales",
|
|
1701
|
+
uso: "eventos operaciones-especiales <archivo.json | -> [--idempotency-key <k>] [--yes]",
|
|
1702
|
+
resumen: "Informa facturas simplificadas o comprobantes de control interno (evento 17)",
|
|
1703
|
+
opciones: ["idempotency-key", "yes"],
|
|
1704
|
+
args: [1, 1],
|
|
1705
|
+
accion: eventosOperacionesEspeciales
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
nombre: "eventos list",
|
|
1709
|
+
uso: "eventos list [--tipo retorno|operaciones_especiales]",
|
|
1710
|
+
resumen: "Lista los eventos de retorno y operaciones especiales",
|
|
1711
|
+
opciones: ["tipo"],
|
|
1712
|
+
args: [0, 0],
|
|
1713
|
+
accion: eventosList
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
nombre: "eventos get",
|
|
1717
|
+
uso: "eventos get <codigoGeneracion>",
|
|
1718
|
+
resumen: "Muestra un evento",
|
|
1719
|
+
opciones: [],
|
|
1720
|
+
args: [1, 1],
|
|
1721
|
+
accion: eventosGet
|
|
1722
|
+
},
|
|
1723
|
+
{
|
|
1724
|
+
nombre: "eventos json",
|
|
1725
|
+
uso: "eventos json <codigoGeneracion> [-o <archivo.json>]",
|
|
1726
|
+
resumen: "Descarga el archivo JSON oficial del evento",
|
|
1727
|
+
opciones: ["output"],
|
|
1728
|
+
args: [1, 1],
|
|
1729
|
+
accion: eventosJson
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
nombre: "eventos pdf",
|
|
1733
|
+
uso: "eventos pdf <codigoGeneracion> [-o <archivo.pdf>]",
|
|
1734
|
+
resumen: "Descarga la representaci\xF3n gr\xE1fica del evento",
|
|
1735
|
+
opciones: ["output"],
|
|
1736
|
+
args: [1, 1],
|
|
1737
|
+
accion: eventosPdf
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
nombre: "contingencia transmitir",
|
|
1741
|
+
uso: "contingencia transmitir [--yes]",
|
|
1742
|
+
resumen: "Transmite al MH los DTE en contingencia",
|
|
1743
|
+
opciones: ["yes"],
|
|
1744
|
+
args: [0, 0],
|
|
1745
|
+
accion: contingenciaTransmitir
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
nombre: "webhooks list",
|
|
1749
|
+
uso: "webhooks list",
|
|
1750
|
+
resumen: "Lista los webhooks (sin su secreto)",
|
|
1751
|
+
opciones: [],
|
|
1752
|
+
args: [0, 0],
|
|
1753
|
+
accion: webhooksList
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
nombre: "webhooks add",
|
|
1757
|
+
uso: "webhooks add <https://url> [--eventos <e1,e2>] [--descripcion <texto>]",
|
|
1758
|
+
resumen: "Crea un webhook y muestra su secreto de firma una sola vez",
|
|
1759
|
+
opciones: ["eventos", "descripcion"],
|
|
1760
|
+
args: [1, 1],
|
|
1761
|
+
accion: webhooksAdd
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
nombre: "webhooks remove",
|
|
1765
|
+
uso: "webhooks remove <id>",
|
|
1766
|
+
resumen: "Elimina un webhook y su historial de entregas",
|
|
1767
|
+
opciones: [],
|
|
1768
|
+
args: [1, 1],
|
|
1769
|
+
accion: webhooksRemove
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
nombre: "webhooks test",
|
|
1773
|
+
uso: "webhooks test <id>",
|
|
1774
|
+
resumen: "Env\xEDa un evento webhook.prueba (salida 1 si no se entrega)",
|
|
1775
|
+
opciones: [],
|
|
1776
|
+
args: [1, 1],
|
|
1777
|
+
accion: webhooksTest
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
nombre: "webhooks deliveries",
|
|
1781
|
+
uso: "webhooks deliveries <id>",
|
|
1782
|
+
resumen: "\xDAltimas 50 entregas de un webhook",
|
|
1783
|
+
opciones: [],
|
|
1784
|
+
args: [1, 1],
|
|
1785
|
+
accion: webhooksDeliveries
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
nombre: "webhooks rotate",
|
|
1789
|
+
uso: "webhooks rotate <id>",
|
|
1790
|
+
resumen: "Genera un secreto de firma nuevo y lo muestra una vez",
|
|
1791
|
+
opciones: [],
|
|
1792
|
+
args: [1, 1],
|
|
1793
|
+
accion: webhooksRotate
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
nombre: "mcp",
|
|
1797
|
+
uso: "mcp",
|
|
1798
|
+
resumen: "Servidor MCP por stdio para Claude y otros clientes",
|
|
1799
|
+
opciones: [],
|
|
1800
|
+
args: [0, 0],
|
|
1801
|
+
accion: mcp
|
|
1802
|
+
}
|
|
1803
|
+
];
|
|
1804
|
+
|
|
1805
|
+
// src/cli.ts
|
|
1806
|
+
var VERSION = package_default.version;
|
|
1807
|
+
var OPCIONES = {
|
|
1808
|
+
help: { type: "boolean", short: "h", ayuda: "Muestra esta ayuda" },
|
|
1809
|
+
version: { type: "boolean", short: "v", ayuda: "Muestra la versi\xF3n" },
|
|
1810
|
+
json: { type: "boolean", ayuda: "Imprime el JSON de la API (para scripts)" },
|
|
1811
|
+
url: { type: "string", ayuda: "URL del servidor (o OPENFACTURA_URL)" },
|
|
1812
|
+
profile: {
|
|
1813
|
+
type: "string",
|
|
1814
|
+
ayuda: "Perfil de configuraci\xF3n (o OPENFACTURA_PROFILE; por defecto \xABdefault\xBB)"
|
|
1815
|
+
},
|
|
1816
|
+
yes: { type: "boolean", short: "y", ayuda: "No pedir confirmaci\xF3n con claves de producci\xF3n" },
|
|
1817
|
+
key: { type: "string", ayuda: "Clave de API; si se omite, se pide sin mostrarla" },
|
|
1818
|
+
q: { type: "string", ayuda: "Texto a buscar" },
|
|
1819
|
+
file: {
|
|
1820
|
+
type: "string",
|
|
1821
|
+
short: "f",
|
|
1822
|
+
ayuda: "Archivo JSON con los datos (\xAB-\xBB lee la entrada est\xE1ndar)"
|
|
1823
|
+
},
|
|
1824
|
+
output: { type: "string", short: "o", ayuda: "Archivo de salida" },
|
|
1825
|
+
"idempotency-key": {
|
|
1826
|
+
type: "string",
|
|
1827
|
+
ayuda: "Clave de idempotencia; por defecto se genera un UUID"
|
|
1828
|
+
},
|
|
1829
|
+
tipo: {
|
|
1830
|
+
type: "string",
|
|
1831
|
+
ayuda: "Tipo de DTE (01, 03\u2026); al invalidar, tipo de anulaci\xF3n (1, 2, 3); en eventos, retorno | operaciones_especiales"
|
|
1832
|
+
},
|
|
1833
|
+
estado: {
|
|
1834
|
+
type: "string",
|
|
1835
|
+
ayuda: "pendiente | procesado | rechazado | contingencia | invalidado"
|
|
1836
|
+
},
|
|
1837
|
+
desde: { type: "string", ayuda: "Fecha inicial AAAA-MM-DD" },
|
|
1838
|
+
hasta: { type: "string", ayuda: "Fecha final AAAA-MM-DD" },
|
|
1839
|
+
pagina: { type: "string", ayuda: "N\xFAmero de p\xE1gina" },
|
|
1840
|
+
motivo: { type: "string", ayuda: "Motivo de la invalidaci\xF3n" },
|
|
1841
|
+
reemplazo: { type: "string", ayuda: "C\xF3digo de generaci\xF3n del DTE que reemplaza al invalidado" },
|
|
1842
|
+
"tipo-documento": {
|
|
1843
|
+
type: "string",
|
|
1844
|
+
ayuda: "36 NIT | 13 DUI | 03 pasaporte | 02 carn\xE9 de residente | 37 otro"
|
|
1845
|
+
},
|
|
1846
|
+
"num-documento": { type: "string", ayuda: "N\xFAmero del documento" },
|
|
1847
|
+
nrc: { type: "string", ayuda: "NRC del contribuyente" },
|
|
1848
|
+
nombre: { type: "string", ayuda: "Nombre o raz\xF3n social" },
|
|
1849
|
+
"nombre-comercial": { type: "string", ayuda: "Nombre comercial" },
|
|
1850
|
+
"cod-actividad": { type: "string", ayuda: "C\xF3digo de actividad econ\xF3mica (CAT-019)" },
|
|
1851
|
+
"desc-actividad": { type: "string", ayuda: "Descripci\xF3n de la actividad econ\xF3mica" },
|
|
1852
|
+
departamento: { type: "string", ayuda: "C\xF3digo de departamento (CAT-012)" },
|
|
1853
|
+
municipio: { type: "string", ayuda: "C\xF3digo de municipio (CAT-013)" },
|
|
1854
|
+
distrito: { type: "string", ayuda: "C\xF3digo de distrito (CAT-008)" },
|
|
1855
|
+
complemento: { type: "string", ayuda: "Direcci\xF3n" },
|
|
1856
|
+
telefono: { type: "string", ayuda: "Tel\xE9fono" },
|
|
1857
|
+
correo: { type: "string", ayuda: "Correo electr\xF3nico" },
|
|
1858
|
+
codigo: { type: "string", ayuda: "C\xF3digo del producto" },
|
|
1859
|
+
descripcion: { type: "string", ayuda: "Descripci\xF3n (del producto o del webhook)" },
|
|
1860
|
+
eventos: {
|
|
1861
|
+
type: "string",
|
|
1862
|
+
ayuda: "Eventos del webhook separados por coma (por defecto todos): dte.procesado, dte.rechazado, dte.contingencia, dte.invalidado, retorno.procesado, operaciones_especiales.procesado"
|
|
1863
|
+
},
|
|
1864
|
+
precio: { type: "string", ayuda: "Precio unitario SIN IVA" },
|
|
1865
|
+
"tipo-item": {
|
|
1866
|
+
type: "string",
|
|
1867
|
+
ayuda: "1 bien | 2 servicio | 3 ambos | 4 otros tributos (por defecto 1)"
|
|
1868
|
+
},
|
|
1869
|
+
"uni-medida": { type: "string", ayuda: "Unidad de medida, CAT-014 (por defecto 59 = unidad)" },
|
|
1870
|
+
"tipo-venta": { type: "string", ayuda: "gravada | exenta | noSujeta (por defecto gravada)" }
|
|
1871
|
+
};
|
|
1872
|
+
var GLOBALES = ["help", "json", "url", "profile"];
|
|
1873
|
+
var bandera = (nombre) => {
|
|
1874
|
+
const o = OPCIONES[nombre];
|
|
1875
|
+
return `${o.short ? `-${o.short}, ` : ""}--${nombre}${o.type === "string" ? " <valor>" : ""}`;
|
|
1876
|
+
};
|
|
1877
|
+
var listaOpciones = (nombres) => {
|
|
1878
|
+
const ancho = Math.max(...nombres.map((n) => bandera(n).length));
|
|
1879
|
+
return nombres.map((n) => ` ${bandera(n).padEnd(ancho)} ${OPCIONES[n].ayuda}`).join("\n");
|
|
1880
|
+
};
|
|
1881
|
+
function ayudaGeneral(grupo) {
|
|
1882
|
+
const comandos = grupo ? COMANDOS.filter((c) => c.nombre.startsWith(`${grupo} `)) : COMANDOS;
|
|
1883
|
+
const ancho = Math.max(...comandos.map((c) => c.nombre.length));
|
|
1884
|
+
return [
|
|
1885
|
+
grupo ? `openfactura ${grupo} \u2014 ${GRUPOS[grupo]}` : `openfactura ${VERSION} \u2014 facturaci\xF3n electr\xF3nica de El Salvador (DTE) desde la terminal`,
|
|
1886
|
+
"",
|
|
1887
|
+
`Uso: openfactura ${grupo ? `${grupo} <subcomando>` : "<comando>"} [opciones]`,
|
|
1888
|
+
"",
|
|
1889
|
+
"Comandos:",
|
|
1890
|
+
...comandos.map((c) => ` ${c.nombre.padEnd(ancho)} ${c.resumen}`),
|
|
1891
|
+
"",
|
|
1892
|
+
"Opciones globales:",
|
|
1893
|
+
listaOpciones(grupo ? GLOBALES : [...GLOBALES, "version"]),
|
|
1894
|
+
"",
|
|
1895
|
+
"Use `openfactura <comando> --help` para ver las opciones de un comando.",
|
|
1896
|
+
...grupo ? [] : [
|
|
1897
|
+
"",
|
|
1898
|
+
"Entorno: OPENFACTURA_API_KEY, OPENFACTURA_URL, OPENFACTURA_PROFILE.",
|
|
1899
|
+
"Salida: 0 bien \xB7 1 error de la API o de validaci\xF3n \xB7 2 uso incorrecto \xB7 3 sin autenticar \xB7 4 l\xEDmite de peticiones."
|
|
1900
|
+
]
|
|
1901
|
+
].join("\n");
|
|
1902
|
+
}
|
|
1903
|
+
function ayudaComando(comando) {
|
|
1904
|
+
return [
|
|
1905
|
+
comando.resumen,
|
|
1906
|
+
"",
|
|
1907
|
+
`Uso: openfactura ${comando.uso}`,
|
|
1908
|
+
...comando.opciones.length ? ["", "Opciones:", listaOpciones(comando.opciones)] : [],
|
|
1909
|
+
"",
|
|
1910
|
+
"Opciones globales:",
|
|
1911
|
+
listaOpciones(GLOBALES)
|
|
1912
|
+
].join("\n");
|
|
1913
|
+
}
|
|
1914
|
+
function analizar(argv) {
|
|
1915
|
+
try {
|
|
1916
|
+
const opciones = Object.fromEntries(
|
|
1917
|
+
Object.entries(OPCIONES).map(([nombre, { type, short }]) => [
|
|
1918
|
+
nombre,
|
|
1919
|
+
short ? { type, short } : { type }
|
|
1920
|
+
])
|
|
1921
|
+
);
|
|
1922
|
+
return parseArgs({ args: argv, options: opciones, allowPositionals: true, strict: true });
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
const { code, message } = error;
|
|
1925
|
+
const opcion = /'(-[^' ]*)/.exec(message)?.[1];
|
|
1926
|
+
if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION")
|
|
1927
|
+
throw new ErrorUso(`Opci\xF3n desconocida: ${opcion ?? message}`);
|
|
1928
|
+
if (code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") {
|
|
1929
|
+
throw new ErrorUso(
|
|
1930
|
+
opcion ? `La opci\xF3n ${opcion} est\xE1 mal usada: revise si lleva o no un valor.` : message
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
throw new ErrorUso(message);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function informarError(error, ent, json) {
|
|
1937
|
+
if (error instanceof ErrorUso) {
|
|
1938
|
+
ent.stderr(`Error: ${error.message}`);
|
|
1939
|
+
ent.stderr("Use `openfactura --help` para ver los comandos.");
|
|
1940
|
+
return SALIDA.uso;
|
|
1941
|
+
}
|
|
1942
|
+
if (error instanceof ErrorApi) {
|
|
1943
|
+
if (json) {
|
|
1944
|
+
ent.stderr(
|
|
1945
|
+
JSON.stringify(
|
|
1946
|
+
error.retryAfter === null ? error.cuerpo : { ...error.cuerpo, retryAfter: error.retryAfter },
|
|
1947
|
+
null,
|
|
1948
|
+
2
|
|
1949
|
+
)
|
|
1950
|
+
);
|
|
1951
|
+
return error.salida;
|
|
1952
|
+
}
|
|
1953
|
+
ent.stderr(`Error [${error.code}]: ${error.message}`);
|
|
1954
|
+
for (const linea of detalles(error)) ent.stderr(` ${linea}`);
|
|
1955
|
+
if (error.status === 401)
|
|
1956
|
+
ent.stderr(" Revise OPENFACTURA_API_KEY o ejecute `openfactura login`.");
|
|
1957
|
+
if (error.status === 429) {
|
|
1958
|
+
ent.stderr(
|
|
1959
|
+
` Retry-After: ${error.retryAfter ?? "?"} (segundos que debe esperar antes de reintentar)`
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
return error.salida;
|
|
1963
|
+
}
|
|
1964
|
+
if (error instanceof ErrorCli) {
|
|
1965
|
+
ent.stderr(
|
|
1966
|
+
json ? JSON.stringify(error.cuerpo, null, 2) : `Error [${error.code}]: ${error.message}`
|
|
1967
|
+
);
|
|
1968
|
+
return error.salida;
|
|
1969
|
+
}
|
|
1970
|
+
ent.stderr(
|
|
1971
|
+
`Error inesperado: ${error instanceof Error ? error.stack ?? error.message : String(error)}`
|
|
1972
|
+
);
|
|
1973
|
+
return SALIDA.error;
|
|
1974
|
+
}
|
|
1975
|
+
function detalles(error) {
|
|
1976
|
+
const d = error.details;
|
|
1977
|
+
if (d === void 0 || d === null) return [];
|
|
1978
|
+
const problema = (p) => `${p.ruta || "(ra\xEDz)"}: ${p.mensaje}`;
|
|
1979
|
+
if (Array.isArray(d)) return d.map((p) => problema(p));
|
|
1980
|
+
if (Array.isArray(d.problemas)) return d.problemas.map((p) => problema(p));
|
|
1981
|
+
if (d.mh && typeof d.mh === "object") {
|
|
1982
|
+
const mh = d.mh;
|
|
1983
|
+
const observaciones = Array.isArray(mh.observaciones) ? mh.observaciones : [];
|
|
1984
|
+
return [
|
|
1985
|
+
`MH ${mh.codigo ?? ""}: ${mh.mensaje ?? ""}`.trim(),
|
|
1986
|
+
...observaciones.map((o) => `- ${o}`)
|
|
1987
|
+
];
|
|
1988
|
+
}
|
|
1989
|
+
if (typeof d.permiso === "string") return [`Permiso que falta en la clave: ${d.permiso}`];
|
|
1990
|
+
return [JSON.stringify(d)];
|
|
1991
|
+
}
|
|
1992
|
+
async function ejecutar(argv, ent) {
|
|
1993
|
+
let json = argv.includes("--json");
|
|
1994
|
+
try {
|
|
1995
|
+
const { values, positionals } = analizar(argv);
|
|
1996
|
+
const valores = values;
|
|
1997
|
+
json = valores.json === true;
|
|
1998
|
+
const [primero, segundo] = positionals;
|
|
1999
|
+
if (!primero) {
|
|
2000
|
+
if (valores.version) {
|
|
2001
|
+
ent.stdout(VERSION);
|
|
2002
|
+
return SALIDA.ok;
|
|
2003
|
+
}
|
|
2004
|
+
if (valores.help) {
|
|
2005
|
+
ent.stdout(ayudaGeneral());
|
|
2006
|
+
return SALIDA.ok;
|
|
2007
|
+
}
|
|
2008
|
+
throw new ErrorUso("Falta el comando.");
|
|
2009
|
+
}
|
|
2010
|
+
const esGrupo = Object.hasOwn(GRUPOS, primero);
|
|
2011
|
+
const nombre = esGrupo ? `${primero} ${segundo ?? ""}`.trim() : primero;
|
|
2012
|
+
const comando = COMANDOS.find((c) => c.nombre === nombre);
|
|
2013
|
+
if (!comando) {
|
|
2014
|
+
if (esGrupo && !segundo && valores.help) {
|
|
2015
|
+
ent.stdout(ayudaGeneral(primero));
|
|
2016
|
+
return SALIDA.ok;
|
|
2017
|
+
}
|
|
2018
|
+
const disponibles = COMANDOS.filter((c) => c.nombre.startsWith(`${primero} `)).map(
|
|
2019
|
+
(c) => c.nombre.split(" ")[1]
|
|
2020
|
+
);
|
|
2021
|
+
throw new ErrorUso(
|
|
2022
|
+
esGrupo ? `${segundo ? `Subcomando desconocido: ${primero} ${segundo}.` : `Falta el subcomando de \xAB${primero}\xBB.`} Disponibles: ${disponibles.join(", ")}` : `Comando desconocido: ${primero}`
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
if (valores.help) {
|
|
2026
|
+
ent.stdout(ayudaComando(comando));
|
|
2027
|
+
return SALIDA.ok;
|
|
2028
|
+
}
|
|
2029
|
+
const permitidas = /* @__PURE__ */ new Set([...GLOBALES, ...comando.opciones]);
|
|
2030
|
+
const sobrante = Object.keys(valores).find((opcion) => !permitidas.has(opcion));
|
|
2031
|
+
if (sobrante)
|
|
2032
|
+
throw new ErrorUso(
|
|
2033
|
+
`La opci\xF3n --${sobrante} no aplica a \xAB${comando.nombre}\xBB.
|
|
2034
|
+
Uso: openfactura ${comando.uso}`
|
|
2035
|
+
);
|
|
2036
|
+
const args = positionals.slice(esGrupo ? 2 : 1);
|
|
2037
|
+
const [minimo, maximo] = comando.args;
|
|
2038
|
+
if (args.length < minimo || args.length > maximo) {
|
|
2039
|
+
throw new ErrorUso(
|
|
2040
|
+
`${args.length < minimo ? "Faltan argumentos" : `Sobran argumentos (${args.slice(maximo).join(" ")})`}.
|
|
2041
|
+
Uso: openfactura ${comando.uso}`
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
const cfg = resolver(ent.env, {
|
|
2045
|
+
url: valores.url,
|
|
2046
|
+
perfil: valores.profile
|
|
2047
|
+
});
|
|
2048
|
+
let cliente2;
|
|
2049
|
+
const ctx = {
|
|
2050
|
+
ent,
|
|
2051
|
+
version: VERSION,
|
|
2052
|
+
args,
|
|
2053
|
+
json,
|
|
2054
|
+
si: valores.yes === true,
|
|
2055
|
+
cfg,
|
|
2056
|
+
op: (opcion) => typeof valores[opcion] === "string" ? valores[opcion] : void 0,
|
|
2057
|
+
api: () => {
|
|
2058
|
+
if (!cfg.apiKey) {
|
|
2059
|
+
throw new ErrorCli(
|
|
2060
|
+
"No hay una clave de API. Ejecute `openfactura login` o defina OPENFACTURA_API_KEY.",
|
|
2061
|
+
"no_autenticado",
|
|
2062
|
+
SALIDA.noAutenticado
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
if (!cliente2) {
|
|
2066
|
+
const { protocol, hostname } = new URL(cfg.url);
|
|
2067
|
+
if (protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(hostname)) {
|
|
2068
|
+
ent.stderr(`Aviso: ${cfg.url} no usa HTTPS; la clave de API viaja sin cifrar.`);
|
|
2069
|
+
}
|
|
2070
|
+
cliente2 = new ClienteApi({
|
|
2071
|
+
url: cfg.url,
|
|
2072
|
+
apiKey: cfg.apiKey,
|
|
2073
|
+
version: VERSION,
|
|
2074
|
+
fetch: ent.fetch
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
return cliente2;
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
return await comando.accion(ctx) ?? SALIDA.ok;
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
return informarError(error, ent, json);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// src/index.ts
|
|
2087
|
+
async function leerEntrada() {
|
|
2088
|
+
const partes = [];
|
|
2089
|
+
for await (const parte of process.stdin) partes.push(Buffer.from(parte));
|
|
2090
|
+
return Buffer.concat(partes).toString("utf8");
|
|
2091
|
+
}
|
|
2092
|
+
function preguntar(pregunta, oculto = false) {
|
|
2093
|
+
const { stdin, stderr } = process;
|
|
2094
|
+
return new Promise((resolve2, reject) => {
|
|
2095
|
+
let texto2 = "";
|
|
2096
|
+
stderr.write(pregunta);
|
|
2097
|
+
if (oculto) stdin.setRawMode(true);
|
|
2098
|
+
stdin.setEncoding("utf8");
|
|
2099
|
+
stdin.resume();
|
|
2100
|
+
const terminar = (accion) => {
|
|
2101
|
+
stdin.off("data", alRecibir);
|
|
2102
|
+
if (oculto) {
|
|
2103
|
+
stdin.setRawMode(false);
|
|
2104
|
+
stderr.write("\n");
|
|
2105
|
+
}
|
|
2106
|
+
stdin.pause();
|
|
2107
|
+
accion();
|
|
2108
|
+
};
|
|
2109
|
+
const alRecibir = (dato) => {
|
|
2110
|
+
for (const letra of dato) {
|
|
2111
|
+
if (letra === "\n" || letra === "\r" || letra === "")
|
|
2112
|
+
return terminar(() => resolve2(texto2));
|
|
2113
|
+
if (letra === "")
|
|
2114
|
+
return terminar(() => reject(new ErrorCli("Operaci\xF3n cancelada.", "cancelado")));
|
|
2115
|
+
if (letra === "\x7F" || letra === "\b") texto2 = texto2.slice(0, -1);
|
|
2116
|
+
else if (letra >= " ") texto2 += letra;
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
stdin.on("data", alRecibir);
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
var entorno = {
|
|
2123
|
+
env: process.env,
|
|
2124
|
+
fetch: (...args) => fetch(...args),
|
|
2125
|
+
stdout: (linea) => void process.stdout.write(`${linea}
|
|
2126
|
+
`),
|
|
2127
|
+
stderr: (linea) => void process.stderr.write(`${linea}
|
|
2128
|
+
`),
|
|
2129
|
+
leerEntrada,
|
|
2130
|
+
esTTY: Boolean(process.stdin.isTTY),
|
|
2131
|
+
preguntar,
|
|
2132
|
+
cwd: process.cwd()
|
|
2133
|
+
};
|
|
2134
|
+
process.stdout.on("error", (error) => {
|
|
2135
|
+
if (error.code === "EPIPE") process.exit(0);
|
|
2136
|
+
});
|
|
2137
|
+
process.exitCode = await ejecutar(process.argv.slice(2), entorno);
|