@factucat/cli 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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1023 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +28 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/current-draft.d.ts +3 -0
- package/dist/lib/current-draft.d.ts.map +1 -0
- package/dist/lib/current-draft.js +13 -0
- package/dist/lib/current-draft.js.map +1 -0
- package/dist/lib/errors.d.ts +3 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +23 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/output.d.ts +3 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +9 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/sdk.d.ts +2 -0
- package/dist/lib/sdk.d.ts.map +1 -0
- package/dist/lib/sdk.js +15 -0
- package/dist/lib/sdk.js.map +1 -0
- package/package.json +37 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { confirm, intro, isCancel, outro, select, spinner, text } from '@clack/prompts';
|
|
6
|
+
import { cac } from 'cac';
|
|
7
|
+
import { readConfig, writeConfig } from './lib/config';
|
|
8
|
+
import { getCurrentDraftId, setCurrentDraftId } from './lib/current-draft';
|
|
9
|
+
import { exitCodeFromError, getErrorMessage } from './lib/errors';
|
|
10
|
+
import { printHuman, printJson } from './lib/output';
|
|
11
|
+
import { getSdk } from './lib/sdk';
|
|
12
|
+
const HELP_TEXT = `factucat
|
|
13
|
+
|
|
14
|
+
Uso:
|
|
15
|
+
factucat <comando> [opciones]
|
|
16
|
+
|
|
17
|
+
Comandos:
|
|
18
|
+
auth api-key set
|
|
19
|
+
auth api-key show
|
|
20
|
+
auth api-key clear
|
|
21
|
+
auth status
|
|
22
|
+
invoice create
|
|
23
|
+
invoice use <draftId>
|
|
24
|
+
invoice current
|
|
25
|
+
invoice show
|
|
26
|
+
invoice list
|
|
27
|
+
invoice set-customer
|
|
28
|
+
invoice set-receiver
|
|
29
|
+
invoice set-meta
|
|
30
|
+
invoice add-item
|
|
31
|
+
invoice update-item <itemId>
|
|
32
|
+
invoice remove-item <itemId>
|
|
33
|
+
invoice items
|
|
34
|
+
invoice discard
|
|
35
|
+
invoice stamp
|
|
36
|
+
invoice get <invoiceRef>
|
|
37
|
+
invoice download xml <invoiceRef>
|
|
38
|
+
invoice download pdf <invoiceRef>
|
|
39
|
+
customer list
|
|
40
|
+
customer search <query>
|
|
41
|
+
customer create
|
|
42
|
+
|
|
43
|
+
Notas:
|
|
44
|
+
invoice add-item infiere automáticamente Clave SAT, unidad, IVA e ISR si no los especificas.
|
|
45
|
+
invoice set-meta --currency USD consulta automáticamente el tipo de cambio oficial del DOF si no mandas --exchange-rate.
|
|
46
|
+
invoice stamp muestra una previa antes de timbrar cuando hay terminal interactiva.
|
|
47
|
+
invoice stamp --send-to-customer-contacts solicita el envío por email y WhatsApp registrados en el cliente, si existen.
|
|
48
|
+
`;
|
|
49
|
+
function withJsonFlag(command) {
|
|
50
|
+
return command.option('--json', 'Imprime salida JSON estable');
|
|
51
|
+
}
|
|
52
|
+
function ensureDraftId(value) {
|
|
53
|
+
if (!value) {
|
|
54
|
+
throw new Error('No hay borrador activo. Usa `factucat invoice create` o `factucat invoice use <draft-id>`.');
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
async function resolveDraftId(explicit) {
|
|
59
|
+
if (explicit) {
|
|
60
|
+
return explicit;
|
|
61
|
+
}
|
|
62
|
+
return ensureDraftId(await getCurrentDraftId());
|
|
63
|
+
}
|
|
64
|
+
async function readStdinValue() {
|
|
65
|
+
if (process.stdin.isTTY) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const chunks = [];
|
|
69
|
+
for await (const chunk of process.stdin) {
|
|
70
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
71
|
+
}
|
|
72
|
+
const value = Buffer.concat(chunks).toString('utf8').trim();
|
|
73
|
+
return value.length > 0 ? value : undefined;
|
|
74
|
+
}
|
|
75
|
+
function normalizeArgv(rawArgv) {
|
|
76
|
+
const argv = rawArgv[2] === '--'
|
|
77
|
+
? [rawArgv[0], rawArgv[1], ...rawArgv.slice(3)]
|
|
78
|
+
: rawArgv;
|
|
79
|
+
const args = argv.slice(2);
|
|
80
|
+
if (args.length === 0) {
|
|
81
|
+
return { argv, showHelp: true };
|
|
82
|
+
}
|
|
83
|
+
if (args.length === 1 && ['--help', '-h', 'help'].includes(args[0] ?? '')) {
|
|
84
|
+
return { argv, showHelp: true };
|
|
85
|
+
}
|
|
86
|
+
const rewrites = [
|
|
87
|
+
{ from: ['auth', 'api-key', 'set'], to: 'auth-api-key-set' },
|
|
88
|
+
{ from: ['auth', 'api-key', 'show'], to: 'auth-api-key-show' },
|
|
89
|
+
{ from: ['auth', 'api-key', 'clear'], to: 'auth-api-key-clear' },
|
|
90
|
+
{ from: ['auth', 'status'], to: 'auth-status' },
|
|
91
|
+
{ from: ['invoice', 'create'], to: 'invoice-create' },
|
|
92
|
+
{ from: ['invoice', 'use'], to: 'invoice-use' },
|
|
93
|
+
{ from: ['invoice', 'current'], to: 'invoice-current' },
|
|
94
|
+
{ from: ['invoice', 'show'], to: 'invoice-show' },
|
|
95
|
+
{ from: ['invoice', 'list'], to: 'invoice-list' },
|
|
96
|
+
{ from: ['invoice', 'set-customer'], to: 'invoice-set-customer' },
|
|
97
|
+
{ from: ['invoice', 'set-receiver'], to: 'invoice-set-receiver' },
|
|
98
|
+
{ from: ['invoice', 'set-meta'], to: 'invoice-set-meta' },
|
|
99
|
+
{ from: ['invoice', 'add-item'], to: 'invoice-add-item' },
|
|
100
|
+
{ from: ['invoice', 'update-item'], to: 'invoice-update-item' },
|
|
101
|
+
{ from: ['invoice', 'remove-item'], to: 'invoice-remove-item' },
|
|
102
|
+
{ from: ['invoice', 'items'], to: 'invoice-items' },
|
|
103
|
+
{ from: ['invoice', 'summary'], to: 'invoice-show' },
|
|
104
|
+
{ from: ['invoice', 'discard'], to: 'invoice-discard' },
|
|
105
|
+
{ from: ['invoice', 'stamp'], to: 'invoice-stamp' },
|
|
106
|
+
{ from: ['invoice', 'get'], to: 'invoice-get' },
|
|
107
|
+
{ from: ['invoice', 'download', 'xml'], to: 'invoice-download-xml' },
|
|
108
|
+
{ from: ['invoice', 'download', 'pdf'], to: 'invoice-download-pdf' },
|
|
109
|
+
{ from: ['customer', 'list'], to: 'customer-list' },
|
|
110
|
+
{ from: ['customer', 'search'], to: 'customer-search' },
|
|
111
|
+
{ from: ['customer', 'create'], to: 'customer-create' },
|
|
112
|
+
];
|
|
113
|
+
for (const rewrite of rewrites) {
|
|
114
|
+
const matches = rewrite.from.every((part, index) => args[index] === part);
|
|
115
|
+
if (!matches) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
argv: [argv[0] ?? process.argv[0], argv[1] ?? process.argv[1], rewrite.to, ...args.slice(rewrite.from.length)],
|
|
120
|
+
showHelp: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { argv, showHelp: false };
|
|
124
|
+
}
|
|
125
|
+
function wantsJson(argv) {
|
|
126
|
+
return argv.includes('--json');
|
|
127
|
+
}
|
|
128
|
+
function canPromptInteractively(options) {
|
|
129
|
+
return !options.json && !options.noInput && process.stdin.isTTY && process.stdout.isTTY;
|
|
130
|
+
}
|
|
131
|
+
async function resolveDownloadPath(input) {
|
|
132
|
+
if (!input.output?.trim()) {
|
|
133
|
+
return resolve(process.cwd(), input.fileName);
|
|
134
|
+
}
|
|
135
|
+
const requestedPath = resolve(process.cwd(), input.output);
|
|
136
|
+
try {
|
|
137
|
+
const stats = await stat(requestedPath);
|
|
138
|
+
if (stats.isDirectory()) {
|
|
139
|
+
return resolve(requestedPath, input.fileName);
|
|
140
|
+
}
|
|
141
|
+
return requestedPath;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
if (input.output.endsWith('/') || input.output.endsWith('\\')) {
|
|
145
|
+
return resolve(requestedPath, input.fileName);
|
|
146
|
+
}
|
|
147
|
+
return requestedPath;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function saveDownloadedArtifact(input) {
|
|
151
|
+
const outputPath = await resolveDownloadPath({
|
|
152
|
+
fileName: input.fileName,
|
|
153
|
+
output: input.output,
|
|
154
|
+
});
|
|
155
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
156
|
+
await writeFile(outputPath, Buffer.from(input.base64, 'base64'));
|
|
157
|
+
return outputPath;
|
|
158
|
+
}
|
|
159
|
+
function isUuidLike(value) {
|
|
160
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value.trim());
|
|
161
|
+
}
|
|
162
|
+
async function resolveInvoiceReference(input) {
|
|
163
|
+
const normalizedRef = input.invoiceRef.trim();
|
|
164
|
+
if (!normalizedRef) {
|
|
165
|
+
throw new Error('Debes indicar un folio o invoiceId válido.');
|
|
166
|
+
}
|
|
167
|
+
if (isUuidLike(normalizedRef)) {
|
|
168
|
+
return normalizedRef;
|
|
169
|
+
}
|
|
170
|
+
const invoices = await input.sdk.listInvoices({
|
|
171
|
+
query: normalizedRef,
|
|
172
|
+
limit: 20,
|
|
173
|
+
});
|
|
174
|
+
const exactMatches = invoices.filter((invoice) => {
|
|
175
|
+
const target = normalizedRef.toLowerCase();
|
|
176
|
+
return (invoice.id.trim().toLowerCase() === target ||
|
|
177
|
+
invoice.folio.trim().toLowerCase() === target);
|
|
178
|
+
});
|
|
179
|
+
if (exactMatches.length === 1) {
|
|
180
|
+
return exactMatches[0].id;
|
|
181
|
+
}
|
|
182
|
+
if (exactMatches.length > 1) {
|
|
183
|
+
throw new Error(`Hay varias facturas con el folio \`${normalizedRef}\`. Usa el UUID de la factura para desambiguar.`);
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`No encontré una factura con el folio o invoiceId \`${normalizedRef}\`.`);
|
|
186
|
+
}
|
|
187
|
+
async function resolveCustomerReference(input) {
|
|
188
|
+
const normalizedRef = input.customerRef.trim();
|
|
189
|
+
if (!normalizedRef) {
|
|
190
|
+
throw new Error('Debes indicar un cliente válido.');
|
|
191
|
+
}
|
|
192
|
+
if (isUuidLike(normalizedRef)) {
|
|
193
|
+
return input.sdk.getCustomer(normalizedRef);
|
|
194
|
+
}
|
|
195
|
+
const customers = await input.sdk.listCustomers({
|
|
196
|
+
query: normalizedRef,
|
|
197
|
+
limit: 20,
|
|
198
|
+
});
|
|
199
|
+
const normalizedNeedle = normalizedRef.toLowerCase();
|
|
200
|
+
const exactMatches = customers.filter((customer) => {
|
|
201
|
+
return (customer.id.trim().toLowerCase() === normalizedNeedle ||
|
|
202
|
+
customer.legalName.trim().toLowerCase() === normalizedNeedle ||
|
|
203
|
+
customer.rfc.trim().toLowerCase() === normalizedNeedle);
|
|
204
|
+
});
|
|
205
|
+
if (exactMatches.length === 1) {
|
|
206
|
+
return exactMatches[0];
|
|
207
|
+
}
|
|
208
|
+
if (exactMatches.length > 1) {
|
|
209
|
+
throw new Error(`Hay varios clientes que coinciden exactamente con \`${normalizedRef}\`. Usa \`--customer-id\` para elegir uno.`);
|
|
210
|
+
}
|
|
211
|
+
if (customers.length === 1) {
|
|
212
|
+
return customers[0];
|
|
213
|
+
}
|
|
214
|
+
if (customers.length === 0) {
|
|
215
|
+
throw new Error(`No encontré un cliente con \`${normalizedRef}\`. Usa \`factucat customer search "${normalizedRef}"\` para revisar coincidencias.`);
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`Encontré varios clientes para \`${normalizedRef}\`. Usa \`factucat customer search "${normalizedRef}"\` o pasa \`--customer-id\`.`);
|
|
218
|
+
}
|
|
219
|
+
function getOwnerUserLabel(me) {
|
|
220
|
+
if (me.ownerUserName && me.ownerUserEmail) {
|
|
221
|
+
return `${me.ownerUserName} (${me.ownerUserEmail})`;
|
|
222
|
+
}
|
|
223
|
+
if (me.ownerUserEmail) {
|
|
224
|
+
return me.ownerUserEmail;
|
|
225
|
+
}
|
|
226
|
+
if (me.ownerUserName) {
|
|
227
|
+
return me.ownerUserName;
|
|
228
|
+
}
|
|
229
|
+
return me.ownerUserId;
|
|
230
|
+
}
|
|
231
|
+
function formatTaxRateLabel(rate) {
|
|
232
|
+
return `${Number((rate * 100).toFixed(2))}%`;
|
|
233
|
+
}
|
|
234
|
+
function formatDraftItemLine(input) {
|
|
235
|
+
const satCode = input.productCode ?? input.suggestedProductCode ?? '-';
|
|
236
|
+
return [
|
|
237
|
+
`${input.id} ${input.quantity} x ${input.description} ${input.amount} ${input.currency}`,
|
|
238
|
+
` SAT: ${satCode} · Unidad: ${input.unitCode} · IVA trasladado: ${formatTaxRateLabel(input.vatRate)} · ISR retenido: ${formatTaxRateLabel(input.isrWithholdingRate)}`,
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
function formatIsoDateLabel(value) {
|
|
242
|
+
if (!value) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
return value.split('T')[0] ?? value;
|
|
246
|
+
}
|
|
247
|
+
function formatOptionalValue(label, value) {
|
|
248
|
+
return `${label}: ${value === undefined || value === null || value === '' ? '-' : value}`;
|
|
249
|
+
}
|
|
250
|
+
function formatPreviewItemLine(input) {
|
|
251
|
+
const satCode = input.productCode ?? input.suggestedProductCode ?? '-';
|
|
252
|
+
return [
|
|
253
|
+
`${input.index}. ${input.description}`,
|
|
254
|
+
` Cantidad: ${input.quantity} · Precio unitario: ${input.unitPrice} ${input.currency} · Importe: ${input.amount} ${input.currency}`,
|
|
255
|
+
` SAT: ${satCode} · Unidad: ${input.unitCode} · IVA trasladado: ${formatTaxRateLabel(input.vatRate)} · ISR retenido: ${formatTaxRateLabel(input.isrWithholdingRate)}`,
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
function formatDraftPreview(draft) {
|
|
259
|
+
const lines = [
|
|
260
|
+
`Factura: ${draft.id}`,
|
|
261
|
+
`Estado: ${draft.status}`,
|
|
262
|
+
formatOptionalValue('Serie', draft.serie),
|
|
263
|
+
formatOptionalValue('Folio', draft.folio),
|
|
264
|
+
formatOptionalValue('UUID SAT', draft.satUuid),
|
|
265
|
+
'',
|
|
266
|
+
'Receptor:',
|
|
267
|
+
formatOptionalValue(' Razón social', draft.receiverLegalName),
|
|
268
|
+
formatOptionalValue(' RFC', draft.receiverRfc),
|
|
269
|
+
formatOptionalValue(' Código postal', draft.receiverPostalCode),
|
|
270
|
+
formatOptionalValue(' Régimen fiscal', draft.receiverTaxRegime),
|
|
271
|
+
formatOptionalValue(' Residencia fiscal', draft.receiverFiscalResidence),
|
|
272
|
+
formatOptionalValue(' Tax ID', draft.receiverTaxId),
|
|
273
|
+
formatOptionalValue(' Correo', draft.receiverEmail),
|
|
274
|
+
'',
|
|
275
|
+
'Comprobante:',
|
|
276
|
+
formatOptionalValue(' Tipo CFDI', draft.cfdiType),
|
|
277
|
+
formatOptionalValue(' Uso CFDI', draft.cfdiUse),
|
|
278
|
+
formatOptionalValue(' Método de pago', draft.paymentMethod),
|
|
279
|
+
formatOptionalValue(' Forma de pago', draft.paymentForm),
|
|
280
|
+
formatOptionalValue(' Moneda', draft.currency),
|
|
281
|
+
formatOptionalValue(' Tipo de cambio', draft.exchangeRate),
|
|
282
|
+
formatOptionalValue(' Fecha TC', formatIsoDateLabel(draft.exchangeRateDate)),
|
|
283
|
+
formatOptionalValue(' Idioma PDF', draft.pdfLanguage),
|
|
284
|
+
formatOptionalValue(' Notas', draft.customerNotes),
|
|
285
|
+
'',
|
|
286
|
+
`Conceptos (${draft.items.length}):`,
|
|
287
|
+
...(draft.items.length > 0
|
|
288
|
+
? draft.items.flatMap((item, index) => formatPreviewItemLine({
|
|
289
|
+
index: index + 1,
|
|
290
|
+
description: item.description,
|
|
291
|
+
quantity: item.quantity,
|
|
292
|
+
unitPrice: item.unitPrice,
|
|
293
|
+
amount: item.amount,
|
|
294
|
+
currency: draft.currency,
|
|
295
|
+
productCode: item.productCode,
|
|
296
|
+
suggestedProductCode: item.suggestedProductCode,
|
|
297
|
+
unitCode: item.unitCode,
|
|
298
|
+
vatRate: item.vatRate,
|
|
299
|
+
isrWithholdingRate: item.isrWithholdingRate,
|
|
300
|
+
}))
|
|
301
|
+
: ['Sin conceptos todavía.']),
|
|
302
|
+
'',
|
|
303
|
+
'Totales:',
|
|
304
|
+
` Subtotal: ${draft.subtotal} ${draft.currency}`,
|
|
305
|
+
` Impuestos: ${draft.taxTotal} ${draft.currency}`,
|
|
306
|
+
` Total: ${draft.total} ${draft.currency}`,
|
|
307
|
+
formatOptionalValue(' Emitida', formatIsoDateLabel(draft.issuedAt)),
|
|
308
|
+
formatOptionalValue(' Cancelada', formatIsoDateLabel(draft.cancelledAt)),
|
|
309
|
+
formatOptionalValue(' Error', draft.errorMessage),
|
|
310
|
+
];
|
|
311
|
+
return lines;
|
|
312
|
+
}
|
|
313
|
+
function formatDeliverySummary(input) {
|
|
314
|
+
const lines = [];
|
|
315
|
+
const requestedDelivery = Boolean(input.sendCustomerEmail || input.sendCustomerWhatsApp);
|
|
316
|
+
const sendInvoicesEnabled = input.customer?.sendInvoices ?? false;
|
|
317
|
+
if (requestedDelivery && !input.customer) {
|
|
318
|
+
return [
|
|
319
|
+
'Se solicitó entrega al cliente, pero este borrador no tiene un cliente ligado con contactos registrados.',
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
if (requestedDelivery && !sendInvoicesEnabled) {
|
|
323
|
+
return [
|
|
324
|
+
'Se solicitó entrega al cliente, pero el cliente tiene desactivado el envío de facturas.',
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
if (!sendInvoicesEnabled) {
|
|
328
|
+
return lines;
|
|
329
|
+
}
|
|
330
|
+
if (input.sendCustomerEmail) {
|
|
331
|
+
lines.push(input.customer?.email
|
|
332
|
+
? `Entrega solicitada por correo: ${input.customer.email}`
|
|
333
|
+
: 'Se solicitó entrega por correo, pero el cliente no tiene email registrado.');
|
|
334
|
+
}
|
|
335
|
+
if (input.sendCustomerWhatsApp) {
|
|
336
|
+
lines.push(input.customer?.whatsappPhone
|
|
337
|
+
? `Entrega solicitada por WhatsApp: ${input.customer.whatsappPhone}`
|
|
338
|
+
: 'Se solicitó entrega por WhatsApp, pero el cliente no tiene número registrado.');
|
|
339
|
+
}
|
|
340
|
+
return lines;
|
|
341
|
+
}
|
|
342
|
+
async function loadDraftCustomer(sdk, draft) {
|
|
343
|
+
if (!draft.customerId) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
return await sdk.getCustomer(draft.customerId);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function resolveInteractiveStampInput(input) {
|
|
354
|
+
const customer = await loadDraftCustomer(input.sdk, input.draft);
|
|
355
|
+
printHuman(formatDraftPreview(input.draft));
|
|
356
|
+
const confirmed = await confirm({
|
|
357
|
+
message: '¿Timbramos esta factura ahora?',
|
|
358
|
+
active: 'Sí, timbrar',
|
|
359
|
+
inactive: 'No, cancelar',
|
|
360
|
+
initialValue: true,
|
|
361
|
+
});
|
|
362
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
363
|
+
outro('Timbrado cancelado.');
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
if (input.options.sendToCustomerContacts !== undefined ||
|
|
367
|
+
input.options.sendCustomerEmail !== undefined ||
|
|
368
|
+
input.options.sendCustomerWhatsApp !== undefined) {
|
|
369
|
+
return {
|
|
370
|
+
stampInput: {
|
|
371
|
+
sendToCustomerContacts: input.options.sendToCustomerContacts,
|
|
372
|
+
sendCustomerEmail: input.options.sendCustomerEmail,
|
|
373
|
+
sendCustomerWhatsApp: input.options.sendCustomerWhatsApp,
|
|
374
|
+
},
|
|
375
|
+
customer,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (!customer?.sendInvoices) {
|
|
379
|
+
return {
|
|
380
|
+
stampInput: {},
|
|
381
|
+
customer,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const canSendEmail = Boolean(customer.email);
|
|
385
|
+
const canSendWhatsApp = Boolean(customer.whatsappPhone);
|
|
386
|
+
if (!canSendEmail && !canSendWhatsApp) {
|
|
387
|
+
return {
|
|
388
|
+
stampInput: {},
|
|
389
|
+
customer,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const deliveryChoice = await select({
|
|
393
|
+
message: 'Después de timbrarla, ¿quieres que la enviemos al cliente?',
|
|
394
|
+
options: [
|
|
395
|
+
{ value: 'none', label: 'No enviar todavía' },
|
|
396
|
+
...(canSendEmail && canSendWhatsApp
|
|
397
|
+
? [{
|
|
398
|
+
value: 'both',
|
|
399
|
+
label: 'Sí, por correo y WhatsApp',
|
|
400
|
+
hint: `${customer.email} · ${customer.whatsappPhone}`,
|
|
401
|
+
}]
|
|
402
|
+
: []),
|
|
403
|
+
...(canSendEmail
|
|
404
|
+
? [{ value: 'email', label: 'Sí, solo por correo', hint: customer.email }]
|
|
405
|
+
: []),
|
|
406
|
+
...(canSendWhatsApp
|
|
407
|
+
? [{
|
|
408
|
+
value: 'whatsapp',
|
|
409
|
+
label: 'Sí, solo por WhatsApp',
|
|
410
|
+
hint: customer.whatsappPhone,
|
|
411
|
+
}]
|
|
412
|
+
: []),
|
|
413
|
+
],
|
|
414
|
+
initialValue: canSendEmail && canSendWhatsApp
|
|
415
|
+
? 'both'
|
|
416
|
+
: canSendEmail
|
|
417
|
+
? 'email'
|
|
418
|
+
: canSendWhatsApp
|
|
419
|
+
? 'whatsapp'
|
|
420
|
+
: 'none',
|
|
421
|
+
});
|
|
422
|
+
if (isCancel(deliveryChoice)) {
|
|
423
|
+
outro('Timbrado cancelado.');
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
stampInput: {
|
|
428
|
+
sendCustomerEmail: deliveryChoice === 'both' || deliveryChoice === 'email',
|
|
429
|
+
sendCustomerWhatsApp: deliveryChoice === 'both' || deliveryChoice === 'whatsapp',
|
|
430
|
+
},
|
|
431
|
+
customer,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function parseVatFlag(value) {
|
|
435
|
+
const normalized = value.trim();
|
|
436
|
+
const parsed = Number(normalized);
|
|
437
|
+
if (Number.isNaN(parsed)) {
|
|
438
|
+
throw new Error('IVA inválido. Usa 0, 10 o 16.');
|
|
439
|
+
}
|
|
440
|
+
if (parsed === 0 || parsed === 0.1 || parsed === 0.16) {
|
|
441
|
+
return parsed;
|
|
442
|
+
}
|
|
443
|
+
if (parsed === 10) {
|
|
444
|
+
return 0.1;
|
|
445
|
+
}
|
|
446
|
+
if (parsed === 16) {
|
|
447
|
+
return 0.16;
|
|
448
|
+
}
|
|
449
|
+
throw new Error('IVA inválido. Usa 0, 10 o 16.');
|
|
450
|
+
}
|
|
451
|
+
function parseIsrWithholdingFlag(value) {
|
|
452
|
+
const normalized = value.trim();
|
|
453
|
+
const parsed = Number(normalized);
|
|
454
|
+
if (Number.isNaN(parsed)) {
|
|
455
|
+
throw new Error('ISR retenido inválido. Usa 0, 1.25 o 10.');
|
|
456
|
+
}
|
|
457
|
+
if (parsed === 0 || parsed === 0.0125 || parsed === 0.1) {
|
|
458
|
+
return parsed;
|
|
459
|
+
}
|
|
460
|
+
if (parsed === 1.25) {
|
|
461
|
+
return 0.0125;
|
|
462
|
+
}
|
|
463
|
+
if (parsed === 10) {
|
|
464
|
+
return 0.1;
|
|
465
|
+
}
|
|
466
|
+
throw new Error('ISR retenido inválido. Usa 0, 1.25 o 10.');
|
|
467
|
+
}
|
|
468
|
+
async function main() {
|
|
469
|
+
const { argv, showHelp } = normalizeArgv(process.argv);
|
|
470
|
+
if (showHelp) {
|
|
471
|
+
printHuman(HELP_TEXT.trim().split('\n'));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const cli = cac('factucat');
|
|
475
|
+
cli
|
|
476
|
+
.command('auth-api-key-set', 'Guardar API key')
|
|
477
|
+
.option('--value <apiKey>', 'API key a guardar')
|
|
478
|
+
.action(async (options) => {
|
|
479
|
+
const stdinValue = await readStdinValue();
|
|
480
|
+
let value = options.value?.trim() || stdinValue;
|
|
481
|
+
if (!value) {
|
|
482
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
483
|
+
throw new Error('No se detectó terminal interactiva. Usa `--value <apiKey>` o envía la API key por stdin.');
|
|
484
|
+
}
|
|
485
|
+
intro('Configurar API key');
|
|
486
|
+
const promptValue = await text({
|
|
487
|
+
message: 'Pega tu API key de FactuCat',
|
|
488
|
+
placeholder: 'fc_live_...',
|
|
489
|
+
validate(input) {
|
|
490
|
+
return (input ?? '').trim().length > 0
|
|
491
|
+
? undefined
|
|
492
|
+
: 'La API key es obligatoria.';
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
if (isCancel(promptValue)) {
|
|
496
|
+
outro('Cancelado.');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
value = promptValue.trim();
|
|
500
|
+
}
|
|
501
|
+
const config = await readConfig();
|
|
502
|
+
await writeConfig({
|
|
503
|
+
...config,
|
|
504
|
+
apiKey: value,
|
|
505
|
+
});
|
|
506
|
+
outro('API key guardada.');
|
|
507
|
+
});
|
|
508
|
+
cli
|
|
509
|
+
.command('auth-api-key-show', 'Mostrar API key actual')
|
|
510
|
+
.action(async () => {
|
|
511
|
+
const config = await readConfig();
|
|
512
|
+
const apiKey = process.env.FACTUCAT_API_KEY ?? config.apiKey;
|
|
513
|
+
printHuman([apiKey ? apiKey : 'No hay API key configurada.']);
|
|
514
|
+
});
|
|
515
|
+
cli
|
|
516
|
+
.command('auth-api-key-clear', 'Eliminar API key guardada')
|
|
517
|
+
.action(async () => {
|
|
518
|
+
const config = await readConfig();
|
|
519
|
+
await writeConfig({
|
|
520
|
+
...config,
|
|
521
|
+
apiKey: undefined,
|
|
522
|
+
});
|
|
523
|
+
outro('API key eliminada.');
|
|
524
|
+
});
|
|
525
|
+
withJsonFlag(cli.command('auth-status', 'Validar autenticación')).action(async (options) => {
|
|
526
|
+
const sdk = await getSdk();
|
|
527
|
+
const me = await sdk.getMe();
|
|
528
|
+
if (options.json) {
|
|
529
|
+
printJson(me);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
printHuman([
|
|
533
|
+
'Autenticación correcta.',
|
|
534
|
+
`Actor: ${me.actorType}`,
|
|
535
|
+
`Owner user: ${getOwnerUserLabel(me)}`,
|
|
536
|
+
`Scopes: ${me.scopes.join(', ')}`,
|
|
537
|
+
]);
|
|
538
|
+
});
|
|
539
|
+
withJsonFlag(cli
|
|
540
|
+
.command('invoice-create', 'Crear borrador remoto')
|
|
541
|
+
.option('--customer-id <customerId>', 'ID del cliente')
|
|
542
|
+
.option('--customer <customerName>', 'Busca el cliente por nombre o RFC y lo asigna automáticamente al borrador')
|
|
543
|
+
.option('--draft', 'Crear y dejar como borrador')
|
|
544
|
+
.option('--no-input', 'Nunca preguntar datos faltantes')).action(async (options) => {
|
|
545
|
+
const commandOptions = options;
|
|
546
|
+
const sdk = await getSdk();
|
|
547
|
+
const resolvedCustomer = commandOptions.customerId
|
|
548
|
+
? await sdk.getCustomer(commandOptions.customerId)
|
|
549
|
+
: commandOptions.customer
|
|
550
|
+
? await resolveCustomerReference({
|
|
551
|
+
sdk,
|
|
552
|
+
customerRef: commandOptions.customer,
|
|
553
|
+
})
|
|
554
|
+
: null;
|
|
555
|
+
const draft = await sdk.createDraft(resolvedCustomer ? { customerId: resolvedCustomer.id } : {}, { idempotencyKey: randomUUID() });
|
|
556
|
+
await setCurrentDraftId(draft.id);
|
|
557
|
+
if (commandOptions.json) {
|
|
558
|
+
printJson(draft);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
printHuman([
|
|
562
|
+
`Borrador creado: ${draft.id}`,
|
|
563
|
+
`Cliente: ${resolvedCustomer?.legalName ?? 'sin resolver'}`,
|
|
564
|
+
'Usa `factucat invoice add-item` para agregar conceptos.',
|
|
565
|
+
]);
|
|
566
|
+
});
|
|
567
|
+
withJsonFlag(cli.command('invoice-use <draftId>', 'Seleccionar borrador actual')).action(async (draftId, options) => {
|
|
568
|
+
await setCurrentDraftId(draftId);
|
|
569
|
+
if (options.json) {
|
|
570
|
+
printJson({ currentDraftId: draftId });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
printHuman([`Borrador activo: ${draftId}`]);
|
|
574
|
+
});
|
|
575
|
+
withJsonFlag(cli.command('invoice-current', 'Ver borrador activo')).action(async (options) => {
|
|
576
|
+
const currentDraftId = await getCurrentDraftId();
|
|
577
|
+
if (options.json) {
|
|
578
|
+
printJson({ currentDraftId: currentDraftId ?? null });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
printHuman([
|
|
582
|
+
currentDraftId
|
|
583
|
+
? `Borrador activo: ${currentDraftId}`
|
|
584
|
+
: 'No hay borrador activo.',
|
|
585
|
+
]);
|
|
586
|
+
});
|
|
587
|
+
withJsonFlag(cli
|
|
588
|
+
.command('invoice-show', 'Mostrar previa completa del borrador')
|
|
589
|
+
.option('--draft-id <draftId>', 'ID del borrador')).action(async (options) => {
|
|
590
|
+
const sdk = await getSdk();
|
|
591
|
+
const draftId = await resolveDraftId(options.draftId);
|
|
592
|
+
const draft = await sdk.getDraft(draftId);
|
|
593
|
+
if (options.json) {
|
|
594
|
+
printJson(draft);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
printHuman(formatDraftPreview(draft));
|
|
598
|
+
});
|
|
599
|
+
withJsonFlag(cli
|
|
600
|
+
.command('invoice-list', 'Listar facturas')
|
|
601
|
+
.option('--status <status>', 'Estado')
|
|
602
|
+
.option('--query <query>', 'Búsqueda')
|
|
603
|
+
.option('--limit <limit>', 'Límite')).action(async (options) => {
|
|
604
|
+
const commandOptions = options;
|
|
605
|
+
const sdk = await getSdk();
|
|
606
|
+
const input = {
|
|
607
|
+
status: commandOptions.status,
|
|
608
|
+
query: commandOptions.query,
|
|
609
|
+
limit: commandOptions.limit ? Number(commandOptions.limit) : undefined,
|
|
610
|
+
};
|
|
611
|
+
const invoices = await sdk.listInvoices(input);
|
|
612
|
+
if (commandOptions.json) {
|
|
613
|
+
printJson(invoices);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
printHuman(invoices.map((invoice) => `${invoice.id} ${invoice.folio} ${invoice.status} ${invoice.receiverLegalName}`));
|
|
617
|
+
});
|
|
618
|
+
withJsonFlag(cli
|
|
619
|
+
.command('invoice-set-customer', 'Asignar cliente al borrador actual')
|
|
620
|
+
.option('--id <customerId>', 'ID del cliente')
|
|
621
|
+
.option('--draft-id <draftId>', 'ID del borrador')).action(async (options) => {
|
|
622
|
+
if (!options.id) {
|
|
623
|
+
throw new Error('Debes indicar `--id <customerId>`.');
|
|
624
|
+
}
|
|
625
|
+
const sdk = await getSdk();
|
|
626
|
+
const draftId = await resolveDraftId(options.draftId);
|
|
627
|
+
const draft = await sdk.setDraftReceiver(draftId, { customerId: options.id }, { idempotencyKey: randomUUID() });
|
|
628
|
+
if (options.json) {
|
|
629
|
+
printJson(draft);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
printHuman([`Cliente asignado al borrador ${draft.id}.`]);
|
|
633
|
+
});
|
|
634
|
+
withJsonFlag(cli
|
|
635
|
+
.command('invoice-set-receiver', 'Actualizar datos del receptor')
|
|
636
|
+
.option('--draft-id <draftId>', 'ID del borrador')
|
|
637
|
+
.option('--rfc <rfc>', 'RFC del receptor')
|
|
638
|
+
.option('--legal-name <legalName>', 'Razón social del receptor')
|
|
639
|
+
.option('--postal-code <postalCode>', 'Código postal del receptor')
|
|
640
|
+
.option('--tax-regime <taxRegime>', 'Régimen fiscal del receptor')
|
|
641
|
+
.option('--email <email>', 'Correo del receptor')).action(async (options) => {
|
|
642
|
+
const sdk = await getSdk();
|
|
643
|
+
const draftId = await resolveDraftId(options.draftId);
|
|
644
|
+
const input = {
|
|
645
|
+
receiverRfc: options.rfc,
|
|
646
|
+
receiverLegalName: options.legalName,
|
|
647
|
+
receiverPostalCode: options.postalCode,
|
|
648
|
+
receiverTaxRegime: options.taxRegime,
|
|
649
|
+
receiverEmail: options.email,
|
|
650
|
+
};
|
|
651
|
+
const draft = await sdk.setDraftReceiver(draftId, input, {
|
|
652
|
+
idempotencyKey: randomUUID(),
|
|
653
|
+
});
|
|
654
|
+
if (options.json) {
|
|
655
|
+
printJson(draft);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
printHuman([`Receptor actualizado en ${draft.id}.`]);
|
|
659
|
+
});
|
|
660
|
+
withJsonFlag(cli
|
|
661
|
+
.command('invoice-set-meta', 'Actualizar metadatos del borrador. Si usas USD sin --exchange-rate, se consulta automáticamente el DOF.')
|
|
662
|
+
.option('--draft-id <draftId>', 'ID del borrador')
|
|
663
|
+
.option('--cfdi-use <cfdiUse>', 'Uso CFDI')
|
|
664
|
+
.option('--payment-method <paymentMethod>', 'Método de pago')
|
|
665
|
+
.option('--payment-form <paymentForm>', 'Forma de pago')
|
|
666
|
+
.option('--currency <currency>', 'Moneda')
|
|
667
|
+
.option('--exchange-rate <exchangeRate>', 'Tipo de cambio. Si lo omites y usas USD, se infiere automáticamente desde el DOF.')
|
|
668
|
+
.option('--vat-rate <vatRate>', 'Tasa de IVA')
|
|
669
|
+
.option('--customer-notes <customerNotes>', 'Notas para el cliente')).action(async (options) => {
|
|
670
|
+
const sdk = await getSdk();
|
|
671
|
+
const draftId = await resolveDraftId(options.draftId);
|
|
672
|
+
const input = {
|
|
673
|
+
cfdiUse: options.cfdiUse,
|
|
674
|
+
paymentMethod: options.paymentMethod,
|
|
675
|
+
paymentForm: options.paymentForm,
|
|
676
|
+
currency: options.currency,
|
|
677
|
+
exchangeRate: options.exchangeRate === undefined ? undefined : Number(options.exchangeRate),
|
|
678
|
+
vatRate: options.vatRate === undefined
|
|
679
|
+
? undefined
|
|
680
|
+
: Number(options.vatRate),
|
|
681
|
+
customerNotes: options.customerNotes,
|
|
682
|
+
};
|
|
683
|
+
const autoResolvedExchangeRate = options.currency !== undefined && options.exchangeRate === undefined;
|
|
684
|
+
const exchangeRateLoading = !options.json &&
|
|
685
|
+
options.currency === 'USD' &&
|
|
686
|
+
options.exchangeRate === undefined
|
|
687
|
+
? spinner()
|
|
688
|
+
: null;
|
|
689
|
+
if (exchangeRateLoading) {
|
|
690
|
+
exchangeRateLoading.start('Consultando tipo de cambio oficial del DOF...');
|
|
691
|
+
}
|
|
692
|
+
const draft = await sdk.setDraftMeta(draftId, input, {
|
|
693
|
+
idempotencyKey: randomUUID(),
|
|
694
|
+
});
|
|
695
|
+
if (exchangeRateLoading) {
|
|
696
|
+
exchangeRateLoading.stop('Tipo de cambio aplicado al borrador.');
|
|
697
|
+
}
|
|
698
|
+
if (options.json) {
|
|
699
|
+
printJson(draft);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const lines = [`Metadatos actualizados en ${draft.id}.`];
|
|
703
|
+
if (autoResolvedExchangeRate) {
|
|
704
|
+
const exchangeRateDate = formatIsoDateLabel(draft.exchangeRateDate);
|
|
705
|
+
lines.push(exchangeRateDate
|
|
706
|
+
? `Tipo de cambio aplicado automáticamente: ${draft.exchangeRate} · Fecha: ${exchangeRateDate}`
|
|
707
|
+
: `Tipo de cambio aplicado automáticamente: ${draft.exchangeRate}`);
|
|
708
|
+
}
|
|
709
|
+
printHuman(lines);
|
|
710
|
+
});
|
|
711
|
+
withJsonFlag(cli
|
|
712
|
+
.command('invoice-add-item', 'Agregar concepto al borrador. Si omites Clave SAT, unidad o impuestos, FactuCat intenta inferirlos automáticamente.')
|
|
713
|
+
.option('--draft-id <draftId>', 'ID del borrador')
|
|
714
|
+
.option('--description <description>', 'Descripción')
|
|
715
|
+
.option('--unit-price <unitPrice>', 'Precio unitario')
|
|
716
|
+
.option('--quantity <quantity>', 'Cantidad', { default: '1' })
|
|
717
|
+
.option('--iva <iva>', 'IVA del concepto: 0, 10 o 16. Si lo omites, se infiere automáticamente.')
|
|
718
|
+
.option('--vat <vat>', 'Alias de compatibilidad para `--iva`')
|
|
719
|
+
.option('--isr-retenido <isrRetenido>', 'ISR retenido: 0, 1.25 o 10. Si lo omites, se infiere automáticamente.')
|
|
720
|
+
.option('--isr-withholding <isrWithholding>', 'Alias de compatibilidad para `--isr-retenido`')
|
|
721
|
+
.option('--product-code <productCode>', 'Clave SAT del producto. Si la omites, se intenta inferir automáticamente.')
|
|
722
|
+
.option('--unit-code <unitCode>', 'Clave SAT de unidad. Si la omites, se intenta inferir automáticamente.')).action(async (options) => {
|
|
723
|
+
if (!options.description || !options.unitPrice) {
|
|
724
|
+
throw new Error('Debes indicar `--description <description>` y `--unit-price <unitPrice>`.');
|
|
725
|
+
}
|
|
726
|
+
const sdk = await getSdk();
|
|
727
|
+
const draftId = await resolveDraftId(options.draftId);
|
|
728
|
+
const iva = options.iva ?? options.vat;
|
|
729
|
+
const isrRetenido = options.isrRetenido ?? options.isrWithholding;
|
|
730
|
+
const input = {
|
|
731
|
+
description: options.description,
|
|
732
|
+
quantity: Number(options.quantity),
|
|
733
|
+
unitPrice: Number(options.unitPrice),
|
|
734
|
+
vatRate: iva === undefined ? undefined : parseVatFlag(iva),
|
|
735
|
+
vatSource: iva === undefined ? undefined : 'manual',
|
|
736
|
+
isrWithholdingRate: isrRetenido === undefined
|
|
737
|
+
? undefined
|
|
738
|
+
: parseIsrWithholdingFlag(isrRetenido),
|
|
739
|
+
isrWithholdingSource: isrRetenido === undefined ? undefined : 'manual',
|
|
740
|
+
productCode: options.productCode,
|
|
741
|
+
unitCode: options.unitCode,
|
|
742
|
+
};
|
|
743
|
+
const shouldShowSatInferenceLoading = !options.json && !options.productCode && !options.unitCode;
|
|
744
|
+
const loading = shouldShowSatInferenceLoading ? spinner() : null;
|
|
745
|
+
if (loading) {
|
|
746
|
+
loading.start('Infiriendo la clave SAT de este concepto...');
|
|
747
|
+
}
|
|
748
|
+
const draft = await sdk.addDraftItem(draftId, input, {
|
|
749
|
+
idempotencyKey: randomUUID(),
|
|
750
|
+
});
|
|
751
|
+
if (loading) {
|
|
752
|
+
loading.stop('Concepto clasificado y agregado al borrador.');
|
|
753
|
+
}
|
|
754
|
+
if (options.json) {
|
|
755
|
+
printJson(draft);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
printHuman([`Concepto agregado. Total actual: ${draft.total} ${draft.currency}`]);
|
|
759
|
+
});
|
|
760
|
+
withJsonFlag(cli
|
|
761
|
+
.command('invoice-update-item <itemId>', 'Actualizar concepto')
|
|
762
|
+
.option('--draft-id <draftId>', 'ID del borrador')
|
|
763
|
+
.option('--description <description>', 'Descripción')
|
|
764
|
+
.option('--unit-price <unitPrice>', 'Precio unitario')
|
|
765
|
+
.option('--quantity <quantity>', 'Cantidad')
|
|
766
|
+
.option('--product-code <productCode>', 'Clave SAT del producto')
|
|
767
|
+
.option('--unit-code <unitCode>', 'Clave SAT de unidad')).action(async (itemId, options) => {
|
|
768
|
+
const sdk = await getSdk();
|
|
769
|
+
const draftId = await resolveDraftId(options.draftId);
|
|
770
|
+
const input = {
|
|
771
|
+
description: options.description,
|
|
772
|
+
quantity: options.quantity === undefined ? undefined : Number(options.quantity),
|
|
773
|
+
unitPrice: options.unitPrice === undefined ? undefined : Number(options.unitPrice),
|
|
774
|
+
productCode: options.productCode,
|
|
775
|
+
unitCode: options.unitCode,
|
|
776
|
+
};
|
|
777
|
+
const draft = await sdk.updateDraftItem(draftId, itemId, input, {
|
|
778
|
+
idempotencyKey: randomUUID(),
|
|
779
|
+
});
|
|
780
|
+
if (options.json) {
|
|
781
|
+
printJson(draft);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
printHuman([`Concepto actualizado. Total actual: ${draft.total} ${draft.currency}`]);
|
|
785
|
+
});
|
|
786
|
+
withJsonFlag(cli
|
|
787
|
+
.command('invoice-remove-item <itemId>', 'Eliminar concepto')
|
|
788
|
+
.option('--draft-id <draftId>', 'ID del borrador')).action(async (itemId, options) => {
|
|
789
|
+
const sdk = await getSdk();
|
|
790
|
+
const draftId = await resolveDraftId(options.draftId);
|
|
791
|
+
const draft = await sdk.removeDraftItem(draftId, itemId, {
|
|
792
|
+
idempotencyKey: randomUUID(),
|
|
793
|
+
});
|
|
794
|
+
if (options.json) {
|
|
795
|
+
printJson(draft);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
printHuman([`Concepto eliminado. Total actual: ${draft.total} ${draft.currency}`]);
|
|
799
|
+
});
|
|
800
|
+
withJsonFlag(cli
|
|
801
|
+
.command('invoice-items', 'Listar conceptos del borrador')
|
|
802
|
+
.option('--draft-id <draftId>', 'ID del borrador')).action(async (options) => {
|
|
803
|
+
const sdk = await getSdk();
|
|
804
|
+
const draft = await sdk.getDraft(await resolveDraftId(options.draftId));
|
|
805
|
+
if (options.json) {
|
|
806
|
+
printJson(draft.items);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
printHuman(draft.items.flatMap((item) => formatDraftItemLine({
|
|
810
|
+
id: item.id,
|
|
811
|
+
quantity: item.quantity,
|
|
812
|
+
description: item.description,
|
|
813
|
+
amount: item.amount,
|
|
814
|
+
currency: draft.currency,
|
|
815
|
+
productCode: item.productCode,
|
|
816
|
+
suggestedProductCode: item.suggestedProductCode,
|
|
817
|
+
unitCode: item.unitCode,
|
|
818
|
+
vatRate: item.vatRate,
|
|
819
|
+
isrWithholdingRate: item.isrWithholdingRate,
|
|
820
|
+
})));
|
|
821
|
+
});
|
|
822
|
+
withJsonFlag(cli
|
|
823
|
+
.command('invoice-discard', 'Eliminar borrador actual')
|
|
824
|
+
.option('--draft-id <draftId>', 'ID del borrador')).action(async (options) => {
|
|
825
|
+
const sdk = await getSdk();
|
|
826
|
+
const draftId = await resolveDraftId(options.draftId);
|
|
827
|
+
const result = await sdk.discardDraft(draftId, {
|
|
828
|
+
idempotencyKey: randomUUID(),
|
|
829
|
+
});
|
|
830
|
+
if ((await getCurrentDraftId()) === draftId) {
|
|
831
|
+
await setCurrentDraftId(undefined);
|
|
832
|
+
}
|
|
833
|
+
if (options.json) {
|
|
834
|
+
printJson(result);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
printHuman([`Borrador eliminado: ${result.id}`]);
|
|
838
|
+
});
|
|
839
|
+
withJsonFlag(cli
|
|
840
|
+
.command('invoice-stamp', 'Timbrar borrador actual. En modo interactivo muestra la previa y pregunta si quieres enviarla al cliente después.')
|
|
841
|
+
.option('--draft-id <draftId>', 'ID del borrador')
|
|
842
|
+
.option('--no-input', 'Nunca preguntar nada; útil para agentes y scripts')
|
|
843
|
+
.option('--send-to-customer-contacts', 'Solicita entrega por email y WhatsApp registrados en el cliente, si existen.')
|
|
844
|
+
.option('--send-customer-email', 'Solicita entrega por correo del cliente')
|
|
845
|
+
.option('--send-customer-whatsapp', 'Solicita entrega por WhatsApp del cliente')).action(async (options) => {
|
|
846
|
+
const sdk = await getSdk();
|
|
847
|
+
const draftId = await resolveDraftId(options.draftId);
|
|
848
|
+
const interactiveMode = canPromptInteractively(options);
|
|
849
|
+
const draft = interactiveMode ? await sdk.getDraft(draftId) : null;
|
|
850
|
+
const interactiveResolution = interactiveMode && draft
|
|
851
|
+
? await resolveInteractiveStampInput({
|
|
852
|
+
sdk,
|
|
853
|
+
draft,
|
|
854
|
+
options,
|
|
855
|
+
})
|
|
856
|
+
: null;
|
|
857
|
+
if (interactiveMode && !interactiveResolution) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const shouldLoadCustomerForSummary = options.sendToCustomerContacts ||
|
|
861
|
+
options.sendCustomerEmail ||
|
|
862
|
+
options.sendCustomerWhatsApp;
|
|
863
|
+
const customer = interactiveResolution?.customer ??
|
|
864
|
+
(shouldLoadCustomerForSummary
|
|
865
|
+
? await loadDraftCustomer(sdk, draft ?? (await sdk.getDraft(draftId)))
|
|
866
|
+
: null);
|
|
867
|
+
const stampInput = interactiveResolution?.stampInput ?? {
|
|
868
|
+
sendToCustomerContacts: options.sendToCustomerContacts,
|
|
869
|
+
sendCustomerEmail: options.sendCustomerEmail,
|
|
870
|
+
sendCustomerWhatsApp: options.sendCustomerWhatsApp,
|
|
871
|
+
};
|
|
872
|
+
const loading = !options.json ? spinner() : null;
|
|
873
|
+
if (loading) {
|
|
874
|
+
loading.start('Timbrando factura...');
|
|
875
|
+
}
|
|
876
|
+
const invoice = await sdk.stampDraft(draftId, stampInput, { idempotencyKey: randomUUID() });
|
|
877
|
+
if (loading) {
|
|
878
|
+
loading.stop('Factura timbrada.');
|
|
879
|
+
}
|
|
880
|
+
if ((await getCurrentDraftId()) === draftId) {
|
|
881
|
+
await setCurrentDraftId(undefined);
|
|
882
|
+
}
|
|
883
|
+
if (options.json) {
|
|
884
|
+
printJson(invoice);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
printHuman([
|
|
888
|
+
`Factura timbrada: ${invoice.id}`,
|
|
889
|
+
`Folio: ${invoice.folio ?? 'sin folio'}`,
|
|
890
|
+
`UUID SAT: ${invoice.satUuid ?? 'pendiente'}`,
|
|
891
|
+
...formatDeliverySummary({
|
|
892
|
+
sendCustomerEmail: stampInput.sendCustomerEmail ??
|
|
893
|
+
stampInput.sendToCustomerContacts,
|
|
894
|
+
sendCustomerWhatsApp: stampInput.sendCustomerWhatsApp ??
|
|
895
|
+
stampInput.sendToCustomerContacts,
|
|
896
|
+
customer,
|
|
897
|
+
}),
|
|
898
|
+
]);
|
|
899
|
+
});
|
|
900
|
+
withJsonFlag(cli.command('invoice-get <invoiceRef>', 'Obtener factura por UUID o folio')).action(async (invoiceRef, options) => {
|
|
901
|
+
const sdk = await getSdk();
|
|
902
|
+
const invoiceId = await resolveInvoiceReference({ sdk, invoiceRef });
|
|
903
|
+
const invoice = await sdk.getInvoice(invoiceId);
|
|
904
|
+
if (options.json) {
|
|
905
|
+
printJson(invoice);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
printHuman([
|
|
909
|
+
`Factura: ${invoice.id}`,
|
|
910
|
+
`Estado: ${invoice.status}`,
|
|
911
|
+
`Receptor: ${invoice.receiverLegalName}`,
|
|
912
|
+
`Total: ${invoice.total} ${invoice.currency}`,
|
|
913
|
+
]);
|
|
914
|
+
});
|
|
915
|
+
withJsonFlag(cli.command('invoice-download-xml <invoiceRef>', 'Descargar XML timbrado por UUID o folio').option('--output <path>', 'Ruta destino. Puede ser archivo o carpeta. Si lo omites, se guarda en el directorio actual.')).action(async (invoiceRef, options) => {
|
|
916
|
+
const sdk = await getSdk();
|
|
917
|
+
const invoiceId = await resolveInvoiceReference({ sdk, invoiceRef });
|
|
918
|
+
const xml = await sdk.downloadInvoiceXml(invoiceId);
|
|
919
|
+
if (options.json) {
|
|
920
|
+
printJson(xml);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const savedPath = await saveDownloadedArtifact({
|
|
924
|
+
base64: xml.base64,
|
|
925
|
+
fileName: xml.fileName,
|
|
926
|
+
output: options.output,
|
|
927
|
+
});
|
|
928
|
+
printHuman([`Archivo guardado en: ${savedPath}`, xml.mimeType]);
|
|
929
|
+
});
|
|
930
|
+
withJsonFlag(cli.command('invoice-download-pdf <invoiceRef>', 'Descargar PDF timbrado por UUID o folio').option('--output <path>', 'Ruta destino. Puede ser archivo o carpeta. Si lo omites, se guarda en el directorio actual.')).action(async (invoiceRef, options) => {
|
|
931
|
+
const sdk = await getSdk();
|
|
932
|
+
const invoiceId = await resolveInvoiceReference({ sdk, invoiceRef });
|
|
933
|
+
const pdf = await sdk.downloadInvoicePdf(invoiceId);
|
|
934
|
+
if (options.json) {
|
|
935
|
+
printJson(pdf);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const savedPath = await saveDownloadedArtifact({
|
|
939
|
+
base64: pdf.base64,
|
|
940
|
+
fileName: pdf.fileName,
|
|
941
|
+
output: options.output,
|
|
942
|
+
});
|
|
943
|
+
printHuman([`Archivo guardado en: ${savedPath}`, pdf.mimeType]);
|
|
944
|
+
});
|
|
945
|
+
withJsonFlag(cli
|
|
946
|
+
.command('customer-list', 'Listar clientes')
|
|
947
|
+
.option('--query <query>', 'Búsqueda')
|
|
948
|
+
.option('--limit <limit>', 'Límite')).action(async (options) => {
|
|
949
|
+
const sdk = await getSdk();
|
|
950
|
+
const customers = await sdk.listCustomers({
|
|
951
|
+
query: options.query,
|
|
952
|
+
limit: options.limit ? Number(options.limit) : undefined,
|
|
953
|
+
});
|
|
954
|
+
if (options.json) {
|
|
955
|
+
printJson(customers);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
printHuman(customers.map((customer) => `${customer.id} ${customer.legalName}`));
|
|
959
|
+
});
|
|
960
|
+
withJsonFlag(cli.command('customer-search <query>', 'Buscar clientes')).action(async (query, options) => {
|
|
961
|
+
const sdk = await getSdk();
|
|
962
|
+
const customers = await sdk.listCustomers({ query });
|
|
963
|
+
if (options.json) {
|
|
964
|
+
printJson(customers);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
printHuman(customers.map((customer) => `${customer.id} ${customer.legalName}`));
|
|
968
|
+
});
|
|
969
|
+
withJsonFlag(cli
|
|
970
|
+
.command('customer-create', 'Crear cliente')
|
|
971
|
+
.option('--legal-name <legalName>', 'Razón social')
|
|
972
|
+
.option('--rfc <rfc>', 'RFC')
|
|
973
|
+
.option('--postal-code <postalCode>', 'Código postal')
|
|
974
|
+
.option('--tax-regime <taxRegime>', 'Régimen fiscal')
|
|
975
|
+
.option('--email <email>', 'Correo')
|
|
976
|
+
.option('--whatsapp-phone <whatsappPhone>', 'WhatsApp')
|
|
977
|
+
.option('--currency <currency>', 'Moneda')
|
|
978
|
+
.option('--invoice-language <invoiceLanguage>', 'Idioma de factura')).action(async (options) => {
|
|
979
|
+
if (!options.legalName || !options.rfc || !options.postalCode || !options.taxRegime) {
|
|
980
|
+
throw new Error('Debes indicar `--legal-name`, `--rfc`, `--postal-code` y `--tax-regime`.');
|
|
981
|
+
}
|
|
982
|
+
const sdk = await getSdk();
|
|
983
|
+
const input = {
|
|
984
|
+
legalName: options.legalName,
|
|
985
|
+
rfc: options.rfc,
|
|
986
|
+
postalCode: options.postalCode,
|
|
987
|
+
taxRegime: options.taxRegime,
|
|
988
|
+
email: options.email,
|
|
989
|
+
whatsappPhone: options.whatsappPhone,
|
|
990
|
+
currency: options.currency ?? 'MXN',
|
|
991
|
+
invoiceLanguage: options.invoiceLanguage ?? 'es',
|
|
992
|
+
sendInvoices: true,
|
|
993
|
+
};
|
|
994
|
+
const customer = await sdk.createCustomer(input, {
|
|
995
|
+
idempotencyKey: randomUUID(),
|
|
996
|
+
});
|
|
997
|
+
if (options.json) {
|
|
998
|
+
printJson(customer);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
printHuman([`Cliente creado: ${customer.id}`, customer.legalName]);
|
|
1002
|
+
});
|
|
1003
|
+
cli.help();
|
|
1004
|
+
try {
|
|
1005
|
+
cli.parse(argv, { run: false });
|
|
1006
|
+
await Promise.resolve(cli.runMatchedCommand());
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
if (wantsJson(argv)) {
|
|
1010
|
+
printJson({
|
|
1011
|
+
error: {
|
|
1012
|
+
message: getErrorMessage(error),
|
|
1013
|
+
},
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
process.stderr.write(`${getErrorMessage(error)}\n`);
|
|
1018
|
+
}
|
|
1019
|
+
process.exitCode = exitCodeFromError(error);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
void main();
|
|
1023
|
+
//# sourceMappingURL=index.js.map
|