gigstack 0.1.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 +444 -0
- package/dist/cli.mjs +3068 -0
- package/package.json +42 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,3068 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createRequire}from'module';const require=createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import pc15 from "picocolors";
|
|
7
|
+
|
|
8
|
+
// src/output.ts
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
var jsonMode = false;
|
|
11
|
+
function setJsonMode(enabled) {
|
|
12
|
+
jsonMode = enabled;
|
|
13
|
+
}
|
|
14
|
+
function isJsonMode() {
|
|
15
|
+
return jsonMode;
|
|
16
|
+
}
|
|
17
|
+
function printJson(data) {
|
|
18
|
+
console.log(JSON.stringify(data, null, 2));
|
|
19
|
+
}
|
|
20
|
+
function printTable(rows, columns) {
|
|
21
|
+
if (jsonMode) return printJson(rows);
|
|
22
|
+
if (rows.length === 0) {
|
|
23
|
+
console.log(pc.dim("Sin resultados"));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const cols = columns || Object.keys(rows[0]);
|
|
27
|
+
const widths = cols.map(
|
|
28
|
+
(c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length))
|
|
29
|
+
);
|
|
30
|
+
const header = cols.map((c, i) => pc.bold(c.padEnd(widths[i]))).join(" ");
|
|
31
|
+
const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u2500");
|
|
32
|
+
console.log(header);
|
|
33
|
+
console.log(pc.dim(separator));
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
console.log(cols.map((c, i) => String(row[c] ?? "").padEnd(widths[i])).join(" "));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function printKeyValue(data) {
|
|
39
|
+
if (jsonMode) return printJson(data);
|
|
40
|
+
const maxKey = Math.max(...Object.keys(data).map((k) => k.length));
|
|
41
|
+
for (const [k, v] of Object.entries(data)) {
|
|
42
|
+
console.log(`${pc.bold(k.padEnd(maxKey))} ${v}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function success(msg) {
|
|
46
|
+
console.log(pc.green(`\u2713 ${msg}`));
|
|
47
|
+
}
|
|
48
|
+
function error(msg) {
|
|
49
|
+
console.error(pc.red(`\u2717 ${msg}`));
|
|
50
|
+
}
|
|
51
|
+
function warn(msg) {
|
|
52
|
+
console.log(pc.yellow(`! ${msg}`));
|
|
53
|
+
}
|
|
54
|
+
function formatMoney(amount, currency = "MXN") {
|
|
55
|
+
return `$${(amount || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency.toUpperCase()}`;
|
|
56
|
+
}
|
|
57
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
58
|
+
function spinner(message) {
|
|
59
|
+
if (jsonMode) return { stop() {
|
|
60
|
+
} };
|
|
61
|
+
let i = 0;
|
|
62
|
+
const stream = process.stderr;
|
|
63
|
+
const interval = setInterval(() => {
|
|
64
|
+
stream.write(`\r${pc.cyan(SPINNER_FRAMES[i++ % SPINNER_FRAMES.length])} ${pc.dim(message)}`);
|
|
65
|
+
}, 80);
|
|
66
|
+
return {
|
|
67
|
+
stop(finalMessage) {
|
|
68
|
+
clearInterval(interval);
|
|
69
|
+
stream.write("\r" + " ".repeat(message.length + 4) + "\r");
|
|
70
|
+
if (finalMessage) stream.write(finalMessage + "\n");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function spin(message, fn) {
|
|
75
|
+
const s = spinner(message);
|
|
76
|
+
try {
|
|
77
|
+
const result = await fn();
|
|
78
|
+
s.stop();
|
|
79
|
+
return result;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
s.stop();
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function formatDate(val) {
|
|
86
|
+
if (!val) return "\u2014";
|
|
87
|
+
if (typeof val === "string") return val.slice(0, 10);
|
|
88
|
+
if (typeof val === "number") {
|
|
89
|
+
const ms = val > 1e12 ? val : val * 1e3;
|
|
90
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
91
|
+
}
|
|
92
|
+
if (val._seconds) return new Date(val._seconds * 1e3).toISOString().slice(0, 10);
|
|
93
|
+
if (val.seconds) return new Date(val.seconds * 1e3).toISOString().slice(0, 10);
|
|
94
|
+
if (val instanceof Date) return val.toISOString().slice(0, 10);
|
|
95
|
+
return String(val);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/commands/auth.ts
|
|
99
|
+
import pc4 from "picocolors";
|
|
100
|
+
|
|
101
|
+
// src/config.ts
|
|
102
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
103
|
+
import { homedir } from "node:os";
|
|
104
|
+
import { join } from "node:path";
|
|
105
|
+
var CONFIG_DIR = join(homedir(), ".config", "gigstack");
|
|
106
|
+
var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
107
|
+
function ensureConfigDir() {
|
|
108
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
109
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function readCredentials() {
|
|
113
|
+
if (!existsSync(CREDENTIALS_FILE)) return null;
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function writeCredentials(creds) {
|
|
121
|
+
ensureConfigDir();
|
|
122
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
123
|
+
}
|
|
124
|
+
function saveProfile(name, apiKey, environment) {
|
|
125
|
+
const creds = readCredentials() || { profiles: {}, activeProfile: name };
|
|
126
|
+
creds.profiles[name] = { apiKey, environment };
|
|
127
|
+
creds.activeProfile = name;
|
|
128
|
+
writeCredentials(creds);
|
|
129
|
+
}
|
|
130
|
+
function removeProfile(name) {
|
|
131
|
+
const creds = readCredentials();
|
|
132
|
+
if (!creds) return;
|
|
133
|
+
delete creds.profiles[name];
|
|
134
|
+
if (creds.activeProfile === name) {
|
|
135
|
+
creds.activeProfile = Object.keys(creds.profiles)[0] || "";
|
|
136
|
+
}
|
|
137
|
+
writeCredentials(creds);
|
|
138
|
+
}
|
|
139
|
+
function switchProfile(name) {
|
|
140
|
+
const creds = readCredentials();
|
|
141
|
+
if (!creds || !creds.profiles[name]) return false;
|
|
142
|
+
creds.activeProfile = name;
|
|
143
|
+
writeCredentials(creds);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
function getActiveProfile() {
|
|
147
|
+
const envKey = process.env.GIGSTACK_API_KEY;
|
|
148
|
+
if (envKey) return { name: "env", apiKey: envKey, environment: "production" };
|
|
149
|
+
const creds = readCredentials();
|
|
150
|
+
if (!creds || !creds.activeProfile || !creds.profiles[creds.activeProfile]) return null;
|
|
151
|
+
return { name: creds.activeProfile, ...creds.profiles[creds.activeProfile] };
|
|
152
|
+
}
|
|
153
|
+
function parseJwtPayload(apiKey) {
|
|
154
|
+
try {
|
|
155
|
+
const parts = apiKey.split(".");
|
|
156
|
+
if (parts.length === 3) {
|
|
157
|
+
return JSON.parse(Buffer.from(parts[1], "base64").toString());
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function isTestKey(apiKey) {
|
|
164
|
+
if (apiKey.startsWith("sk_test")) return true;
|
|
165
|
+
const payload = parseJwtPayload(apiKey);
|
|
166
|
+
if (payload) {
|
|
167
|
+
if (payload.livemode === false) return true;
|
|
168
|
+
if (payload.key_id?.includes("test")) return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
function getTeamFromKey(apiKey) {
|
|
173
|
+
const payload = parseJwtPayload(apiKey);
|
|
174
|
+
return payload?.team || null;
|
|
175
|
+
}
|
|
176
|
+
function listProfiles() {
|
|
177
|
+
const creds = readCredentials();
|
|
178
|
+
if (!creds) return [];
|
|
179
|
+
return Object.keys(creds.profiles).map((name) => ({
|
|
180
|
+
name,
|
|
181
|
+
active: name === creds.activeProfile
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/api.ts
|
|
186
|
+
import pc2 from "picocolors";
|
|
187
|
+
var BASE_URL = "https://api.gigstack.io/v2";
|
|
188
|
+
var ApiError = class extends Error {
|
|
189
|
+
constructor(status, body) {
|
|
190
|
+
const detail = body?.error?.message || body?.error || "";
|
|
191
|
+
const msg = body?.message || `API error ${status}`;
|
|
192
|
+
super(detail ? `${msg}: ${typeof detail === "string" ? detail : JSON.stringify(detail)}` : msg);
|
|
193
|
+
this.status = status;
|
|
194
|
+
this.body = body;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
function getApiKey(override) {
|
|
198
|
+
if (override) return override;
|
|
199
|
+
const profile = getActiveProfile();
|
|
200
|
+
if (!profile) {
|
|
201
|
+
console.error(pc2.red("No autenticado. Ejecuta: gigstack login"));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
return profile.apiKey;
|
|
205
|
+
}
|
|
206
|
+
async function api(method, path, opts) {
|
|
207
|
+
const apiKey = getApiKey(opts?.apiKey);
|
|
208
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
209
|
+
if (opts?.query) {
|
|
210
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
211
|
+
if (v !== void 0) url.searchParams.set(k, v);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (opts?.team) url.searchParams.set("team", opts.team);
|
|
215
|
+
let res;
|
|
216
|
+
try {
|
|
217
|
+
res = await fetch(url.toString(), {
|
|
218
|
+
method,
|
|
219
|
+
headers: {
|
|
220
|
+
Authorization: `Bearer ${apiKey}`,
|
|
221
|
+
"Content-Type": "application/json"
|
|
222
|
+
},
|
|
223
|
+
body: opts?.body ? JSON.stringify(opts.body) : void 0
|
|
224
|
+
});
|
|
225
|
+
} catch (e) {
|
|
226
|
+
if (e.code === "ENOTFOUND" || e.cause?.code === "ENOTFOUND") {
|
|
227
|
+
throw new Error("Sin conexi\xF3n a internet. Verifica tu red.");
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`Error de conexi\xF3n: ${e.message}`);
|
|
230
|
+
}
|
|
231
|
+
const data = await res.json();
|
|
232
|
+
if (!res.ok) {
|
|
233
|
+
throw new ApiError(res.status, data);
|
|
234
|
+
}
|
|
235
|
+
return data;
|
|
236
|
+
}
|
|
237
|
+
async function resolveTeam(apiKey) {
|
|
238
|
+
const key = apiKey || getApiKey();
|
|
239
|
+
const jwtTeamId = getTeamFromKey(key);
|
|
240
|
+
if (jwtTeamId) {
|
|
241
|
+
try {
|
|
242
|
+
const res2 = await api("GET", `/teams/${jwtTeamId}`, { apiKey: key });
|
|
243
|
+
if (res2.data) return res2.data;
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const res = await api("GET", "/teams", { apiKey: key });
|
|
248
|
+
const teams = res.data || [];
|
|
249
|
+
if (jwtTeamId) {
|
|
250
|
+
const match = teams.find((t) => t.id === jwtTeamId);
|
|
251
|
+
if (match) return match;
|
|
252
|
+
}
|
|
253
|
+
return teams[0] || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/prompt.ts
|
|
257
|
+
import { createInterface } from "node:readline";
|
|
258
|
+
import pc3 from "picocolors";
|
|
259
|
+
var rl = () => createInterface({ input: process.stdin, output: process.stdout });
|
|
260
|
+
function ask(question, defaultVal) {
|
|
261
|
+
const suffix = defaultVal ? pc3.dim(` (${defaultVal})`) : "";
|
|
262
|
+
return new Promise((resolve) => {
|
|
263
|
+
const r = rl();
|
|
264
|
+
r.question(`${pc3.bold(question)}${suffix}: `, (answer) => {
|
|
265
|
+
r.close();
|
|
266
|
+
resolve(answer.trim() || defaultVal || "");
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function askRequired(question) {
|
|
271
|
+
return new Promise(async (resolve) => {
|
|
272
|
+
let val = "";
|
|
273
|
+
while (!val) {
|
|
274
|
+
val = await ask(question);
|
|
275
|
+
if (!val) console.log(pc3.red(" Campo requerido"));
|
|
276
|
+
}
|
|
277
|
+
resolve(val);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function askHidden(question) {
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
const r = rl();
|
|
283
|
+
process.stdout.write(`${pc3.bold(question)}: `);
|
|
284
|
+
if (process.stdin.setRawMode) {
|
|
285
|
+
process.stdin.setRawMode(true);
|
|
286
|
+
process.stdin.resume();
|
|
287
|
+
let input = "";
|
|
288
|
+
const onData = (ch) => {
|
|
289
|
+
const c = ch.toString();
|
|
290
|
+
if (c === "\n" || c === "\r") {
|
|
291
|
+
process.stdin.setRawMode(false);
|
|
292
|
+
process.stdin.removeListener("data", onData);
|
|
293
|
+
process.stdout.write("\n");
|
|
294
|
+
r.close();
|
|
295
|
+
resolve(input.trim());
|
|
296
|
+
} else if (c === "\x7F" || c === "\b") {
|
|
297
|
+
input = input.slice(0, -1);
|
|
298
|
+
} else if (c === "") {
|
|
299
|
+
process.exit(0);
|
|
300
|
+
} else {
|
|
301
|
+
input += c;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
process.stdin.on("data", onData);
|
|
305
|
+
} else {
|
|
306
|
+
r.question("", (answer) => {
|
|
307
|
+
r.close();
|
|
308
|
+
resolve(answer.trim());
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
async function select(question, options) {
|
|
314
|
+
console.log(pc3.bold(question));
|
|
315
|
+
options.forEach((o, i) => console.log(` ${pc3.dim(`${i + 1})`)} ${o.label}`));
|
|
316
|
+
const answer = await ask("Selecciona", "1");
|
|
317
|
+
const idx = parseInt(answer) - 1;
|
|
318
|
+
if (idx >= 0 && idx < options.length) return options[idx].value;
|
|
319
|
+
const match = options.find((o) => o.value === answer || o.label.toLowerCase().includes(answer.toLowerCase()));
|
|
320
|
+
return match?.value || options[0].value;
|
|
321
|
+
}
|
|
322
|
+
async function confirm(question, defaultVal = true) {
|
|
323
|
+
const hint = defaultVal ? "S/n" : "s/N";
|
|
324
|
+
const answer = await ask(`${question} (${hint})`);
|
|
325
|
+
if (!answer) return defaultVal;
|
|
326
|
+
return answer.toLowerCase().startsWith("s") || answer.toLowerCase().startsWith("y");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/commands/auth.ts
|
|
330
|
+
function registerAuthCommands(program2) {
|
|
331
|
+
program2.command("login").description("Autenticarse con tu API key de gigstack").option("-k, --api-key <key>", "API key (o se pedir\xE1 interactivamente)").option("-p, --profile <name>", "Nombre del perfil", "default").action(async (opts) => {
|
|
332
|
+
let apiKey = opts.apiKey || process.env.GIGSTACK_API_KEY;
|
|
333
|
+
if (!apiKey) {
|
|
334
|
+
console.log(pc4.dim("Obt\xE9n tu API key en: app.gigstack.pro/settings \u2192 API\n"));
|
|
335
|
+
apiKey = await askHidden("API Key");
|
|
336
|
+
}
|
|
337
|
+
if (!apiKey) {
|
|
338
|
+
error("API key requerida");
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const team = await resolveTeam(apiKey);
|
|
343
|
+
const isTest = isTestKey(apiKey);
|
|
344
|
+
saveProfile(opts.profile, apiKey, isTest ? "test" : "production");
|
|
345
|
+
success(`Autenticado como ${pc4.bold(team?.legal_name || team?.brand?.alias || "equipo gigstack")}`);
|
|
346
|
+
if (isTest) console.log(pc4.yellow(" Modo prueba (test key)"));
|
|
347
|
+
console.log(pc4.dim(` Perfil "${opts.profile}" guardado en ~/.config/gigstack/credentials.json`));
|
|
348
|
+
} catch (e) {
|
|
349
|
+
error(`Error de autenticaci\xF3n: ${e.message}`);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
program2.command("logout").description("Eliminar credenciales").option("-p, --profile <name>", "Perfil a eliminar", "default").action((opts) => {
|
|
354
|
+
removeProfile(opts.profile);
|
|
355
|
+
success(`Perfil "${opts.profile}" eliminado`);
|
|
356
|
+
});
|
|
357
|
+
program2.command("whoami").description("Mostrar perfil y cuenta actual").action(async () => {
|
|
358
|
+
const profile = getActiveProfile();
|
|
359
|
+
if (!profile) {
|
|
360
|
+
error("No autenticado. Ejecuta: gigstack login");
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const team = await resolveTeam();
|
|
365
|
+
printKeyValue({
|
|
366
|
+
Perfil: profile.name,
|
|
367
|
+
Modo: isTestKey(profile.apiKey) ? pc4.yellow("test") : pc4.green("producci\xF3n"),
|
|
368
|
+
RFC: team?.tax_id || pc4.dim("no configurado"),
|
|
369
|
+
Nombre: team?.legal_name || team?.brand?.alias || "\u2014",
|
|
370
|
+
SAT: team?.sat?.completed ? pc4.green("conectado") : pc4.red("no conectado"),
|
|
371
|
+
"API Key": profile.apiKey.slice(0, 12) + "..." + profile.apiKey.slice(-4)
|
|
372
|
+
});
|
|
373
|
+
} catch (e) {
|
|
374
|
+
error(e.message);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
program2.command("switch <profile>").description("Cambiar perfil activo").action((name) => {
|
|
378
|
+
if (switchProfile(name)) {
|
|
379
|
+
success(`Perfil activo: ${name}`);
|
|
380
|
+
} else {
|
|
381
|
+
error(`Perfil "${name}" no encontrado`);
|
|
382
|
+
const profiles = listProfiles();
|
|
383
|
+
if (profiles.length) {
|
|
384
|
+
console.log(pc4.dim("Perfiles disponibles: " + profiles.map((p) => p.name).join(", ")));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
program2.command("profiles").description("Listar perfiles guardados").action(() => {
|
|
389
|
+
const profiles = listProfiles();
|
|
390
|
+
if (!profiles.length) {
|
|
391
|
+
console.log(pc4.dim("Sin perfiles. Ejecuta: gigstack login"));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
for (const p of profiles) {
|
|
395
|
+
console.log(`${p.active ? pc4.green("\u25CF") : pc4.dim("\u25CB")} ${p.name}`);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/commands/clients.ts
|
|
401
|
+
import pc6 from "picocolors";
|
|
402
|
+
|
|
403
|
+
// src/list-opts.ts
|
|
404
|
+
import pc5 from "picocolors";
|
|
405
|
+
function withListOpts(cmd) {
|
|
406
|
+
return cmd.option("-l, --limit <n>", "L\xEDmite de resultados (1-100)", "20").option("--next <token>", "Token de paginaci\xF3n (de respuesta anterior)").option("--from <date>", "Fecha inicio (YYYY-MM-DD, YYYY-MM, 30d, 7d)").option("--to <date>", "Fecha fin (YYYY-MM-DD, YYYY-MM, today)").option("--sort <dir>", "Orden: asc o desc", "desc").option("--order-by <field>", "Ordenar por: timestamp o name", "timestamp").option("--team <id>", "Team ID");
|
|
407
|
+
}
|
|
408
|
+
function buildListQuery(opts) {
|
|
409
|
+
const query = { limit: opts.limit };
|
|
410
|
+
if (opts.next) query.next = opts.next;
|
|
411
|
+
if (opts.sort && opts.sort !== "desc") query.sort = opts.sort;
|
|
412
|
+
if (opts.orderBy && opts.orderBy !== "timestamp") query.order_by = opts.orderBy;
|
|
413
|
+
if (opts.from || opts.to) {
|
|
414
|
+
const from = opts.from ? parseDate(opts.from) : void 0;
|
|
415
|
+
const to = opts.to ? parseDate(opts.to, true) : void 0;
|
|
416
|
+
if (from) query["created[gte]"] = String(Math.floor(new Date(from).getTime() / 1e3));
|
|
417
|
+
if (to) query["created[lte]"] = String(Math.floor((/* @__PURE__ */ new Date(to + "T23:59:59")).getTime() / 1e3));
|
|
418
|
+
}
|
|
419
|
+
return query;
|
|
420
|
+
}
|
|
421
|
+
function printPaginationHint(res) {
|
|
422
|
+
if (res.has_more && res.next) {
|
|
423
|
+
console.log(pc5.dim(`
|
|
424
|
+
... ${res.total_results ?? "m\xE1s"} resultados. Usa --next ${res.next} para la siguiente p\xE1gina`));
|
|
425
|
+
} else if (res.has_more) {
|
|
426
|
+
console.log(pc5.dim(`
|
|
427
|
+
... m\xE1s resultados disponibles`));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function parseDate(input, isEnd = false) {
|
|
431
|
+
const trimmed = input.trim().toLowerCase();
|
|
432
|
+
const relMatch = trimmed.match(/^(\d+)d$/);
|
|
433
|
+
if (relMatch) {
|
|
434
|
+
const d = /* @__PURE__ */ new Date();
|
|
435
|
+
d.setDate(d.getDate() - parseInt(relMatch[1]));
|
|
436
|
+
return d.toISOString().slice(0, 10);
|
|
437
|
+
}
|
|
438
|
+
if (trimmed === "today") return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
439
|
+
if (/^\d{4}-\d{2}$/.test(trimmed)) {
|
|
440
|
+
if (isEnd) {
|
|
441
|
+
const [y, m] = trimmed.split("-").map(Number);
|
|
442
|
+
const last = new Date(y, m, 0).getDate();
|
|
443
|
+
return `${trimmed}-${String(last).padStart(2, "0")}`;
|
|
444
|
+
}
|
|
445
|
+
return `${trimmed}-01`;
|
|
446
|
+
}
|
|
447
|
+
return trimmed;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/commands/clients.ts
|
|
451
|
+
var TAX_SYSTEMS = [
|
|
452
|
+
{ label: "601 \u2014 General de Ley (Personas Morales)", value: "601" },
|
|
453
|
+
{ label: "612 \u2014 Personas F\xEDsicas con Actividad Empresarial", value: "612" },
|
|
454
|
+
{ label: "616 \u2014 Sin obligaciones fiscales", value: "616" },
|
|
455
|
+
{ label: "621 \u2014 Incorporaci\xF3n Fiscal", value: "621" },
|
|
456
|
+
{ label: "625 \u2014 R\xE9gimen de actividades agr\xEDcolas", value: "625" },
|
|
457
|
+
{ label: "626 \u2014 RESICO", value: "626" }
|
|
458
|
+
];
|
|
459
|
+
function registerClientCommands(program2) {
|
|
460
|
+
const clients = program2.command("clients").description("Gestionar clientes");
|
|
461
|
+
withListOpts(
|
|
462
|
+
clients.command("list").description("Listar clientes")
|
|
463
|
+
).action(async (opts) => {
|
|
464
|
+
try {
|
|
465
|
+
const query = buildListQuery(opts);
|
|
466
|
+
const res = await spin("Cargando clientes\u2026", () => api("GET", "/clients", { query, team: opts.team }));
|
|
467
|
+
const items = res.data || [];
|
|
468
|
+
if (isJsonMode()) return printJson(items);
|
|
469
|
+
printTable(
|
|
470
|
+
items.map((c) => ({
|
|
471
|
+
id: c.id,
|
|
472
|
+
nombre: c.legal_name || c.name || "\u2014",
|
|
473
|
+
rfc: c.tax_id || "\u2014",
|
|
474
|
+
email: c.email || "\u2014"
|
|
475
|
+
}))
|
|
476
|
+
);
|
|
477
|
+
printPaginationHint(res);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
error(e.message);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
clients.command("get <id>").description("Ver detalle de un cliente").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
483
|
+
try {
|
|
484
|
+
const res = await api("GET", `/clients/${id}`, { team: opts.team });
|
|
485
|
+
const c = res.data;
|
|
486
|
+
if (isJsonMode()) return printJson(c);
|
|
487
|
+
printKeyValue({
|
|
488
|
+
ID: c.id,
|
|
489
|
+
Nombre: c.legal_name || c.name,
|
|
490
|
+
RFC: c.tax_id || "\u2014",
|
|
491
|
+
Email: c.email || "\u2014",
|
|
492
|
+
"R\xE9gimen fiscal": c.tax_system || "\u2014",
|
|
493
|
+
"Uso CFDI": c.use || "\u2014",
|
|
494
|
+
"C\xF3digo postal": c.address?.zip || "\u2014",
|
|
495
|
+
V\u00E1lido: c.is_valid ? "S\xED" : "No",
|
|
496
|
+
Creado: formatDate(c.created_at)
|
|
497
|
+
});
|
|
498
|
+
} catch (e) {
|
|
499
|
+
error(e.message);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
clients.command("create").description("Crear un cliente (interactivo si no se pasan flags)").option("--name <name>", "Nombre o raz\xF3n social").option("--email <email>", "Email").option("--rfc <rfc>", "RFC").option("--tax-system <code>", "R\xE9gimen fiscal (ej: 601, 612, 626)").option("--zip <zip>", "C\xF3digo postal").option("--use <use>", "Uso CFDI", "G03").option("--team <id>", "Team ID").action(async (opts) => {
|
|
503
|
+
try {
|
|
504
|
+
const interactive = !opts.name && !opts.rfc;
|
|
505
|
+
const name = opts.name || (interactive ? await askRequired("Nombre / raz\xF3n social") : "");
|
|
506
|
+
const email = opts.email || (interactive ? await ask("Email") : "");
|
|
507
|
+
const rfc = opts.rfc || (interactive ? await askRequired("RFC") : "");
|
|
508
|
+
const taxSystem = opts.taxSystem || (interactive ? await select("R\xE9gimen fiscal", TAX_SYSTEMS) : "");
|
|
509
|
+
const zip = opts.zip || (interactive ? await ask("C\xF3digo postal") : "");
|
|
510
|
+
if (!name || !rfc) {
|
|
511
|
+
error("Nombre y RFC son requeridos");
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
const res = await spin("Creando cliente\u2026", () => api("POST", "/clients", {
|
|
515
|
+
body: {
|
|
516
|
+
name,
|
|
517
|
+
legal_name: name,
|
|
518
|
+
email: email || void 0,
|
|
519
|
+
tax_id: rfc,
|
|
520
|
+
tax_system: taxSystem || void 0,
|
|
521
|
+
use: opts.use,
|
|
522
|
+
address: zip ? { zip } : void 0
|
|
523
|
+
},
|
|
524
|
+
team: opts.team
|
|
525
|
+
}));
|
|
526
|
+
success(`Cliente creado: ${res.data.id}`);
|
|
527
|
+
if (!isJsonMode()) console.log(` RFC: ${res.data.tax_id} Email: ${res.data.email || "\u2014"}`);
|
|
528
|
+
else printJson(res.data);
|
|
529
|
+
} catch (e) {
|
|
530
|
+
error(e.message);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
clients.command("update <id>").description("Actualizar un cliente").option("--name <name>", "Nombre o raz\xF3n social").option("--email <email>", "Email").option("--rfc <rfc>", "RFC").option("--tax-system <code>", "R\xE9gimen fiscal").option("--zip <zip>", "C\xF3digo postal").option("--use <use>", "Uso CFDI").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
534
|
+
try {
|
|
535
|
+
const hasFlags = opts.name || opts.email || opts.rfc || opts.taxSystem || opts.zip || opts.use;
|
|
536
|
+
let body = {};
|
|
537
|
+
if (hasFlags) {
|
|
538
|
+
if (opts.name) {
|
|
539
|
+
body.name = opts.name;
|
|
540
|
+
body.legal_name = opts.name;
|
|
541
|
+
}
|
|
542
|
+
if (opts.email) body.email = opts.email;
|
|
543
|
+
if (opts.rfc) body.tax_id = opts.rfc;
|
|
544
|
+
if (opts.taxSystem) body.tax_system = opts.taxSystem;
|
|
545
|
+
if (opts.zip) body.address = { zip: opts.zip };
|
|
546
|
+
if (opts.use) body.use = opts.use;
|
|
547
|
+
} else {
|
|
548
|
+
const current = await spin("Cargando cliente\u2026", () => api("GET", `/clients/${id}`, { team: opts.team }));
|
|
549
|
+
const c = current.data;
|
|
550
|
+
console.log(pc6.dim(`Editando: ${c.legal_name || c.name} (${c.tax_id || "sin RFC"})
|
|
551
|
+
`));
|
|
552
|
+
console.log(pc6.dim("Deja vac\xEDo para mantener el valor actual.\n"));
|
|
553
|
+
const name = await ask("Nombre", c.legal_name || c.name || "");
|
|
554
|
+
const email = await ask("Email", c.email || "");
|
|
555
|
+
const rfc = await ask("RFC", c.tax_id || "");
|
|
556
|
+
const taxSystem = await ask("R\xE9gimen fiscal", c.tax_system || "");
|
|
557
|
+
const zip = await ask("C\xF3digo postal", c.address?.zip || "");
|
|
558
|
+
if (name && name !== (c.legal_name || c.name)) {
|
|
559
|
+
body.name = name;
|
|
560
|
+
body.legal_name = name;
|
|
561
|
+
}
|
|
562
|
+
if (email && email !== c.email) body.email = email;
|
|
563
|
+
if (rfc && rfc !== c.tax_id) body.tax_id = rfc;
|
|
564
|
+
if (taxSystem && taxSystem !== c.tax_system) body.tax_system = taxSystem;
|
|
565
|
+
if (zip && zip !== c.address?.zip) body.address = { zip };
|
|
566
|
+
}
|
|
567
|
+
if (Object.keys(body).length === 0) {
|
|
568
|
+
console.log(pc6.dim("Sin cambios"));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const res = await spin("Actualizando cliente\u2026", () => api("PUT", `/clients/${id}`, { body, team: opts.team }));
|
|
572
|
+
success(`Cliente ${id} actualizado`);
|
|
573
|
+
if (isJsonMode()) printJson(res.data);
|
|
574
|
+
} catch (e) {
|
|
575
|
+
error(e.message);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
clients.command("search <query>").description("Buscar clientes").option("--team <id>", "Team ID").action(async (query, opts) => {
|
|
579
|
+
try {
|
|
580
|
+
const res = await spin("Buscando clientes\u2026", () => api("GET", "/clients/search", { query: { q: query }, team: opts.team }));
|
|
581
|
+
const items = res.data || [];
|
|
582
|
+
if (isJsonMode()) return printJson(items);
|
|
583
|
+
printTable(
|
|
584
|
+
items.map((c) => ({
|
|
585
|
+
id: c.id,
|
|
586
|
+
nombre: c.legal_name || c.name || "\u2014",
|
|
587
|
+
rfc: c.tax_id || "\u2014",
|
|
588
|
+
email: c.email || "\u2014"
|
|
589
|
+
}))
|
|
590
|
+
);
|
|
591
|
+
} catch (e) {
|
|
592
|
+
error(e.message);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
clients.command("validate <id>").description("Validar datos fiscales de un cliente contra el SAT").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
596
|
+
try {
|
|
597
|
+
const res = await spin("Validando contra el SAT\u2026", () => api("POST", `/clients/validate/${id}`, { team: opts.team }));
|
|
598
|
+
success("Validaci\xF3n completada");
|
|
599
|
+
if (isJsonMode()) printJson(res.data);
|
|
600
|
+
else printKeyValue(res.data);
|
|
601
|
+
} catch (e) {
|
|
602
|
+
error(e.message);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
clients.command("portal").description("Generar link del portal de cliente (autofactura, documentos, pagos)").option("--id <id>", "Client ID").option("--email <email>", "Email del cliente").option("--team <id>", "Team ID").action(async (opts) => {
|
|
606
|
+
try {
|
|
607
|
+
if (!opts.id && !opts.email) {
|
|
608
|
+
const query = await askRequired("Email o ID del cliente");
|
|
609
|
+
if (query.startsWith("client_") || query.startsWith("cus_")) opts.id = query;
|
|
610
|
+
else opts.email = query;
|
|
611
|
+
}
|
|
612
|
+
const body = {};
|
|
613
|
+
if (opts.id) body.id = opts.id;
|
|
614
|
+
if (opts.email) body.email = opts.email;
|
|
615
|
+
const res = await spin("Generando portal\u2026", () => api("POST", "/clients/customerportal", { body, team: opts.team }));
|
|
616
|
+
const data = res.data;
|
|
617
|
+
if (isJsonMode()) return printJson(data);
|
|
618
|
+
success("Portal generado");
|
|
619
|
+
console.log(` URL: ${pc6.bold(data.url)}`);
|
|
620
|
+
if (data.expires_at) {
|
|
621
|
+
const expires = new Date(data.expires_at > 1e12 ? data.expires_at : data.expires_at * 1e3);
|
|
622
|
+
console.log(pc6.dim(` Expira: ${expires.toISOString().slice(0, 10)} (5 d\xEDas)`));
|
|
623
|
+
}
|
|
624
|
+
} catch (e) {
|
|
625
|
+
error(e.message);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
clients.command("delete <id>").description("Eliminar un cliente").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
629
|
+
try {
|
|
630
|
+
await api("DELETE", `/clients/${id}`, { team: opts.team });
|
|
631
|
+
success(`Cliente ${id} eliminado`);
|
|
632
|
+
} catch (e) {
|
|
633
|
+
error(e.message);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/commands/invoices.ts
|
|
639
|
+
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
640
|
+
import { join as join2 } from "node:path";
|
|
641
|
+
import pc7 from "picocolors";
|
|
642
|
+
function uid(item) {
|
|
643
|
+
return item.uuid ? item.uuid.slice(0, 8) + "\u2026" : item.id ? item.id.slice(0, 12) + "\u2026" : "\u2014";
|
|
644
|
+
}
|
|
645
|
+
function registerInvoiceCommands(program2) {
|
|
646
|
+
const invoices = program2.command("invoices").description("Gestionar facturas CFDI");
|
|
647
|
+
withListOpts(
|
|
648
|
+
invoices.command("list").description("Listar facturas de ingreso")
|
|
649
|
+
).option("--status <status>", "Filtrar por status (valid, cancelled)").option("--client <id>", "Filtrar por cliente").option("--series <series>", "Filtrar por serie").action(async (opts) => {
|
|
650
|
+
try {
|
|
651
|
+
const query = buildListQuery(opts);
|
|
652
|
+
if (opts.status) query.status = opts.status;
|
|
653
|
+
if (opts.client) query.client_id = opts.client;
|
|
654
|
+
if (opts.series) query.series = opts.series;
|
|
655
|
+
const res = await spin("Cargando facturas\u2026", () => api("GET", "/invoices/income", { query, team: opts.team }));
|
|
656
|
+
const items = res.data || [];
|
|
657
|
+
if (isJsonMode()) return printJson(items);
|
|
658
|
+
printTable(
|
|
659
|
+
items.map((i) => ({
|
|
660
|
+
uuid: uid(i),
|
|
661
|
+
cliente: (i.client?.legal_name || i.client?.name || "\u2014").slice(0, 25),
|
|
662
|
+
total: formatMoney(i.total, i.currency),
|
|
663
|
+
m\u00E9todo: i.payment_method || "\u2014",
|
|
664
|
+
status: i.status || "\u2014",
|
|
665
|
+
fecha: formatDate(i.created_at)
|
|
666
|
+
}))
|
|
667
|
+
);
|
|
668
|
+
printPaginationHint(res);
|
|
669
|
+
} catch (e) {
|
|
670
|
+
error(e.message);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
invoices.command("get <uuid>").description("Ver detalle de una factura").option("--team <id>", "Team ID").action(async (uuid, opts) => {
|
|
674
|
+
try {
|
|
675
|
+
const res = await api("GET", `/invoices/income/${uuid}`, { team: opts.team });
|
|
676
|
+
const i = res.data;
|
|
677
|
+
if (isJsonMode()) return printJson(i);
|
|
678
|
+
printKeyValue({
|
|
679
|
+
UUID: i.uuid || i.id || "\u2014",
|
|
680
|
+
Status: i.status || "\u2014",
|
|
681
|
+
Cliente: i.client?.legal_name || i.client?.name || "\u2014",
|
|
682
|
+
RFC: i.client?.tax_id || "\u2014",
|
|
683
|
+
Email: i.client?.email || "\u2014",
|
|
684
|
+
Subtotal: formatMoney(i.subtotal, i.currency),
|
|
685
|
+
Total: formatMoney(i.total, i.currency),
|
|
686
|
+
"M\xE9todo pago": i.payment_method || "\u2014",
|
|
687
|
+
"Forma pago": i.payment_form || "\u2014",
|
|
688
|
+
Serie: i.series || "\u2014",
|
|
689
|
+
Folio: i.folio_number || "\u2014",
|
|
690
|
+
"Saldo pendiente": i.last_balance !== void 0 ? formatMoney(i.last_balance, i.currency) : "\u2014",
|
|
691
|
+
Complementos: i.payment_complements ?? "\u2014",
|
|
692
|
+
Creado: formatDate(i.created_at)
|
|
693
|
+
});
|
|
694
|
+
} catch (e) {
|
|
695
|
+
error(e.message);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
invoices.command("create").description("Crear factura de ingreso (CFDI 4.0) \u2014 interactivo si no se pasan flags").option("--client <id>", "ID del cliente").option("--items <json>", 'Items JSON: [{"description":"...","quantity":1,"unit_price":100,"product_key":"84111506","unit_key":"E48"}]').option("--payment-form <code>", "Forma de pago (ej: 01=Efectivo, 03=Transferencia, 04=Tarjeta)", "03").option("--payment-method <code>", "M\xE9todo de pago (PUE o PPD)", "PUE").option("--use <use>", "Uso CFDI", "G03").option("--currency <code>", "Moneda", "MXN").option("--series <series>", "Serie").option("--send-email", "Enviar factura por email al cliente").option("--emails <emails>", "Emails adicionales (separados por coma)").option("--team <id>", "Team ID").action(async (opts) => {
|
|
699
|
+
try {
|
|
700
|
+
const interactive = !opts.client && !opts.items;
|
|
701
|
+
let clientId = opts.client;
|
|
702
|
+
let items;
|
|
703
|
+
if (interactive) {
|
|
704
|
+
const clientQuery = await askRequired("Buscar cliente (nombre, RFC, email o ID)");
|
|
705
|
+
if (clientQuery.startsWith("client_") || clientQuery.match(/^[a-zA-Z0-9_-]{10,}$/)) {
|
|
706
|
+
try {
|
|
707
|
+
const directRes = await spin("Buscando cliente\u2026", () => api("GET", `/clients/${clientQuery}`, { team: opts.team }));
|
|
708
|
+
if (directRes.data) {
|
|
709
|
+
clientId = directRes.data.id;
|
|
710
|
+
const c = directRes.data;
|
|
711
|
+
console.log(pc7.dim(` \u2192 ${c.legal_name || c.name || "\u2014"} (${c.tax_id || "sin RFC"})`));
|
|
712
|
+
}
|
|
713
|
+
} catch {
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (!clientId) {
|
|
717
|
+
const searchRes = await spin("Buscando cliente\u2026", () => api("GET", "/clients/search", { query: { q: clientQuery }, team: opts.team }));
|
|
718
|
+
const found = searchRes.data || [];
|
|
719
|
+
if (found.length === 0) {
|
|
720
|
+
error("No se encontr\xF3 el cliente");
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
if (found.length === 1) {
|
|
724
|
+
clientId = found[0].id;
|
|
725
|
+
console.log(pc7.dim(` \u2192 ${found[0].legal_name || found[0].name} (${found[0].tax_id || "sin RFC"})`));
|
|
726
|
+
} else {
|
|
727
|
+
clientId = await select(
|
|
728
|
+
"Selecciona cliente",
|
|
729
|
+
found.slice(0, 8).map((c) => ({
|
|
730
|
+
label: `${c.legal_name || c.name} \u2014 ${c.tax_id || "sin RFC"}`,
|
|
731
|
+
value: c.id
|
|
732
|
+
}))
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
items = [];
|
|
737
|
+
let addMore = true;
|
|
738
|
+
while (addMore) {
|
|
739
|
+
console.log(pc7.bold(`
|
|
740
|
+
Concepto ${items.length + 1}`));
|
|
741
|
+
const description = await askRequired("Descripci\xF3n");
|
|
742
|
+
const quantity = parseFloat(await ask("Cantidad", "1")) || 1;
|
|
743
|
+
const unitPrice = parseFloat(await askRequired("Precio unitario"));
|
|
744
|
+
const productKey = await ask("Clave producto SAT", "84111506");
|
|
745
|
+
const unitKey = await ask("Clave unidad SAT", "E48");
|
|
746
|
+
const addIva = await confirm("Agregar IVA 16%?");
|
|
747
|
+
const item = { description, quantity, unit_price: unitPrice, product_key: productKey, unit_key: unitKey };
|
|
748
|
+
if (addIva) item.taxes = [{ type: "IVA", rate: 0.16, factor: "Tasa", withholding: false }];
|
|
749
|
+
items.push(item);
|
|
750
|
+
addMore = await confirm("Agregar otro concepto?", false);
|
|
751
|
+
}
|
|
752
|
+
opts.paymentForm = await select("Forma de pago", [
|
|
753
|
+
{ label: "03 \u2014 Transferencia electr\xF3nica", value: "03" },
|
|
754
|
+
{ label: "01 \u2014 Efectivo", value: "01" },
|
|
755
|
+
{ label: "04 \u2014 Tarjeta de cr\xE9dito", value: "04" },
|
|
756
|
+
{ label: "28 \u2014 Tarjeta de d\xE9bito", value: "28" },
|
|
757
|
+
{ label: "99 \u2014 Por definir", value: "99" }
|
|
758
|
+
]);
|
|
759
|
+
opts.paymentMethod = await select("M\xE9todo de pago", [
|
|
760
|
+
{ label: "PUE \u2014 Pago en una sola exhibici\xF3n", value: "PUE" },
|
|
761
|
+
{ label: "PPD \u2014 Pago en parcialidades o diferido", value: "PPD" }
|
|
762
|
+
]);
|
|
763
|
+
const subtotal = items.reduce((sum3, i) => sum3 + i.quantity * i.unit_price, 0);
|
|
764
|
+
console.log(`
|
|
765
|
+
${pc7.bold("Resumen:")}`);
|
|
766
|
+
console.log(` Cliente: ${clientId}`);
|
|
767
|
+
console.log(` Conceptos: ${items.length}`);
|
|
768
|
+
console.log(` Subtotal: ${formatMoney(subtotal, opts.currency)}`);
|
|
769
|
+
console.log(` Forma pago: ${opts.paymentForm} M\xE9todo: ${opts.paymentMethod}`);
|
|
770
|
+
const proceed = await confirm("\nTimbrar factura?");
|
|
771
|
+
if (!proceed) {
|
|
772
|
+
console.log(pc7.dim("Cancelado"));
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
if (!clientId) {
|
|
777
|
+
error("--client es requerido");
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
if (!opts.items) {
|
|
781
|
+
error("--items es requerido");
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
items = JSON.parse(opts.items);
|
|
786
|
+
} catch {
|
|
787
|
+
error("Items JSON inv\xE1lido");
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const body = {
|
|
792
|
+
client: { id: clientId },
|
|
793
|
+
items,
|
|
794
|
+
payment_form: opts.paymentForm,
|
|
795
|
+
payment_method: opts.paymentMethod,
|
|
796
|
+
use: opts.use,
|
|
797
|
+
currency: opts.currency,
|
|
798
|
+
series: opts.series || void 0
|
|
799
|
+
};
|
|
800
|
+
if (opts.sendEmail) body.send_email = true;
|
|
801
|
+
if (opts.emails) body.emails = opts.emails.split(",").map((e) => e.trim());
|
|
802
|
+
const res = await spin("Timbrando factura\u2026", () => api("POST", "/invoices/income", { body, team: opts.team }));
|
|
803
|
+
success(`Factura creada: ${res.data.uuid || res.data.id}`);
|
|
804
|
+
if (isJsonMode()) printJson(res.data);
|
|
805
|
+
else console.log(` Total: ${formatMoney(res.data.total, res.data.currency)}`);
|
|
806
|
+
} catch (e) {
|
|
807
|
+
error(e.message);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
invoices.command("send <uuid>").description("Reenviar factura por email (PDF + XML adjuntos)").option("--to <emails>", "Emails adicionales separados por coma").option("--team <id>", "Team ID").action(async (uuid, opts) => {
|
|
811
|
+
try {
|
|
812
|
+
const body = {};
|
|
813
|
+
if (opts.to) body.emails = opts.to.split(",").map((e) => e.trim());
|
|
814
|
+
const res = await spin("Enviando factura por email\u2026", () => api("POST", `/invoices/${uuid}/send`, { body, team: opts.team }));
|
|
815
|
+
if (isJsonMode()) return printJson(res.data);
|
|
816
|
+
success(`Email enviado a: ${(res.data?.recipients || []).join(", ")}`);
|
|
817
|
+
if (res.data?.attachments?.length) {
|
|
818
|
+
console.log(pc7.dim(` Adjuntos: ${res.data.attachments.join(", ")}`));
|
|
819
|
+
}
|
|
820
|
+
} catch (e) {
|
|
821
|
+
error(e.message);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
invoices.command("cancel <uuid>").description("Cancelar una factura").requiredOption("--motive <code>", "Motivo de cancelaci\xF3n (01, 02, 03, 04)").option("--replacement <uuid>", "UUID de factura de reemplazo (para motivo 01)").option("--team <id>", "Team ID").action(async (uuid, opts) => {
|
|
825
|
+
try {
|
|
826
|
+
await spin("Cancelando factura\u2026", () => api("DELETE", `/invoices/${uuid}`, {
|
|
827
|
+
body: { motive: opts.motive, replacement: opts.replacement },
|
|
828
|
+
team: opts.team
|
|
829
|
+
}));
|
|
830
|
+
success(`Factura ${uuid} cancelada`);
|
|
831
|
+
} catch (e) {
|
|
832
|
+
error(e.message);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
invoices.command("search <query>").description("Buscar facturas").option("--team <id>", "Team ID").action(async (query, opts) => {
|
|
836
|
+
try {
|
|
837
|
+
const res = await spin("Buscando facturas\u2026", () => api("GET", "/invoices/search", { query: { q: query }, team: opts.team }));
|
|
838
|
+
const items = res.data || [];
|
|
839
|
+
if (isJsonMode()) return printJson(items);
|
|
840
|
+
printTable(
|
|
841
|
+
items.map((i) => ({
|
|
842
|
+
uuid: uid(i),
|
|
843
|
+
cliente: (i.client?.legal_name || "\u2014").slice(0, 25),
|
|
844
|
+
total: formatMoney(i.total, i.currency),
|
|
845
|
+
status: i.status || "\u2014"
|
|
846
|
+
}))
|
|
847
|
+
);
|
|
848
|
+
} catch (e) {
|
|
849
|
+
error(e.message);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
invoices.command("files <uuid>").description("Obtener archivos PDF/XML de una factura").option("--team <id>", "Team ID").action(async (uuid, opts) => {
|
|
853
|
+
try {
|
|
854
|
+
const res = await api("GET", `/invoices/${uuid}/files`, { team: opts.team });
|
|
855
|
+
if (isJsonMode()) return printJson(res.data);
|
|
856
|
+
const files = res.data;
|
|
857
|
+
if (files.pdf) console.log(`PDF: ${files.pdf}`);
|
|
858
|
+
if (files.xml) console.log(`XML: ${files.xml}`);
|
|
859
|
+
} catch (e) {
|
|
860
|
+
error(e.message);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
invoices.command("download <uuid>").description("Descargar PDF y XML de una factura al directorio actual").option("-o, --out <dir>", "Directorio de salida", ".").option("--team <id>", "Team ID").action(async (uuid, opts) => {
|
|
864
|
+
try {
|
|
865
|
+
const res = await spin("Obteniendo archivos\u2026", () => api("GET", `/invoices/${uuid}/files`, { team: opts.team }));
|
|
866
|
+
const files = res.data;
|
|
867
|
+
const saved = [];
|
|
868
|
+
if (files.pdf) {
|
|
869
|
+
const pdfRes = await spin("Descargando PDF\u2026", () => fetch(files.pdf));
|
|
870
|
+
const buf = Buffer.from(await pdfRes.arrayBuffer());
|
|
871
|
+
const path = join2(opts.out, `${uuid}.pdf`);
|
|
872
|
+
writeFileSync2(path, buf);
|
|
873
|
+
saved.push(path);
|
|
874
|
+
}
|
|
875
|
+
if (files.xml) {
|
|
876
|
+
const xmlRes = await spin("Descargando XML\u2026", () => fetch(files.xml));
|
|
877
|
+
const buf = Buffer.from(await xmlRes.arrayBuffer());
|
|
878
|
+
const path = join2(opts.out, `${uuid}.xml`);
|
|
879
|
+
writeFileSync2(path, buf);
|
|
880
|
+
saved.push(path);
|
|
881
|
+
}
|
|
882
|
+
if (saved.length === 0) error("No se encontraron archivos para esta factura");
|
|
883
|
+
else success(`Descargado: ${saved.join(", ")}`);
|
|
884
|
+
} catch (e) {
|
|
885
|
+
error(e.message);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
const drafts = invoices.command("drafts").description("Pre-facturas / borradores");
|
|
889
|
+
withListOpts(drafts.command("list").description("Listar borradores")).action(async (opts) => {
|
|
890
|
+
try {
|
|
891
|
+
const query = buildListQuery(opts);
|
|
892
|
+
const res = await spin("Cargando borradores\u2026", () => api("GET", "/invoices/draft", { query, team: opts.team }));
|
|
893
|
+
const items = res.data || [];
|
|
894
|
+
if (isJsonMode()) return printJson(items);
|
|
895
|
+
printTable(
|
|
896
|
+
items.map((i) => ({
|
|
897
|
+
uuid: uid(i),
|
|
898
|
+
cliente: (i.client?.legal_name || "\u2014").slice(0, 25),
|
|
899
|
+
total: formatMoney(i.total || 0, i.currency),
|
|
900
|
+
fecha: formatDate(i.created_at)
|
|
901
|
+
}))
|
|
902
|
+
);
|
|
903
|
+
printPaginationHint(res);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
error(e.message);
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
drafts.command("stamp <uuid>").description("Timbrar borrador (convertir a CFDI)").option("--team <id>", "Team ID").action(async (uuid, opts) => {
|
|
909
|
+
try {
|
|
910
|
+
const res = await spin("Timbrando borrador\u2026", () => api("POST", `/invoices/draft/${uuid}/stamp`, { team: opts.team }));
|
|
911
|
+
success(`Borrador timbrado: ${res.data.uuid || res.data.id}`);
|
|
912
|
+
if (isJsonMode()) printJson(res.data);
|
|
913
|
+
} catch (e) {
|
|
914
|
+
error(e.message);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
withListOpts(invoices.command("credit-notes").description("Listar notas de cr\xE9dito")).action(async (opts) => {
|
|
918
|
+
try {
|
|
919
|
+
const query = buildListQuery(opts);
|
|
920
|
+
const res = await spin("Cargando notas de cr\xE9dito\u2026", () => api("GET", "/invoices/egress", { query, team: opts.team }));
|
|
921
|
+
const items = res.data || [];
|
|
922
|
+
if (isJsonMode()) return printJson(items);
|
|
923
|
+
printTable(
|
|
924
|
+
items.map((i) => ({
|
|
925
|
+
uuid: uid(i),
|
|
926
|
+
cliente: (i.client?.legal_name || "\u2014").slice(0, 25),
|
|
927
|
+
total: formatMoney(i.total, i.currency),
|
|
928
|
+
status: i.status || "\u2014"
|
|
929
|
+
}))
|
|
930
|
+
);
|
|
931
|
+
printPaginationHint(res);
|
|
932
|
+
} catch (e) {
|
|
933
|
+
error(e.message);
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
withListOpts(invoices.command("complements").description("Listar complementos de pago")).option("--invoice <uuid>", "Filtrar por factura PPD relacionada").action(async (opts) => {
|
|
937
|
+
try {
|
|
938
|
+
const query = buildListQuery(opts);
|
|
939
|
+
if (opts.invoice) query.invoice_id = opts.invoice;
|
|
940
|
+
const res = await spin("Cargando complementos\u2026", () => api("GET", "/invoices/complements", { query, team: opts.team }));
|
|
941
|
+
const items = res.data || [];
|
|
942
|
+
if (isJsonMode()) return printJson(items);
|
|
943
|
+
printTable(
|
|
944
|
+
items.map((i) => ({
|
|
945
|
+
uuid: uid(i),
|
|
946
|
+
cliente: (i.client?.legal_name || "\u2014").slice(0, 25),
|
|
947
|
+
total: formatMoney(i.total, i.currency),
|
|
948
|
+
status: i.status || "\u2014"
|
|
949
|
+
}))
|
|
950
|
+
);
|
|
951
|
+
printPaginationHint(res);
|
|
952
|
+
} catch (e) {
|
|
953
|
+
error(e.message);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/commands/payments.ts
|
|
959
|
+
function registerPaymentCommands(program2) {
|
|
960
|
+
const payments = program2.command("payments").description("Gestionar pagos y cobros");
|
|
961
|
+
withListOpts(
|
|
962
|
+
payments.command("list").description("Listar pagos")
|
|
963
|
+
).option("--status <status>", "Filtrar: pending, succeeded, failed, cancelled, refunded").option("--client <id>", "Filtrar por cliente").option("--currency <code>", "Filtrar por moneda (MXN, USD)").option("--email <email>", "Filtrar por email del cliente").option("--rfc <rfc>", "Filtrar por RFC del cliente").action(async (opts) => {
|
|
964
|
+
try {
|
|
965
|
+
const query = buildListQuery(opts);
|
|
966
|
+
if (opts.status) query.status = opts.status;
|
|
967
|
+
if (opts.client) query.client_id = opts.client;
|
|
968
|
+
if (opts.currency) query.currency = opts.currency;
|
|
969
|
+
if (opts.email) query.email = opts.email;
|
|
970
|
+
if (opts.rfc) query.tax_id = opts.rfc;
|
|
971
|
+
const res = await spin("Cargando pagos\u2026", () => api("GET", "/payments", { query, team: opts.team }));
|
|
972
|
+
const items = res.data || [];
|
|
973
|
+
if (isJsonMode()) return printJson(items);
|
|
974
|
+
printTable(
|
|
975
|
+
items.map((p) => ({
|
|
976
|
+
id: p.id ? p.id.slice(0, 12) + "\u2026" : "\u2014",
|
|
977
|
+
cliente: (p.client?.legal_name || p.client?.name || "\u2014").slice(0, 25),
|
|
978
|
+
total: formatMoney(p.total, p.currency),
|
|
979
|
+
status: p.status,
|
|
980
|
+
fecha: formatDate(p.created_at)
|
|
981
|
+
}))
|
|
982
|
+
);
|
|
983
|
+
printPaginationHint(res);
|
|
984
|
+
} catch (e) {
|
|
985
|
+
error(e.message);
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
payments.command("get <id>").description("Ver detalle de un pago").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
989
|
+
try {
|
|
990
|
+
const res = await api("GET", `/payments/${id}`, { team: opts.team });
|
|
991
|
+
const p = res.data;
|
|
992
|
+
if (isJsonMode()) return printJson(p);
|
|
993
|
+
printKeyValue({
|
|
994
|
+
ID: p.id || "\u2014",
|
|
995
|
+
Status: p.status,
|
|
996
|
+
Cliente: p.client?.legal_name || p.client?.name || "\u2014",
|
|
997
|
+
Email: p.client?.email || "\u2014",
|
|
998
|
+
RFC: p.client?.tax_id || "\u2014",
|
|
999
|
+
Total: formatMoney(p.total, p.currency),
|
|
1000
|
+
"Forma pago": p.payment_form || "\u2014",
|
|
1001
|
+
"Link de pago": p.short_url || "\u2014",
|
|
1002
|
+
Creado: formatDate(p.created_at)
|
|
1003
|
+
});
|
|
1004
|
+
} catch (e) {
|
|
1005
|
+
error(e.message);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
payments.command("request").description("Solicitar un pago (genera link de cobro)").requiredOption("--client <id>", "ID del cliente").requiredOption("--items <json>", "Items JSON").option("--currency <code>", "Moneda", "MXN").option("--methods <list>", "M\xE9todos permitidos (card,bank,oxxo,stripe-spei)", "card,bank").option("--send-email", "Enviar link por email al cliente").option("--team <id>", "Team ID").action(async (opts) => {
|
|
1009
|
+
try {
|
|
1010
|
+
let items;
|
|
1011
|
+
try {
|
|
1012
|
+
items = JSON.parse(opts.items);
|
|
1013
|
+
} catch {
|
|
1014
|
+
error("Items JSON inv\xE1lido");
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
const body = {
|
|
1018
|
+
client: { id: opts.client },
|
|
1019
|
+
items,
|
|
1020
|
+
currency: opts.currency,
|
|
1021
|
+
allowed_payment_methods: opts.methods.split(",")
|
|
1022
|
+
};
|
|
1023
|
+
if (opts.sendEmail) body.send_email = true;
|
|
1024
|
+
const res = await spin("Creando solicitud de pago\u2026", () => api("POST", "/payments/request", { body, team: opts.team }));
|
|
1025
|
+
success(`Pago solicitado: ${res.data.id}`);
|
|
1026
|
+
if (res.data.short_url) console.log(` Link: ${res.data.short_url}`);
|
|
1027
|
+
if (isJsonMode()) printJson(res.data);
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
error(e.message);
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
payments.command("register").description("Registrar un pago recibido").requiredOption("--client <id>", "ID del cliente").requiredOption("--items <json>", "Items JSON").requiredOption("--payment-form <code>", "Forma de pago (03=Transferencia, etc)").option("--currency <code>", "Moneda", "MXN").option("--send-email", "Enviar confirmaci\xF3n por email").option("--team <id>", "Team ID").action(async (opts) => {
|
|
1033
|
+
try {
|
|
1034
|
+
let items;
|
|
1035
|
+
try {
|
|
1036
|
+
items = JSON.parse(opts.items);
|
|
1037
|
+
} catch {
|
|
1038
|
+
error("Items JSON inv\xE1lido");
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
const body = {
|
|
1042
|
+
automation_type: "stamp_invoice",
|
|
1043
|
+
client: { id: opts.client },
|
|
1044
|
+
items,
|
|
1045
|
+
currency: opts.currency,
|
|
1046
|
+
payment_form: opts.paymentForm,
|
|
1047
|
+
idempotency_key: `cli_${Date.now()}`
|
|
1048
|
+
};
|
|
1049
|
+
if (opts.sendEmail) body.send_email = true;
|
|
1050
|
+
const res = await spin("Registrando pago\u2026", () => api("POST", "/payments/register", { body, team: opts.team }));
|
|
1051
|
+
success(`Pago registrado: ${res.data.id}`);
|
|
1052
|
+
if (isJsonMode()) printJson(res.data);
|
|
1053
|
+
} catch (e) {
|
|
1054
|
+
error(e.message);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
payments.command("refund <id>").description("Reembolsar un pago").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
1058
|
+
try {
|
|
1059
|
+
const res = await spin("Procesando reembolso\u2026", () => api("POST", `/payments/${id}/refund`, { team: opts.team }));
|
|
1060
|
+
success(`Pago ${id} reembolsado`);
|
|
1061
|
+
if (isJsonMode()) printJson(res.data);
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
error(e.message);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/commands/services.ts
|
|
1069
|
+
import pc8 from "picocolors";
|
|
1070
|
+
function registerServiceCommands(program2) {
|
|
1071
|
+
const services = program2.command("services").description("Gestionar productos y servicios");
|
|
1072
|
+
withListOpts(
|
|
1073
|
+
services.command("list").description("Listar servicios")
|
|
1074
|
+
).action(async (opts) => {
|
|
1075
|
+
try {
|
|
1076
|
+
const query = buildListQuery(opts);
|
|
1077
|
+
const res = await spin("Cargando servicios\u2026", () => api("GET", "/services", { query, team: opts.team }));
|
|
1078
|
+
const items = res.data || [];
|
|
1079
|
+
if (isJsonMode()) return printJson(items);
|
|
1080
|
+
printTable(
|
|
1081
|
+
items.map((s) => ({
|
|
1082
|
+
id: s.id ? s.id.slice(0, 12) + "\u2026" : "\u2014",
|
|
1083
|
+
descripcion: (s.description || "\u2014").slice(0, 35),
|
|
1084
|
+
precio: formatMoney(s.unit_price, "MXN"),
|
|
1085
|
+
clave_prod: s.product_key || "\u2014",
|
|
1086
|
+
clave_unit: s.unit_key || "\u2014"
|
|
1087
|
+
}))
|
|
1088
|
+
);
|
|
1089
|
+
printPaginationHint(res);
|
|
1090
|
+
} catch (e) {
|
|
1091
|
+
error(e.message);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
services.command("get <id>").description("Ver detalle de un servicio").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
1095
|
+
try {
|
|
1096
|
+
const res = await api("GET", `/services/${id}`, { team: opts.team });
|
|
1097
|
+
const s = res.data;
|
|
1098
|
+
if (isJsonMode()) return printJson(s);
|
|
1099
|
+
printKeyValue({
|
|
1100
|
+
ID: s.id,
|
|
1101
|
+
Descripci\u00F3n: s.description,
|
|
1102
|
+
SKU: s.sku || "\u2014",
|
|
1103
|
+
"Precio unitario": formatMoney(s.unit_price, "MXN"),
|
|
1104
|
+
"Clave producto": s.product_key,
|
|
1105
|
+
"Clave unidad": s.unit_key,
|
|
1106
|
+
"Nombre unidad": s.unit_name || "\u2014",
|
|
1107
|
+
Impuestos: (s.taxes || []).map((t) => `${t.type} ${(t.rate * 100).toFixed(0)}%${t.withholding ? " (ret)" : ""}`).join(", ") || "\u2014"
|
|
1108
|
+
});
|
|
1109
|
+
} catch (e) {
|
|
1110
|
+
error(e.message);
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
services.command("create").description("Crear un servicio").requiredOption("--description <desc>", "Descripci\xF3n del servicio").requiredOption("--price <price>", "Precio unitario").requiredOption("--product-key <key>", "Clave SAT del producto (ej: 84111506)").requiredOption("--unit-key <key>", "Clave SAT de unidad (ej: E48, H87)").option("--sku <sku>", "SKU interno").option("--unit-name <name>", "Nombre de unidad").option("--iva", "Agregar IVA 16%").option("--team <id>", "Team ID").action(async (opts) => {
|
|
1114
|
+
try {
|
|
1115
|
+
const taxes = opts.iva ? [{ type: "IVA", rate: 0.16, factor: "Tasa", withholding: false }] : [];
|
|
1116
|
+
const res = await spin("Creando servicio\u2026", () => api("POST", "/services", {
|
|
1117
|
+
body: {
|
|
1118
|
+
description: opts.description,
|
|
1119
|
+
unit_price: parseFloat(opts.price),
|
|
1120
|
+
product_key: opts.productKey,
|
|
1121
|
+
unit_key: opts.unitKey,
|
|
1122
|
+
unit_name: opts.unitName,
|
|
1123
|
+
sku: opts.sku,
|
|
1124
|
+
taxes
|
|
1125
|
+
},
|
|
1126
|
+
team: opts.team
|
|
1127
|
+
}));
|
|
1128
|
+
success(`Servicio creado: ${res.data.id}`);
|
|
1129
|
+
if (isJsonMode()) printJson(res.data);
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
error(e.message);
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
services.command("update <id>").description("Actualizar un servicio").option("--description <desc>", "Descripci\xF3n").option("--price <price>", "Precio unitario").option("--product-key <key>", "Clave SAT del producto").option("--unit-key <key>", "Clave SAT de unidad").option("--sku <sku>", "SKU interno").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
1135
|
+
try {
|
|
1136
|
+
const hasFlags = opts.description || opts.price || opts.productKey || opts.unitKey || opts.sku;
|
|
1137
|
+
let body = {};
|
|
1138
|
+
if (hasFlags) {
|
|
1139
|
+
if (opts.description) body.description = opts.description;
|
|
1140
|
+
if (opts.price) body.unit_price = parseFloat(opts.price);
|
|
1141
|
+
if (opts.productKey) body.product_key = opts.productKey;
|
|
1142
|
+
if (opts.unitKey) body.unit_key = opts.unitKey;
|
|
1143
|
+
if (opts.sku) body.sku = opts.sku;
|
|
1144
|
+
} else {
|
|
1145
|
+
const current = await spin("Cargando servicio\u2026", () => api("GET", `/services/${id}`, { team: opts.team }));
|
|
1146
|
+
const s = current.data;
|
|
1147
|
+
console.log(pc8.dim(`Editando: ${s.description}
|
|
1148
|
+
`));
|
|
1149
|
+
console.log(pc8.dim("Deja vac\xEDo para mantener el valor actual.\n"));
|
|
1150
|
+
const description = await ask("Descripci\xF3n", s.description || "");
|
|
1151
|
+
const price = await ask("Precio unitario", String(s.unit_price || ""));
|
|
1152
|
+
const productKey = await ask("Clave producto SAT", s.product_key || "");
|
|
1153
|
+
const unitKey = await ask("Clave unidad SAT", s.unit_key || "");
|
|
1154
|
+
const sku = await ask("SKU", s.sku || "");
|
|
1155
|
+
if (description && description !== s.description) body.description = description;
|
|
1156
|
+
if (price && parseFloat(price) !== s.unit_price) body.unit_price = parseFloat(price);
|
|
1157
|
+
if (productKey && productKey !== s.product_key) body.product_key = productKey;
|
|
1158
|
+
if (unitKey && unitKey !== s.unit_key) body.unit_key = unitKey;
|
|
1159
|
+
if (sku && sku !== s.sku) body.sku = sku;
|
|
1160
|
+
}
|
|
1161
|
+
if (Object.keys(body).length === 0) {
|
|
1162
|
+
console.log(pc8.dim("Sin cambios"));
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const res = await spin("Actualizando servicio\u2026", () => api("PUT", `/services/${id}`, { body, team: opts.team }));
|
|
1166
|
+
success(`Servicio ${id} actualizado`);
|
|
1167
|
+
if (isJsonMode()) printJson(res.data);
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
error(e.message);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
services.command("delete <id>").description("Eliminar un servicio").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
1173
|
+
try {
|
|
1174
|
+
await api("DELETE", `/services/${id}`, { team: opts.team });
|
|
1175
|
+
success(`Servicio ${id} eliminado`);
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
error(e.message);
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/commands/webhooks.ts
|
|
1183
|
+
function registerWebhookCommands(program2) {
|
|
1184
|
+
const webhooks = program2.command("webhooks").description("Gestionar webhooks");
|
|
1185
|
+
withListOpts(
|
|
1186
|
+
webhooks.command("list").description("Listar webhooks configurados")
|
|
1187
|
+
).action(async (opts) => {
|
|
1188
|
+
try {
|
|
1189
|
+
const query = buildListQuery(opts);
|
|
1190
|
+
const res = await spin("Cargando webhooks\u2026", () => api("GET", "/webhooks", { query, team: opts.team }));
|
|
1191
|
+
const items = res.data || [];
|
|
1192
|
+
if (isJsonMode()) return printJson(items);
|
|
1193
|
+
printTable(
|
|
1194
|
+
items.map((w) => ({
|
|
1195
|
+
id: w.id ? w.id.slice(0, 12) + "\u2026" : "\u2014",
|
|
1196
|
+
url: (w.url || "\u2014").slice(0, 40),
|
|
1197
|
+
events: (w.events || []).join(", ").slice(0, 30) || "all"
|
|
1198
|
+
}))
|
|
1199
|
+
);
|
|
1200
|
+
printPaginationHint(res);
|
|
1201
|
+
} catch (e) {
|
|
1202
|
+
error(e.message);
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
webhooks.command("create").description("Crear un webhook").requiredOption("--url <url>", "URL del webhook").option("--events <events>", "Eventos separados por coma (ej: invoice.created,payment.succeeded)").option("--team <id>", "Team ID").action(async (opts) => {
|
|
1206
|
+
try {
|
|
1207
|
+
const body = { url: opts.url };
|
|
1208
|
+
if (opts.events) body.events = opts.events.split(",");
|
|
1209
|
+
const res = await spin("Creando webhook\u2026", () => api("POST", "/webhooks", { body, team: opts.team }));
|
|
1210
|
+
success(`Webhook creado: ${res.data.id}`);
|
|
1211
|
+
if (isJsonMode()) printJson(res.data);
|
|
1212
|
+
} catch (e) {
|
|
1213
|
+
error(e.message);
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
webhooks.command("delete <id>").description("Eliminar un webhook").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
1217
|
+
try {
|
|
1218
|
+
await api("DELETE", `/webhooks/${id}`, { team: opts.team });
|
|
1219
|
+
success(`Webhook ${id} eliminado`);
|
|
1220
|
+
} catch (e) {
|
|
1221
|
+
error(e.message);
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/commands/teams.ts
|
|
1227
|
+
function registerTeamCommands(program2) {
|
|
1228
|
+
const teams = program2.command("teams").description("Gestionar equipos");
|
|
1229
|
+
teams.command("list").description("Listar equipos").action(async () => {
|
|
1230
|
+
try {
|
|
1231
|
+
const res = await api("GET", "/teams");
|
|
1232
|
+
const items = res.data || [];
|
|
1233
|
+
if (isJsonMode()) return printJson(items);
|
|
1234
|
+
printTable(
|
|
1235
|
+
items.map((t) => ({
|
|
1236
|
+
id: t.id,
|
|
1237
|
+
nombre: t.legal_name || t.name || "\u2014",
|
|
1238
|
+
rfc: t.tax_id || "\u2014"
|
|
1239
|
+
}))
|
|
1240
|
+
);
|
|
1241
|
+
} catch (e) {
|
|
1242
|
+
error(e.message);
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
teams.command("get <id>").description("Ver detalle de un equipo").action(async (id) => {
|
|
1246
|
+
try {
|
|
1247
|
+
const res = await api("GET", `/teams/${id}`);
|
|
1248
|
+
const t = res.data;
|
|
1249
|
+
if (isJsonMode()) return printJson(t);
|
|
1250
|
+
printKeyValue({
|
|
1251
|
+
ID: t.id,
|
|
1252
|
+
Nombre: t.legal_name || t.name || "\u2014",
|
|
1253
|
+
RFC: t.tax_id || "\u2014",
|
|
1254
|
+
"R\xE9gimen fiscal": t.tax_system || "\u2014",
|
|
1255
|
+
"C\xF3digo postal": t.address?.zip || "\u2014"
|
|
1256
|
+
});
|
|
1257
|
+
} catch (e) {
|
|
1258
|
+
error(e.message);
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
teams.command("integrations").description("Ver integraciones del equipo").action(async () => {
|
|
1262
|
+
try {
|
|
1263
|
+
const res = await api("GET", "/teams/integrations");
|
|
1264
|
+
if (isJsonMode()) return printJson(res.data);
|
|
1265
|
+
printKeyValue(res.data);
|
|
1266
|
+
} catch (e) {
|
|
1267
|
+
error(e.message);
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/commands/receipts.ts
|
|
1273
|
+
function registerReceiptCommands(program2) {
|
|
1274
|
+
const receipts = program2.command("receipts").description("Gestionar recibos de venta");
|
|
1275
|
+
withListOpts(
|
|
1276
|
+
receipts.command("list").description("Listar recibos")
|
|
1277
|
+
).option("--status <status>", "Filtrar: pending, open, invoiced, completed, expired, cancelled").option("--client <id>", "Filtrar por cliente").action(async (opts) => {
|
|
1278
|
+
try {
|
|
1279
|
+
const query = buildListQuery(opts);
|
|
1280
|
+
if (opts.status) query.status = opts.status;
|
|
1281
|
+
if (opts.client) query.client_id = opts.client;
|
|
1282
|
+
const res = await spin("Cargando recibos\u2026", () => api("GET", "/receipts", { query, team: opts.team }));
|
|
1283
|
+
const items = (res.data || []).filter((r) => r != null);
|
|
1284
|
+
if (isJsonMode()) return printJson(items);
|
|
1285
|
+
printTable(
|
|
1286
|
+
items.map((r) => ({
|
|
1287
|
+
id: r.id ? r.id.slice(0, 12) + "\u2026" : "\u2014",
|
|
1288
|
+
cliente: (r.client?.legal_name || r.client?.name || "p\xFAblico general").slice(0, 25),
|
|
1289
|
+
total: formatMoney(r.total, r.currency),
|
|
1290
|
+
status: r.status || "\u2014",
|
|
1291
|
+
v\u00E1lido: formatDate(r.validUntil)
|
|
1292
|
+
}))
|
|
1293
|
+
);
|
|
1294
|
+
printPaginationHint(res);
|
|
1295
|
+
} catch (e) {
|
|
1296
|
+
error(e.message);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
receipts.command("stamp <id>").description("Timbrar un recibo").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
1300
|
+
try {
|
|
1301
|
+
const res = await spin("Timbrando recibo\u2026", () => api("POST", `/receipts/${id}/stamp`, { team: opts.team }));
|
|
1302
|
+
success(`Recibo timbrado: ${res.data.id}`);
|
|
1303
|
+
if (isJsonMode()) printJson(res.data);
|
|
1304
|
+
} catch (e) {
|
|
1305
|
+
error(e.message);
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
receipts.command("cancel <id>").description("Cancelar un recibo").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
1309
|
+
try {
|
|
1310
|
+
await api("DELETE", `/receipts/${id}`, { team: opts.team });
|
|
1311
|
+
success(`Recibo ${id} cancelado`);
|
|
1312
|
+
} catch (e) {
|
|
1313
|
+
error(e.message);
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// src/commands/doctor.ts
|
|
1319
|
+
import pc9 from "picocolors";
|
|
1320
|
+
function pass(msg) {
|
|
1321
|
+
console.log(pc9.green(` \u2713 ${msg}`));
|
|
1322
|
+
}
|
|
1323
|
+
function fail(msg) {
|
|
1324
|
+
console.log(pc9.red(` \u2717 ${msg}`));
|
|
1325
|
+
}
|
|
1326
|
+
function info(msg) {
|
|
1327
|
+
console.log(pc9.dim(` ${msg}`));
|
|
1328
|
+
}
|
|
1329
|
+
function warn2(msg) {
|
|
1330
|
+
console.log(pc9.yellow(` ! ${msg}`));
|
|
1331
|
+
}
|
|
1332
|
+
function registerDoctorCommand(program2) {
|
|
1333
|
+
program2.command("doctor").description("Diagn\xF3stico del CLI: verifica auth, conexi\xF3n, SAT y configuraci\xF3n").action(async () => {
|
|
1334
|
+
console.log(pc9.bold("\ngigstack doctor\n"));
|
|
1335
|
+
let allGood = true;
|
|
1336
|
+
const nodeVersion = process.version;
|
|
1337
|
+
const major = parseInt(nodeVersion.slice(1));
|
|
1338
|
+
if (major >= 18) {
|
|
1339
|
+
pass(`Node.js ${nodeVersion}`);
|
|
1340
|
+
} else {
|
|
1341
|
+
fail(`Node.js ${nodeVersion} \u2014 se requiere v18+`);
|
|
1342
|
+
allGood = false;
|
|
1343
|
+
}
|
|
1344
|
+
console.log();
|
|
1345
|
+
const profile = getActiveProfile();
|
|
1346
|
+
if (!profile) {
|
|
1347
|
+
fail("No hay credenciales configuradas");
|
|
1348
|
+
info("Ejecuta: gigstack login");
|
|
1349
|
+
allGood = false;
|
|
1350
|
+
return printSummary(allGood);
|
|
1351
|
+
}
|
|
1352
|
+
const profiles = listProfiles();
|
|
1353
|
+
pass(`Perfil activo: ${pc9.bold(profile.name)}`);
|
|
1354
|
+
if (profiles.length > 1) {
|
|
1355
|
+
info(`${profiles.length} perfiles configurados`);
|
|
1356
|
+
}
|
|
1357
|
+
if (isTestKey(profile.apiKey)) {
|
|
1358
|
+
warn2("Usando API key de prueba (test mode)");
|
|
1359
|
+
} else {
|
|
1360
|
+
pass("Usando API key de producci\xF3n (live mode)");
|
|
1361
|
+
}
|
|
1362
|
+
console.log();
|
|
1363
|
+
try {
|
|
1364
|
+
const start = Date.now();
|
|
1365
|
+
const team = await resolveTeam(profile.apiKey);
|
|
1366
|
+
const latency = Date.now() - start;
|
|
1367
|
+
pass(`Conexi\xF3n a API: ${latency}ms`);
|
|
1368
|
+
if (!team) {
|
|
1369
|
+
fail("No se encontraron equipos");
|
|
1370
|
+
allGood = false;
|
|
1371
|
+
} else {
|
|
1372
|
+
pass(`Equipo: ${pc9.bold(team.legal_name || team.brand?.alias || team.name || "\u2014")}`);
|
|
1373
|
+
if (team.tax_id) {
|
|
1374
|
+
pass(`RFC: ${team.tax_id}`);
|
|
1375
|
+
} else {
|
|
1376
|
+
warn2("RFC no configurado");
|
|
1377
|
+
}
|
|
1378
|
+
console.log();
|
|
1379
|
+
if (team.sat?.completed) {
|
|
1380
|
+
pass("SAT conectado");
|
|
1381
|
+
if (team.sat.csd_expires_at) {
|
|
1382
|
+
const expires = formatDate(team.sat.csd_expires_at);
|
|
1383
|
+
const expiresDate = new Date(team.sat.csd_expires_at > 1e12 ? team.sat.csd_expires_at : team.sat.csd_expires_at * 1e3);
|
|
1384
|
+
const daysLeft = Math.floor((expiresDate.getTime() - Date.now()) / 864e5);
|
|
1385
|
+
if (daysLeft < 30) {
|
|
1386
|
+
warn2(`CSD expira en ${daysLeft} d\xEDas (${expires})`);
|
|
1387
|
+
allGood = false;
|
|
1388
|
+
} else {
|
|
1389
|
+
pass(`CSD v\xE1lido hasta ${expires} (${daysLeft} d\xEDas)`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (team.sat.connected_at) {
|
|
1393
|
+
info(`Conectado: ${formatDate(team.sat.connected_at)}`);
|
|
1394
|
+
}
|
|
1395
|
+
} else {
|
|
1396
|
+
fail("SAT no conectado \u2014 no podr\xE1s timbrar facturas");
|
|
1397
|
+
info("Conecta tu CSD en app.gigstack.pro/settings");
|
|
1398
|
+
allGood = false;
|
|
1399
|
+
}
|
|
1400
|
+
console.log();
|
|
1401
|
+
const integrations = team.integrations || {};
|
|
1402
|
+
const connected = Object.entries(integrations).filter(([_, v]) => v?.completed).map(([k]) => k);
|
|
1403
|
+
if (connected.length > 0) {
|
|
1404
|
+
pass(`Integraciones: ${connected.join(", ")}`);
|
|
1405
|
+
} else {
|
|
1406
|
+
info("Sin integraciones conectadas");
|
|
1407
|
+
}
|
|
1408
|
+
console.log();
|
|
1409
|
+
const endpoints = [
|
|
1410
|
+
{ name: "Clientes", path: "/clients", query: { limit: "1" } },
|
|
1411
|
+
{ name: "Facturas", path: "/invoices/income", query: { limit: "1" } },
|
|
1412
|
+
{ name: "Pagos", path: "/payments", query: { limit: "1" } },
|
|
1413
|
+
{ name: "Servicios", path: "/services", query: { limit: "1" } },
|
|
1414
|
+
{ name: "Webhooks", path: "/webhooks" },
|
|
1415
|
+
{ name: "Recibos", path: "/receipts", query: { limit: "1" } }
|
|
1416
|
+
];
|
|
1417
|
+
for (const ep of endpoints) {
|
|
1418
|
+
try {
|
|
1419
|
+
await api("GET", ep.path, { query: ep.query });
|
|
1420
|
+
pass(`${ep.name} (${ep.path})`);
|
|
1421
|
+
} catch (e) {
|
|
1422
|
+
fail(`${ep.name} (${ep.path}): ${e.message}`);
|
|
1423
|
+
allGood = false;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
} catch (e) {
|
|
1428
|
+
fail(`Error de conexi\xF3n: ${e.message}`);
|
|
1429
|
+
allGood = false;
|
|
1430
|
+
}
|
|
1431
|
+
printSummary(allGood);
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
function printSummary(allGood) {
|
|
1435
|
+
console.log();
|
|
1436
|
+
if (allGood) {
|
|
1437
|
+
console.log(pc9.green(pc9.bold("Todo en orden. Tu CLI est\xE1 listo.")));
|
|
1438
|
+
} else {
|
|
1439
|
+
console.log(pc9.yellow(pc9.bold("Algunos problemas detectados. Revisa los items marcados arriba.")));
|
|
1440
|
+
}
|
|
1441
|
+
console.log();
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/commands/pay.ts
|
|
1445
|
+
import pc10 from "picocolors";
|
|
1446
|
+
function registerPayCommand(program2) {
|
|
1447
|
+
program2.command("pay").description("Registrar un pago y enviar portal de autofactura al cliente").option("--email <email>", "Email del cliente").option("--name <name>", "Nombre del cliente").option("--rfc <rfc>", "RFC del cliente (busca o crea autom\xE1ticamente)").option("--description <desc>", "Descripci\xF3n del servicio/producto").option("--amount <amount>", "Monto (sin IVA)").option("--iva", "Agregar IVA 16%").option("--no-iva", "Sin IVA").option("--payment-form <code>", "Forma de pago (01=Efectivo, 03=Transferencia, 04=Tarjeta)").option("--currency <code>", "Moneda", "MXN").option("--automation <type>", "Tipo de automatizaci\xF3n (pue_invoice, ppd_invoice_and_complement, none)", "pue_invoice").option("--product-key <key>", "Clave producto SAT", "84111506").option("--unit-key <key>", "Clave unidad SAT", "E48").option("--metadata <json>", "Metadata JSON adicional").option("--stdin", "Leer input desde stdin (JSON)").option("--team <id>", "Team ID").action(async (opts) => {
|
|
1448
|
+
try {
|
|
1449
|
+
let email;
|
|
1450
|
+
let name;
|
|
1451
|
+
let rfc;
|
|
1452
|
+
let description;
|
|
1453
|
+
let amount;
|
|
1454
|
+
let addIva;
|
|
1455
|
+
let paymentForm;
|
|
1456
|
+
if (opts.stdin) {
|
|
1457
|
+
const chunks = [];
|
|
1458
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1459
|
+
const input = JSON.parse(Buffer.concat(chunks).toString());
|
|
1460
|
+
email = input.email;
|
|
1461
|
+
name = input.name;
|
|
1462
|
+
rfc = input.rfc;
|
|
1463
|
+
description = input.description;
|
|
1464
|
+
amount = input.amount;
|
|
1465
|
+
addIva = input.iva !== false;
|
|
1466
|
+
paymentForm = input.payment_form || "03";
|
|
1467
|
+
opts.currency = input.currency || opts.currency;
|
|
1468
|
+
opts.automation = input.automation || opts.automation;
|
|
1469
|
+
opts.productKey = input.product_key || opts.productKey;
|
|
1470
|
+
opts.unitKey = input.unit_key || opts.unitKey;
|
|
1471
|
+
if (input.metadata) opts.metadata = JSON.stringify(input.metadata);
|
|
1472
|
+
} else if (opts.email && opts.amount && opts.description) {
|
|
1473
|
+
email = opts.email;
|
|
1474
|
+
name = opts.name;
|
|
1475
|
+
rfc = opts.rfc;
|
|
1476
|
+
description = opts.description;
|
|
1477
|
+
amount = parseFloat(opts.amount);
|
|
1478
|
+
addIva = opts.iva !== false;
|
|
1479
|
+
paymentForm = opts.paymentForm || "03";
|
|
1480
|
+
} else {
|
|
1481
|
+
email = opts.email || await askRequired("Email del cliente");
|
|
1482
|
+
name = opts.name || await ask("Nombre del cliente (opcional)");
|
|
1483
|
+
rfc = opts.rfc || await ask("RFC (opcional, el cliente puede completarlo en el portal)");
|
|
1484
|
+
description = opts.description || await askRequired("Descripci\xF3n del servicio/producto");
|
|
1485
|
+
const amountStr = opts.amount || await askRequired("Monto (sin IVA)");
|
|
1486
|
+
amount = parseFloat(amountStr);
|
|
1487
|
+
if (isNaN(amount) || amount <= 0) {
|
|
1488
|
+
error("Monto inv\xE1lido");
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
addIva = opts.iva !== void 0 ? opts.iva : await confirm("Agregar IVA 16%?");
|
|
1492
|
+
paymentForm = opts.paymentForm || await select("Forma de pago", [
|
|
1493
|
+
{ label: "03 \u2014 Transferencia electr\xF3nica", value: "03" },
|
|
1494
|
+
{ label: "01 \u2014 Efectivo", value: "01" },
|
|
1495
|
+
{ label: "04 \u2014 Tarjeta de cr\xE9dito", value: "04" },
|
|
1496
|
+
{ label: "28 \u2014 Tarjeta de d\xE9bito", value: "28" },
|
|
1497
|
+
{ label: "99 \u2014 Por definir", value: "99" }
|
|
1498
|
+
]);
|
|
1499
|
+
const iva2 = addIva ? amount * 0.16 : 0;
|
|
1500
|
+
const total2 = amount + iva2;
|
|
1501
|
+
console.log(`
|
|
1502
|
+
${pc10.bold("Resumen:")}`);
|
|
1503
|
+
console.log(` Cliente: ${email}${name ? ` (${name})` : ""}`);
|
|
1504
|
+
console.log(` Descripci\xF3n: ${description}`);
|
|
1505
|
+
console.log(` Subtotal: ${formatMoney(amount, opts.currency)}`);
|
|
1506
|
+
if (addIva) console.log(` IVA 16%: ${formatMoney(iva2, opts.currency)}`);
|
|
1507
|
+
console.log(` ${pc10.bold("Total:")} ${pc10.bold(formatMoney(total2, opts.currency))}`);
|
|
1508
|
+
console.log(` Forma pago: ${paymentForm}`);
|
|
1509
|
+
console.log(` Automaci\xF3n: ${opts.automation}`);
|
|
1510
|
+
const proceed = await confirm("\nRegistrar pago?");
|
|
1511
|
+
if (!proceed) {
|
|
1512
|
+
console.log(pc10.dim("Cancelado"));
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const client = { email };
|
|
1517
|
+
if (name) {
|
|
1518
|
+
client.name = name;
|
|
1519
|
+
client.legal_name = name;
|
|
1520
|
+
}
|
|
1521
|
+
if (rfc) {
|
|
1522
|
+
client.tax_id = rfc;
|
|
1523
|
+
client.search = { on_key: "tax_id", on_value: rfc, auto_create: true };
|
|
1524
|
+
} else {
|
|
1525
|
+
client.search = { on_key: "email", on_value: email, auto_create: true };
|
|
1526
|
+
}
|
|
1527
|
+
const item = {
|
|
1528
|
+
description,
|
|
1529
|
+
quantity: 1,
|
|
1530
|
+
unit_price: amount,
|
|
1531
|
+
product_key: opts.productKey,
|
|
1532
|
+
unit_key: opts.unitKey
|
|
1533
|
+
};
|
|
1534
|
+
if (addIva) {
|
|
1535
|
+
item.taxes = [{ type: "IVA", rate: 0.16, factor: "Tasa", withholding: false }];
|
|
1536
|
+
}
|
|
1537
|
+
const body = {
|
|
1538
|
+
client,
|
|
1539
|
+
items: [item],
|
|
1540
|
+
currency: opts.currency,
|
|
1541
|
+
payment_form: paymentForm,
|
|
1542
|
+
automation_type: opts.automation,
|
|
1543
|
+
idempotency_key: `cli_${Date.now()}`
|
|
1544
|
+
};
|
|
1545
|
+
if (opts.metadata) {
|
|
1546
|
+
try {
|
|
1547
|
+
body.metadata = JSON.parse(opts.metadata);
|
|
1548
|
+
} catch {
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const res = await spin("Registrando pago\u2026", () => api("POST", "/payments/register", { body, team: opts.team }));
|
|
1552
|
+
const payment = res.data;
|
|
1553
|
+
if (isJsonMode()) return printJson(payment);
|
|
1554
|
+
const iva = addIva ? amount * 0.16 : 0;
|
|
1555
|
+
const total = amount + iva;
|
|
1556
|
+
success("Pago registrado");
|
|
1557
|
+
console.log(` ID: ${payment.id || "\u2014"}`);
|
|
1558
|
+
console.log(` Total: ${formatMoney(total, opts.currency)}`);
|
|
1559
|
+
if (payment.client?.id) console.log(` Cliente: ${payment.client.id}`);
|
|
1560
|
+
if (payment.short_url) console.log(` Link pago: ${payment.short_url}`);
|
|
1561
|
+
console.log(`
|
|
1562
|
+
${pc10.dim("Portal de autofactura enviado a")} ${pc10.bold(email)}`);
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
error(e.message);
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/commands/status.ts
|
|
1570
|
+
import pc11 from "picocolors";
|
|
1571
|
+
function sum(items, key = "total") {
|
|
1572
|
+
return items.reduce((s, i) => s + (i?.[key] || 0), 0);
|
|
1573
|
+
}
|
|
1574
|
+
function fmt(amount, currency = "MXN") {
|
|
1575
|
+
return formatMoney(amount, currency);
|
|
1576
|
+
}
|
|
1577
|
+
function parseDate2(input, isEnd = false) {
|
|
1578
|
+
const trimmed = input.trim().toLowerCase();
|
|
1579
|
+
const relMatch = trimmed.match(/^(\d+)d$/);
|
|
1580
|
+
if (relMatch) {
|
|
1581
|
+
const d = /* @__PURE__ */ new Date();
|
|
1582
|
+
d.setDate(d.getDate() - parseInt(relMatch[1]));
|
|
1583
|
+
return d.toISOString().slice(0, 10);
|
|
1584
|
+
}
|
|
1585
|
+
if (trimmed === "today") return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1586
|
+
if (/^\d{4}-\d{2}$/.test(trimmed)) {
|
|
1587
|
+
if (isEnd) {
|
|
1588
|
+
const [y, m] = trimmed.split("-").map(Number);
|
|
1589
|
+
const last = new Date(y, m, 0).getDate();
|
|
1590
|
+
return `${trimmed}-${String(last).padStart(2, "0")}`;
|
|
1591
|
+
}
|
|
1592
|
+
return `${trimmed}-01`;
|
|
1593
|
+
}
|
|
1594
|
+
return trimmed;
|
|
1595
|
+
}
|
|
1596
|
+
function defaultFrom() {
|
|
1597
|
+
const d = /* @__PURE__ */ new Date();
|
|
1598
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-01`;
|
|
1599
|
+
}
|
|
1600
|
+
function defaultTo() {
|
|
1601
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1602
|
+
}
|
|
1603
|
+
function registerStatusCommand(program2) {
|
|
1604
|
+
program2.command("status").description("Resumen financiero: facturas, pagos, recibos y conciliaci\xF3n").option("--from <date>", "Fecha inicio (YYYY-MM-DD, YYYY-MM, 30d, 7d)").option("--to <date>", "Fecha fin (YYYY-MM-DD, YYYY-MM, today)").option("--team <id>", "Team ID").action(async (opts) => {
|
|
1605
|
+
try {
|
|
1606
|
+
const from = opts.from ? parseDate2(opts.from) : defaultFrom();
|
|
1607
|
+
const to = opts.to ? parseDate2(opts.to, true) : defaultTo();
|
|
1608
|
+
const fromTs = String(Math.floor(new Date(from).getTime() / 1e3));
|
|
1609
|
+
const toTs = String(Math.floor((/* @__PURE__ */ new Date(to + "T23:59:59")).getTime() / 1e3));
|
|
1610
|
+
const dateQuery = {
|
|
1611
|
+
"created[gte]": fromTs,
|
|
1612
|
+
"created[lte]": toTs,
|
|
1613
|
+
limit: "100"
|
|
1614
|
+
};
|
|
1615
|
+
const noDatesQuery = { limit: "100" };
|
|
1616
|
+
const hasDateFilter = opts.from || opts.to;
|
|
1617
|
+
const [team, invoices, payments, receipts] = await spin(
|
|
1618
|
+
"Cargando resumen\u2026",
|
|
1619
|
+
() => Promise.all([
|
|
1620
|
+
resolveTeam(),
|
|
1621
|
+
api("GET", "/invoices/income", { query: hasDateFilter ? dateQuery : noDatesQuery, team: opts.team }).catch(() => ({ data: [] })),
|
|
1622
|
+
api("GET", "/payments", { query: hasDateFilter ? dateQuery : noDatesQuery, team: opts.team }).catch(() => ({ data: [] })),
|
|
1623
|
+
api("GET", "/receipts", { query: hasDateFilter ? dateQuery : noDatesQuery, team: opts.team }).catch(() => ({ data: [] }))
|
|
1624
|
+
])
|
|
1625
|
+
);
|
|
1626
|
+
const inv = (invoices.data || []).filter((i) => i != null);
|
|
1627
|
+
const pay = (payments.data || []).filter((p) => p != null);
|
|
1628
|
+
const rec = (receipts.data || []).filter((r) => r != null);
|
|
1629
|
+
const cur = inv[0]?.currency || pay[0]?.currency || "MXN";
|
|
1630
|
+
const validInv = inv.filter((i) => i.status === "valid");
|
|
1631
|
+
const cancelledInv = inv.filter((i) => i.status === "cancelled");
|
|
1632
|
+
const pueInv = validInv.filter((i) => i.payment_method === "PUE");
|
|
1633
|
+
const ppdInv = validInv.filter((i) => i.payment_method === "PPD");
|
|
1634
|
+
const succeededPay = pay.filter((p) => p.status === "succeeded");
|
|
1635
|
+
const pendingPay = pay.filter((p) => p.status === "pending" || p.status === "requires_payment_method");
|
|
1636
|
+
const failedPay = pay.filter((p) => p.status === "failed" || p.status === "cancelled");
|
|
1637
|
+
const pendingRec = rec.filter((r) => r.status === "pending" || r.status === "open");
|
|
1638
|
+
const invoicedRec = rec.filter((r) => r.status === "invoiced" || r.status === "completed");
|
|
1639
|
+
const ppdWithBalance = ppdInv.filter((i) => {
|
|
1640
|
+
const balance = i.last_balance ?? i.total;
|
|
1641
|
+
return balance > 0 && (!i.payment_complements || i.payment_complements === 0 || balance > 0.01);
|
|
1642
|
+
});
|
|
1643
|
+
const ppdPaid = ppdInv.filter((i) => {
|
|
1644
|
+
const balance = i.last_balance;
|
|
1645
|
+
return balance !== void 0 && balance !== null && balance <= 0.01;
|
|
1646
|
+
});
|
|
1647
|
+
const ppdPartial = ppdInv.filter((i) => {
|
|
1648
|
+
const balance = i.last_balance;
|
|
1649
|
+
return balance !== void 0 && balance !== null && balance > 0.01 && balance < (i.total || 0);
|
|
1650
|
+
});
|
|
1651
|
+
const ppdPendingTotal = ppdWithBalance.reduce((s, i) => s + (i.last_balance ?? i.total ?? 0), 0);
|
|
1652
|
+
const now = Date.now();
|
|
1653
|
+
const dayMs = 864e5;
|
|
1654
|
+
const aging = { current: [], d30: [], d60: [], d90: [], over90: [] };
|
|
1655
|
+
for (const i of ppdWithBalance) {
|
|
1656
|
+
const ts = i.created_at ? typeof i.created_at === "string" ? new Date(i.created_at).getTime() : i.created_at > 1e12 ? i.created_at : i.created_at * 1e3 : now;
|
|
1657
|
+
const daysOld = Math.floor((now - ts) / dayMs);
|
|
1658
|
+
const balance = i.last_balance ?? i.total ?? 0;
|
|
1659
|
+
const entry = { ...i, _balance: balance, _daysOld: daysOld };
|
|
1660
|
+
if (daysOld <= 15) aging.current.push(entry);
|
|
1661
|
+
else if (daysOld <= 30) aging.d30.push(entry);
|
|
1662
|
+
else if (daysOld <= 60) aging.d60.push(entry);
|
|
1663
|
+
else if (daysOld <= 90) aging.d90.push(entry);
|
|
1664
|
+
else aging.over90.push(entry);
|
|
1665
|
+
}
|
|
1666
|
+
if (isJsonMode()) {
|
|
1667
|
+
return printJson({
|
|
1668
|
+
period: hasDateFilter ? { from, to } : { note: "last 100 records" },
|
|
1669
|
+
team: { name: team?.legal_name || team?.brand?.alias, tax_id: team?.tax_id },
|
|
1670
|
+
invoices: {
|
|
1671
|
+
total: inv.length,
|
|
1672
|
+
valid: validInv.length,
|
|
1673
|
+
cancelled: cancelledInv.length,
|
|
1674
|
+
pue: { count: pueInv.length, total: sum(pueInv) },
|
|
1675
|
+
ppd: { count: ppdInv.length, total: sum(ppdInv) },
|
|
1676
|
+
total_amount: sum(validInv)
|
|
1677
|
+
},
|
|
1678
|
+
payments: {
|
|
1679
|
+
total: pay.length,
|
|
1680
|
+
succeeded: succeededPay.length,
|
|
1681
|
+
pending: pendingPay.length,
|
|
1682
|
+
failed: failedPay.length,
|
|
1683
|
+
succeeded_amount: sum(succeededPay),
|
|
1684
|
+
pending_amount: sum(pendingPay)
|
|
1685
|
+
},
|
|
1686
|
+
receipts: {
|
|
1687
|
+
total: rec.length,
|
|
1688
|
+
pending: pendingRec.length,
|
|
1689
|
+
invoiced: invoicedRec.length,
|
|
1690
|
+
pending_amount: sum(pendingRec),
|
|
1691
|
+
total_amount: sum(rec)
|
|
1692
|
+
},
|
|
1693
|
+
cobranza: {
|
|
1694
|
+
ppd_pending: ppdWithBalance.length,
|
|
1695
|
+
ppd_paid: ppdPaid.length,
|
|
1696
|
+
ppd_partial: ppdPartial.length,
|
|
1697
|
+
pending_amount: ppdPendingTotal,
|
|
1698
|
+
aging: {
|
|
1699
|
+
current: { count: aging.current.length, amount: aging.current.reduce((s, i) => s + i._balance, 0) },
|
|
1700
|
+
d30: { count: aging.d30.length, amount: aging.d30.reduce((s, i) => s + i._balance, 0) },
|
|
1701
|
+
d60: { count: aging.d60.length, amount: aging.d60.reduce((s, i) => s + i._balance, 0) },
|
|
1702
|
+
d90: { count: aging.d90.length, amount: aging.d90.reduce((s, i) => s + i._balance, 0) },
|
|
1703
|
+
over90: { count: aging.over90.length, amount: aging.over90.reduce((s, i) => s + i._balance, 0) }
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
const teamName = team?.legal_name || team?.brand?.alias || "\u2014";
|
|
1709
|
+
console.log(`
|
|
1710
|
+
${pc11.bold(teamName)} ${team?.tax_id ? pc11.dim(`(${team.tax_id})`) : ""}`);
|
|
1711
|
+
if (hasDateFilter) {
|
|
1712
|
+
console.log(pc11.dim(`${from} \u2192 ${to}`));
|
|
1713
|
+
} else {
|
|
1714
|
+
console.log(pc11.dim(`\xDAltimos 100 registros (usa --from para filtrar)`));
|
|
1715
|
+
}
|
|
1716
|
+
console.log(`
|
|
1717
|
+
${pc11.bold(pc11.underline("Facturas"))}`);
|
|
1718
|
+
console.log(` V\xE1lidas ${String(validInv.length).padStart(4)} ${pc11.green(fmt(sum(validInv), cur))}`);
|
|
1719
|
+
console.log(` PUE ${String(pueInv.length).padStart(4)} ${fmt(sum(pueInv), cur)}`);
|
|
1720
|
+
console.log(` PPD ${String(ppdInv.length).padStart(4)} ${fmt(sum(ppdInv), cur)}`);
|
|
1721
|
+
if (cancelledInv.length > 0) {
|
|
1722
|
+
console.log(` Canceladas ${String(cancelledInv.length).padStart(4)} ${pc11.dim(fmt(sum(cancelledInv), cur))}`);
|
|
1723
|
+
}
|
|
1724
|
+
console.log(`
|
|
1725
|
+
${pc11.bold(pc11.underline("Pagos"))}`);
|
|
1726
|
+
console.log(` Cobrados ${String(succeededPay.length).padStart(4)} ${pc11.green(fmt(sum(succeededPay), cur))}`);
|
|
1727
|
+
if (pendingPay.length > 0) {
|
|
1728
|
+
console.log(` Pendientes ${String(pendingPay.length).padStart(4)} ${pc11.yellow(fmt(sum(pendingPay), cur))}`);
|
|
1729
|
+
}
|
|
1730
|
+
if (failedPay.length > 0) {
|
|
1731
|
+
console.log(` Fallidos ${String(failedPay.length).padStart(4)} ${pc11.red(fmt(sum(failedPay), cur))}`);
|
|
1732
|
+
}
|
|
1733
|
+
console.log(`
|
|
1734
|
+
${pc11.bold(pc11.underline("Recibos"))}`);
|
|
1735
|
+
console.log(` Total ${String(rec.length).padStart(4)} ${fmt(sum(rec), cur)}`);
|
|
1736
|
+
if (pendingRec.length > 0) {
|
|
1737
|
+
console.log(` Por facturar ${String(pendingRec.length).padStart(4)} ${pc11.yellow(fmt(sum(pendingRec), cur))}`);
|
|
1738
|
+
}
|
|
1739
|
+
if (invoicedRec.length > 0) {
|
|
1740
|
+
console.log(` Facturados ${String(invoicedRec.length).padStart(4)} ${pc11.green(fmt(sum(invoicedRec), cur))}`);
|
|
1741
|
+
}
|
|
1742
|
+
if (ppdInv.length > 0) {
|
|
1743
|
+
console.log(`
|
|
1744
|
+
${pc11.bold(pc11.underline("Cobranza PPD"))}`);
|
|
1745
|
+
console.log(` Pendientes ${String(ppdWithBalance.length).padStart(4)} ${ppdPendingTotal > 0 ? pc11.red(fmt(ppdPendingTotal, cur)) : pc11.green(fmt(0, cur))}`);
|
|
1746
|
+
if (ppdPartial.length > 0) {
|
|
1747
|
+
console.log(` Pago parcial ${String(ppdPartial.length).padStart(4)}`);
|
|
1748
|
+
}
|
|
1749
|
+
if (ppdPaid.length > 0) {
|
|
1750
|
+
console.log(` Liquidadas ${String(ppdPaid.length).padStart(4)} ${pc11.green(fmt(sum(ppdPaid), cur))}`);
|
|
1751
|
+
}
|
|
1752
|
+
const agingBuckets = [
|
|
1753
|
+
{ label: "0-15 d\xEDas", data: aging.current },
|
|
1754
|
+
{ label: "16-30 d\xEDas", data: aging.d30 },
|
|
1755
|
+
{ label: "31-60 d\xEDas", data: aging.d60 },
|
|
1756
|
+
{ label: "61-90 d\xEDas", data: aging.d90 },
|
|
1757
|
+
{ label: "+90 d\xEDas", data: aging.over90 }
|
|
1758
|
+
].filter((b) => b.data.length > 0);
|
|
1759
|
+
if (agingBuckets.length > 0) {
|
|
1760
|
+
console.log(pc11.dim(" Antig\xFCedad:"));
|
|
1761
|
+
for (const bucket of agingBuckets) {
|
|
1762
|
+
const bucketTotal = bucket.data.reduce((s, i) => s + i._balance, 0);
|
|
1763
|
+
const color = bucket === agingBuckets[agingBuckets.length - 1] && bucket.data === aging.over90 ? pc11.red : pc11.yellow;
|
|
1764
|
+
console.log(` ${bucket.label.padEnd(12)} ${String(bucket.data.length).padStart(4)} ${color(fmt(bucketTotal, cur))}`);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
const invoicedTotal = sum(validInv);
|
|
1769
|
+
const collectedTotal = sum(succeededPay);
|
|
1770
|
+
const pendingRecTotal = sum(pendingRec);
|
|
1771
|
+
const diff = collectedTotal - invoicedTotal;
|
|
1772
|
+
console.log(`
|
|
1773
|
+
${pc11.bold(pc11.underline("Conciliaci\xF3n"))}`);
|
|
1774
|
+
console.log(` Facturado (PUE) ${pc11.green(fmt(sum(pueInv), cur))}`);
|
|
1775
|
+
console.log(` Facturado (PPD) ${fmt(sum(ppdInv), cur)}`);
|
|
1776
|
+
console.log(` Cobrado total ${pc11.green(fmt(collectedTotal, cur))}`);
|
|
1777
|
+
console.log(` Recibos por facturar ${pc11.yellow(fmt(pendingRecTotal, cur))}`);
|
|
1778
|
+
if (Math.abs(diff) > 0.01) {
|
|
1779
|
+
const diffColor = diff > 0 ? pc11.yellow : pc11.red;
|
|
1780
|
+
console.log(` Diferencia (cobro-fact) ${diffColor(fmt(diff, cur))}`);
|
|
1781
|
+
} else {
|
|
1782
|
+
console.log(` Diferencia ${pc11.green("$0.00 \u2014 cuadrado")}`);
|
|
1783
|
+
}
|
|
1784
|
+
console.log();
|
|
1785
|
+
if (team?.sat?.completed) {
|
|
1786
|
+
console.log(`${pc11.green("\u25CF")} SAT conectado`);
|
|
1787
|
+
} else {
|
|
1788
|
+
console.log(`${pc11.red("\u25CF")} SAT no conectado`);
|
|
1789
|
+
}
|
|
1790
|
+
if (team?.billing?.credits !== void 0) {
|
|
1791
|
+
const credits = team.billing.credits;
|
|
1792
|
+
console.log(`${credits > 0 ? pc11.green("\u25CF") : pc11.red("\u25CF")} ${credits} cr\xE9ditos restantes`);
|
|
1793
|
+
}
|
|
1794
|
+
console.log();
|
|
1795
|
+
} catch (e) {
|
|
1796
|
+
console.error(pc11.red(`\u2717 ${e.message}`));
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// src/commands/context.ts
|
|
1802
|
+
import pc12 from "picocolors";
|
|
1803
|
+
var KNOWLEDGE = {
|
|
1804
|
+
payments: {
|
|
1805
|
+
title: "Pagos (Payments)",
|
|
1806
|
+
summary: "A payment represents money collected from a client. Payments can come from multiple sources (Stripe, PayPal, MercadoPago, bank transfer, cash, etc.) or be registered manually via the API/CLI. A payment is NOT an invoice \u2014 a payment triggers automations that can create invoices, receipts, or payment complements automatically.",
|
|
1807
|
+
concepts: {
|
|
1808
|
+
payment: "Money received from a client. Has items, a total, a currency, and a payment form (how the money was received).",
|
|
1809
|
+
payment_form: "SAT-defined code for HOW money was received. 01=Cash, 03=Wire transfer, 04=Credit card, 28=Debit card, 99=To be defined.",
|
|
1810
|
+
payment_method: "PUE (paid in full at once) or PPD (installments/deferred). Determines invoice type and whether payment complements are needed.",
|
|
1811
|
+
automation_type: "What happens after payment is registered: 'pue_invoice' (stamp invoice immediately), 'ppd_invoice_and_complement' (create PPD invoice + complement), 'receipt' (create receipt for self-invoicing), 'none' (just record the payment).",
|
|
1812
|
+
idempotency_key: "Unique key to prevent duplicate payments. Always use one when registering payments programmatically.",
|
|
1813
|
+
payment_source: "Where the payment originated: stripe, paypal, mercadopago, openpay, conekta, clip, dlocal, woocommerce, shopify, manual, api."
|
|
1814
|
+
},
|
|
1815
|
+
statuses: {
|
|
1816
|
+
pending: "Payment created but not yet collected. Client has not paid yet. Waiting for payment method or processing.",
|
|
1817
|
+
requires_payment_method: "Payment link sent but client hasn't chosen how to pay yet.",
|
|
1818
|
+
processing: "Payment is being processed by the payment processor (card charge in progress, etc.).",
|
|
1819
|
+
succeeded: "Money collected successfully. Automations will fire (invoice, receipt, etc.).",
|
|
1820
|
+
failed: "Payment attempt failed (card declined, insufficient funds, etc.). No money was collected.",
|
|
1821
|
+
cancelled: "Payment was cancelled before collection. No money was collected.",
|
|
1822
|
+
refunded: "Payment was collected but then returned to the client. A credit note (nota de cr\xE9dito) may be needed."
|
|
1823
|
+
},
|
|
1824
|
+
actions: {
|
|
1825
|
+
"payments list": "List payments. Supports --limit, --next (cursor pagination), --from/--to (date filter), --sort, --order-by, --status, --client, --currency, --email, --rfc.",
|
|
1826
|
+
"payments request": "Create a payment link. Client receives a URL to pay via card, bank, OXXO, etc. Supports --send-email.",
|
|
1827
|
+
"payments register": "Record a payment already received (cash, wire transfer, etc.). Triggers automations. Supports --send-email.",
|
|
1828
|
+
"payments refund <id>": "Refund a succeeded payment. Money returned to client.",
|
|
1829
|
+
"pay": "Shortcut: register payment + auto-create client + send self-invoice portal. Best for quick charges."
|
|
1830
|
+
},
|
|
1831
|
+
relationships: [
|
|
1832
|
+
"Payment \u2192 triggers automation \u2192 creates Invoice (PUE) or Invoice (PPD) + Complement",
|
|
1833
|
+
"Payment \u2192 triggers automation \u2192 creates Receipt (for self-invoicing by client)",
|
|
1834
|
+
"Payment succeeded \u2192 automation fires. Payment failed/cancelled \u2192 nothing happens.",
|
|
1835
|
+
"Refunded payment \u2192 may need credit note (nota de cr\xE9dito / egress invoice)",
|
|
1836
|
+
"A payment can have a client attached. If client has RFC, invoice is auto-stamped. If not, a receipt is created for self-invoicing."
|
|
1837
|
+
],
|
|
1838
|
+
tips: [
|
|
1839
|
+
"To charge someone: use 'gigstack pay' for the simplest flow.",
|
|
1840
|
+
"To send a payment link: use 'gigstack payments request'.",
|
|
1841
|
+
"To record money already received: use 'gigstack payments register'.",
|
|
1842
|
+
"Pending payments are NOT missing money \u2014 they are payment links waiting for the client to pay.",
|
|
1843
|
+
"'succeeded' is the only status that means money was actually collected.",
|
|
1844
|
+
"Use --from/--to for date ranges, --status to filter by payment status, --currency for currency, --email or --rfc to find payments by client.",
|
|
1845
|
+
"Pagination: the CLI shows a --next token at the bottom of results. Pass it to get the next page."
|
|
1846
|
+
]
|
|
1847
|
+
},
|
|
1848
|
+
invoices: {
|
|
1849
|
+
title: "Facturas CFDI (Invoices)",
|
|
1850
|
+
summary: "A CFDI invoice is a legally-binding Mexican tax document stamped by the SAT. There are 4 types: Income (I), Egress/Credit note (E), Payment complement (P), and Transfer (T). Invoices are created after a payment, manually, or via automation. An invoice is NOT a payment \u2014 it's the fiscal proof of a transaction.",
|
|
1851
|
+
concepts: {
|
|
1852
|
+
cfdi: "Comprobante Fiscal Digital por Internet \u2014 the official Mexican digital invoice format required by SAT.",
|
|
1853
|
+
income_invoice: "Type 'I' (Ingreso). The most common: proves revenue was received. Created for each sale/payment.",
|
|
1854
|
+
credit_note: "Type 'E' (Egreso). Cancels or reduces a previous income invoice. Used for refunds, discounts, returns.",
|
|
1855
|
+
payment_complement: "Type 'P' (Pago). Required for PPD invoices to prove each installment payment was received. Links back to the original PPD invoice.",
|
|
1856
|
+
transfer_invoice: "Type 'T' (Traslado). For transferring goods between locations. Rare in gigstack.",
|
|
1857
|
+
pue: "Pago en Una sola Exhibici\xF3n \u2014 paid in full at once. One invoice, done. Most common for small transactions.",
|
|
1858
|
+
ppd: "Pago en Parcialidades o Diferido \u2014 payment in installments or deferred. Requires payment complements for each payment received. The invoice is stamped first, payments come later.",
|
|
1859
|
+
folio: "Sequential invoice number within a series (e.g., A-001, A-002). Managed by gigstack, no gaps.",
|
|
1860
|
+
series: "Letter/prefix for invoice numbering (e.g., 'A', 'B', 'WEB'). Different series for different purposes.",
|
|
1861
|
+
use_cfdi: "SAT code for how the client will use the invoice. G03=General expenses (most common), G01=Acquisition of merchandise, S01=No fiscal effects.",
|
|
1862
|
+
global_invoice: "Monthly consolidated invoice for all receipts that were NOT self-invoiced by clients. Created by the EOM (End of Month) process."
|
|
1863
|
+
},
|
|
1864
|
+
statuses: {
|
|
1865
|
+
valid: "Invoice is stamped and valid with the SAT. This is the normal state.",
|
|
1866
|
+
cancelled: "Invoice was cancelled with the SAT. Requires a motive code (01-04).",
|
|
1867
|
+
pending: "Invoice is being processed (rare, usually stamping is instant).",
|
|
1868
|
+
draft: "Pre-invoice / borrador. Not yet stamped. Can be edited before stamping."
|
|
1869
|
+
},
|
|
1870
|
+
actions: {
|
|
1871
|
+
"invoices list": "List invoices. Supports --limit, --next (cursor pagination), --from/--to (date filter), --sort, --order-by, --status, --client, --series.",
|
|
1872
|
+
"invoices create": "Stamp a new income invoice (CFDI 4.0). Interactive or via flags. Supports --send-email and --emails.",
|
|
1873
|
+
"invoices cancel <uuid>": "Cancel an invoice with the SAT. Requires --motive (01=replacement, 02=no commercial activity, 03=wrong operation, 04=related to global invoice).",
|
|
1874
|
+
"invoices download <uuid>": "Download PDF and XML files to disk.",
|
|
1875
|
+
"invoices files <uuid>": "Get PDF/XML download URLs.",
|
|
1876
|
+
"invoices search <query>": "Search invoices by client name, RFC, or UUID.",
|
|
1877
|
+
"invoices drafts list": "List pre-invoices that haven't been stamped yet.",
|
|
1878
|
+
"invoices drafts stamp <uuid>": "Stamp a draft, converting it to a real CFDI.",
|
|
1879
|
+
"invoices credit-notes": "List credit notes (egress invoices).",
|
|
1880
|
+
"invoices complements": "List payment complements. Supports --invoice <uuid> to filter by parent PPD invoice."
|
|
1881
|
+
},
|
|
1882
|
+
relationships: [
|
|
1883
|
+
"Payment (succeeded) \u2192 triggers \u2192 Income Invoice (PUE) \u2014 one invoice per payment.",
|
|
1884
|
+
"Payment (succeeded) \u2192 triggers \u2192 Income Invoice (PPD) + Payment Complement \u2014 invoice first, complement proves each payment.",
|
|
1885
|
+
"PPD Invoice \u2192 needs Payment Complements to track installment payments. Without complements, the PPD invoice is 'unpaid'.",
|
|
1886
|
+
"Receipt (not self-invoiced) \u2192 grouped at End of Month \u2192 Global Invoice.",
|
|
1887
|
+
"Invoice cancelled \u2192 may need replacement invoice (motive 01) or credit note.",
|
|
1888
|
+
"Credit note (E) \u2192 reduces/cancels a previous income invoice (I)."
|
|
1889
|
+
],
|
|
1890
|
+
tips: [
|
|
1891
|
+
"PPD invoices with last_balance > 0 are unpaid or partially paid \u2014 this is your 'cobranza' (collections).",
|
|
1892
|
+
"PUE invoices are fully paid by definition \u2014 the payment happened before/at stamping.",
|
|
1893
|
+
"Don't confuse invoice status with payment status. An invoice can be 'valid' but the PPD payment still pending.",
|
|
1894
|
+
"Cancelled invoices need a motive. Use 01 if you're replacing it, 02-04 for other reasons.",
|
|
1895
|
+
"Use --status to filter by invoice status (valid, cancelled, pending), --client for client ID, --series for invoice series.",
|
|
1896
|
+
"Use --from/--to for date ranges. Pagination: the CLI shows a --next token at the bottom of results."
|
|
1897
|
+
]
|
|
1898
|
+
},
|
|
1899
|
+
receipts: {
|
|
1900
|
+
title: "Recibos de Venta (Sales Receipts)",
|
|
1901
|
+
summary: "A receipt is a NON-fiscal sales document generated when a payment is received but the client hasn't provided their fiscal data (RFC) for invoicing. The receipt includes a link to a self-invoice portal where the client can enter their RFC and generate their own CFDI invoice. Receipts that are NOT self-invoiced by month end are grouped into a 'global invoice'. A receipt is NOT a missing payment \u2014 the money was already collected.",
|
|
1902
|
+
concepts: {
|
|
1903
|
+
receipt: "Proof of sale WITHOUT fiscal value. Generated when payment is received but client RFC is unknown.",
|
|
1904
|
+
self_invoice_portal: "Web page where the client enters their RFC and fiscal data to generate their own CFDI from the receipt. URL sent via email automatically.",
|
|
1905
|
+
global_invoice: "At end of month, all receipts NOT self-invoiced are grouped into one consolidated CFDI invoice to 'p\xFAblico general' (XAXX010101000). This fulfills the fiscal obligation.",
|
|
1906
|
+
eom: "End of Month process. Runs daily at 23:59 Mexico City time. Groups pending receipts \u2192 global invoices."
|
|
1907
|
+
},
|
|
1908
|
+
statuses: {
|
|
1909
|
+
pending: "Receipt created, waiting for client to self-invoice. The money IS collected \u2014 only the CFDI is missing.",
|
|
1910
|
+
open: "Same as pending \u2014 receipt is available for self-invoicing.",
|
|
1911
|
+
invoiced: "Client completed self-invoicing. A CFDI was generated from this receipt.",
|
|
1912
|
+
completed: "Same as invoiced \u2014 receipt was converted to a CFDI.",
|
|
1913
|
+
expired: "Self-invoice window closed. Receipt will be included in the global invoice at EOM.",
|
|
1914
|
+
cancelled: "Receipt was cancelled. Will not be included in any invoice."
|
|
1915
|
+
},
|
|
1916
|
+
actions: {
|
|
1917
|
+
"receipts list": "List all receipts. Supports --limit, --next (cursor pagination), --from/--to (date filter), --sort, --order-by, --status, --client.",
|
|
1918
|
+
"receipts stamp <id>": "Manually stamp a receipt (generate invoice from it).",
|
|
1919
|
+
"receipts cancel <id>": "Cancel a receipt."
|
|
1920
|
+
},
|
|
1921
|
+
relationships: [
|
|
1922
|
+
"Payment (succeeded, no client RFC) \u2192 creates Receipt \u2192 client self-invoices \u2192 Income Invoice.",
|
|
1923
|
+
"Receipt (pending at EOM) \u2192 grouped into Global Invoice to p\xFAblico general.",
|
|
1924
|
+
"Receipt pending does NOT mean payment pending. The money is collected. Only the CFDI is missing.",
|
|
1925
|
+
"Receipt amount = payment amount. There is no 'debt' on a receipt \u2014 it's a documentation task, not a collection task."
|
|
1926
|
+
],
|
|
1927
|
+
tips: [
|
|
1928
|
+
"IMPORTANT: 'Receipts pending' means self-invoicing is pending, NOT that money is owed. The payment was already collected.",
|
|
1929
|
+
"If you want to know who owes you money, look at PPD invoices with balance > 0 (cobranza), NOT at pending receipts.",
|
|
1930
|
+
"To reduce pending receipts, remind clients to self-invoice before month end.",
|
|
1931
|
+
"Global invoicing at EOM handles the fiscal obligation for any receipt not self-invoiced.",
|
|
1932
|
+
"Use --status to filter receipts (pending, invoiced, expired, cancelled), --client for client ID.",
|
|
1933
|
+
"Pagination: the CLI shows a --next token at the bottom of results. Pass it to get the next page."
|
|
1934
|
+
]
|
|
1935
|
+
},
|
|
1936
|
+
clients: {
|
|
1937
|
+
title: "Clientes (Clients)",
|
|
1938
|
+
summary: "A client represents a person or company you transact with. Clients have fiscal data (RFC, tax system, address) needed for invoicing. Clients can be created manually or auto-created when registering a payment with client search.",
|
|
1939
|
+
concepts: {
|
|
1940
|
+
rfc: "Registro Federal de Contribuyentes \u2014 Mexican tax ID. Required for invoicing. Format: 12-13 alphanumeric characters.",
|
|
1941
|
+
tax_system: "R\xE9gimen fiscal \u2014 SAT tax regime code. Common: 601 (General/Corp), 612 (Business individual), 616 (No obligations), 626 (RESICO).",
|
|
1942
|
+
use_cfdi: "How the client will use the invoice. G03 (general expenses) is the safe default.",
|
|
1943
|
+
publico_general: "Generic client (XAXX010101000) used for global invoices when no specific client RFC is available.",
|
|
1944
|
+
auto_create: "When registering a payment, you can pass client.search with auto_create=true. If the client doesn't exist, it's created automatically."
|
|
1945
|
+
},
|
|
1946
|
+
statuses: {
|
|
1947
|
+
is_valid: "Client's fiscal data has been validated against the SAT."
|
|
1948
|
+
},
|
|
1949
|
+
actions: {
|
|
1950
|
+
"clients list": "List clients. Supports --limit, --next (cursor pagination), --from/--to (date filter), --sort, --order-by.",
|
|
1951
|
+
"clients create": "Create a client interactively or via flags.",
|
|
1952
|
+
"clients update <id>": "Update client fiscal data (name, RFC, tax system, etc.).",
|
|
1953
|
+
"clients search <query>": "Search by name, RFC, or email.",
|
|
1954
|
+
"clients validate <id>": "Validate client's fiscal data against the SAT.",
|
|
1955
|
+
"clients delete <id>": "Delete a client."
|
|
1956
|
+
},
|
|
1957
|
+
relationships: [
|
|
1958
|
+
"Client \u2192 attached to Payments, Invoices, Receipts.",
|
|
1959
|
+
"Client with RFC \u2192 payment triggers invoice automation.",
|
|
1960
|
+
"Client without RFC \u2192 payment triggers receipt (self-invoice portal).",
|
|
1961
|
+
"Client.search in payment body \u2192 auto-find or auto-create client by RFC or email."
|
|
1962
|
+
],
|
|
1963
|
+
tips: [
|
|
1964
|
+
"Always validate new clients with 'clients validate' to avoid CFDI stamping errors.",
|
|
1965
|
+
"Use client.search with auto_create when registering payments \u2014 avoids manual client creation.",
|
|
1966
|
+
"You can search clients by ID directly: pass the client_xxx ID in the search prompt."
|
|
1967
|
+
]
|
|
1968
|
+
},
|
|
1969
|
+
cobranza: {
|
|
1970
|
+
title: "Cobranza (Collections / Accounts Receivable)",
|
|
1971
|
+
summary: "Cobranza tracks money that is OWED to you. Only PPD invoices can have outstanding balances \u2014 PUE invoices are paid in full by definition. Receipts are NOT cobranza: receipt money is already collected, only the self-invoice is pending. To find who owes you money, look at PPD invoices where last_balance > 0.",
|
|
1972
|
+
concepts: {
|
|
1973
|
+
ppd_balance: "The remaining amount on a PPD invoice. Starts at the invoice total, decreases with each payment complement. When balance = 0, the invoice is fully paid.",
|
|
1974
|
+
payment_complement: "CFDI type 'P' \u2014 proves a payment was received against a PPD invoice. Each complement reduces the PPD balance.",
|
|
1975
|
+
installment: "Each payment complement is an installment. Installment 1 is the first payment, 2 is the second, etc.",
|
|
1976
|
+
aging: "How many days since the PPD invoice was created. Older = higher risk of non-payment. Buckets: 0-15, 16-30, 31-60, 61-90, 90+ days.",
|
|
1977
|
+
partial_payment: "A PPD invoice where some payment complements exist but balance > 0. Client started paying but hasn't finished."
|
|
1978
|
+
},
|
|
1979
|
+
statuses: {
|
|
1980
|
+
"last_balance > 0, complements = 0": "PPD invoice with NO payments received. Full amount is outstanding.",
|
|
1981
|
+
"last_balance > 0, complements > 0": "Partial payment. Some money received, but balance remains.",
|
|
1982
|
+
"last_balance = 0": "Fully paid. All payment complements received. No action needed."
|
|
1983
|
+
},
|
|
1984
|
+
actions: {
|
|
1985
|
+
"status": "Run 'gigstack status' to see cobranza summary with aging breakdown.",
|
|
1986
|
+
"invoices list --json": "Get all invoices as JSON, filter by payment_method=PPD and last_balance > 0 to find outstanding invoices.",
|
|
1987
|
+
"invoices complements": "List payment complements to see which PPD invoices have received payments."
|
|
1988
|
+
},
|
|
1989
|
+
relationships: [
|
|
1990
|
+
"PPD Invoice (last_balance > 0) = money owed to you = cobranza.",
|
|
1991
|
+
"PUE Invoice = already paid, never appears in cobranza.",
|
|
1992
|
+
"Receipt pending = money already collected, NOT cobranza. Only the self-invoice document is missing.",
|
|
1993
|
+
"Payment complement received \u2192 reduces PPD balance \u2192 moves toward fully paid."
|
|
1994
|
+
],
|
|
1995
|
+
tips: [
|
|
1996
|
+
"CRITICAL: Receipts are NOT cobranza. A pending receipt means the client hasn't self-invoiced yet, but the MONEY IS ALREADY COLLECTED.",
|
|
1997
|
+
"To find who owes money: filter PPD invoices where last_balance > 0.",
|
|
1998
|
+
"Prioritize by aging: invoices over 90 days are at highest risk of non-payment.",
|
|
1999
|
+
"Partial payments (balance > 0 but complements > 0) need a nudge \u2014 the client started paying but stopped.",
|
|
2000
|
+
"'gigstack status' shows a complete cobranza breakdown with aging buckets.",
|
|
2001
|
+
"Use 'gigstack invoices list --status valid --json' and filter client-side by payment_method=PPD and last_balance > 0 for collections data. Server-side payment_method filter is not yet available."
|
|
2002
|
+
]
|
|
2003
|
+
},
|
|
2004
|
+
automations: {
|
|
2005
|
+
title: "Automatizaciones (Automations)",
|
|
2006
|
+
summary: "Automations are actions triggered by events (mainly payments). When a payment is registered or received via webhook, gigstack can automatically create invoices, receipts, payment complements, or send emails. Automations are configured per team and can be overridden per payment.",
|
|
2007
|
+
concepts: {
|
|
2008
|
+
pue_invoice: "Automation: stamp a PUE income invoice immediately when payment succeeds.",
|
|
2009
|
+
ppd_invoice_and_complement: "Automation: stamp a PPD invoice (if not exists) and a payment complement for this payment.",
|
|
2010
|
+
receipt: "Automation: create a receipt with self-invoice portal link. Used when client RFC is unknown.",
|
|
2011
|
+
none: "No automation. Just record the payment, do nothing else.",
|
|
2012
|
+
stamp_invoice: "Legacy automation name, equivalent to pue_invoice in the payments/register endpoint."
|
|
2013
|
+
},
|
|
2014
|
+
relationships: [
|
|
2015
|
+
"Payment registered with automation_type \u2192 proserver fires the automation.",
|
|
2016
|
+
"Webhook payment (Stripe, PayPal, etc.) \u2192 uses team's default automation config.",
|
|
2017
|
+
"Manual payment (API/CLI) \u2192 uses the automation_type specified in the request."
|
|
2018
|
+
],
|
|
2019
|
+
tips: [
|
|
2020
|
+
"Default automation for 'gigstack pay' is pue_invoice \u2014 stamps an invoice immediately.",
|
|
2021
|
+
"Use 'none' if you just want to record the payment without any fiscal documents.",
|
|
2022
|
+
"PPD automations are for installment scenarios \u2014 the invoice is created once, complements are added per payment."
|
|
2023
|
+
]
|
|
2024
|
+
},
|
|
2025
|
+
services: {
|
|
2026
|
+
title: "Servicios/Productos (Services/Products)",
|
|
2027
|
+
summary: "Services are your product/service catalog. Each service has a description, price, SAT product key, and SAT unit key. Services can be reused across invoices and payments to avoid re-entering item details.",
|
|
2028
|
+
concepts: {
|
|
2029
|
+
product_key: "SAT catalog code for the product/service. Common: 84111506 (consulting), 80161500 (admin services), 43232100 (software licenses).",
|
|
2030
|
+
unit_key: "SAT unit code. Common: E48 (service unit), H87 (piece), ACT (activity), HUR (hour), DAY (day).",
|
|
2031
|
+
taxes: "Tax configuration per service. Typically IVA 16% (type: 'IVA', rate: 0.16, factor: 'Tasa'). Can include withholdings (retenciones)."
|
|
2032
|
+
},
|
|
2033
|
+
actions: {
|
|
2034
|
+
"services list": "List all services in your catalog. Supports --limit, --next (cursor pagination), --sort, --order-by.",
|
|
2035
|
+
"services create": "Create a new service with SAT keys and pricing.",
|
|
2036
|
+
"services update <id>": "Update service description, price, or SAT keys.",
|
|
2037
|
+
"services delete <id>": "Delete a service."
|
|
2038
|
+
},
|
|
2039
|
+
tips: [
|
|
2040
|
+
"Create services for your common offerings to avoid retyping SAT keys every time.",
|
|
2041
|
+
"IVA 16% is the standard tax rate in Mexico. Add it to most services unless exempt.",
|
|
2042
|
+
"Product key 84111506 and unit key E48 are safe defaults for professional services."
|
|
2043
|
+
]
|
|
2044
|
+
},
|
|
2045
|
+
webhooks: {
|
|
2046
|
+
title: "Webhooks",
|
|
2047
|
+
summary: "Webhooks let you receive real-time notifications when events happen in gigstack (invoice created, payment received, etc.). Configure a URL and optionally filter by event types.",
|
|
2048
|
+
concepts: {
|
|
2049
|
+
events: "Event types you can subscribe to: invoice.created, invoice.cancelled, payment.succeeded, payment.failed, receipt.created, etc."
|
|
2050
|
+
},
|
|
2051
|
+
actions: {
|
|
2052
|
+
"webhooks list": "List configured webhooks.",
|
|
2053
|
+
"webhooks create": "Create a webhook with a URL and optional event filter.",
|
|
2054
|
+
"webhooks delete <id>": "Delete a webhook."
|
|
2055
|
+
},
|
|
2056
|
+
tips: [
|
|
2057
|
+
"If no events are specified, the webhook receives ALL events.",
|
|
2058
|
+
"Webhooks include a signature header for verification."
|
|
2059
|
+
]
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
var TOPIC_LIST = Object.keys(KNOWLEDGE);
|
|
2063
|
+
function registerContextCommand(program2) {
|
|
2064
|
+
program2.command("context [topic]").description("Domain knowledge for agents: understand gigstack concepts, statuses, and relationships").option("--short", "Brief summary only").option("--all", "Dump all topics at once (for AI agents to load full domain knowledge)").action((topic, opts) => {
|
|
2065
|
+
if (opts.all) {
|
|
2066
|
+
if (isJsonMode()) {
|
|
2067
|
+
const topics = {};
|
|
2068
|
+
for (const [id, t2] of Object.entries(KNOWLEDGE)) {
|
|
2069
|
+
topics[id] = t2;
|
|
2070
|
+
}
|
|
2071
|
+
return printJson({ topics });
|
|
2072
|
+
}
|
|
2073
|
+
console.log(pc12.bold("\ngigstack context --all \u2014 full domain knowledge\n"));
|
|
2074
|
+
for (const [id, t2] of Object.entries(KNOWLEDGE)) {
|
|
2075
|
+
console.log(`${pc12.bold(pc12.underline(t2.title))} ${pc12.dim(`(${id})`)}`);
|
|
2076
|
+
console.log(`${t2.summary}
|
|
2077
|
+
`);
|
|
2078
|
+
}
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (!topic) {
|
|
2082
|
+
if (isJsonMode()) {
|
|
2083
|
+
return printJson({ topics: TOPIC_LIST.map((t2) => ({ id: t2, title: KNOWLEDGE[t2].title, summary: KNOWLEDGE[t2].summary })) });
|
|
2084
|
+
}
|
|
2085
|
+
console.log(pc12.bold("\ngigstack context \u2014 domain knowledge for agents\n"));
|
|
2086
|
+
console.log("Available topics:\n");
|
|
2087
|
+
for (const [id, t2] of Object.entries(KNOWLEDGE)) {
|
|
2088
|
+
console.log(` ${pc12.bold(id.padEnd(14))} ${t2.title}`);
|
|
2089
|
+
console.log(` ${" ".repeat(14)} ${pc12.dim(t2.summary.slice(0, 90))}\u2026
|
|
2090
|
+
`);
|
|
2091
|
+
}
|
|
2092
|
+
console.log(pc12.dim(`Usage: gigstack context <topic> [--json] [--short] [--all]
|
|
2093
|
+
`));
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
const t = KNOWLEDGE[topic.toLowerCase()];
|
|
2097
|
+
if (!t) {
|
|
2098
|
+
console.error(pc12.red(`Unknown topic: ${topic}`));
|
|
2099
|
+
console.log(pc12.dim(`Available: ${TOPIC_LIST.join(", ")}`));
|
|
2100
|
+
process.exit(1);
|
|
2101
|
+
}
|
|
2102
|
+
if (isJsonMode()) {
|
|
2103
|
+
return printJson(t);
|
|
2104
|
+
}
|
|
2105
|
+
console.log(`
|
|
2106
|
+
${pc12.bold(pc12.underline(t.title))}
|
|
2107
|
+
`);
|
|
2108
|
+
console.log(t.summary);
|
|
2109
|
+
if (opts.short) {
|
|
2110
|
+
console.log();
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
console.log(`
|
|
2114
|
+
${pc12.bold("Concepts:")}`);
|
|
2115
|
+
for (const [k, v] of Object.entries(t.concepts)) {
|
|
2116
|
+
console.log(` ${pc12.cyan(k.padEnd(28))} ${v}`);
|
|
2117
|
+
}
|
|
2118
|
+
if (t.statuses) {
|
|
2119
|
+
console.log(`
|
|
2120
|
+
${pc12.bold("Statuses:")}`);
|
|
2121
|
+
for (const [k, v] of Object.entries(t.statuses)) {
|
|
2122
|
+
console.log(` ${pc12.yellow(k.padEnd(28))} ${v}`);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (t.actions) {
|
|
2126
|
+
console.log(`
|
|
2127
|
+
${pc12.bold("CLI Actions:")}`);
|
|
2128
|
+
for (const [k, v] of Object.entries(t.actions)) {
|
|
2129
|
+
console.log(` ${pc12.green(("gigstack " + k).padEnd(38))} ${v}`);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (t.relationships) {
|
|
2133
|
+
console.log(`
|
|
2134
|
+
${pc12.bold("Relationships:")}`);
|
|
2135
|
+
for (const r of t.relationships) {
|
|
2136
|
+
console.log(` ${pc12.dim("\u2192")} ${r}`);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
if (t.tips) {
|
|
2140
|
+
console.log(`
|
|
2141
|
+
${pc12.bold("Agent Tips:")}`);
|
|
2142
|
+
for (const tip of t.tips) {
|
|
2143
|
+
console.log(` ${pc12.dim("!")} ${tip}`);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
console.log();
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// src/commands/completions.ts
|
|
2151
|
+
var COMMANDS = {
|
|
2152
|
+
"": [
|
|
2153
|
+
"login",
|
|
2154
|
+
"logout",
|
|
2155
|
+
"whoami",
|
|
2156
|
+
"switch",
|
|
2157
|
+
"profiles",
|
|
2158
|
+
"context",
|
|
2159
|
+
"status",
|
|
2160
|
+
"doctor",
|
|
2161
|
+
"pay",
|
|
2162
|
+
"clients",
|
|
2163
|
+
"invoices",
|
|
2164
|
+
"payments",
|
|
2165
|
+
"services",
|
|
2166
|
+
"webhooks",
|
|
2167
|
+
"receipts",
|
|
2168
|
+
"teams",
|
|
2169
|
+
"export",
|
|
2170
|
+
"completions"
|
|
2171
|
+
],
|
|
2172
|
+
clients: ["list", "get", "create", "update", "search", "validate", "delete"],
|
|
2173
|
+
invoices: ["list", "get", "create", "cancel", "search", "files", "download", "drafts", "credit-notes", "complements"],
|
|
2174
|
+
payments: ["list", "get", "request", "register", "refund"],
|
|
2175
|
+
services: ["list", "get", "create", "update", "delete"],
|
|
2176
|
+
receipts: ["list", "stamp", "cancel"],
|
|
2177
|
+
webhooks: ["list", "create", "delete"],
|
|
2178
|
+
teams: ["list", "get", "integrations"],
|
|
2179
|
+
export: ["invoices", "payments", "receipts", "clients"],
|
|
2180
|
+
context: ["payments", "invoices", "receipts", "clients", "cobranza", "automations", "services", "webhooks"],
|
|
2181
|
+
completions: ["bash", "zsh", "fish"]
|
|
2182
|
+
};
|
|
2183
|
+
function bashScript() {
|
|
2184
|
+
const topLevel = COMMANDS[""].join(" ");
|
|
2185
|
+
const cases = Object.entries(COMMANDS).filter(([k]) => k !== "").map(([cmd, subs]) => ` ${cmd}) COMPREPLY=( $(compgen -W "${subs.join(" ")}" -- "$cur") ) ;;`).join("\n");
|
|
2186
|
+
return `# gigstack bash completion
|
|
2187
|
+
# Add to ~/.bashrc: eval "$(gigstack completions bash)"
|
|
2188
|
+
_gigstack_completions() {
|
|
2189
|
+
local cur prev words cword
|
|
2190
|
+
_init_completion || return
|
|
2191
|
+
|
|
2192
|
+
local top_commands="${topLevel}"
|
|
2193
|
+
|
|
2194
|
+
if [[ $cword -eq 1 ]]; then
|
|
2195
|
+
COMPREPLY=( $(compgen -W "$top_commands" -- "$cur") )
|
|
2196
|
+
return
|
|
2197
|
+
fi
|
|
2198
|
+
|
|
2199
|
+
case "\${words[1]}" in
|
|
2200
|
+
${cases}
|
|
2201
|
+
esac
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
complete -F _gigstack_completions gigstack
|
|
2205
|
+
`;
|
|
2206
|
+
}
|
|
2207
|
+
function zshScript() {
|
|
2208
|
+
const topLevel = COMMANDS[""].join(" ");
|
|
2209
|
+
const cases = Object.entries(COMMANDS).filter(([k]) => k !== "").map(([cmd, subs]) => ` ${cmd}) compadd -- ${subs.join(" ")} ;;`).join("\n");
|
|
2210
|
+
return `# gigstack zsh completion
|
|
2211
|
+
# Add to ~/.zshrc: eval "$(gigstack completions zsh)"
|
|
2212
|
+
_gigstack_completions() {
|
|
2213
|
+
local -a top_commands
|
|
2214
|
+
top_commands=(${topLevel})
|
|
2215
|
+
|
|
2216
|
+
if (( CURRENT == 2 )); then
|
|
2217
|
+
compadd -- \${top_commands[@]}
|
|
2218
|
+
return
|
|
2219
|
+
fi
|
|
2220
|
+
|
|
2221
|
+
case "\${words[2]}" in
|
|
2222
|
+
${cases}
|
|
2223
|
+
esac
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
compdef _gigstack_completions gigstack
|
|
2227
|
+
`;
|
|
2228
|
+
}
|
|
2229
|
+
function fishScript() {
|
|
2230
|
+
const lines = [
|
|
2231
|
+
"# gigstack fish completion",
|
|
2232
|
+
"# Save to ~/.config/fish/completions/gigstack.fish",
|
|
2233
|
+
"",
|
|
2234
|
+
"# Disable file completions",
|
|
2235
|
+
"complete -c gigstack -f",
|
|
2236
|
+
"",
|
|
2237
|
+
"# Top-level commands"
|
|
2238
|
+
];
|
|
2239
|
+
for (const cmd of COMMANDS[""]) {
|
|
2240
|
+
lines.push(`complete -c gigstack -n '__fish_use_subcommand' -a '${cmd}'`);
|
|
2241
|
+
}
|
|
2242
|
+
lines.push("");
|
|
2243
|
+
for (const [cmd, subs] of Object.entries(COMMANDS)) {
|
|
2244
|
+
if (cmd === "") continue;
|
|
2245
|
+
lines.push(`# ${cmd} subcommands`);
|
|
2246
|
+
for (const sub of subs) {
|
|
2247
|
+
lines.push(`complete -c gigstack -n '__fish_seen_subcommand_from ${cmd}' -a '${sub}'`);
|
|
2248
|
+
}
|
|
2249
|
+
lines.push("");
|
|
2250
|
+
}
|
|
2251
|
+
return lines.join("\n") + "\n";
|
|
2252
|
+
}
|
|
2253
|
+
function registerCompletionsCommand(program2) {
|
|
2254
|
+
program2.command("completions").description("Generar script de autocompletado para tu shell").argument("<shell>", "Shell: bash, zsh, o fish").action((shell) => {
|
|
2255
|
+
switch (shell) {
|
|
2256
|
+
case "bash":
|
|
2257
|
+
process.stdout.write(bashScript());
|
|
2258
|
+
break;
|
|
2259
|
+
case "zsh":
|
|
2260
|
+
process.stdout.write(zshScript());
|
|
2261
|
+
break;
|
|
2262
|
+
case "fish":
|
|
2263
|
+
process.stdout.write(fishScript());
|
|
2264
|
+
break;
|
|
2265
|
+
default:
|
|
2266
|
+
console.error(`Shell no soportado: ${shell}. Usa bash, zsh, o fish.`);
|
|
2267
|
+
process.exit(1);
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// src/commands/export.ts
|
|
2273
|
+
var INVOICE_COLUMNS = [
|
|
2274
|
+
{ key: "uuid", extract: (i) => i.uuid || "" },
|
|
2275
|
+
{ key: "status", extract: (i) => i.status || "" },
|
|
2276
|
+
{ key: "payment_method", extract: (i) => i.payment_method || "" },
|
|
2277
|
+
{ key: "payment_form", extract: (i) => i.payment_form || "" },
|
|
2278
|
+
{ key: "client_name", extract: (i) => i.client?.legal_name || i.client?.name || "" },
|
|
2279
|
+
{ key: "client_rfc", extract: (i) => i.client?.tax_id || "" },
|
|
2280
|
+
{ key: "client_email", extract: (i) => i.client?.email || "" },
|
|
2281
|
+
{ key: "subtotal", extract: (i) => String(i.subtotal ?? "") },
|
|
2282
|
+
{ key: "total", extract: (i) => String(i.total ?? "") },
|
|
2283
|
+
{ key: "currency", extract: (i) => i.currency || "" },
|
|
2284
|
+
{ key: "series", extract: (i) => i.series || "" },
|
|
2285
|
+
{ key: "folio_number", extract: (i) => String(i.folio_number ?? "") },
|
|
2286
|
+
{ key: "last_balance", extract: (i) => String(i.last_balance ?? "") },
|
|
2287
|
+
{ key: "created_at", extract: (i) => formatDate(i.created_at) }
|
|
2288
|
+
];
|
|
2289
|
+
var PAYMENT_COLUMNS = [
|
|
2290
|
+
{ key: "id", extract: (p) => p.id || "" },
|
|
2291
|
+
{ key: "status", extract: (p) => p.status || "" },
|
|
2292
|
+
{ key: "client_name", extract: (p) => p.client?.legal_name || p.client?.name || "" },
|
|
2293
|
+
{ key: "client_rfc", extract: (p) => p.client?.tax_id || "" },
|
|
2294
|
+
{ key: "client_email", extract: (p) => p.client?.email || "" },
|
|
2295
|
+
{ key: "total", extract: (p) => String(p.total ?? "") },
|
|
2296
|
+
{ key: "currency", extract: (p) => p.currency || "" },
|
|
2297
|
+
{ key: "payment_form", extract: (p) => p.payment_form || "" },
|
|
2298
|
+
{ key: "short_url", extract: (p) => p.short_url || "" },
|
|
2299
|
+
{ key: "created_at", extract: (p) => formatDate(p.created_at) }
|
|
2300
|
+
];
|
|
2301
|
+
var RECEIPT_COLUMNS = [
|
|
2302
|
+
{ key: "id", extract: (r) => r.id || "" },
|
|
2303
|
+
{ key: "status", extract: (r) => r.status || "" },
|
|
2304
|
+
{ key: "client_name", extract: (r) => r.client?.legal_name || r.client?.name || "" },
|
|
2305
|
+
{ key: "client_rfc", extract: (r) => r.client?.tax_id || "" },
|
|
2306
|
+
{ key: "total", extract: (r) => String(r.total ?? "") },
|
|
2307
|
+
{ key: "currency", extract: (r) => r.currency || "" },
|
|
2308
|
+
{ key: "validUntil", extract: (r) => formatDate(r.validUntil) },
|
|
2309
|
+
{ key: "created_at", extract: (r) => formatDate(r.created_at) }
|
|
2310
|
+
];
|
|
2311
|
+
var CLIENT_COLUMNS = [
|
|
2312
|
+
{ key: "id", extract: (c) => c.id || "" },
|
|
2313
|
+
{ key: "legal_name", extract: (c) => c.legal_name || c.name || "" },
|
|
2314
|
+
{ key: "tax_id", extract: (c) => c.tax_id || "" },
|
|
2315
|
+
{ key: "email", extract: (c) => c.email || "" },
|
|
2316
|
+
{ key: "tax_system", extract: (c) => c.tax_system || "" },
|
|
2317
|
+
{ key: "use", extract: (c) => c.use || "" },
|
|
2318
|
+
{ key: "zip", extract: (c) => c.address?.zip || "" },
|
|
2319
|
+
{ key: "is_valid", extract: (c) => c.is_valid === void 0 ? "" : String(c.is_valid) },
|
|
2320
|
+
{ key: "created_at", extract: (c) => formatDate(c.created_at) }
|
|
2321
|
+
];
|
|
2322
|
+
function csvEscape(value) {
|
|
2323
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
2324
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
2325
|
+
}
|
|
2326
|
+
return value;
|
|
2327
|
+
}
|
|
2328
|
+
function toCsvRow(columns, item) {
|
|
2329
|
+
return columns.map((col) => csvEscape(col.extract(item))).join(",");
|
|
2330
|
+
}
|
|
2331
|
+
function csvHeader(columns) {
|
|
2332
|
+
return columns.map((col) => csvEscape(col.key)).join(",");
|
|
2333
|
+
}
|
|
2334
|
+
async function fetchAllPages(endpoint, baseQuery, team) {
|
|
2335
|
+
const all = [];
|
|
2336
|
+
let nextToken;
|
|
2337
|
+
let page = 0;
|
|
2338
|
+
baseQuery.limit = "100";
|
|
2339
|
+
let s = spinner("Exportando datos\u2026");
|
|
2340
|
+
try {
|
|
2341
|
+
do {
|
|
2342
|
+
const query = { ...baseQuery };
|
|
2343
|
+
if (nextToken) query.next = nextToken;
|
|
2344
|
+
const res = await api("GET", endpoint, { query, team });
|
|
2345
|
+
const items = (res.data || []).filter((r) => r != null);
|
|
2346
|
+
all.push(...items);
|
|
2347
|
+
page++;
|
|
2348
|
+
nextToken = res.has_more && res.next ? res.next : void 0;
|
|
2349
|
+
s.stop();
|
|
2350
|
+
if (nextToken) {
|
|
2351
|
+
s = spinner(`Exportando datos\u2026 (${all.length} registros, p\xE1gina ${page + 1})`);
|
|
2352
|
+
}
|
|
2353
|
+
} while (nextToken);
|
|
2354
|
+
} finally {
|
|
2355
|
+
s.stop();
|
|
2356
|
+
}
|
|
2357
|
+
process.stderr.write(`Exportados ${all.length} registros
|
|
2358
|
+
`);
|
|
2359
|
+
return all;
|
|
2360
|
+
}
|
|
2361
|
+
var SUBCOMMANDS = [
|
|
2362
|
+
{
|
|
2363
|
+
name: "invoices",
|
|
2364
|
+
description: "Exportar facturas de ingreso",
|
|
2365
|
+
endpoint: "/invoices/income",
|
|
2366
|
+
columns: INVOICE_COLUMNS,
|
|
2367
|
+
extraOpts: (cmd) => cmd.option("--status <status>", "Filtrar por status (valid, cancelled)").option("--client <id>", "Filtrar por cliente").option("--series <series>", "Filtrar por serie"),
|
|
2368
|
+
applyFilters: (opts, query) => {
|
|
2369
|
+
if (opts.status) query.status = opts.status;
|
|
2370
|
+
if (opts.client) query.client_id = opts.client;
|
|
2371
|
+
if (opts.series) query.series = opts.series;
|
|
2372
|
+
}
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
name: "payments",
|
|
2376
|
+
description: "Exportar pagos",
|
|
2377
|
+
endpoint: "/payments",
|
|
2378
|
+
columns: PAYMENT_COLUMNS,
|
|
2379
|
+
extraOpts: (cmd) => cmd.option("--status <status>", "Filtrar: pending, succeeded, failed, cancelled, refunded").option("--client <id>", "Filtrar por cliente").option("--currency <code>", "Filtrar por moneda (MXN, USD)").option("--email <email>", "Filtrar por email del cliente").option("--rfc <rfc>", "Filtrar por RFC del cliente"),
|
|
2380
|
+
applyFilters: (opts, query) => {
|
|
2381
|
+
if (opts.status) query.status = opts.status;
|
|
2382
|
+
if (opts.client) query.client_id = opts.client;
|
|
2383
|
+
if (opts.currency) query.currency = opts.currency;
|
|
2384
|
+
if (opts.email) query.email = opts.email;
|
|
2385
|
+
if (opts.rfc) query.tax_id = opts.rfc;
|
|
2386
|
+
}
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
name: "receipts",
|
|
2390
|
+
description: "Exportar recibos de venta",
|
|
2391
|
+
endpoint: "/receipts",
|
|
2392
|
+
columns: RECEIPT_COLUMNS,
|
|
2393
|
+
extraOpts: (cmd) => cmd.option("--status <status>", "Filtrar: pending, open, invoiced, completed, expired, cancelled").option("--client <id>", "Filtrar por cliente"),
|
|
2394
|
+
applyFilters: (opts, query) => {
|
|
2395
|
+
if (opts.status) query.status = opts.status;
|
|
2396
|
+
if (opts.client) query.client_id = opts.client;
|
|
2397
|
+
}
|
|
2398
|
+
},
|
|
2399
|
+
{
|
|
2400
|
+
name: "clients",
|
|
2401
|
+
description: "Exportar clientes",
|
|
2402
|
+
endpoint: "/clients",
|
|
2403
|
+
columns: CLIENT_COLUMNS
|
|
2404
|
+
}
|
|
2405
|
+
];
|
|
2406
|
+
function registerExportCommand(program2) {
|
|
2407
|
+
const exportCmd = program2.command("export").description("Exportar datos a CSV o JSON (stdout)");
|
|
2408
|
+
for (const sub of SUBCOMMANDS) {
|
|
2409
|
+
let cmd = exportCmd.command(sub.name).description(sub.description).option("--from <date>", "Fecha inicio (YYYY-MM-DD, YYYY-MM, 30d, 7d)").option("--to <date>", "Fecha fin (YYYY-MM-DD, YYYY-MM, today)").option("--sort <dir>", "Orden: asc o desc", "desc").option("--order-by <field>", "Ordenar por: timestamp o name", "timestamp").option("--format <fmt>", "Formato de salida: csv o json", "csv").option("--team <id>", "Team ID");
|
|
2410
|
+
if (sub.extraOpts) cmd = sub.extraOpts(cmd);
|
|
2411
|
+
cmd.action(async (opts) => {
|
|
2412
|
+
try {
|
|
2413
|
+
const query = buildListQuery(opts);
|
|
2414
|
+
if (sub.applyFilters) sub.applyFilters(opts, query);
|
|
2415
|
+
const items = await fetchAllPages(sub.endpoint, query, opts.team);
|
|
2416
|
+
if (opts.format === "json") {
|
|
2417
|
+
process.stdout.write(JSON.stringify(items, null, 2) + "\n");
|
|
2418
|
+
} else {
|
|
2419
|
+
process.stdout.write(csvHeader(sub.columns) + "\n");
|
|
2420
|
+
for (const item of items) {
|
|
2421
|
+
process.stdout.write(toCsvRow(sub.columns, item) + "\n");
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
} catch (e) {
|
|
2425
|
+
error(e.message);
|
|
2426
|
+
process.exit(1);
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// src/commands/explain.ts
|
|
2433
|
+
import pc13 from "picocolors";
|
|
2434
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2435
|
+
function detectType(id) {
|
|
2436
|
+
if (id.startsWith("client_") || id.startsWith("cus_")) return "client";
|
|
2437
|
+
if (id.startsWith("payment_")) return "payment";
|
|
2438
|
+
if (id.startsWith("receipt_")) return "receipt";
|
|
2439
|
+
if (id.startsWith("service_")) return "service";
|
|
2440
|
+
if (UUID_RE.test(id)) return "invoice";
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
async function fetchInvoice(id, team) {
|
|
2444
|
+
const res = await api("GET", `/invoices/income/${id}`, { team });
|
|
2445
|
+
return { type: "invoice", data: res.data };
|
|
2446
|
+
}
|
|
2447
|
+
async function fetchPayment(id, team) {
|
|
2448
|
+
const res = await api("GET", `/payments/${id}`, { team });
|
|
2449
|
+
return { type: "payment", data: res.data };
|
|
2450
|
+
}
|
|
2451
|
+
async function fetchReceipt(id, team) {
|
|
2452
|
+
const res = await api("GET", `/receipts/${id}`, { team });
|
|
2453
|
+
return { type: "receipt", data: res.data };
|
|
2454
|
+
}
|
|
2455
|
+
async function fetchClient(id, team) {
|
|
2456
|
+
const res = await api("GET", `/clients/${id}`, { team });
|
|
2457
|
+
return { type: "client", data: res.data };
|
|
2458
|
+
}
|
|
2459
|
+
async function fetchService(id, team) {
|
|
2460
|
+
const res = await api("GET", `/services/${id}`, { team });
|
|
2461
|
+
return { type: "service", data: res.data };
|
|
2462
|
+
}
|
|
2463
|
+
async function fetchByType(type, id, team) {
|
|
2464
|
+
switch (type) {
|
|
2465
|
+
case "invoice":
|
|
2466
|
+
return fetchInvoice(id, team);
|
|
2467
|
+
case "payment":
|
|
2468
|
+
return fetchPayment(id, team);
|
|
2469
|
+
case "receipt":
|
|
2470
|
+
return fetchReceipt(id, team);
|
|
2471
|
+
case "client":
|
|
2472
|
+
return fetchClient(id, team);
|
|
2473
|
+
case "service":
|
|
2474
|
+
return fetchService(id, team);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
async function tryAll(id, team) {
|
|
2478
|
+
const order = ["payment", "invoice", "receipt", "client", "service"];
|
|
2479
|
+
for (const type of order) {
|
|
2480
|
+
try {
|
|
2481
|
+
return await fetchByType(type, id, team);
|
|
2482
|
+
} catch {
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
throw new Error(`No se encontr\xF3 ning\xFAn recurso con ID: ${id}`);
|
|
2486
|
+
}
|
|
2487
|
+
function explainInvoice(i) {
|
|
2488
|
+
const clientName = i.client?.legal_name || i.client?.name || "desconocido";
|
|
2489
|
+
const isPPD = (i.payment_method || "").toUpperCase() === "PPD";
|
|
2490
|
+
const isCancelled = (i.status || "").toLowerCase() === "cancelled";
|
|
2491
|
+
printKeyValue({
|
|
2492
|
+
UUID: i.uuid || i.id || "\u2014",
|
|
2493
|
+
Tipo: "Factura de ingreso",
|
|
2494
|
+
Status: i.status || "\u2014",
|
|
2495
|
+
Cliente: clientName,
|
|
2496
|
+
RFC: i.client?.tax_id || "\u2014",
|
|
2497
|
+
Subtotal: formatMoney(i.subtotal, i.currency),
|
|
2498
|
+
Total: formatMoney(i.total, i.currency),
|
|
2499
|
+
"M\xE9todo pago": i.payment_method || "\u2014",
|
|
2500
|
+
"Forma pago": i.payment_form || "\u2014",
|
|
2501
|
+
Serie: i.series || "\u2014",
|
|
2502
|
+
Folio: i.folio_number || "\u2014",
|
|
2503
|
+
Creado: formatDate(i.created_at)
|
|
2504
|
+
});
|
|
2505
|
+
if (isPPD) {
|
|
2506
|
+
console.log("");
|
|
2507
|
+
console.log(pc13.bold(pc13.yellow("PPD \u2014 Pago en parcialidades")));
|
|
2508
|
+
console.log(` Saldo pendiente: ${i.last_balance !== void 0 ? formatMoney(i.last_balance, i.currency) : "desconocido"}`);
|
|
2509
|
+
console.log(` Complementos: ${i.payment_complements ?? 0}`);
|
|
2510
|
+
const fullyPaid = i.last_balance !== void 0 && i.last_balance <= 0;
|
|
2511
|
+
console.log(` Liquidada: ${fullyPaid ? pc13.green("S\xED") : pc13.yellow("No")}`);
|
|
2512
|
+
}
|
|
2513
|
+
if (isCancelled) {
|
|
2514
|
+
console.log("");
|
|
2515
|
+
console.log(pc13.bold(pc13.red("Cancelada")));
|
|
2516
|
+
if (i.cancellation_status) console.log(` Status cancelaci\xF3n: ${i.cancellation_status}`);
|
|
2517
|
+
if (i.cancellation_receipt) console.log(` Acuse: ${i.cancellation_receipt}`);
|
|
2518
|
+
}
|
|
2519
|
+
console.log("");
|
|
2520
|
+
const methodLabel = isPPD ? "PPD" : "PUE";
|
|
2521
|
+
let narrative = `Factura de ingreso ${methodLabel} por ${formatMoney(i.total, i.currency)} a ${clientName} creada el ${formatDate(i.created_at)}.`;
|
|
2522
|
+
if (isCancelled) {
|
|
2523
|
+
narrative += ` Status: ${pc13.red("cancelada")}.`;
|
|
2524
|
+
} else if (isPPD && i.last_balance !== void 0 && i.last_balance <= 0) {
|
|
2525
|
+
narrative += ` Status: ${pc13.green("v\xE1lida")} \u2014 completamente liquidada con ${i.payment_complements ?? 0} complemento(s).`;
|
|
2526
|
+
} else if (isPPD) {
|
|
2527
|
+
narrative += ` Status: ${pc13.yellow("v\xE1lida")} \u2014 saldo pendiente ${formatMoney(i.last_balance, i.currency)}.`;
|
|
2528
|
+
} else {
|
|
2529
|
+
narrative += ` Status: ${pc13.green("v\xE1lida")}.`;
|
|
2530
|
+
}
|
|
2531
|
+
console.log(pc13.dim(narrative));
|
|
2532
|
+
}
|
|
2533
|
+
function explainPayment(p) {
|
|
2534
|
+
const clientName = p.client?.legal_name || p.client?.name || "desconocido";
|
|
2535
|
+
printKeyValue({
|
|
2536
|
+
ID: p.id || "\u2014",
|
|
2537
|
+
Tipo: "Pago",
|
|
2538
|
+
Status: p.status || "\u2014",
|
|
2539
|
+
Cliente: clientName,
|
|
2540
|
+
Email: p.client?.email || "\u2014",
|
|
2541
|
+
RFC: p.client?.tax_id || "\u2014",
|
|
2542
|
+
Total: formatMoney(p.total, p.currency),
|
|
2543
|
+
"Forma pago": p.payment_form || "\u2014",
|
|
2544
|
+
Procesador: p.processor || p.payment_processor || "\u2014",
|
|
2545
|
+
"Link de pago": p.short_url || "\u2014",
|
|
2546
|
+
Creado: formatDate(p.created_at)
|
|
2547
|
+
});
|
|
2548
|
+
if (p.automation_type || p.invoice_id || p.invoice?.uuid) {
|
|
2549
|
+
console.log("");
|
|
2550
|
+
console.log(pc13.bold("Automatizaci\xF3n"));
|
|
2551
|
+
if (p.automation_type) console.log(` Tipo: ${p.automation_type}`);
|
|
2552
|
+
const invoiceRef2 = p.invoice?.uuid || p.invoice_id || p.invoiceId;
|
|
2553
|
+
if (invoiceRef2) console.log(` Factura generada: ${invoiceRef2}`);
|
|
2554
|
+
if (p.receipt_id || p.receiptId) console.log(` Recibo generado: ${p.receipt_id || p.receiptId}`);
|
|
2555
|
+
}
|
|
2556
|
+
console.log("");
|
|
2557
|
+
const formLabel = p.payment_form ? ` via forma ${p.payment_form}` : "";
|
|
2558
|
+
const processorLabel = p.processor || p.payment_processor ? ` (${p.processor || p.payment_processor})` : "";
|
|
2559
|
+
let narrative = `Pago de ${formatMoney(p.total, p.currency)} recibido de ${clientName} el ${formatDate(p.created_at)}${formLabel}${processorLabel}.`;
|
|
2560
|
+
narrative += ` Status: ${p.status || "desconocido"}.`;
|
|
2561
|
+
const invoiceRef = p.invoice?.uuid || p.invoice_id || p.invoiceId;
|
|
2562
|
+
if (invoiceRef) narrative += ` Se gener\xF3 factura ${invoiceRef}.`;
|
|
2563
|
+
const receiptRef = p.receipt_id || p.receiptId;
|
|
2564
|
+
if (receiptRef) narrative += ` Se gener\xF3 recibo ${receiptRef}.`;
|
|
2565
|
+
console.log(pc13.dim(narrative));
|
|
2566
|
+
}
|
|
2567
|
+
async function explainClient(c, team) {
|
|
2568
|
+
printKeyValue({
|
|
2569
|
+
ID: c.id || "\u2014",
|
|
2570
|
+
Tipo: "Cliente",
|
|
2571
|
+
Nombre: c.legal_name || c.name || "\u2014",
|
|
2572
|
+
RFC: c.tax_id || "\u2014",
|
|
2573
|
+
Email: c.email || "\u2014",
|
|
2574
|
+
"R\xE9gimen fiscal": c.tax_system || "\u2014",
|
|
2575
|
+
"Uso CFDI": c.use || "\u2014",
|
|
2576
|
+
"C\xF3digo postal": c.address?.zip || "\u2014",
|
|
2577
|
+
V\u00E1lido: c.is_valid ? pc13.green("S\xED") : pc13.yellow("No"),
|
|
2578
|
+
Creado: formatDate(c.created_at)
|
|
2579
|
+
});
|
|
2580
|
+
let invoiceCount = 0;
|
|
2581
|
+
let paymentCount = 0;
|
|
2582
|
+
try {
|
|
2583
|
+
const invoiceRes = await api("GET", "/invoices/income", { query: { client_id: c.id, limit: "1" }, team });
|
|
2584
|
+
invoiceCount = invoiceRes.pagination?.totalItems ?? (invoiceRes.data || []).length;
|
|
2585
|
+
} catch {
|
|
2586
|
+
}
|
|
2587
|
+
try {
|
|
2588
|
+
const paymentRes = await api("GET", "/payments", { query: { client_id: c.id, limit: "1" }, team });
|
|
2589
|
+
paymentCount = paymentRes.pagination?.totalItems ?? (paymentRes.data || []).length;
|
|
2590
|
+
} catch {
|
|
2591
|
+
}
|
|
2592
|
+
console.log("");
|
|
2593
|
+
console.log(pc13.bold("Actividad"));
|
|
2594
|
+
console.log(` Facturas: ${invoiceCount}`);
|
|
2595
|
+
console.log(` Pagos: ${paymentCount}`);
|
|
2596
|
+
console.log("");
|
|
2597
|
+
const clientName = c.legal_name || c.name || "desconocido";
|
|
2598
|
+
const rfcLabel = c.tax_id ? `RFC: ${c.tax_id}` : "sin RFC";
|
|
2599
|
+
const validLabel = c.is_valid ? "datos fiscales v\xE1lidos" : "datos fiscales sin validar";
|
|
2600
|
+
const narrative = `Cliente ${clientName} (${rfcLabel}). ${invoiceCount} factura(s), ${paymentCount} pago(s) registrados. ${validLabel}.`;
|
|
2601
|
+
console.log(pc13.dim(narrative));
|
|
2602
|
+
}
|
|
2603
|
+
function explainReceipt(r) {
|
|
2604
|
+
const clientName = r.client?.legal_name || r.client?.name || "p\xFAblico general";
|
|
2605
|
+
const status = (r.status || "").toLowerCase();
|
|
2606
|
+
printKeyValue({
|
|
2607
|
+
ID: r.id || "\u2014",
|
|
2608
|
+
Tipo: "Recibo de venta",
|
|
2609
|
+
Status: r.status || "\u2014",
|
|
2610
|
+
Cliente: clientName,
|
|
2611
|
+
Total: formatMoney(r.total, r.currency),
|
|
2612
|
+
"V\xE1lido hasta": formatDate(r.validUntil),
|
|
2613
|
+
Creado: formatDate(r.created_at)
|
|
2614
|
+
});
|
|
2615
|
+
if (r.invoice_id || r.invoiceId || r.invoice?.uuid) {
|
|
2616
|
+
console.log("");
|
|
2617
|
+
console.log(pc13.bold("Autofactura"));
|
|
2618
|
+
console.log(` Factura: ${r.invoice?.uuid || r.invoice_id || r.invoiceId}`);
|
|
2619
|
+
if (r.invoiced_at) console.log(` Fecha: ${formatDate(r.invoiced_at)}`);
|
|
2620
|
+
}
|
|
2621
|
+
console.log("");
|
|
2622
|
+
let narrative;
|
|
2623
|
+
if (status === "pending" || status === "open") {
|
|
2624
|
+
narrative = `Recibo pendiente de autofactura por ${formatMoney(r.total, r.currency)} para ${clientName}. El dinero YA fue cobrado.`;
|
|
2625
|
+
if (r.validUntil) narrative += ` Vigente hasta ${formatDate(r.validUntil)}.`;
|
|
2626
|
+
} else if (status === "invoiced" || status === "completed") {
|
|
2627
|
+
const invoiceRef = r.invoice?.uuid || r.invoice_id || r.invoiceId;
|
|
2628
|
+
narrative = `Recibo por ${formatMoney(r.total, r.currency)}. El cliente se autofactur\xF3${r.invoiced_at ? ` el ${formatDate(r.invoiced_at)}` : ""}.`;
|
|
2629
|
+
if (invoiceRef) narrative += ` Factura: ${invoiceRef}.`;
|
|
2630
|
+
} else if (status === "expired") {
|
|
2631
|
+
narrative = `Recibo expirado por ${formatMoney(r.total, r.currency)}. El cliente no se autofactur\xF3 a tiempo. Se incluir\xE1 en factura global.`;
|
|
2632
|
+
} else if (status === "cancelled") {
|
|
2633
|
+
narrative = `Recibo cancelado por ${formatMoney(r.total, r.currency)}.`;
|
|
2634
|
+
} else {
|
|
2635
|
+
narrative = `Recibo por ${formatMoney(r.total, r.currency)} para ${clientName}. Status: ${r.status || "desconocido"}.`;
|
|
2636
|
+
}
|
|
2637
|
+
console.log(pc13.dim(narrative));
|
|
2638
|
+
}
|
|
2639
|
+
function explainService(s) {
|
|
2640
|
+
printKeyValue({
|
|
2641
|
+
ID: s.id || "\u2014",
|
|
2642
|
+
Tipo: "Servicio / Producto",
|
|
2643
|
+
Descripci\u00F3n: s.description || "\u2014",
|
|
2644
|
+
SKU: s.sku || "\u2014",
|
|
2645
|
+
"Precio unitario": formatMoney(s.unit_price, "MXN"),
|
|
2646
|
+
"Clave producto": s.product_key || "\u2014",
|
|
2647
|
+
"Clave unidad": s.unit_key || "\u2014",
|
|
2648
|
+
"Nombre unidad": s.unit_name || "\u2014",
|
|
2649
|
+
Impuestos: (s.taxes || []).map((t) => `${t.type} ${(t.rate * 100).toFixed(0)}%${t.withholding ? " (ret)" : ""}`).join(", ") || "Sin impuestos"
|
|
2650
|
+
});
|
|
2651
|
+
console.log("");
|
|
2652
|
+
const taxLabel = (s.taxes || []).length > 0 ? `Con ${(s.taxes || []).map((t) => `${t.type} ${(t.rate * 100).toFixed(0)}%`).join(" + ")}.` : "Sin impuestos configurados.";
|
|
2653
|
+
const narrative = `Servicio "${s.description || "\u2014"}" a ${formatMoney(s.unit_price, "MXN")} por unidad (${s.unit_key || "\u2014"}). ${taxLabel}`;
|
|
2654
|
+
console.log(pc13.dim(narrative));
|
|
2655
|
+
}
|
|
2656
|
+
function registerExplainCommand(program2) {
|
|
2657
|
+
program2.command("explain <id>").description("Explicar cualquier recurso de gigstack (factura, pago, recibo, cliente, servicio)").option("--team <id>", "Team ID").action(async (id, opts) => {
|
|
2658
|
+
try {
|
|
2659
|
+
const detectedType = detectType(id);
|
|
2660
|
+
let result;
|
|
2661
|
+
if (detectedType) {
|
|
2662
|
+
try {
|
|
2663
|
+
result = await spin(
|
|
2664
|
+
`Cargando ${detectedType}\u2026`,
|
|
2665
|
+
() => fetchByType(detectedType, id, opts.team)
|
|
2666
|
+
);
|
|
2667
|
+
} catch {
|
|
2668
|
+
warn(`No se encontr\xF3 como ${detectedType}, buscando en otros tipos\u2026`);
|
|
2669
|
+
result = await spin("Buscando recurso\u2026", () => tryAll(id, opts.team));
|
|
2670
|
+
}
|
|
2671
|
+
} else {
|
|
2672
|
+
result = await spin("Detectando tipo de recurso\u2026", () => tryAll(id, opts.team));
|
|
2673
|
+
}
|
|
2674
|
+
if (isJsonMode()) {
|
|
2675
|
+
return printJson({ type: result.type, ...result.data });
|
|
2676
|
+
}
|
|
2677
|
+
console.log(pc13.bold(`
|
|
2678
|
+
${pc13.cyan("\u2500\u2500\u2500 gigstack explain \u2500\u2500\u2500")}
|
|
2679
|
+
`));
|
|
2680
|
+
switch (result.type) {
|
|
2681
|
+
case "invoice":
|
|
2682
|
+
explainInvoice(result.data);
|
|
2683
|
+
break;
|
|
2684
|
+
case "payment":
|
|
2685
|
+
explainPayment(result.data);
|
|
2686
|
+
break;
|
|
2687
|
+
case "client":
|
|
2688
|
+
await explainClient(result.data, opts.team);
|
|
2689
|
+
break;
|
|
2690
|
+
case "receipt":
|
|
2691
|
+
explainReceipt(result.data);
|
|
2692
|
+
break;
|
|
2693
|
+
case "service":
|
|
2694
|
+
explainService(result.data);
|
|
2695
|
+
break;
|
|
2696
|
+
}
|
|
2697
|
+
console.log("");
|
|
2698
|
+
} catch (e) {
|
|
2699
|
+
error(e.message);
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// src/commands/forecast.ts
|
|
2705
|
+
import pc14 from "picocolors";
|
|
2706
|
+
var DAY_MS = 864e5;
|
|
2707
|
+
function fmt2(amount, currency = "MXN") {
|
|
2708
|
+
return formatMoney(amount, currency);
|
|
2709
|
+
}
|
|
2710
|
+
function sum2(items, key = "total") {
|
|
2711
|
+
return items.reduce((s, i) => s + (i?.[key] || 0), 0);
|
|
2712
|
+
}
|
|
2713
|
+
function toUnix(d) {
|
|
2714
|
+
return String(Math.floor(d.getTime() / 1e3));
|
|
2715
|
+
}
|
|
2716
|
+
function monthRange(year, month) {
|
|
2717
|
+
const start = new Date(year, month, 1);
|
|
2718
|
+
const end = new Date(year, month + 1, 0, 23, 59, 59);
|
|
2719
|
+
return { gte: toUnix(start), lte: toUnix(end) };
|
|
2720
|
+
}
|
|
2721
|
+
function ageDays(item) {
|
|
2722
|
+
const now = Date.now();
|
|
2723
|
+
const ts = item.created_at ? typeof item.created_at === "string" ? new Date(item.created_at).getTime() : item.created_at > 1e12 ? item.created_at : item.created_at * 1e3 : now;
|
|
2724
|
+
return Math.floor((now - ts) / DAY_MS);
|
|
2725
|
+
}
|
|
2726
|
+
function collectionProbability(days) {
|
|
2727
|
+
if (days <= 30) return 0.9;
|
|
2728
|
+
if (days <= 60) return 0.75;
|
|
2729
|
+
if (days <= 90) return 0.5;
|
|
2730
|
+
if (days <= 120) return 0.3;
|
|
2731
|
+
return 0.15;
|
|
2732
|
+
}
|
|
2733
|
+
function calcTrend(monthlyValues) {
|
|
2734
|
+
const valid = monthlyValues.filter((v) => v > 0);
|
|
2735
|
+
if (valid.length === 0) return { avgAll: 0, trend: 0, arrow: "\u2192" };
|
|
2736
|
+
const avgAll = valid.reduce((a, b) => a + b, 0) / valid.length;
|
|
2737
|
+
if (valid.length < 4) {
|
|
2738
|
+
if (valid.length >= 2) {
|
|
2739
|
+
const first = valid[0];
|
|
2740
|
+
const last = valid[valid.length - 1];
|
|
2741
|
+
const pct = first > 0 ? (last - first) / first * 100 / (valid.length - 1) : 0;
|
|
2742
|
+
const arrow2 = pct > 5 ? "\u2191" : pct < -5 ? "\u2193" : "\u2192";
|
|
2743
|
+
return { avgAll, trend: pct, arrow: arrow2 };
|
|
2744
|
+
}
|
|
2745
|
+
return { avgAll, trend: 0, arrow: "\u2192" };
|
|
2746
|
+
}
|
|
2747
|
+
const mid = Math.floor(valid.length / 2);
|
|
2748
|
+
const prior = valid.slice(0, mid);
|
|
2749
|
+
const recent = valid.slice(mid);
|
|
2750
|
+
const avgPrior = prior.reduce((a, b) => a + b, 0) / prior.length;
|
|
2751
|
+
const avgRecent = recent.reduce((a, b) => a + b, 0) / recent.length;
|
|
2752
|
+
if (avgPrior === 0) return { avgAll, trend: 0, arrow: "\u2192" };
|
|
2753
|
+
const monthlyPct = (avgRecent - avgPrior) / avgPrior * 100 / mid;
|
|
2754
|
+
const arrow = monthlyPct > 5 ? "\u2191" : monthlyPct < -5 ? "\u2193" : "\u2192";
|
|
2755
|
+
return { avgAll, trend: monthlyPct, arrow };
|
|
2756
|
+
}
|
|
2757
|
+
function project(base, monthlyPctChange, months) {
|
|
2758
|
+
const factor = 1 + monthlyPctChange / 100;
|
|
2759
|
+
return base * Math.pow(factor, months);
|
|
2760
|
+
}
|
|
2761
|
+
function monthName(month, year) {
|
|
2762
|
+
const names = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];
|
|
2763
|
+
return `${names[month]} ${year}`;
|
|
2764
|
+
}
|
|
2765
|
+
async function fetchMonth(resource, year, month, team) {
|
|
2766
|
+
const range = monthRange(year, month);
|
|
2767
|
+
try {
|
|
2768
|
+
const res = await api("GET", resource, {
|
|
2769
|
+
query: { "created[gte]": range.gte, "created[lte]": range.lte, limit: "100" },
|
|
2770
|
+
team
|
|
2771
|
+
});
|
|
2772
|
+
return (res.data || []).filter((d) => d != null);
|
|
2773
|
+
} catch {
|
|
2774
|
+
return [];
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
function registerForecastCommand(program2) {
|
|
2778
|
+
program2.command("forecast").description("Proyecci\xF3n de ingresos, cobranza, recibos EOM y flujo de efectivo").option("--months <n>", "Meses a proyectar (default: 3)", "3").option("--team <id>", "Team ID").action(async (opts) => {
|
|
2779
|
+
try {
|
|
2780
|
+
const projMonths = Math.max(1, Math.min(12, parseInt(opts.months) || 3));
|
|
2781
|
+
const now = /* @__PURE__ */ new Date();
|
|
2782
|
+
const curYear = now.getFullYear();
|
|
2783
|
+
const curMonth = now.getMonth();
|
|
2784
|
+
const [team, histData, currentData] = await spin("Cargando datos hist\xF3ricos\u2026", async () => {
|
|
2785
|
+
const t = await resolveTeam();
|
|
2786
|
+
const months = [];
|
|
2787
|
+
for (let i = 6; i >= 1; i--) {
|
|
2788
|
+
const d = new Date(curYear, curMonth - i, 1);
|
|
2789
|
+
months.push({ year: d.getFullYear(), month: d.getMonth() });
|
|
2790
|
+
}
|
|
2791
|
+
const histPromises = months.map(async (m) => {
|
|
2792
|
+
const [invoices, payments, receipts] = await Promise.all([
|
|
2793
|
+
fetchMonth("/invoices/income", m.year, m.month, opts.team),
|
|
2794
|
+
fetchMonth("/payments", m.year, m.month, opts.team),
|
|
2795
|
+
fetchMonth("/receipts", m.year, m.month, opts.team)
|
|
2796
|
+
]);
|
|
2797
|
+
return { ...m, invoices, payments, receipts };
|
|
2798
|
+
});
|
|
2799
|
+
const currentPromises = Promise.all([
|
|
2800
|
+
fetchMonth("/invoices/income", curYear, curMonth, opts.team),
|
|
2801
|
+
fetchMonth("/payments", curYear, curMonth, opts.team),
|
|
2802
|
+
fetchMonth("/receipts", curYear, curMonth, opts.team),
|
|
2803
|
+
// PPD invoices (all, not just current month) — we need those with balance
|
|
2804
|
+
api("GET", "/invoices/income", {
|
|
2805
|
+
query: { limit: "100", payment_method: "PPD" },
|
|
2806
|
+
team: opts.team
|
|
2807
|
+
}).then((r) => (r.data || []).filter((d) => d != null)).catch(() => []),
|
|
2808
|
+
// Pending payments (payment links)
|
|
2809
|
+
api("GET", "/payments", {
|
|
2810
|
+
query: { limit: "100", status: "pending" },
|
|
2811
|
+
team: opts.team
|
|
2812
|
+
}).then((r) => (r.data || []).filter((d) => d != null)).catch(() => [])
|
|
2813
|
+
]);
|
|
2814
|
+
const [hist, current2] = await Promise.all([
|
|
2815
|
+
Promise.all(histPromises),
|
|
2816
|
+
currentPromises
|
|
2817
|
+
]);
|
|
2818
|
+
return [
|
|
2819
|
+
t,
|
|
2820
|
+
hist,
|
|
2821
|
+
{
|
|
2822
|
+
invoices: current2[0],
|
|
2823
|
+
payments: current2[1],
|
|
2824
|
+
receipts: current2[2],
|
|
2825
|
+
ppdAll: current2[3],
|
|
2826
|
+
pendingPayments: current2[4]
|
|
2827
|
+
}
|
|
2828
|
+
];
|
|
2829
|
+
});
|
|
2830
|
+
const history = histData;
|
|
2831
|
+
const current = currentData;
|
|
2832
|
+
const allInvoices = [...history.flatMap((h) => h.invoices), ...current.invoices];
|
|
2833
|
+
const allPayments = [...history.flatMap((h) => h.payments), ...current.payments];
|
|
2834
|
+
const cur = allInvoices[0]?.currency || allPayments[0]?.currency || "MXN";
|
|
2835
|
+
const monthlyInvoiced = history.map((h) => sum2(h.invoices.filter((i) => i.status === "valid")));
|
|
2836
|
+
const monthlyCollected = history.map((h) => sum2(h.payments.filter((p) => p.status === "succeeded")));
|
|
2837
|
+
const monthlyReceiptCount = history.map((h) => h.receipts.length);
|
|
2838
|
+
const monthlyReceiptTotal = history.map((h) => sum2(h.receipts));
|
|
2839
|
+
const monthlyInvoicedReceiptCount = history.map(
|
|
2840
|
+
(h) => h.receipts.filter((r) => r.status === "invoiced" || r.status === "completed").length
|
|
2841
|
+
);
|
|
2842
|
+
const revTrend = calcTrend(monthlyInvoiced);
|
|
2843
|
+
const projectedThisMonth = revTrend.avgAll * (1 + revTrend.trend / 100);
|
|
2844
|
+
const futureProjections = [];
|
|
2845
|
+
for (let i = 1; i <= projMonths; i++) {
|
|
2846
|
+
const d = new Date(curYear, curMonth + i, 1);
|
|
2847
|
+
futureProjections.push({
|
|
2848
|
+
month: monthName(d.getMonth(), d.getFullYear()),
|
|
2849
|
+
revenue: project(revTrend.avgAll, revTrend.trend, i),
|
|
2850
|
+
collections: project(
|
|
2851
|
+
calcTrend(monthlyCollected).avgAll,
|
|
2852
|
+
calcTrend(monthlyCollected).trend,
|
|
2853
|
+
i
|
|
2854
|
+
),
|
|
2855
|
+
risk: 0
|
|
2856
|
+
// filled below
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
const ppdWithBalance = current.ppdAll.filter((i) => {
|
|
2860
|
+
const balance = i.last_balance ?? i.total;
|
|
2861
|
+
return i.status === "valid" && balance > 0.01;
|
|
2862
|
+
});
|
|
2863
|
+
const ppdAnalysis = ppdWithBalance.map((i) => {
|
|
2864
|
+
const balance = i.last_balance ?? i.total ?? 0;
|
|
2865
|
+
const days = ageDays(i);
|
|
2866
|
+
const prob = collectionProbability(days);
|
|
2867
|
+
const clientName = i.client?.legal_name || i.client?.name || i.client_name || "Sin nombre";
|
|
2868
|
+
return { ...i, _balance: balance, _days: days, _prob: prob, _clientName: clientName };
|
|
2869
|
+
});
|
|
2870
|
+
const totalAtRisk = ppdAnalysis.reduce((s, i) => s + i._balance, 0);
|
|
2871
|
+
const expectedRecovery = ppdAnalysis.reduce((s, i) => s + i._balance * i._prob, 0);
|
|
2872
|
+
const likelyLoss = totalAtRisk - expectedRecovery;
|
|
2873
|
+
const highRisk = ppdAnalysis.filter((i) => i._days > 90);
|
|
2874
|
+
const highRiskAmount = highRisk.reduce((s, i) => s + i._balance, 0);
|
|
2875
|
+
const topRisk = [...ppdAnalysis].sort((a, b) => a._prob - b._prob || b._balance - a._balance).slice(0, 3);
|
|
2876
|
+
for (let i = 0; i < futureProjections.length; i++) {
|
|
2877
|
+
futureProjections[i].risk = highRiskAmount * Math.pow(0.85, i + 1);
|
|
2878
|
+
}
|
|
2879
|
+
const pendingReceipts = current.receipts.filter(
|
|
2880
|
+
(r) => r.status === "pending" || r.status === "open"
|
|
2881
|
+
);
|
|
2882
|
+
const pendingReceiptsTotal = sum2(pendingReceipts);
|
|
2883
|
+
const pendingReceiptsCount = pendingReceipts.length;
|
|
2884
|
+
const totalHistReceipts = monthlyReceiptCount.reduce((a, b) => a + b, 0);
|
|
2885
|
+
const totalHistInvoiced = monthlyInvoicedReceiptCount.reduce((a, b) => a + b, 0);
|
|
2886
|
+
const selfInvoiceRate = totalHistReceipts > 0 ? totalHistInvoiced / totalHistReceipts : 0;
|
|
2887
|
+
const estGlobalCount = Math.round(pendingReceiptsCount * (1 - selfInvoiceRate));
|
|
2888
|
+
const estGlobalAmount = pendingReceiptsTotal * (1 - selfInvoiceRate);
|
|
2889
|
+
const pendingPayCount = current.pendingPayments.length;
|
|
2890
|
+
const pendingPayTotal = sum2(current.pendingPayments);
|
|
2891
|
+
const totalHistPayments = history.reduce((s, h) => s + h.payments.length, 0);
|
|
2892
|
+
const totalHistSucceeded = history.reduce(
|
|
2893
|
+
(s, h) => s + h.payments.filter((p) => p.status === "succeeded").length,
|
|
2894
|
+
0
|
|
2895
|
+
);
|
|
2896
|
+
const conversionRate = totalHistPayments > 0 ? totalHistSucceeded / totalHistPayments : 0.85;
|
|
2897
|
+
const expectedFromLinks = pendingPayTotal * conversionRate;
|
|
2898
|
+
const expectedFromPPD = expectedRecovery;
|
|
2899
|
+
const totalExpectedCash = expectedFromLinks + expectedFromPPD;
|
|
2900
|
+
if (isJsonMode()) {
|
|
2901
|
+
return printJson({
|
|
2902
|
+
period: monthName(curMonth, curYear),
|
|
2903
|
+
team: { name: team?.legal_name || team?.brand?.alias, tax_id: team?.tax_id },
|
|
2904
|
+
revenue: {
|
|
2905
|
+
monthly_average_6m: Math.round(revTrend.avgAll * 100) / 100,
|
|
2906
|
+
trend_pct: Math.round(revTrend.trend * 100) / 100,
|
|
2907
|
+
trend_direction: revTrend.arrow === "\u2191" ? "growing" : revTrend.arrow === "\u2193" ? "declining" : "stable",
|
|
2908
|
+
projected_this_month: Math.round(projectedThisMonth * 100) / 100,
|
|
2909
|
+
projections: futureProjections.map((p) => ({
|
|
2910
|
+
month: p.month,
|
|
2911
|
+
revenue: Math.round(p.revenue * 100) / 100,
|
|
2912
|
+
collections: Math.round(p.collections * 100) / 100,
|
|
2913
|
+
risk: Math.round(p.risk * 100) / 100
|
|
2914
|
+
})),
|
|
2915
|
+
monthly_history: history.map((h, idx) => ({
|
|
2916
|
+
month: monthName(h.month, h.year),
|
|
2917
|
+
invoiced: monthlyInvoiced[idx],
|
|
2918
|
+
collected: monthlyCollected[idx],
|
|
2919
|
+
receipts: monthlyReceiptCount[idx]
|
|
2920
|
+
}))
|
|
2921
|
+
},
|
|
2922
|
+
collections_risk: {
|
|
2923
|
+
total_pending: Math.round(totalAtRisk * 100) / 100,
|
|
2924
|
+
invoice_count: ppdWithBalance.length,
|
|
2925
|
+
expected_recovery: Math.round(expectedRecovery * 100) / 100,
|
|
2926
|
+
recovery_pct: totalAtRisk > 0 ? Math.round(expectedRecovery / totalAtRisk * 100) : 0,
|
|
2927
|
+
likely_loss: Math.round(likelyLoss * 100) / 100,
|
|
2928
|
+
high_risk: {
|
|
2929
|
+
amount: Math.round(highRiskAmount * 100) / 100,
|
|
2930
|
+
count: highRisk.length
|
|
2931
|
+
},
|
|
2932
|
+
top_risk: topRisk.map((i) => ({
|
|
2933
|
+
client: i._clientName,
|
|
2934
|
+
balance: i._balance,
|
|
2935
|
+
days: i._days,
|
|
2936
|
+
probability: i._prob
|
|
2937
|
+
}))
|
|
2938
|
+
},
|
|
2939
|
+
eom_estimate: {
|
|
2940
|
+
pending_receipts: pendingReceiptsCount,
|
|
2941
|
+
pending_amount: Math.round(pendingReceiptsTotal * 100) / 100,
|
|
2942
|
+
self_invoice_rate: Math.round(selfInvoiceRate * 100),
|
|
2943
|
+
estimated_global_count: estGlobalCount,
|
|
2944
|
+
estimated_global_amount: Math.round(estGlobalAmount * 100) / 100
|
|
2945
|
+
},
|
|
2946
|
+
cash_flow: {
|
|
2947
|
+
pending_payment_links: pendingPayCount,
|
|
2948
|
+
pending_links_amount: Math.round(pendingPayTotal * 100) / 100,
|
|
2949
|
+
conversion_rate: Math.round(conversionRate * 100),
|
|
2950
|
+
expected_from_links: Math.round(expectedFromLinks * 100) / 100,
|
|
2951
|
+
expected_from_ppd: Math.round(expectedFromPPD * 100) / 100,
|
|
2952
|
+
total_expected: Math.round(totalExpectedCash * 100) / 100
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
const teamName = team?.legal_name || team?.brand?.alias || "\u2014";
|
|
2957
|
+
console.log(`
|
|
2958
|
+
${pc14.bold(`Proyecci\xF3n \u2014 ${monthName(curMonth, curYear)}`)}`);
|
|
2959
|
+
console.log(pc14.dim(teamName));
|
|
2960
|
+
console.log(`
|
|
2961
|
+
${pc14.bold(pc14.underline("Facturaci\xF3n"))}`);
|
|
2962
|
+
console.log(` Promedio mensual (6m) ${fmt2(revTrend.avgAll, cur)}`);
|
|
2963
|
+
const trendColor = revTrend.arrow === "\u2191" ? pc14.green : revTrend.arrow === "\u2193" ? pc14.red : pc14.yellow;
|
|
2964
|
+
const trendSign = revTrend.trend >= 0 ? "+" : "";
|
|
2965
|
+
console.log(` Tendencia ${trendColor(`${revTrend.arrow} ${trendSign}${Math.round(revTrend.trend)}% mensual`)}`);
|
|
2966
|
+
console.log(` Proyecci\xF3n este mes ${pc14.green(fmt2(projectedThisMonth, cur))}`);
|
|
2967
|
+
if (futureProjections.length > 0) {
|
|
2968
|
+
const projStr = futureProjections.map((p) => fmt2(p.revenue, cur)).join(" \u2192 ");
|
|
2969
|
+
console.log(` Pr\xF3ximos ${projMonths} meses ${projStr}`);
|
|
2970
|
+
}
|
|
2971
|
+
console.log(`
|
|
2972
|
+
${pc14.bold(pc14.underline("Cobranza PPD"))}`);
|
|
2973
|
+
if (ppdAnalysis.length === 0) {
|
|
2974
|
+
console.log(` ${pc14.dim("Sin facturas PPD pendientes")}`);
|
|
2975
|
+
} else {
|
|
2976
|
+
const recoveryPct = totalAtRisk > 0 ? Math.round(expectedRecovery / totalAtRisk * 100) : 0;
|
|
2977
|
+
console.log(` Total pendiente ${pc14.red(fmt2(totalAtRisk, cur))} ${pc14.dim(`(${ppdWithBalance.length} facturas)`)}`);
|
|
2978
|
+
console.log(` Recuperaci\xF3n esperada ${pc14.green(fmt2(expectedRecovery, cur))} ${pc14.dim(`(${recoveryPct}%)`)}`);
|
|
2979
|
+
if (highRisk.length > 0) {
|
|
2980
|
+
console.log(` En riesgo alto ${pc14.red(fmt2(highRiskAmount, cur))} ${pc14.dim(`(${highRisk.length} facturas +90 d\xEDas)`)}`);
|
|
2981
|
+
}
|
|
2982
|
+
if (topRisk.length > 0) {
|
|
2983
|
+
console.log(pc14.dim(" Mayor riesgo:"));
|
|
2984
|
+
for (const r of topRisk) {
|
|
2985
|
+
const probStr = `${Math.round(r._prob * 100)}%`;
|
|
2986
|
+
const name = r._clientName.length > 22 ? r._clientName.slice(0, 22) + "\u2026" : r._clientName;
|
|
2987
|
+
console.log(
|
|
2988
|
+
` ${name.padEnd(24)} ${fmt2(r._balance, cur).padEnd(16)} ${String(r._days).padStart(3)} d\xEDas ${pc14.dim(`prob: ${probStr}`)}`
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
console.log(`
|
|
2994
|
+
${pc14.bold(pc14.underline("Recibos \u2192 Factura Global"))}`);
|
|
2995
|
+
console.log(` Pendientes hoy ${String(pendingReceiptsCount).padEnd(5)} recibos ${fmt2(pendingReceiptsTotal, cur)}`);
|
|
2996
|
+
console.log(` Tasa hist\xF3rica autofact ${selfInvoiceRate > 0 ? `${Math.round(selfInvoiceRate * 100)}%` : pc14.dim("sin datos")}`);
|
|
2997
|
+
console.log(` Estimado factura global ~${String(estGlobalCount).padEnd(4)} recibos ${fmt2(estGlobalAmount, cur)}`);
|
|
2998
|
+
console.log(`
|
|
2999
|
+
${pc14.bold(pc14.underline("Flujo de Efectivo"))}`);
|
|
3000
|
+
console.log(` Pagos pendientes ${String(pendingPayCount).padEnd(5)} links ${fmt2(pendingPayTotal, cur)}`);
|
|
3001
|
+
console.log(` Conversi\xF3n hist\xF3rica ${Math.round(conversionRate * 100)}%`);
|
|
3002
|
+
console.log(` Cobro esperado (links) ${pc14.green(fmt2(expectedFromLinks, cur))}`);
|
|
3003
|
+
console.log(` Cobro esperado (PPD) ${pc14.green(fmt2(expectedFromPPD, cur))}`);
|
|
3004
|
+
console.log(` ${pc14.bold("Total entrada esperada")} ${pc14.green(pc14.bold(fmt2(totalExpectedCash, cur)))}`);
|
|
3005
|
+
if (projMonths > 1) {
|
|
3006
|
+
console.log(`
|
|
3007
|
+
${pc14.bold(pc14.underline("Proyecci\xF3n multi-mes"))}`);
|
|
3008
|
+
const colTrend = calcTrend(monthlyCollected);
|
|
3009
|
+
const header = ` ${"Mes".padEnd(20)} ${"Ingresos".padStart(18)} ${"Cobranza".padStart(18)} ${"Riesgo".padStart(18)}`;
|
|
3010
|
+
console.log(pc14.dim(header));
|
|
3011
|
+
console.log(pc14.dim(" " + "\u2500".repeat(76)));
|
|
3012
|
+
for (const p of futureProjections) {
|
|
3013
|
+
console.log(
|
|
3014
|
+
` ${p.month.padEnd(20)} ${pc14.green(fmt2(p.revenue, cur).padStart(18))} ${fmt2(p.collections, cur).padStart(18)} ${pc14.red(fmt2(p.risk, cur).padStart(18))}`
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
console.log();
|
|
3019
|
+
} catch (e) {
|
|
3020
|
+
console.error(pc14.red(`\u2717 ${e.message}`));
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// src/cli.ts
|
|
3026
|
+
var program = new Command();
|
|
3027
|
+
program.name("gigstack").description("gigstack CLI \u2014 facturaci\xF3n electr\xF3nica desde tu terminal").version("0.1.0").option("--json", "Salida en formato JSON").option("--team <id>", "Team ID para operaciones multi-equipo").hook("preAction", (thisCommand) => {
|
|
3028
|
+
const opts = thisCommand.opts();
|
|
3029
|
+
if (opts.json) setJsonMode(true);
|
|
3030
|
+
});
|
|
3031
|
+
registerAuthCommands(program);
|
|
3032
|
+
registerContextCommand(program);
|
|
3033
|
+
registerStatusCommand(program);
|
|
3034
|
+
registerDoctorCommand(program);
|
|
3035
|
+
registerPayCommand(program);
|
|
3036
|
+
registerClientCommands(program);
|
|
3037
|
+
registerInvoiceCommands(program);
|
|
3038
|
+
registerPaymentCommands(program);
|
|
3039
|
+
registerServiceCommands(program);
|
|
3040
|
+
registerWebhookCommands(program);
|
|
3041
|
+
registerTeamCommands(program);
|
|
3042
|
+
registerReceiptCommands(program);
|
|
3043
|
+
registerCompletionsCommand(program);
|
|
3044
|
+
registerExportCommand(program);
|
|
3045
|
+
registerExplainCommand(program);
|
|
3046
|
+
registerForecastCommand(program);
|
|
3047
|
+
program.addHelpText("after", `
|
|
3048
|
+
${pc15.bold("Ejemplos:")}
|
|
3049
|
+
${pc15.dim("$")} gigstack login Autenticarse
|
|
3050
|
+
${pc15.dim("$")} gigstack context payments Entender pagos (para agentes)
|
|
3051
|
+
${pc15.dim("$")} gigstack status Resumen r\xE1pido del equipo
|
|
3052
|
+
${pc15.dim("$")} gigstack doctor Diagn\xF3stico completo
|
|
3053
|
+
${pc15.dim("$")} gigstack pay Registrar pago + autofactura
|
|
3054
|
+
${pc15.dim("$")} gigstack whoami Ver cuenta actual
|
|
3055
|
+
${pc15.dim("$")} gigstack clients list Listar clientes
|
|
3056
|
+
${pc15.dim("$")} gigstack clients create Crear cliente (interactivo)
|
|
3057
|
+
${pc15.dim("$")} gigstack invoices list Listar facturas
|
|
3058
|
+
${pc15.dim("$")} gigstack invoices create Crear factura (interactivo)
|
|
3059
|
+
${pc15.dim("$")} gigstack invoices list --json Salida JSON
|
|
3060
|
+
${pc15.dim("$")} gigstack payments list Listar pagos
|
|
3061
|
+
${pc15.dim("$")} gigstack services list Listar servicios
|
|
3062
|
+
${pc15.dim("$")} gigstack export invoices --from 2026-01 Exportar facturas a CSV
|
|
3063
|
+
${pc15.dim("$")} gigstack export payments --format json Exportar pagos a JSON
|
|
3064
|
+
${pc15.dim("$")} gigstack explain <id> Explicar cualquier recurso
|
|
3065
|
+
|
|
3066
|
+
${pc15.bold("Docs:")} https://docs.gigstack.io
|
|
3067
|
+
`);
|
|
3068
|
+
program.parse();
|